Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ override LDFLAGS += # Nothing
### On macOS, include <argp.h> 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

Expand Down
2 changes: 1 addition & 1 deletion include/drat/io.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion include/drat/print-fs-records.c
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
#include <drat/string/j.h> // 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++;
Expand Down
4 changes: 2 additions & 2 deletions src/commands/modify.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
Expand Down
51 changes: 51 additions & 0 deletions src/commands/recover.c
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
#include <drat/string/fs.h>
#include <drat/string/j.h>

#include "recursive-recovery.h"

typedef struct {
int64_t fsoid; // Needs to be `int64_t to work with `parse_number()`
char* path;
Expand Down Expand Up @@ -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) {
Expand Down
232 changes: 232 additions & 0 deletions src/commands/recursive-recovery.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include <sys/stat.h>
#include <sys/errno.h>
#include <unistd.h>

#include <apfs/object.h>
#include <apfs/fs.h>
#include <apfs/j.h>
#include <apfs/jconst.h>
#include <apfs/dstream.h>
#include <apfs/sibling.h>
#include <apfs/snap.h>

#include <drat/io.h>
#include <drat/globals.h>
#include <drat/print-fs-records.h>
#include <drat/func/btree.h>
#include <drat/func/j.h>

#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;
}
Loading