diff --git a/Makefile b/Makefile index df20544..ca4b3e1 100644 --- a/Makefile +++ b/Makefile @@ -26,8 +26,10 @@ override LDFLAGS += # Nothing ### On macOS, include from Homebrew package `argp-standalone` ifneq ($(OS),Windows_NT) ifeq ($(shell uname -s),Darwin) - override CFLAGS += -I/usr/local/Cellar/argp-standalone/1.3/include/ - override LDFLAGS += -L/usr/local/Cellar/argp-standalone/1.3/lib/ -largp + # Try Apple Silicon path first, fall back to Intel path + ARGP_PREFIX := $(if $(wildcard /opt/homebrew/opt/argp-standalone),/opt/homebrew/opt/argp-standalone,/usr/local/Cellar/argp-standalone/1.3) + override CFLAGS += -I$(ARGP_PREFIX)/include/ + override LDFLAGS += -L$(ARGP_PREFIX)/lib/ -largp endif endif diff --git a/include/drat/io.h b/include/drat/io.h index ee387b3..a5eeeae 100644 --- a/include/drat/io.h +++ b/include/drat/io.h @@ -11,7 +11,7 @@ char* get_fopen_error_msg(); char* get_fseeko_error_msg(); -int open_container__info_stream(); +int open_container__info_stream(FILE* info_stream); int open_container(); int close_container(); int end_of_container(); diff --git a/include/drat/print-fs-records.c b/include/drat/print-fs-records.c index 07fc5a3..be0b3d5 100644 --- a/include/drat/print-fs-records.c +++ b/include/drat/print-fs-records.c @@ -15,7 +15,7 @@ #include // drec_val_to_short_type_string() void print_fs_records(FILE* stream, j_rec_t** fs_records) { - size_t num_records = 0; + size_t num_records __attribute__((unused)) = 0; for (j_rec_t** fs_rec_cursor = fs_records; *fs_rec_cursor; fs_rec_cursor++) { num_records++; diff --git a/src/commands/modify.c b/src/commands/modify.c index 372fb70..a5b6609 100644 --- a/src/commands/modify.c +++ b/src/commands/modify.c @@ -549,7 +549,7 @@ int cmd_modify(int argc, char** argv) { char* toc_start = (char*)node->btn_data + node->btn_table_space.off; char* key_start = toc_start + node->btn_table_space.len; - char* val_end = (char*)node + globals.block_size; + char* val_end __attribute__((unused)) = (char*)node + globals.block_size; if (node->btn_flags & BTNODE_ROOT) { val_end -= sizeof(btree_info_t); } @@ -582,7 +582,7 @@ int cmd_modify(int argc, char** argv) { char* toc_start = (char*)node->btn_data + node->btn_table_space.off; char* key_start = toc_start + node->btn_table_space.len; - char* val_end = (char*)node + globals.block_size; + char* val_end __attribute__((unused)) = (char*)node + globals.block_size; if (node->btn_flags & BTNODE_ROOT) { val_end -= sizeof(btree_info_t); } diff --git a/src/commands/recover.c b/src/commands/recover.c index 13bfc90..aa56b10 100644 --- a/src/commands/recover.c +++ b/src/commands/recover.c @@ -34,6 +34,8 @@ #include #include +#include "recursive-recovery.h" + typedef struct { int64_t fsoid; // Needs to be `int64_t to work with `parse_number()` char* path; @@ -546,6 +548,55 @@ int cmd_recover(int argc, char** argv) { fprintf(info_stream, "Filesystem object has %zu records, as follows:\n", num_records); print_fs_records(info_stream, fs_records); + // Check if this is a directory - if so, use recursive recovery + bool is_directory = false; + for (j_rec_t** fs_rec_cursor = fs_records; *fs_rec_cursor; fs_rec_cursor++) { + j_rec_t* fs_rec = *fs_rec_cursor; + j_key_t* hdr = fs_rec->data; + + if ( ((hdr->obj_id_and_type & OBJ_TYPE_MASK) >> OBJ_TYPE_SHIFT) == APFS_TYPE_INODE ) { + j_inode_val_t* inode = fs_rec->data + fs_rec->key_len; + // Check if inode is a directory using POSIX mode bits (S_IFDIR = 0040000) + is_directory = (inode->mode & S_IFDIR) == S_IFDIR; + break; + } + } + + if (is_directory) { + // Use recursive recovery for directories + fprintf(info_stream, "\nDetected directory structure. Using recursive recovery...\n"); + + recovery_context_t recovery_ctx = { + .fs_omap_btree = fs_omap_btree, + .fs_root_btree = fs_root_btree, + .max_xid = globals.max_xid, + .info_stream = info_stream, + .base_output_dir = strcmp(options.output, "-") == 0 ? "." : options.output, + .skip_multilinked_inodes = options.skip_multilinked_inodes + }; + + fprintf(info_stream, "\nRecursively recovering directory contents to `%s`...\n\n", recovery_ctx.base_output_dir); + int recursive_result = recursive_recover_directory(&recovery_ctx, options.fsoid, recovery_ctx.base_output_dir); + + // Cleanup + free_j_rec_array(fs_records); + free(fs_omap_btree); + free(fs_omap); + free(apsb); + free(nx_omap_btree); + free(nx_omap); + free(nxsb); + close_container(); + + if (recursive_result != 0) { + fprintf(info_stream, "\nWarning: Some files could not be recovered.\n"); + } else { + fprintf(info_stream, "\nRecursive recovery completed successfully.\n"); + } + + return 0; + } + // Output content from all matching file extents char* buffer = malloc(globals.block_size); if (!buffer) { diff --git a/src/commands/recursive-recovery.c b/src/commands/recursive-recovery.c new file mode 100644 index 0000000..8a29d5f --- /dev/null +++ b/src/commands/recursive-recovery.c @@ -0,0 +1,232 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "recursive-recovery.h" + +/** + * Helper: Get filesystem records for a given object ID + */ +j_rec_t** get_fs_records_wrapper(recovery_context_t* context, uint64_t fsoid) { + return get_fs_records(context->fs_omap_btree, context->fs_root_btree, fsoid, context->max_xid); +} + +/** + * Recover a single file's contents to the specified output path + */ +int recover_single_file(recovery_context_t* context, uint64_t fsoid, const char* output_path) { + j_rec_t** fs_records = get_fs_records_wrapper(context, fsoid); + if (!fs_records) { + fprintf(context->info_stream, "WARNING: Could not get fs records for FSOID %#"PRIx64"\n", fsoid); + return -1; + } + + // Check if this is a multilinked inode + for (j_rec_t** rec_cursor = fs_records; *rec_cursor; rec_cursor++) { + j_rec_t* rec = *rec_cursor; + j_key_t* hdr = rec->data; + + if (((hdr->obj_id_and_type & OBJ_TYPE_MASK) >> OBJ_TYPE_SHIFT) == APFS_TYPE_INODE) { + j_inode_val_t* inode = rec->data + rec->key_len; + + if (inode->nlink > 1 && context->skip_multilinked_inodes) { + fprintf(context->info_stream, "SKIP: File has %"PRIu32" hardlinks (FSOID %#"PRIx64")\n", inode->nlink, fsoid); + free_j_rec_array(fs_records); + return 0; + } + } + } + + // Create output file + FILE* output_file = fopen(output_path, "wb"); + if (!output_file) { + fprintf(context->info_stream, "ERROR: Could not create file `%s`: %s\n", output_path, strerror(errno)); + free_j_rec_array(fs_records); + return -1; + } + + // Get file size using the proper function + uint64_t file_size = 0; + for (j_rec_t** rec_cursor = fs_records; *rec_cursor; rec_cursor++) { + j_rec_t* rec = *rec_cursor; + j_key_t* hdr = rec->data; + + if (((hdr->obj_id_and_type & OBJ_TYPE_MASK) >> OBJ_TYPE_SHIFT) == APFS_TYPE_INODE) { + j_inode_val_t* inode = rec->data + rec->key_len; + file_size = get_file_size(inode, rec->val_len); + } + } + + if (file_size == 0) { + fprintf(context->info_stream, "OK: Empty file recovered (FSOID %#"PRIx64")\n", fsoid); + fclose(output_file); + free_j_rec_array(fs_records); + return 0; + } + + // Allocate buffer for reading blocks + char* buffer = malloc(globals.block_size); + if (!buffer) { + fprintf(context->info_stream, "ERROR: Could not allocate read buffer\n"); + fclose(output_file); + free_j_rec_array(fs_records); + return -1; + } + + // Write file content from extents + uint64_t bytes_remaining = file_size; + bool found_extent = false; + + for (j_rec_t** rec_cursor = fs_records; *rec_cursor && bytes_remaining > 0; rec_cursor++) { + j_rec_t* rec = *rec_cursor; + j_key_t* hdr = rec->data; + + if (((hdr->obj_id_and_type & OBJ_TYPE_MASK) >> OBJ_TYPE_SHIFT) == APFS_TYPE_FILE_EXTENT) { + found_extent = true; + j_file_extent_val_t* val = rec->data + rec->key_len; + + uint64_t block_addr = val->phys_block_num; + uint64_t extent_len_blocks = (val->len_and_flags & J_FILE_EXTENT_LEN_MASK) / globals.block_size; + + for (uint64_t i = 0; i < extent_len_blocks && bytes_remaining > 0; i++, block_addr++) { + if (read_blocks(buffer, block_addr, 1) != 1) { + fprintf(context->info_stream, "ERROR: Failed to read block %#"PRIx64"\n", block_addr); + free(buffer); + fclose(output_file); + free_j_rec_array(fs_records); + return -1; + } + + uint64_t bytes_to_write = bytes_remaining < (uint64_t)globals.block_size ? + bytes_remaining : (uint64_t)globals.block_size; + if (fwrite(buffer, bytes_to_write, 1, output_file) != 1) { + fprintf(context->info_stream, "ERROR: Failed to write to file `%s`\n", output_path); + free(buffer); + fclose(output_file); + free_j_rec_array(fs_records); + return -1; + } + bytes_remaining -= bytes_to_write; + } + } + } + + if (!found_extent && file_size > 0) { + fprintf(context->info_stream, "WARNING: No file extents found for FSOID %#"PRIx64"\n", fsoid); + } + + // Cleanup + free(buffer); + fclose(output_file); + free_j_rec_array(fs_records); + + fprintf(context->info_stream, "OK: Recovered %"PRIu64" bytes to `%s`\n", file_size, output_path); + return 0; +} + +/** + * Recursively recover all files in a directory + */ +int recursive_recover_directory(recovery_context_t* context, uint64_t fsoid, const char* output_dir) { + j_rec_t** fs_records = get_fs_records_wrapper(context, fsoid); + if (!fs_records) { + fprintf(context->info_stream, "WARNING: Could not get fs records for directory FSOID %#"PRIx64"\n", fsoid); + return -1; + } + + // Create the output directory if it doesn't exist + if (mkdir(output_dir, 0755) != 0 && errno != EEXIST) { + fprintf(context->info_stream, "ERROR: Could not create directory `%s`: %s\n", output_dir, strerror(errno)); + free_j_rec_array(fs_records); + return -1; + } + + int recovery_result = 0; + + // Iterate through all directory entries + for (j_rec_t** rec_cursor = fs_records; *rec_cursor; rec_cursor++) { + j_rec_t* rec = *rec_cursor; + j_key_t* hdr = rec->data; + uint8_t type = (hdr->obj_id_and_type & OBJ_TYPE_MASK) >> OBJ_TYPE_SHIFT; + + if (type == APFS_TYPE_DIR_REC) { + // This is a directory entry + j_drec_hashed_key_t* dentry_key = rec->data; + j_drec_val_t* dentry_val = rec->data + rec->key_len; + + const char* entry_name = (const char*)dentry_key->name; + uint64_t entry_fsoid = dentry_val->file_id; + + // Build the output path + char entry_path[PATH_MAX]; + snprintf(entry_path, sizeof(entry_path), "%s/%s", output_dir, entry_name); + + // Get the target's records to determine if it's a directory + j_rec_t** entry_records = get_fs_records_wrapper(context, entry_fsoid); + bool is_entry_directory = false; + + if (entry_records) { + for (j_rec_t** entry_rec_cursor = entry_records; *entry_rec_cursor; entry_rec_cursor++) { + j_rec_t* entry_rec = *entry_rec_cursor; + j_key_t* entry_hdr = entry_rec->data; + uint8_t entry_type = (entry_hdr->obj_id_and_type & OBJ_TYPE_MASK) >> OBJ_TYPE_SHIFT; + + if (entry_type == APFS_TYPE_INODE) { + j_inode_val_t* entry_inode = entry_rec->data + entry_rec->key_len; + // Check if entry is a directory using POSIX mode bits (S_IFDIR = 0040000) + is_entry_directory = (entry_inode->mode & S_IFDIR) == S_IFDIR; + break; + } + } + } + + fprintf(context->info_stream, "ENTRY: %s `%s` (FSOID %#"PRIx64")\n", + is_entry_directory ? "DIR " : "FILE", + entry_name, + entry_fsoid); + + if (is_entry_directory) { + // Recursively recover the subdirectory + fprintf(context->info_stream, " -> Descending into directory\n"); + int subdir_result = recursive_recover_directory(context, entry_fsoid, entry_path); + if (subdir_result != 0) { + recovery_result = subdir_result; + // Continue processing other entries + } + } else { + // Recover the file + fprintf(context->info_stream, " -> Recovering file to `%s`\n", entry_path); + int file_result = recover_single_file(context, entry_fsoid, entry_path); + if (file_result != 0) { + recovery_result = file_result; + // Continue processing other entries + } + } + + if (entry_records) { + free_j_rec_array(entry_records); + } + } + } + + free_j_rec_array(fs_records); + return recovery_result; +} diff --git a/src/commands/recursive-recovery.h b/src/commands/recursive-recovery.h new file mode 100644 index 0000000..b592b41 --- /dev/null +++ b/src/commands/recursive-recovery.h @@ -0,0 +1,65 @@ +#ifndef DRAT_RECURSIVE_RECOVERY_H +#define DRAT_RECURSIVE_RECOVERY_H + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +typedef struct { + btree_node_phys_t* fs_omap_btree; + btree_node_phys_t* fs_root_btree; + xid_t max_xid; + FILE* info_stream; + char* base_output_dir; + bool skip_multilinked_inodes; +} recovery_context_t; + +typedef struct { + uint64_t fsoid; + char* name; + bool is_directory; + j_inode_val_t* inode; + size_t inode_size; +} fs_entry_info_t; + +/** + * Recursively recover files and directories from an APFS filesystem + * @param context: Recovery context with filesystem info + * @param fsoid: Filesystem object ID to start recovery from + * @param output_dir: Output directory path for recovered files + * @return 0 on success, negative on error + */ +int recursive_recover_directory(recovery_context_t* context, uint64_t fsoid, const char* output_dir); + +/** + * Recover a single file's contents + * @param context: Recovery context + * @param fsoid: File's filesystem object ID + * @param output_path: Full path where file should be written + * @return 0 on success, negative on error + */ +int recover_single_file(recovery_context_t* context, uint64_t fsoid, const char* output_path); + +/** + * Get filesystem records for a given object ID + * @param context: Recovery context + * @param fsoid: Filesystem object ID + * @return Array of j_rec_t pointers, NULL-terminated, or NULL on error + */ +j_rec_t** get_fs_records_wrapper(recovery_context_t* context, uint64_t fsoid); + +#endif // DRAT_RECURSIVE_RECOVERY_H diff --git a/src/commands/search.c b/src/commands/search.c index 40785a0..7aa883f 100644 --- a/src/commands/search.c +++ b/src/commands/search.c @@ -273,7 +273,7 @@ int cmd_search(int argc, char** argv) { char* toc_start = (char*)node->btn_data + node->btn_table_space.off; char* key_start = toc_start + node->btn_table_space.len; - char* val_end = (char*)node + globals.block_size; + char* val_end __attribute__((unused)) = (char*)node + globals.block_size; if (node->btn_flags & BTNODE_ROOT) { val_end -= sizeof(btree_info_t); }