| // SPDX-License-Identifier: GPL-2.0-or-later | |
| /* | |
| * Windows System Compression (WOF) decompression glue. | |
| * | |
| * Copyright (c) 2026 LG Electronics Co., Ltd. | |
| */ | |
| #include <linux/fs.h> | |
| #include <linux/blkdev.h> | |
| #include <linux/overflow.h> | |
| #include <linux/pagemap.h> | |
| #include <linux/sched/mm.h> | |
| #include <linux/slab.h> | |
| #include <linux/unaligned.h> | |
| #include <linux/vmalloc.h> | |
| #include "ntfs.h" | |
| #include "inode.h" | |
| #include "debug.h" | |
| #include "ntfs_codec.h" | |
| #include "attrib.h" | |
| static const __le16 WOF_NAME[] = { | |
| cpu_to_le16('W'), cpu_to_le16('o'), cpu_to_le16('f'), | |
| cpu_to_le16('C'), cpu_to_le16('o'), cpu_to_le16('m'), | |
| cpu_to_le16('p'), cpu_to_le16('r'), cpu_to_le16('e'), | |
| cpu_to_le16('s'), cpu_to_le16('s'), cpu_to_le16('e'), | |
| cpu_to_le16('d'), cpu_to_le16('D'), cpu_to_le16('a'), | |
| cpu_to_le16('t'), cpu_to_le16('a'), | |
| }; | |
| #define WOF_NAME_LEN 17 | |
| #define NTFS_WOF_MAX_COMP_UNIT (1U << 15) | |
| #define NTFS_WOF_MAX_PAGES \ | |
| DIV_ROUND_UP(NTFS_WOF_MAX_COMP_UNIT + PAGE_SIZE - 1, PAGE_SIZE) | |
| struct ntfs_wof_workspace { | |
| struct mutex *lock; | |
| const struct ntfs_codec_ops *codec; | |
| u32 comp_unit; | |
| void *output; | |
| void *scratch; | |
| }; | |
| static DEFINE_MUTEX(ntfs_wof_xpress4k_lock); | |
| static DEFINE_MUTEX(ntfs_wof_xpress8k_lock); | |
| static DEFINE_MUTEX(ntfs_wof_xpress16k_lock); | |
| static DEFINE_MUTEX(ntfs_wof_lzx32k_lock); | |
| static struct ntfs_wof_workspace ntfs_wof_xpress4k_workspace = { | |
| .lock = &ntfs_wof_xpress4k_lock, | |
| .codec = &ntfs_xpress4k_codec_ops, | |
| .comp_unit = 1U << 12, | |
| }; | |
| static struct ntfs_wof_workspace ntfs_wof_xpress8k_workspace = { | |
| .lock = &ntfs_wof_xpress8k_lock, | |
| .codec = &ntfs_xpress8k_codec_ops, | |
| .comp_unit = 1U << 13, | |
| }; | |
| static struct ntfs_wof_workspace ntfs_wof_xpress16k_workspace = { | |
| .lock = &ntfs_wof_xpress16k_lock, | |
| .codec = &ntfs_xpress16k_codec_ops, | |
| .comp_unit = 1U << 14, | |
| }; | |
| static struct ntfs_wof_workspace ntfs_wof_lzx32k_workspace = { | |
| .lock = &ntfs_wof_lzx32k_lock, | |
| .codec = &ntfs_lzx32k_codec_ops, | |
| .comp_unit = 1U << 15, | |
| }; | |
| static struct ntfs_wof_workspace *const ntfs_wof_workspaces[] = { | |
| &ntfs_wof_xpress4k_workspace, | |
| &ntfs_wof_xpress8k_workspace, | |
| &ntfs_wof_xpress16k_workspace, | |
| &ntfs_wof_lzx32k_workspace, | |
| }; | |
| static struct ntfs_wof_workspace *ntfs_wof_workspace(u8 block_size_bits) | |
| { | |
| switch (block_size_bits) { | |
| case 12: | |
| return &ntfs_wof_xpress4k_workspace; | |
| case 13: | |
| return &ntfs_wof_xpress8k_workspace; | |
| case 14: | |
| return &ntfs_wof_xpress16k_workspace; | |
| case 15: | |
| return &ntfs_wof_lzx32k_workspace; | |
| default: | |
| return NULL; | |
| } | |
| } | |
| /* | |
| * Size of the buffer a chunk is read into. A chunk is read straight off the | |
| * device, so the buffer has to hold @comp_unit bytes plus the leading partial | |
| * sector. | |
| */ | |
| static size_t ntfs_wof_input_size(const struct ntfs_wof_workspace *ws) | |
| { | |
| return round_up((size_t)ws->comp_unit + 511, 512); | |
| } | |
| static int ntfs_wof_workspace_prepare(struct ntfs_wof_workspace *ws) | |
| { | |
| void *output, *scratch; | |
| size_t scratch_size; | |
| if (ws->output) | |
| return 0; | |
| scratch_size = ws->codec->scratch_size(ws->comp_unit); | |
| if (!scratch_size) | |
| return -EINVAL; | |
| output = kvmalloc(ws->comp_unit, GFP_NOFS); | |
| scratch = kvzalloc(scratch_size, GFP_NOFS); | |
| if (!output || !scratch) { | |
| kvfree(output); | |
| kvfree(scratch); | |
| return -ENOMEM; | |
| } | |
| ws->output = output; | |
| ws->scratch = scratch; | |
| return 0; | |
| } | |
| void ntfs_wof_free_workspaces(void) | |
| { | |
| unsigned int i; | |
| for (i = 0; i < ARRAY_SIZE(ntfs_wof_workspaces); i++) { | |
| struct ntfs_wof_workspace *ws = ntfs_wof_workspaces[i]; | |
| mutex_lock(ws->lock); | |
| kvfree(ws->output); | |
| kvfree(ws->scratch); | |
| ws->output = NULL; | |
| ws->scratch = NULL; | |
| mutex_unlock(ws->lock); | |
| } | |
| } | |
| static int ntfs_bdev_read_from_rl(struct ntfs_volume *vol, | |
| struct runlist *runlist, | |
| sector_t start_sector, u64 sector_count, | |
| void *buf) | |
| { | |
| struct runlist_element *rl; | |
| u32 sec_per_clu_bits; | |
| s64 vcn; | |
| u64 sec_off; | |
| size_t buf_off = 0; | |
| unsigned int nofs_flags; | |
| int err; | |
| if (vol->cluster_size_bits < 9) | |
| return -EINVAL; | |
| sec_per_clu_bits = vol->cluster_size_bits - 9; | |
| vcn = start_sector >> sec_per_clu_bits; | |
| sec_off = start_sector & ((1ULL << sec_per_clu_bits) - 1); | |
| nofs_flags = memalloc_nofs_save(); | |
| down_read(&runlist->lock); | |
| if (!runlist->rl) { | |
| err = -EINVAL; | |
| goto out_unlock; | |
| } | |
| rl = __ntfs_attr_find_vcn_nolock(runlist, vcn); | |
| if (IS_ERR(rl)) { | |
| err = PTR_ERR(rl); | |
| goto out_unlock; | |
| } | |
| while (sector_count > 0) { | |
| s64 lcn; | |
| s64 rl_end; | |
| u64 byte_off, byte_len, sectors, available; | |
| if (rl->length <= 0 || vcn < rl->vcn) { | |
| err = -EINVAL; | |
| goto out_unlock; | |
| } | |
| lcn = ntfs_rl_vcn_to_lcn(rl, vcn); | |
| if (lcn < 0 && lcn != LCN_HOLE) { | |
| err = -EINVAL; | |
| goto out_unlock; | |
| } | |
| if (check_add_overflow(rl->vcn, rl->length, &rl_end) || | |
| rl_end <= vcn || | |
| (u64)(rl_end - vcn) > (U64_MAX >> sec_per_clu_bits)) { | |
| err = -EOVERFLOW; | |
| goto out_unlock; | |
| } | |
| available = (u64)(rl_end - vcn) << sec_per_clu_bits; | |
| if (available <= sec_off) { | |
| err = -EINVAL; | |
| goto out_unlock; | |
| } | |
| available -= sec_off; | |
| sectors = min_t(u64, sector_count, available); | |
| if (check_mul_overflow(sectors, (u64)SECTOR_SIZE, &byte_len) || | |
| byte_len > SIZE_MAX - buf_off) { | |
| err = -EOVERFLOW; | |
| goto out_unlock; | |
| } | |
| if (lcn == LCN_HOLE) { | |
| memset((u8 *)buf + buf_off, 0, byte_len); | |
| } else { | |
| byte_off = ntfs_cluster_to_bytes(vol, lcn); | |
| if (check_add_overflow(byte_off, sec_off << 9, | |
| &byte_off) || | |
| byte_off > S64_MAX) { | |
| err = -EOVERFLOW; | |
| goto out_unlock; | |
| } | |
| err = ntfs_bdev_read(vol->sb->s_bdev, | |
| (char *)buf + buf_off, | |
| (loff_t)byte_off, byte_len); | |
| if (err) | |
| goto out_unlock; | |
| } | |
| buf_off += byte_len; | |
| sector_count -= sectors; | |
| rl++; | |
| vcn = rl->vcn; | |
| sec_off = 0; | |
| } | |
| err = 0; | |
| out_unlock: | |
| up_read(&runlist->lock); | |
| memalloc_nofs_restore(nofs_flags); | |
| return err; | |
| } | |
| static int parse_wof_chunk_table(struct ntfs_inode *base_ni, | |
| struct ntfs_inode *ni, u64 chunk_idx, | |
| u64 chunk_count, u32 decomp_size, | |
| u64 *chunk_offset, u32 *chunk_size, | |
| void *table_buf, size_t table_buf_size) | |
| { | |
| u8 bytes_per_off; | |
| u8 *buf; | |
| u64 off[2]; | |
| u64 byte_off, chunk_data_size, table_size; | |
| u32 bytes_to_read; | |
| int ret = 0; | |
| if (i_size_read(VFS_I(base_ni)) < (1ULL << 32)) | |
| bytes_per_off = sizeof(__le32); | |
| else | |
| bytes_per_off = sizeof(__le64); | |
| if (!chunk_count || chunk_idx >= chunk_count) | |
| return -EINVAL; | |
| table_size = (chunk_count - 1) * bytes_per_off; | |
| if (ni->data_size < 0 || (u64)ni->data_size < table_size) | |
| return -EINVAL; | |
| chunk_data_size = (u64)ni->data_size - table_size; | |
| if (chunk_count == 1) { | |
| if (chunk_data_size > decomp_size) | |
| return -EINVAL; | |
| *chunk_offset = 0; | |
| *chunk_size = chunk_data_size; | |
| goto out; | |
| } | |
| byte_off = chunk_idx ? (chunk_idx - 1) * bytes_per_off : 0; | |
| bytes_to_read = chunk_idx + 1 == chunk_count ? | |
| bytes_per_off : | |
| (chunk_idx ? 2 : 1) * bytes_per_off; | |
| if (NInoNonResident(ni)) { | |
| sector_t start_sector = byte_off >> 9; | |
| u32 sector_off = byte_off & ((1 << 9) - 1); | |
| u32 sectors = DIV_ROUND_UP(sector_off + bytes_to_read, 512); | |
| if ((size_t)sectors << 9 > table_buf_size) | |
| return -EINVAL; | |
| buf = table_buf; | |
| ret = ntfs_bdev_read_from_rl(ni->vol, &ni->runlist, | |
| start_sector, sectors, buf); | |
| if (ret) | |
| return -EIO; | |
| buf += sector_off; | |
| } else { | |
| struct ntfs_attr_search_ctx *ctx; | |
| u32 value_length; | |
| u16 value_offset; | |
| if (bytes_to_read > table_buf_size) | |
| return -EINVAL; | |
| mutex_lock(&base_ni->mrec_lock); | |
| ctx = ntfs_attr_get_search_ctx(base_ni, NULL); | |
| if (!ctx) { | |
| ret = -ENOMEM; | |
| goto out_unlock_mrec; | |
| } | |
| ret = ntfs_attr_lookup(ni->type, ni->name, ni->name_len, | |
| CASE_SENSITIVE, 0, NULL, 0, ctx); | |
| if (ret) | |
| goto out_put_ctx; | |
| value_length = | |
| le32_to_cpu(ctx->attr->data.resident.value_length); | |
| value_offset = | |
| le16_to_cpu(ctx->attr->data.resident.value_offset); | |
| if (byte_off + bytes_to_read > value_length) { | |
| ret = -EINVAL; | |
| goto out_put_ctx; | |
| } | |
| memcpy(table_buf, (u8 *)ctx->attr + value_offset + byte_off, | |
| bytes_to_read); | |
| buf = table_buf; | |
| out_put_ctx: | |
| ntfs_attr_put_search_ctx(ctx); | |
| out_unlock_mrec: | |
| mutex_unlock(&base_ni->mrec_lock); | |
| if (ret) | |
| return ret; | |
| } | |
| if (bytes_per_off == sizeof(__le32)) { | |
| off[0] = chunk_idx ? get_unaligned_le32(buf) : 0; | |
| if (chunk_idx + 1 == chunk_count) | |
| off[1] = chunk_data_size; | |
| else if (chunk_idx) | |
| off[1] = get_unaligned_le32(buf + bytes_per_off); | |
| else | |
| off[1] = get_unaligned_le32(buf); | |
| } else { | |
| off[0] = chunk_idx ? get_unaligned_le64(buf) : 0; | |
| if (chunk_idx + 1 == chunk_count) | |
| off[1] = chunk_data_size; | |
| else if (chunk_idx) | |
| off[1] = get_unaligned_le64(buf + bytes_per_off); | |
| else | |
| off[1] = get_unaligned_le64(buf); | |
| } | |
| if (off[1] <= off[0] || off[1] > chunk_data_size || | |
| off[1] - off[0] > decomp_size) | |
| return -EINVAL; | |
| *chunk_offset = table_size + off[0]; | |
| *chunk_size = off[1] - off[0]; | |
| out: | |
| if (!*chunk_size) | |
| return -EINVAL; | |
| return 0; | |
| } | |
| static int ntfs_read_wof_chunk(struct ntfs_volume *vol, | |
| struct ntfs_inode *wof_ni, u64 chunk_offset, | |
| u32 chunk_size, void *input, size_t input_size, | |
| char **chunk_mem) | |
| { | |
| struct ntfs_inode *base_ni = wof_ni->ext.base_ntfs_ino; | |
| struct ntfs_attr_search_ctx *ctx; | |
| u32 input_offset = chunk_offset & 511; | |
| u32 input_size_aligned; | |
| u32 value_length; | |
| u16 value_offset; | |
| int err; | |
| input_size_aligned = round_up(chunk_size + input_offset, 512); | |
| if (input_size_aligned > input_size) | |
| return -EINVAL; | |
| if (NInoNonResident(wof_ni)) { | |
| err = ntfs_bdev_read_from_rl(vol, &wof_ni->runlist, | |
| chunk_offset >> 9, | |
| input_size_aligned >> 9, input); | |
| if (err) | |
| return err; | |
| *chunk_mem = (u8 *)input + input_offset; | |
| return 0; | |
| } | |
| mutex_lock(&base_ni->mrec_lock); | |
| ctx = ntfs_attr_get_search_ctx(base_ni, NULL); | |
| if (!ctx) { | |
| err = -ENOMEM; | |
| goto out_unlock_mrec; | |
| } | |
| err = ntfs_attr_lookup(wof_ni->type, wof_ni->name, wof_ni->name_len, | |
| CASE_SENSITIVE, 0, NULL, 0, ctx); | |
| if (err) | |
| goto out_put_ctx; | |
| value_length = le32_to_cpu(ctx->attr->data.resident.value_length); | |
| value_offset = le16_to_cpu(ctx->attr->data.resident.value_offset); | |
| if (chunk_offset + chunk_size > value_length) { | |
| err = -EINVAL; | |
| goto out_put_ctx; | |
| } | |
| memcpy(input, (u8 *)ctx->attr + value_offset + chunk_offset, | |
| chunk_size); | |
| *chunk_mem = input; | |
| out_put_ctx: | |
| ntfs_attr_put_search_ctx(ctx); | |
| out_unlock_mrec: | |
| mutex_unlock(&base_ni->mrec_lock); | |
| return err; | |
| } | |
| struct ntfs_wof_dest { | |
| struct folio *folios[NTFS_WOF_MAX_PAGES]; | |
| struct page *pages[NTFS_WOF_MAX_PAGES]; | |
| unsigned int nr_folios; | |
| unsigned int nr_pages; | |
| }; | |
| static void ntfs_wof_release_dest(struct ntfs_wof_dest *dest, | |
| struct folio *target, bool success) | |
| { | |
| unsigned int i; | |
| for (i = 0; i < dest->nr_folios; i++) { | |
| struct folio *folio = dest->folios[i]; | |
| if (folio == target) | |
| continue; | |
| if (success) { | |
| flush_dcache_folio(folio); | |
| folio_mark_uptodate(folio); | |
| } else { | |
| folio_clear_uptodate(folio); | |
| } | |
| folio_unlock(folio); | |
| folio_put(folio); | |
| } | |
| } | |
| static int ntfs_wof_collect_dest(struct address_space *mapping, | |
| struct folio *target, loff_t chunk_start, | |
| loff_t chunk_end, struct ntfs_wof_dest *dest) | |
| { | |
| pgoff_t index, last, page_index; | |
| unsigned int i; | |
| memset(dest, 0, sizeof(*dest)); | |
| index = chunk_start >> PAGE_SHIFT; | |
| last = (chunk_end - 1) >> PAGE_SHIFT; | |
| while (index <= last) { | |
| struct folio *folio; | |
| pgoff_t next; | |
| bool is_target; | |
| if (folio_contains(target, index)) { | |
| folio = target; | |
| is_target = true; | |
| } else { | |
| folio = __filemap_get_folio( | |
| mapping, index, | |
| FGP_LOCK | FGP_CREAT | FGP_NOFS | FGP_NOWAIT, | |
| GFP_NOFS); | |
| if (IS_ERR(folio)) | |
| return PTR_ERR(folio); | |
| is_target = false; | |
| if (folio_pos(folio) < chunk_start || | |
| folio_next_pos(folio) > chunk_end) { | |
| folio_unlock(folio); | |
| folio_put(folio); | |
| return -EAGAIN; | |
| } | |
| } | |
| if (dest->nr_folios == ARRAY_SIZE(dest->folios)) { | |
| if (!is_target) { | |
| folio_unlock(folio); | |
| folio_put(folio); | |
| } | |
| return -EINVAL; | |
| } | |
| dest->folios[dest->nr_folios++] = folio; | |
| next = folio->index + folio_nr_pages(folio); | |
| if (next <= index) | |
| return -EAGAIN; | |
| index = next; | |
| } | |
| for (page_index = chunk_start >> PAGE_SHIFT; page_index <= last; | |
| page_index++) { | |
| struct folio *folio = NULL; | |
| for (i = 0; i < dest->nr_folios; i++) { | |
| if (folio_contains(dest->folios[i], page_index)) { | |
| folio = dest->folios[i]; | |
| break; | |
| } | |
| } | |
| if (!folio || dest->nr_pages == ARRAY_SIZE(dest->pages)) | |
| return -EAGAIN; | |
| dest->pages[dest->nr_pages++] = | |
| folio_page(folio, page_index - folio->index); | |
| } | |
| return 0; | |
| } | |
| static int ntfs_wof_decode(struct ntfs_wof_workspace *ws, const void *src, | |
| u32 src_len, void *dst, u32 dst_len) | |
| { | |
| if (src_len == dst_len) { | |
| memcpy(dst, src, dst_len); | |
| return 0; | |
| } | |
| return ws->codec->decompress_chunk(ws->scratch, src, src_len, dst, | |
| dst_len, ws->comp_unit); | |
| } | |
| static int ntfs_wof_decode_page_direct(struct ntfs_wof_workspace *ws, | |
| struct folio *target, loff_t chunk_start, | |
| const void *src, u32 src_len, | |
| u32 dst_len) | |
| { | |
| unsigned int page_offset = offset_in_page(chunk_start); | |
| struct page *page; | |
| pgoff_t page_index; | |
| void *addr; | |
| int err; | |
| page_index = chunk_start >> PAGE_SHIFT; | |
| if (!folio_contains(target, page_index)) | |
| return -EAGAIN; | |
| page = folio_page(target, page_index - target->index); | |
| addr = kmap_local_page(page); | |
| err = ntfs_wof_decode(ws, src, src_len, (u8 *)addr + page_offset, | |
| dst_len); | |
| kunmap_local(addr); | |
| if (err) | |
| return -EINVAL; | |
| return 0; | |
| } | |
| static int ntfs_wof_decode_folios_direct(struct ntfs_wof_workspace *ws, | |
| struct address_space *mapping, | |
| struct folio *target, | |
| loff_t chunk_start, loff_t chunk_end, | |
| const void *src, u32 src_len, | |
| u32 dst_len) | |
| { | |
| unsigned int page_offset = offset_in_page(chunk_start); | |
| struct ntfs_wof_dest dest; | |
| void *addr; | |
| unsigned int nofs_flags; | |
| int err; | |
| err = ntfs_wof_collect_dest(mapping, target, chunk_start, chunk_end, | |
| &dest); | |
| if (err) { | |
| ntfs_wof_release_dest(&dest, target, false); | |
| return -EAGAIN; | |
| } | |
| nofs_flags = memalloc_nofs_save(); | |
| addr = vmap(dest.pages, dest.nr_pages, VM_MAP, PAGE_KERNEL); | |
| memalloc_nofs_restore(nofs_flags); | |
| if (!addr) { | |
| ntfs_wof_release_dest(&dest, target, false); | |
| return -EAGAIN; | |
| } | |
| err = ntfs_wof_decode(ws, src, src_len, (u8 *)addr + page_offset, | |
| dst_len); | |
| vunmap(addr); | |
| if (err) { | |
| ntfs_wof_release_dest(&dest, target, false); | |
| return -EINVAL; | |
| } | |
| ntfs_wof_release_dest(&dest, target, true); | |
| return 0; | |
| } | |
| static int ntfs_wof_try_direct(struct ntfs_wof_workspace *ws, | |
| struct address_space *mapping, | |
| struct folio *target, loff_t chunk_start, | |
| loff_t chunk_end, const void *src, u32 src_len, | |
| u32 dst_len) | |
| { | |
| unsigned int page_offset = offset_in_page(chunk_start); | |
| if (dst_len <= PAGE_SIZE - page_offset) | |
| return ntfs_wof_decode_page_direct(ws, target, chunk_start, src, | |
| src_len, dst_len); | |
| return ntfs_wof_decode_folios_direct(ws, mapping, target, chunk_start, | |
| chunk_end, src, src_len, dst_len); | |
| } | |
| /* | |
| * Decompress one chunk into @folio. Only this step needs the workspace, so it | |
| * is the only step that takes the workspace lock. | |
| */ | |
| static int ntfs_wof_decompress_chunk(struct ntfs_wof_workspace *ws, | |
| struct ntfs_volume *vol, | |
| struct address_space *mapping, | |
| struct folio *folio, loff_t folio_start, | |
| loff_t folio_end, u64 chunk_file_offset, | |
| char *chunk_mem, u32 chunk_size, | |
| u32 decomp_size) | |
| { | |
| loff_t chunk_end = chunk_file_offset + decomp_size; | |
| loff_t copy_start, copy_end; | |
| int err; | |
| mutex_lock(ws->lock); | |
| err = ntfs_wof_workspace_prepare(ws); | |
| if (err) | |
| goto out_unlock; | |
| err = ntfs_wof_try_direct(ws, mapping, folio, chunk_file_offset, | |
| chunk_end, chunk_mem, chunk_size, | |
| decomp_size); | |
| if (err != -EAGAIN) | |
| goto out_unlock; | |
| err = ntfs_wof_decode(ws, chunk_mem, chunk_size, ws->output, | |
| decomp_size); | |
| if (err) { | |
| ntfs_error(vol->sb, "Decompression failed: %d", err); | |
| err = -EINVAL; | |
| goto out_unlock; | |
| } | |
| copy_start = max_t(loff_t, folio_start, chunk_file_offset); | |
| copy_end = min_t(loff_t, folio_end, chunk_file_offset + decomp_size); | |
| memcpy_to_folio(folio, copy_start - folio_start, | |
| ws->output + copy_start - chunk_file_offset, | |
| copy_end - copy_start); | |
| out_unlock: | |
| mutex_unlock(ws->lock); | |
| return err; | |
| } | |
| int ntfs_read_wof_compressed_block(struct folio *folio) | |
| { | |
| struct address_space *mapping = folio->mapping; | |
| struct ntfs_inode *ni = NTFS_I(mapping->host), *wof_ni; | |
| struct inode *wof_inode; | |
| struct ntfs_volume *vol = ni->vol; | |
| struct ntfs_wof_workspace *ws; | |
| loff_t i_size = i_size_read(VFS_I(ni)); | |
| loff_t folio_start = folio_pos(folio); | |
| loff_t folio_end = folio_next_pos(folio); | |
| char *chunk_mem; | |
| void *input; | |
| size_t input_size; | |
| u32 decomp_size; | |
| u64 chunk_count, chunk_idx, last_chunk, chunk_offset; | |
| int err = 0; | |
| ws = ntfs_wof_workspace(ni->itype.compressed.block_size_bits); | |
| if (!ws) { | |
| err = -EOPNOTSUPP; | |
| goto out; | |
| } | |
| if (folio_start >= i_size) { | |
| folio_zero_segment(folio, 0, folio_size(folio)); | |
| goto out; | |
| } | |
| wof_inode = ntfs_attr_iget(VFS_I(ni), AT_DATA, (__le16 *)WOF_NAME, | |
| WOF_NAME_LEN); | |
| if (IS_ERR(wof_inode)) { | |
| err = PTR_ERR(wof_inode); | |
| goto out; | |
| } | |
| wof_ni = NTFS_I(wof_inode); | |
| if (wof_ni->initialized_size != wof_ni->data_size) { | |
| ntfs_error(vol->sb, | |
| "WOF compressed stream is not fully initialized (init %lld, data %lld).", | |
| wof_ni->initialized_size, wof_ni->data_size); | |
| err = -EIO; | |
| goto out_iput; | |
| } | |
| if (NInoNonResident(wof_ni) && !NInoFullyMapped(wof_ni)) { | |
| down_write(&wof_ni->runlist.lock); | |
| if (!NInoFullyMapped(wof_ni)) | |
| err = ntfs_attr_map_whole_runlist(wof_ni); | |
| up_write(&wof_ni->runlist.lock); | |
| if (err) | |
| goto out_iput; | |
| } | |
| input_size = ntfs_wof_input_size(ws); | |
| input = kvmalloc(input_size, GFP_NOFS); | |
| if (!input) { | |
| err = -ENOMEM; | |
| goto out_iput; | |
| } | |
| chunk_idx = div_u64(folio_start, ws->comp_unit); | |
| last_chunk = | |
| div_u64(min_t(loff_t, folio_end, i_size) - 1, ws->comp_unit); | |
| chunk_count = DIV_ROUND_UP_ULL(i_size, ws->comp_unit); | |
| for (; chunk_idx <= last_chunk; chunk_idx++) { | |
| u32 chunk_size; | |
| decomp_size = chunk_idx + 1 == chunk_count ? | |
| i_size - chunk_idx * ws->comp_unit : | |
| ws->comp_unit; | |
| err = parse_wof_chunk_table(ni, wof_ni, chunk_idx, chunk_count, | |
| decomp_size, &chunk_offset, | |
| &chunk_size, input, input_size); | |
| if (err) | |
| goto out_free_input; | |
| err = ntfs_read_wof_chunk(vol, wof_ni, chunk_offset, chunk_size, | |
| input, input_size, &chunk_mem); | |
| if (err) | |
| goto out_free_input; | |
| err = ntfs_wof_decompress_chunk(ws, vol, mapping, folio, | |
| folio_start, folio_end, | |
| chunk_idx * ws->comp_unit, | |
| chunk_mem, chunk_size, | |
| decomp_size); | |
| if (err) | |
| goto out_free_input; | |
| } | |
| if (folio_end > i_size) | |
| folio_zero_segment(folio, i_size - folio_start, | |
| folio_size(folio)); | |
| out_free_input: | |
| kvfree(input); | |
| out_iput: | |
| iput(wof_inode); | |
| out: | |
| if (!err) { | |
| flush_dcache_folio(folio); | |
| folio_mark_uptodate(folio); | |
| } else { | |
| folio_clear_uptodate(folio); | |
| } | |
| folio_unlock(folio); | |
| return err; | |
| } |