Skip to content

Commit

Permalink
Support HFS+ compressed files
Browse files Browse the repository at this point in the history
hfsfuse will now transparently present these as regular files and
decompress them when read as the native driver does.

zlib and lzvn/lzfse compression is supported if the relevant libraries
are available. Otherwise files using these compression types will
continue to display as 0-size entries as they did previously, although
their com.apple.decmpfs extended attribute may still be inspected
directly.

hfsdump will also transparently decompress these for reading.
its stat command makes clear when this is the case by printing the
decmpfs type and logical_size separately and otherwise retaining the
display of the file's record structure as-is.
  • Loading branch information
0x09 committed Feb 4, 2024
1 parent 9ec2e0a commit b7a838c
Show file tree
Hide file tree
Showing 9 changed files with 492 additions and 24 deletions.
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ $(eval $(call cccheck,HAVE_STAT_BLOCKS,{ (struct stat){0}.st_blocks; },sys/stat.
$(eval $(call cccheck,HAVE_VSYSLOG,{ vsyslog(0,(const char*){0},(va_list){0}); },syslog.h stdarg.h))
$(eval $(call cccheck,HAVE_PREAD,{ pread(0,(void*){0},0,0); },unistd.h))

$(eval $(call cccheck,HAVE_LZFSE,,lzfse.h))
$(eval $(call cccheck,HAVE_ZLIB,,zlib.h))

$(foreach cfg,OS CC AR RANLIB INSTALL TAR PREFIX WITH_UBLIO WITH_UTF8PROC CONFIG_CFLAGS $(FEATURES),$(eval CONFIG:=$(CONFIG)$(cfg)=$$($(cfg))\n))
$(foreach feature,$(FEATURES),$(if $(filter $($(feature)),1),$(eval CFLAGS+=-D$(feature))))

Expand Down Expand Up @@ -158,6 +161,9 @@ $(error Invalid option "$(WITH_UTF8PROC)" for WITH_UTF8PROC. Use one of: none, s
endif
endif

APP_LIB+=$(if $(filter $(HAVE_ZLIB),1),-lz)
APP_LIB+=$(if $(filter $(HAVE_LZFSE),1),-llzfse)

RELEASE_NAME=hfsfuse
GIT_HEAD_SHA=$(shell git rev-parse --short HEAD 2> /dev/null)
ifneq ($(GIT_HEAD_SHA), )
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ This driver is read-only and cannot write to or alter the target filesystem.
* Resource fork, Finder info, and creation/backup time access via extended attributes
* birthtime (with compatible FUSE)
* User-defined extended attributes
* HFS+ compression with zlib and lzfse

**Not supported**

Expand All @@ -39,8 +40,9 @@ hfsfuse optionally uses these additional libraries to enable certain functionali

* [utf8proc](http://julialang.org/utf8proc/) for working with non-ASCII pathnames
* [ublio](https://www.freshports.org/devel/libublio/) for read caching, which may improve performance
* [zlib](https://www.zlib.net) and [lzfse](https://github.com/lzfse/lzfse) for reading files with HFS+ compression

These are both bundled with hfsfuse and built by default. hfsfuse can be configured to use already-installed versions of these if available, or may be built without them entirely if the respective functionality is not needed (see [Configuring](#Configuring)).
utf8proc and ublio are both bundled with hfsfuse and built by default. hfsfuse can be configured to use already-installed versions of these if available, or may be built without them entirely if the respective functionality is not needed (see [Configuring](#Configuring)).

## Configuring
hfsfuse is configured by passing options directly to `make`, and separate configure and build steps are not needed. For repeated builds using the same options, or to more easily view and edit config values, `make config` can optionally be used to generate a config.mak file which will be used by future invocations.
Expand Down
333 changes: 333 additions & 0 deletions lib/libhfsuser/decmpfs.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,333 @@
/*
* libhfsuser - Userspace support library for NetBSD's libhfs
* Copyright 2013-2017 0x09.net.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

#include "byteorder.h"
#include "features.h"
#include "hfsuser.h"

#include <inttypes.h>
#include <errno.h>

// note: these values are scaled down from the full decmpfs type to account for inline/rsrc variants,
// e.g. a decmpfs_compression value of 2 corresponds to decmpfs types 3 and 4 (zlib compressed, inline or resource fork data respectively)
// see the decmpfs_compression(type) macro below
enum decmpfs_compression {
DECMPFS_COMPRESSION_ZLIB = 2,
DECMPFS_COMPRESSION_SPARSE = 3,
DECMPFS_COMPRESSION_LZVN = 4,
DECMPFS_COMPRESSION_LZFSE = 6,
};

#define decmpfs_compression(type) (((type)+1)/2)

#define decmpfs_compression_zlib(type) (decmpfs_compression(type) == DECMPFS_COMPRESSION_ZLIB)
#define decmpfs_compression_lzvn(type) (decmpfs_compression(type) == DECMPFS_COMPRESSION_LZVN)
#define decmpfs_compression_lzfse(type) (decmpfs_compression(type) == DECMPFS_COMPRESSION_LZFSE)
// liblzfse handles both lzvn and lzfse
#define decmpfs_compression_lzx(type) (decmpfs_compression_lzvn(type) || decmpfs_compression_lzfse(type))

#define decmpfs_storage_inline(type) ((type)%2)

struct hfs_decmpfs_context {
struct hfs_decmpfs_header header;
unsigned char* buf;
size_t buflen;
uint32_t (*chunk_map)[2];
uint32_t nchunks;
// buffered for small reads;
uint16_t current_chunk;
size_t current_chunk_len;
hfs_extent_descriptor_t* extents;
uint16_t nextents;
};

bool hfs_decmpfs_compression_supported(uint8_t type) {
switch(decmpfs_compression(type)) {
case DECMPFS_COMPRESSION_ZLIB:
#if HAVE_ZLIB
return true;
#endif
return false;
case DECMPFS_COMPRESSION_SPARSE:
return decmpfs_storage_inline(type);
case DECMPFS_COMPRESSION_LZVN:
case DECMPFS_COMPRESSION_LZFSE:
#if HAVE_LZFSE
return true;
#endif
return false;
}
return false;
}

bool hfs_decmpfs_get_header(struct hfs_decmpfs_context* ctx, struct hfs_decmpfs_header* h) {
if(!(ctx && h))
return false;
*h = ctx->header;
return true;
}

bool hfs_decmpfs_parse_record(struct hfs_decmpfs_header* h, uint32_t length, unsigned char* data) {
if(!data || length < 16 || memcmp(data,"fpmc",4))
return false;
h->type = data[4];
memcpy(&h->logical_size,data+8,8);
return true;
}

int hfs_decmpfs_decompress(uint8_t type, unsigned char* decompressed_buf, size_t decompressed_buf_len, unsigned char* compressed_buf, size_t compressed_buf_len, size_t* bytes_read, void* scratch_buffer) {
if((decmpfs_compression_zlib(type) && compressed_buf[0] == 0xFF) ||
(decmpfs_compression_lzx(type) && compressed_buf[0] == 0x06)) {
*bytes_read = compressed_buf_len-1;
memcpy(decompressed_buf,compressed_buf+1,*bytes_read);
return 0;
}

#if HAVE_ZLIB
if(decmpfs_compression_zlib(type)) {
unsigned long bytes_decoded = decompressed_buf_len;
int inflate_ret = uncompress(decompressed_buf, &bytes_decoded, compressed_buf, compressed_buf_len);
*bytes_read = bytes_decoded;
return inflate_ret;
}
#endif

#if HAVE_LZFSE
if(decmpfs_compression_lzx(type)) {
*bytes_read = lzfse_decode_buffer(decompressed_buf, decompressed_buf_len, compressed_buf, compressed_buf_len,scratch_buffer);
return 0;
}
#endif

hfslib_error("invalid decmpfs type %" PRIu8 "\n",NULL,0,type);
return 1;
}

struct hfs_decmpfs_context* hfs_decmpfs_create_context(hfs_volume* vol, hfs_cnid_t cnid, uint32_t length, unsigned char* data) {
struct hfs_decmpfs_header h;
if(!hfs_decmpfs_parse_record(&h,length,data))
return NULL;

uint8_t compression_type = decmpfs_compression(h.type);
// only reject entirely unknown types, allow unsupported ones since these may contain uncompressed data
switch(compression_type) {
case DECMPFS_COMPRESSION_ZLIB:
case DECMPFS_COMPRESSION_SPARSE:
case DECMPFS_COMPRESSION_LZVN:
case DECMPFS_COMPRESSION_LZFSE:
break;
default: return NULL;
}

struct hfs_decmpfs_context* ctx = malloc(sizeof(*ctx));
if(!ctx)
return NULL;
ctx->header = h;
ctx->buf = NULL;
ctx->buflen = 0;
ctx->chunk_map = NULL;
ctx->nchunks = 0;
ctx->current_chunk = 0;
ctx->current_chunk_len = 0;
ctx->extents = NULL;
ctx->nextents = 0;

if(compression_type == DECMPFS_COMPRESSION_SPARSE) {
if(!decmpfs_storage_inline(ctx->header.type))
goto err;
}
else if(decmpfs_storage_inline(ctx->header.type)) {
ctx->buflen = ctx->header.logical_size;
if(!(ctx->buf = malloc(ctx->buflen)) ||
hfs_decmpfs_decompress(ctx->header.type, ctx->buf, ctx->buflen, data+16, length-16, &ctx->buflen, NULL))
goto err;
}
else {
// resource fork
if(!(ctx->nextents = hfslib_get_file_extents(vol,cnid,HFS_RSRCFORK,&ctx->extents,NULL)))
goto err;
uint64_t bytes;
uint32_t rsrc_start; // usually 256
if(hfslib_readd_with_extents(vol,&rsrc_start,&bytes,4,0,ctx->extents,ctx->nextents,NULL) || bytes < 4)
goto err;
rsrc_start = be32toh(rsrc_start);

if(hfslib_readd_with_extents(vol,&ctx->nchunks,&bytes,4,rsrc_start+4,ctx->extents,ctx->nextents,NULL) || bytes < 4)
goto err;
if(!(ctx->chunk_map = malloc(sizeof(*ctx->chunk_map)*ctx->nchunks)))
goto err;
if(hfslib_readd_with_extents(vol,ctx->chunk_map,&bytes,ctx->nchunks*sizeof(*ctx->chunk_map),rsrc_start+8,ctx->extents,ctx->nextents,NULL) || bytes < ctx->nchunks*sizeof(*ctx->chunk_map))
goto err;
// adjust offets to be relative to start block
for(size_t i = 0; i < ctx->nchunks; i++)
ctx->chunk_map[i][0] += rsrc_start+4;
}

return ctx;

err:
hfs_decmpfs_destroy_context(ctx);
return NULL;
}

void hfs_decmpfs_destroy_context(struct hfs_decmpfs_context* ctx) {
if(!ctx)
return;
free(ctx->extents);
free(ctx->chunk_map);
free(ctx->buf);
free(ctx);
}

static const size_t CHUNK_SIZE = 65536;

static int decmpfs_read_rsrc(hfs_volume* vol, struct hfs_decmpfs_context* ctx, char* buf, size_t size, off_t offset) {
int ret = 0;
if((uint64_t)offset > ctx->header.logical_size)
return 0;

size = min(size,ctx->header.logical_size-offset);

size_t bytes_written = 0;

size_t chunk_start = offset/CHUNK_SIZE,
chunk_end = chunk_start + size/CHUNK_SIZE + (offset%CHUNK_SIZE + size%CHUNK_SIZE + (CHUNK_SIZE-1))/CHUNK_SIZE; // (offset+size+65535)/65536 with no overflow
chunk_start = min(chunk_start,ctx->nchunks);
chunk_end = min(chunk_end,ctx->nchunks);

size_t decompressed_buf_len = min(ctx->header.logical_size,CHUNK_SIZE);

unsigned char* compressed_buf = NULL;

for(size_t i = chunk_start; i < chunk_end && bytes_written < size; i++) {
uint32_t chunk_len = ctx->chunk_map[i][1],
chunk_offset = ctx->chunk_map[i][0];

size_t bytes_read = 0;
if(!ctx->buf || ctx->current_chunk != i) {
if(!ctx->buf) {
if(!(ctx->buf = malloc(decompressed_buf_len)))
return -ENOMEM;
ctx->buflen = decompressed_buf_len;
}

if(!compressed_buf) {
uint32_t max_chunk_len = ctx->chunk_map[i][1];
for(size_t j = chunk_start+1; j < chunk_end; j++)
if(max_chunk_len < ctx->chunk_map[j][1])
max_chunk_len = ctx->chunk_map[j][1];
if(!(compressed_buf = malloc(max_chunk_len)))
return -ENOMEM;
}

uint64_t compressed_bytes_read;
hfslib_readd_with_extents(vol,compressed_buf,&compressed_bytes_read,chunk_len,chunk_offset,ctx->extents,ctx->nextents,NULL);
if((ret = hfs_decmpfs_decompress(ctx->header.type, ctx->buf, ctx->buflen, compressed_buf, compressed_bytes_read, &bytes_read, NULL)))
break;
ctx->current_chunk = i;
ctx->current_chunk_len = bytes_read;
}
else bytes_read = ctx->current_chunk_len;

size_t decode_offset = i > chunk_start ? 0 : offset%CHUNK_SIZE;
if(decode_offset < bytes_read) {
size_t writesize = min(bytes_read-decode_offset,size-bytes_written);
memcpy(buf+bytes_written,ctx->buf+decode_offset,writesize);
bytes_written += writesize;
}
}
free(compressed_buf);

if(ret < 0)
return ret;
return bytes_written;
}

int hfs_decmpfs_read(hfs_volume* vol, struct hfs_decmpfs_context* ctx, char* buf, size_t size, off_t offset) {
if(offset < 0)
return -EINVAL;

if(decmpfs_compression(ctx->header.type) == DECMPFS_COMPRESSION_SPARSE) {
if((uint64_t)offset >= ctx->header.logical_size)
return 0;
size_t bytes = min(size,ctx->header.logical_size-offset);
memset(buf,0,bytes);
return bytes;
}

if(!decmpfs_storage_inline(ctx->header.type))
return decmpfs_read_rsrc(vol,ctx,buf,size,offset);

if(ctx->buf && (uint64_t)offset < ctx->buflen) {
size_t bytes = min(size,ctx->buflen-offset);
memcpy(buf,ctx->buf+offset,bytes);
return bytes;
}

return 0;
}

size_t hfs_decmpfs_buffer_size(struct hfs_decmpfs_header* h) {
if(!h)
return 0;
return decmpfs_storage_inline(h->type) ? h->logical_size : min(h->logical_size,CHUNK_SIZE);
}

int hfs_decmpfs_lookup(hfs_volume* vol, hfs_file_record_t* file, struct hfs_decmpfs_header* h, uint32_t* length, unsigned char** data) {
if(data)
*data = NULL;
if(length)
*length = 0;

if(file->data_fork.logical_size)
return 1;

hfs_attribute_key_t attrkey;
hfslib_make_attribute_key(file->cnid,0,strlen("com.apple.decmpfs"),u"com.apple.decmpfs",&attrkey);
hfs_attribute_record_t attr;
unsigned char* buf = NULL;
if(hfslib_find_attribute_record_with_key(vol,&attrkey,&attr,(void*)&buf,NULL))
return 1;

// if this is a zlib or lzfse compressed file and hfsfuse wasn't built with these libraries, continue to treat it as a zero-length file
// the com.apple.decmpfs xattr may still be inspected directly to access the compressed data
if(attr.type != HFS_ATTR_INLINE_DATA || !hfs_decmpfs_parse_record(h,attr.inline_record.length,buf)) {
free(buf);
return -EINVAL;
}
if(!hfs_decmpfs_compression_supported(h->type)) {
free(buf);
hfslib_error("unsupported decmpfs type %" PRIu8 " for cnid %" PRIu32 "\n",NULL,0,h->type,file->cnid);
return -ENOTSUP;
}

if(length)
*length = attr.inline_record.length;
if(data)
*data = buf;
else
free(buf);

return 0;
}
14 changes: 14 additions & 0 deletions lib/libhfsuser/features.c
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ enum hfs_lib_features hfs_get_lib_features(void) {
#endif
#ifdef HAVE_UTF8PROC
| HFS_LIB_FEATURES_UTF8PROC
#endif
#if HAVE_ZLIB
| HFS_LIB_FEATURES_ZLIB
#endif
#if HAVE_LZFSE
| HFS_LIB_FEATURES_LZFSE
#endif
;
}
Expand All @@ -57,3 +63,11 @@ const char* hfs_lib_utf8proc_version(void) {
return NULL;
#endif
}

const char* hfs_lib_zlib_version(void) {
#if HAVE_ZLIB
return ZLIB_VERSION;
#else
return NULL;
#endif
}
Loading

0 comments on commit b7a838c

Please sign in to comment.