[PATCH 7/8] ntfs: add non-resident WOF decompression
Hyunchul Lee <[email protected]>
| Newsgroups | dev.linux.lists.ntfs |
|---|---|
| Message-ID | <[email protected]> |
Introduce non-resident Windows System Compression (WOF) decompression support. Add wof.c containing parse_wof_chunk_table() and ntfs_read_wof_compressed_block(), and routing them via transparent codec ops table with dynamic scratch memory allocation. Hook up ntfs_readpage/read_folio paths in aops.c to delegate to the WOF block reader when NInoWofCompressed is set. Signed-off-by: Hyunchul Lee <[email protected]> --- fs/ntfs/Makefile | 2 +- fs/ntfs/aops.c | 7 +- fs/ntfs/attrib.c | 2 +- fs/ntfs/inode.c | 4 +- fs/ntfs/ntfs.h | 3 + fs/ntfs/wof.c | 380 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 393 insertions(+), 5 deletions(-) diff --git a/fs/ntfs/Makefile b/fs/ntfs/Makefile index 5e5fd05f5863..ee8987e496a8 100644 --- a/fs/ntfs/Makefile +++ b/fs/ntfs/Makefile @@ -7,7 +7,7 @@ ntfs-y := aops.o attrib.o collate.o dir.o file.o index.o inode.o \ upcase.o bitmap.o lcnalloc.o logfile.o reparse.o compress.o \ iomap.o debug.o sysctl.o object_id.o bdev-io.o -ntfs-$(CONFIG_NTFS_FS_WOF_COMPRESSION) += \ +ntfs-$(CONFIG_NTFS_FS_WOF_COMPRESSION) += wof.o \ lib/decompress_common.o lib/lzx_decompress.o lib/xpress_decompress.o ccflags-$(CONFIG_NTFS_DEBUG) += -DDEBUG diff --git a/fs/ntfs/aops.c b/fs/ntfs/aops.c index 173de4cbee0f..57dfb99ae567 100644 --- a/fs/ntfs/aops.c +++ b/fs/ntfs/aops.c @@ -93,6 +93,10 @@ static int ntfs_read_folio(struct file *file, struct folio *folio) /* Compressed data streams are handled in compress.c. */ if (NInoNonResident(ni) && NInoCompressed(ni)) return ntfs_read_compressed_block(folio); +#ifdef CONFIG_NTFS_FS_WOF_COMPRESSION + else if (NInoWofCompressed(ni)) + return ntfs_read_wof_compressed_block(folio); +#endif } iomap_read_folio(&ntfs_read_iomap_ops, &ctx, NULL); @@ -234,7 +238,8 @@ static void ntfs_readahead(struct readahead_control *rac) * Resident files are not cached in the page cache, * and readahead is not implemented for compressed files. */ - if (!NInoNonResident(ni) || NInoCompressed(ni)) + if (!NInoNonResident(ni) || NInoCompressed(ni) || + NInoWofCompressed(ni)) return; iomap_readahead(&ntfs_read_iomap_ops, &ctx, NULL); } diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c index c62c8ca8b987..45624e3d1dc9 100644 --- a/fs/ntfs/attrib.c +++ b/fs/ntfs/attrib.c @@ -2137,7 +2137,7 @@ int ntfs_attr_make_non_resident(struct ntfs_inode *ni, const u32 data_size) ni->runlist.count = 0; write_lock_irqsave(&ni->size_lock, flags); ni->allocated_size = new_size; - if (NInoSparse(ni) || NInoCompressed(ni)) { + if ((NInoSparse(ni) && !NInoWofCompressed(ni)) || NInoCompressed(ni)) { ni->itype.compressed.size = ni->allocated_size; if (a->data.non_resident.compression_unit) { ni->itype.compressed.block_size = 1U << diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c index 8d9bbc151309..0d7d19cd6c49 100644 --- a/fs/ntfs/inode.c +++ b/fs/ntfs/inode.c @@ -1119,7 +1119,7 @@ static int ntfs_read_locked_inode(struct inode *vi) } if (a->non_resident) { NInoSetNonResident(ni); - if (NInoCompressed(ni) || NInoSparse(ni)) { + if (NInoCompressed(ni) || (NInoSparse(ni) && !NInoWofCompressed(ni))) { if (NInoCompressed(ni) && a->data.non_resident.compression_unit != 4) { ntfs_error(vi->i_sb, @@ -1398,7 +1398,7 @@ static int ntfs_read_locked_attr_inode(struct inode *base_vi, struct inode *vi) "Attribute name is placed after the mapping pairs array."); goto unm_err_out; } - if (NInoCompressed(ni) || NInoSparse(ni)) { + if (NInoCompressed(ni) || (NInoSparse(ni) && !NInoWofCompressed(ni))) { if (NInoCompressed(ni) && a->data.non_resident.compression_unit != 4) { ntfs_error(vi->i_sb, "Found non-standard compression unit (%u instead of 4). Cannot handle this.", diff --git a/fs/ntfs/ntfs.h b/fs/ntfs/ntfs.h index 45064dbcc2e4..e6dbe382de4e 100644 --- a/fs/ntfs/ntfs.h +++ b/fs/ntfs/ntfs.h @@ -202,6 +202,9 @@ static inline struct ntfs_volume *NTFS_SB(struct super_block *sb) /* From fs/ntfs/compress.c */ int ntfs_read_compressed_block(struct folio *folio); +#ifdef CONFIG_NTFS_FS_WOF_COMPRESSION +int ntfs_read_wof_compressed_block(struct folio *folio); +#endif int allocate_compression_buffers(void); void free_compression_buffers(void); int ntfs_compress_write(struct ntfs_inode *ni, loff_t pos, size_t count, diff --git a/fs/ntfs/wof.c b/fs/ntfs/wof.c new file mode 100644 index 000000000000..4c2a5943ffaf --- /dev/null +++ b/fs/ntfs/wof.c @@ -0,0 +1,380 @@ +// 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/highmem.h> +#include <linux/vmalloc.h> +#include <linux/slab.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 + +static int ntfs_bdev_read_from_rl(struct ntfs_volume *vol, struct runlist *runlist, + sector_t start_sector, u32 sector_count, void *buf) +{ + struct runlist_element *rl; + u32 sec_per_clu_bits = vol->cluster_size_bits - 9; + s64 vcn = start_sector >> sec_per_clu_bits; + u32 sec_off = start_sector & ((1 << sec_per_clu_bits) - 1); + u32 buf_off = 0; + int err; + + down_read(&runlist->lock); + if (!runlist->rl) { + up_read(&runlist->lock); + return -EINVAL; + } + + 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; + loff_t byte_off; + u32 byte_len, sectors; + + if (!rl->length || 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; + } + + /* Sectors available in remaining clusters of this rl element */ + sectors = ((rl->vcn + rl->length - vcn) << sec_per_clu_bits) - sec_off; + sectors = min_t(u32, sector_count, sectors); + byte_off = ntfs_cluster_to_bytes(vol, lcn) + (sec_off << 9); + byte_len = sectors << 9; + + if (lcn == LCN_HOLE) { + memset((u8 *)buf + buf_off, 0, byte_len); + } else { + err = ntfs_bdev_read(vol->sb->s_bdev, (char *)buf + buf_off, + 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); + return err; +} + +static int parse_wof_chunk_table(struct ntfs_inode *ni, u64 chunk_idx, u64 chunk_count, + u64 *chunk_offset, u32 *chunk_size) +{ + u8 bytes_per_off; + u8 *buf_aligned = NULL, *buf = NULL; + u64 off[2]; + u64 chunk_data_size; + int ret = 0; + + /* Determine offset table entry size based on file size */ + if (ni->data_size < (1ULL << 32)) + bytes_per_off = sizeof(__le32); + else + bytes_per_off = sizeof(__le64); + + if (!chunk_count || + ni->data_size < (chunk_count - 1) * bytes_per_off) + return -EINVAL; + chunk_data_size = ni->data_size - (chunk_count - 1) * bytes_per_off; + + if (NInoNonResident(ni)) { + u64 byte_off; + sector_t start_sector; + u32 sector_off, sectors; + + if (chunk_idx > 0) + byte_off = (chunk_idx - 1) * bytes_per_off; + else + byte_off = 0; + + start_sector = byte_off >> 9; + sector_off = byte_off & ((1 << 9) - 1); + sectors = DIV_ROUND_UP(sector_off + 2 * bytes_per_off, 512); + + buf_aligned = kmalloc(sectors << 9, GFP_NOFS); + if (!buf_aligned) + return -ENOMEM; + if (ntfs_bdev_read_from_rl(ni->vol, &ni->runlist, + start_sector, + sectors, + buf_aligned)) { + ret = -EIO; + goto out; + } + buf = buf_aligned + sector_off; + + if (chunk_idx + 1 == chunk_count) { + if (bytes_per_off == sizeof(__le32)) + ((__le32 *)buf)[1] = + cpu_to_le32(ni->data_size - + (chunk_count - 1) * bytes_per_off); + else + ((__le64 *)buf)[1] = + cpu_to_le64(ni->data_size - + (chunk_count - 1) * bytes_per_off); + } + } else { + /* Resident WOF chunk table logic will be added in Commit 8 */ + ret = -EOPNOTSUPP; + goto out; + } + + if (bytes_per_off == sizeof(__le32)) { + __le32 *addr = (__le32 *)buf; + + off[0] = chunk_idx ? le32_to_cpu(addr[0]) : 0; + off[1] = chunk_idx ? le32_to_cpu(addr[1]) : le32_to_cpu(addr[0]); + } else { + __le64 *addr = (__le64 *)buf; + + off[0] = chunk_idx ? le64_to_cpu(addr[0]) : 0; + off[1] = chunk_idx ? le64_to_cpu(addr[1]) : le64_to_cpu(addr[0]); + } + + if (off[1] <= off[0] || off[1] > chunk_data_size) { + ret = -EINVAL; + goto out; + } + + *chunk_offset = (chunk_count - 1) * bytes_per_off + off[0]; + *chunk_size = (u32)(off[1] - off[0]); + +out: + kfree(buf_aligned); + return ret; +} + +int ntfs_read_wof_compressed_block(struct folio *folio) +{ + struct page *page = &folio->page; + 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; + loff_t i_size = i_size_read(VFS_I(ni)); + char *decomp_mem = NULL, *chunk_mem = NULL, *chunk_mem_aligned; + struct page **pages = NULL; + u32 comp_unit, pages_per_chunk, chunk_size, chunk_size_aligned, decomp_size; + u64 chunk_offset_aligned, chunk_idx, chunk_count, chunk_offset; + unsigned long index; + int i, err = 0; + const struct ntfs_codec_ops *codec; + + index = folio->index; + + /* Determine frame size and frame number */ + comp_unit = 1U << ni->itype.compressed.block_size_bits; + chunk_offset_aligned = (u64)index << PAGE_SHIFT; + chunk_offset_aligned &= ~((u64)comp_unit - 1); + chunk_idx = chunk_offset_aligned >> ni->itype.compressed.block_size_bits; + chunk_count = DIV_ROUND_UP_ULL(i_size, comp_unit); + + /* + * If the requested page is past the end of the file, there is no + * chunk to decompress. Zero the page and return success. + */ + if (chunk_idx >= chunk_count) { + folio_zero_segments(folio, 0, PAGE_SIZE, 0, 0); + SetPageUptodate(page); + unlock_page(page); + return 0; + } + + /* Select codec based on block size bits */ + switch (ni->itype.compressed.block_size_bits) { + case 12: + codec = &ntfs_xpress4k_codec_ops; + break; + case 13: + codec = &ntfs_xpress8k_codec_ops; + break; + case 14: + codec = &ntfs_xpress16k_codec_ops; + break; + case 15: + codec = &ntfs_lzx32k_codec_ops; + break; + default: + err = -EINVAL; + 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->runlist.rl) { + err = ntfs_attr_map_whole_runlist(wof_ni); + if (err) + goto out_iput; + } + + err = parse_wof_chunk_table(wof_ni, chunk_idx, chunk_count, + &chunk_offset, &chunk_size); + if (err) + goto out_iput; + + if (chunk_size > comp_unit) { + ntfs_error(vol->sb, "Compressed size (%u) > frame size (%u)", + chunk_size, comp_unit); + err = -EINVAL; + goto out_iput; + } + + if (chunk_idx + 1 == chunk_count) + decomp_size = 1 + ((i_size - 1) & (comp_unit - 1)); + else + decomp_size = comp_unit; + + /* Allocate pages for the uncompressed chunk */ + pages_per_chunk = comp_unit >> PAGE_SHIFT; + pages = kcalloc(pages_per_chunk, sizeof(struct page *), GFP_NOFS); + if (!pages) { + err = -ENOMEM; + goto out_iput; + } + + for (i = 0; i < pages_per_chunk; i++) { + unsigned long pg_index = (chunk_offset_aligned >> PAGE_SHIFT) + i; + struct page *p; + + if (pg_index == index) { + pages[i] = page; + continue; + } + p = grab_cache_page_nowait(mapping, pg_index); + if (!p) { + err = -ENOMEM; + goto out_unlock; + } + pages[i] = p; + } + + decomp_mem = vmap(pages, pages_per_chunk, VM_MAP, PAGE_KERNEL); + if (!decomp_mem) { + err = -ENOMEM; + goto out_unlock; + } + + /* Allocate buffer for compressed data */ + chunk_size_aligned = DIV_ROUND_UP_ULL(chunk_offset + chunk_size, 1 << 9) - + (chunk_offset >> 9); + chunk_size_aligned <<= 9; + chunk_mem_aligned = kvmalloc(chunk_size_aligned, GFP_NOFS); + if (!chunk_mem_aligned) { + err = -ENOMEM; + goto out_unmap; + } + + /* Read compressed data from disk */ + if (!NInoNonResident(wof_ni)) { + /* Resident WOF data read logic will be added in Commit 8 */ + err = -EOPNOTSUPP; + goto out_free; + } else { + err = ntfs_bdev_read_from_rl(vol, &wof_ni->runlist, + chunk_offset >> 9, + chunk_size_aligned >> 9, + chunk_mem_aligned); + if (err) + goto out_free; + chunk_mem = chunk_mem_aligned + (chunk_offset & ((1 << 9) - 1)); + } + + /* Decompress using codec ops with dynamic scratch */ + if (chunk_size == decomp_size) { + memcpy(decomp_mem, chunk_mem, decomp_size); + } else { + void *scratch; + + scratch = kvzalloc(codec->scratch_size(comp_unit), GFP_NOFS); + if (!scratch) { + err = -ENOMEM; + goto out_free; + } + err = codec->decompress_chunk(scratch, chunk_mem, chunk_size, + decomp_mem, decomp_size, comp_unit); + kvfree(scratch); + if (err) { + ntfs_error(vol->sb, "Decompression failed: %d", err); + err = -EINVAL; + goto out_free; + } + } + + /* Zero any partial page at end */ + if (decomp_size < comp_unit) + memset(decomp_mem + decomp_size, 0, comp_unit - decomp_size); + + /* Mark pages as uptodate */ + for (i = 0; i < pages_per_chunk; i++) { + if (pages[i]) { + SetPageUptodate(pages[i]); + flush_dcache_page(pages[i]); + } + } + +out_free: + kvfree(chunk_mem_aligned); +out_unmap: + vunmap(decomp_mem); +out_unlock: + for (i = 0; i < pages_per_chunk; i++) { + if (pages[i] && pages[i] != page) { + if (err) + ClearPageUptodate(pages[i]); + unlock_page(pages[i]); + put_page(pages[i]); + } + } + kfree(pages); +out_iput: + iput(wof_inode); +out: + if (err) + ClearPageUptodate(page); + else + SetPageUptodate(page); + unlock_page(page); + return err; +} -- 2.43.0