diff --git a/README.md b/README.md index 8e7ad1f..5a48206 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,12 @@ licensed LGPLv3+, but I have relicensed the version in this plugin to GPLv2+ for consistency with NTFS-3G's license. (Public domain portions remain public domain.) +# Known issues + +Various problems occur if the NTFS volume is mounted with `lowntfs-3g` instead +of with `ntfs-3g`. This is caused by a bug in `lowntfs-3g`, and a fix to +`lowntfs-3g` has been proposed. + # Notices The NTFS-3G system compression plugin was written by Eric Biggers, with diff --git a/configure.ac b/configure.ac index f315165..4bc52c2 100644 --- a/configure.ac +++ b/configure.ac @@ -1,4 +1,4 @@ -AC_INIT([ntfs-3g-system-compression], [0.2], [ebiggers3@gmail.com]) +AC_INIT([ntfs-3g-system-compression], [0.3], [ebiggers3@gmail.com]) AC_CONFIG_SRCDIR([src/plugin.c]) AC_CONFIG_MACRO_DIR([m4]) diff --git a/src/plugin.c b/src/plugin.c index 7400c8c..2645803 100644 --- a/src/plugin.c +++ b/src/plugin.c @@ -34,6 +34,27 @@ #include "system_compression.h" +/* + * For each open file description for a system-compressed file, we cache an + * ntfs_system_decompression_ctx for the file in the FUSE file handle. + * + * A decompression context includes a decompressor, cached data, and cached + * metadata. It does not include an open ntfs_inode for the file or an open + * ntfs_attr for the file's compressed stream. This is necessary because + * NTFS-3G is not guaranteed to keep the inode open the whole time the file is + * open. Indeed, NTFS-3G may close an inode after a read request and re-open it + * for the next one, though it does maintain an open inode cache. + * + * As a result of the decompression context caching, the results of reads from a + * system-compressed file that has been written to since being opened for + * reading are unspecified. Stale data might be returned. Currently, this + * doesn't matter because this plugin blocks writes to system-compressed files. + * (It might still be possible for adventurous users to play with the + * WofCompressedData named data stream directly.) + */ +#define DECOMPRESSION_CTX(fi) \ + ((struct ntfs_system_decompression_ctx *)(uintptr_t)((fi)->fh)) + static int compressed_getattr(ntfs_inode *ni, const REPARSE_POINT *reparse, struct stat *stbuf) { @@ -51,40 +72,38 @@ static int compressed_getattr(ntfs_inode *ni, const REPARSE_POINT *reparse, return -errno; } -static int compressed_open(ntfs_inode *ni __attribute__((unused)), - const REPARSE_POINT *reparse __attribute__((unused)), +static int compressed_open(ntfs_inode *ni, const REPARSE_POINT *reparse, struct fuse_file_info *fi) { + struct ntfs_system_decompression_ctx *dctx; + if ((fi->flags & O_ACCMODE) != O_RDONLY) return -EOPNOTSUPP; - return 0; -} - -static int compressed_release(ntfs_inode *ni __attribute__((unused)), - const REPARSE_POINT *reparse __attribute__((unused)), - struct fuse_file_info *fi __attribute__((unused))) -{ - return 0; -} - -static int compressed_read(ntfs_inode *ni, const REPARSE_POINT *reparse, - char *buf, size_t size, off_t offset, - struct fuse_file_info *fi __attribute__((unused))) -{ - struct ntfs_system_decompression_ctx *dctx; - ssize_t res; - - /* TODO: there needs to be more investigation into reusing decompression - * contexts for multiple reads. */ dctx = ntfs_open_system_decompression_ctx(ni, reparse); if (!dctx) return -errno; - res = ntfs_read_system_compressed_data(dctx, offset, size, buf); + fi->fh = (uintptr_t)dctx; + return 0; +} - ntfs_close_system_decompression_ctx(dctx); +static int compressed_release(ntfs_inode *ni __attribute__((unused)), + const REPARSE_POINT *reparse __attribute__((unused)), + struct fuse_file_info *fi) +{ + ntfs_close_system_decompression_ctx(DECOMPRESSION_CTX(fi)); + return 0; +} +static int compressed_read(ntfs_inode *ni, const REPARSE_POINT *reparse, + char *buf, size_t size, off_t offset, + struct fuse_file_info *fi) +{ + ssize_t res; + + res = ntfs_read_system_compressed_data(DECOMPRESSION_CTX(fi), ni, + offset, size, buf); if (res < 0) return -errno; return res; diff --git a/src/system_compression.c b/src/system_compression.c index 08fae47..0107126 100644 --- a/src/system_compression.c +++ b/src/system_compression.c @@ -149,12 +149,9 @@ static ntfschar compressed_stream_name[] = { /* A special marker value not used by any chunk index */ #define INVALID_CHUNK_INDEX UINT64_MAX -/* The decompression context for a system compressed file */ +/* A decompression context for a system compressed file */ struct ntfs_system_decompression_ctx { - /* The open compressed stream ("WofCompressedData") */ - ntfs_attr *na; - /* The compression format of the file */ WOF_FILE_PROVIDER_COMPRESSION_FORMAT format; @@ -313,6 +310,30 @@ static u32 get_chunk_order(WOF_FILE_PROVIDER_COMPRESSION_FORMAT format) return 0; } +/* + * Get the compressed size of a system compressed file. This is the size of its + * WofCompressedData stream. + */ +static s64 get_compressed_size(ntfs_inode *ni) +{ + ntfs_attr_search_ctx *actx; + s64 ret; + + actx = ntfs_attr_get_search_ctx(ni, NULL); + if (!actx) + return -1; + + ret = ntfs_attr_lookup(AT_DATA, compressed_stream_name, + sizeof(compressed_stream_name) / + sizeof(compressed_stream_name[0]), + CASE_SENSITIVE, 0, NULL, 0, actx); + if (!ret) + ret = ntfs_get_attribute_value_length(actx->attr); + + ntfs_attr_put_search_ctx(actx); + return ret; +} + /* * ntfs_get_system_compressed_file_size - Return the compressed size of a system * compressed file @@ -328,33 +349,16 @@ s64 ntfs_get_system_compressed_file_size(ntfs_inode *ni, const REPARSE_POINT *reparse) { WOF_FILE_PROVIDER_COMPRESSION_FORMAT format; - ntfs_attr_search_ctx *actx; - s64 ret; /* Verify this is a system compressed file. */ if (get_compression_format(ni, reparse, &format)) return -1; - /* Get the size of the WofCompressedData named data stream. */ - - actx = ntfs_attr_get_search_ctx(ni, NULL); - if (!actx) - return -1; - - ret = ntfs_attr_lookup(AT_DATA, compressed_stream_name, - sizeof(compressed_stream_name) / - sizeof(compressed_stream_name[0]), - CASE_SENSITIVE, 0, NULL, 0, actx); - if (!ret) - ret = ntfs_get_attribute_value_length(actx->attr); - - ntfs_attr_put_search_ctx(actx); - - return ret; + return get_compressed_size(ni); } /* - * ntfs_open_system_decompression_ctx - Open a system-compressed file + * ntfs_open_system_decompression_ctx - Prepare to read a system-compressed file * * @ni: The NTFS inode for the file * @reparse: (Optional) the contents of the file's reparse point attribute @@ -368,6 +372,7 @@ ntfs_open_system_decompression_ctx(ntfs_inode *ni, const REPARSE_POINT *reparse) { WOF_FILE_PROVIDER_COMPRESSION_FORMAT format; struct ntfs_system_decompression_ctx *ctx; + s64 csize; /* Get the compression format. This also validates that the file really * is a system-compressed file. */ @@ -384,12 +389,11 @@ ntfs_open_system_decompression_ctx(ntfs_inode *ni, const REPARSE_POINT *reparse) if (allocate_decompressor(ctx)) goto err_free_ctx; - /* Open the WofCompressedData stream. */ - ctx->na = ntfs_attr_open(ni, AT_DATA, compressed_stream_name, - sizeof(compressed_stream_name) / - sizeof(compressed_stream_name[0])); - if (!ctx->na) + /* Determine the compressed size of the file. */ + csize = get_compressed_size(ni); + if (csize < 0) goto err_free_decompressor; + ctx->compressed_size = csize; /* The uncompressed size of a system-compressed file is the size of its * unnamed data stream, which should be sparse so that it consumes no @@ -404,10 +408,6 @@ ntfs_open_system_decompression_ctx(ntfs_inode *ni, const REPARSE_POINT *reparse) ctx->num_chunks = (ctx->uncompressed_size + ctx->chunk_size - 1) >> ctx->chunk_order; - /* The compressed size of a system compressed file is the size of its - * WofCompressedData stream. */ - ctx->compressed_size = ctx->na->data_size; - /* Initially, no chunk offsets are cached. */ ctx->base_chunk_idx = INVALID_CHUNK_INDEX; @@ -417,14 +417,13 @@ ntfs_open_system_decompression_ctx(ntfs_inode *ni, const REPARSE_POINT *reparse) ctx->cached_chunk = ntfs_malloc(ctx->chunk_size); ctx->cached_chunk_idx = INVALID_CHUNK_INDEX; if (!ctx->temp_buffer || !ctx->cached_chunk) - goto err_close_ctx; + goto err_free_buffers; return ctx; -err_close_ctx: +err_free_buffers: free(ctx->cached_chunk); free(ctx->temp_buffer); - ntfs_attr_close(ctx->na); err_free_decompressor: free_decompressor(ctx); err_free_ctx: @@ -436,7 +435,7 @@ err: /* Retrieve the stored offset and size of a chunk stored in the compressed file * stream. */ static int get_chunk_location(struct ntfs_system_decompression_ctx *ctx, - u64 chunk_idx, + ntfs_attr *na, u64 chunk_idx, u64 *offset_ret, u32 *stored_size_ret) { size_t cache_idx; @@ -475,8 +474,7 @@ static int get_chunk_location(struct ntfs_system_decompression_ctx *ctx, num_entries_to_read++; /* Read the chunk table entries into a temporary buffer. */ - res = ntfs_attr_pread(ctx->na, - first_entry_to_read << entry_shift, + res = ntfs_attr_pread(na, first_entry_to_read << entry_shift, num_entries_to_read << entry_shift, ctx->temp_buffer); @@ -541,7 +539,7 @@ static int get_chunk_location(struct ntfs_system_decompression_ctx *ctx, /* Retrieve into @buffer the uncompressed data of chunk @chunk_idx. */ static int read_and_decompress_chunk(struct ntfs_system_decompression_ctx *ctx, - u64 chunk_idx, void *buffer) + ntfs_attr *na, u64 chunk_idx, void *buffer) { u64 offset; u32 stored_size; @@ -550,7 +548,7 @@ static int read_and_decompress_chunk(struct ntfs_system_decompression_ctx *ctx, s64 res; /* Get the location of the chunk data as stored in the file. */ - if (get_chunk_location(ctx, chunk_idx, &offset, &stored_size)) + if (get_chunk_location(ctx, na, chunk_idx, &offset, &stored_size)) return -1; /* All chunks decompress to 'chunk_size' bytes except possibly the last, @@ -578,7 +576,7 @@ static int read_and_decompress_chunk(struct ntfs_system_decompression_ctx *ctx, } /* Read the stored chunk data. */ - res = ntfs_attr_pread(ctx->na, offset, stored_size, read_buffer); + res = ntfs_attr_pread(na, offset, stored_size, read_buffer); if (res != stored_size) { if (res >= 0) errno = EINVAL; @@ -602,11 +600,12 @@ static int read_and_decompress_chunk(struct ntfs_system_decompression_ctx *ctx, /* Retrieve a pointer to the uncompressed data of the specified chunk. On * failure, return NULL and set errno. */ static const void *get_chunk_data(struct ntfs_system_decompression_ctx *ctx, - u64 chunk_idx) + ntfs_attr *na, u64 chunk_idx) { if (chunk_idx != ctx->cached_chunk_idx) { ctx->cached_chunk_idx = INVALID_CHUNK_INDEX; - if (read_and_decompress_chunk(ctx, chunk_idx, ctx->cached_chunk)) + if (read_and_decompress_chunk(ctx, na, chunk_idx, + ctx->cached_chunk)) return NULL; ctx->cached_chunk_idx = chunk_idx; } @@ -617,6 +616,7 @@ static const void *get_chunk_data(struct ntfs_system_decompression_ctx *ctx, * ntfs_read_system_compressed_data - Read data from a system-compressed file * * @ctx: The decompression context for the file + * @ni: The NTFS inode for the file * @pos: The byte offset into the uncompressed data to read from * @count: The number of bytes of uncompressed data to read * @buf: The buffer into which to read the data @@ -625,16 +625,18 @@ static const void *get_chunk_data(struct ntfs_system_decompression_ctx *ctx, * end-of-file). On complete failure, return -1 and set errno. */ ssize_t ntfs_read_system_compressed_data(struct ntfs_system_decompression_ctx *ctx, - s64 pos, size_t count, void *buf) + ntfs_inode *ni, s64 pos, size_t count, + void *buf) { u64 offset; + ntfs_attr *na; u8 *p; u8 *end_p; u64 chunk_idx; u32 offset_in_chunk; u32 chunk_size; - if (!ctx || pos < 0) { + if (!ctx || !ni || pos < 0) { errno = EINVAL; return -1; } @@ -647,6 +649,12 @@ ssize_t ntfs_read_system_compressed_data(struct ntfs_system_decompression_ctx *c if (!count) return 0; + na = ntfs_attr_open(ni, AT_DATA, compressed_stream_name, + sizeof(compressed_stream_name) / + sizeof(compressed_stream_name[0])); + if (!na) + return -1; + p = buf; end_p = p + count; chunk_idx = offset >> ctx->chunk_order; @@ -663,7 +671,7 @@ ssize_t ntfs_read_system_compressed_data(struct ntfs_system_decompression_ctx *c len_to_copy = min((size_t)(end_p - p), chunk_size - offset_in_chunk); - chunk = get_chunk_data(ctx, chunk_idx); + chunk = get_chunk_data(ctx, na, chunk_idx); if (!chunk) break; @@ -674,6 +682,8 @@ ssize_t ntfs_read_system_compressed_data(struct ntfs_system_decompression_ctx *c offset_in_chunk = 0; } while (p != end_p); + ntfs_attr_close(na); + return (p == buf) ? -1 : p - (u8 *)buf; } @@ -685,7 +695,6 @@ void ntfs_close_system_decompression_ctx(struct ntfs_system_decompression_ctx *c if (ctx) { free(ctx->cached_chunk); free(ctx->temp_buffer); - ntfs_attr_close(ctx->na); free_decompressor(ctx); free(ctx); } diff --git a/src/system_compression.h b/src/system_compression.h index 4453f23..8d4dd54 100644 --- a/src/system_compression.h +++ b/src/system_compression.h @@ -28,7 +28,7 @@ /* System compressed file access */ -struct system_decompression_ctx; +struct ntfs_system_decompression_ctx; extern s64 ntfs_get_system_compressed_file_size(ntfs_inode *ni, const REPARSE_POINT *reparse); @@ -39,7 +39,8 @@ ntfs_open_system_decompression_ctx(ntfs_inode *ni, extern ssize_t ntfs_read_system_compressed_data(struct ntfs_system_decompression_ctx *ctx, - s64 pos, size_t count, void *buf); + ntfs_inode *ni, s64 pos, size_t count, + void *buf); extern void ntfs_close_system_decompression_ctx(struct ntfs_system_decompression_ctx *ctx);