/* * Copyright (c) 2026, Chloe M. * Provided under the BSD-3 clause. */ #define _DEFAULT_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include /* Forward declarations */ struct idata_desc; /* Data descriptor constants */ #define RDAR_MAX_PATHLEN 32 #define RDAR_MAGIC ">RDAR!" #define RDAR_MAGIC_LEN 6 /* Data descriptor file flags */ #define DATA_ROOT (1 << 0) /* Is root data descriptor */ #define DATA_AVL (1 << 1) /* Available to consumer software; not touched by us */ /* Version conversion macros */ #define VERSION_U16(MAJOR, MINOR) \ (((MINOR) << 8) | (MAJOR)) #define VERSION_MAJOR(VERSION) \ ((VERSION) & 0xFF) #define VERSION_MINOR(VERSION) \ (((VERSION) >> 8) & 0xFF) /* Pushes idata descriptor to table */ #define IDATA_PUSH(IDATA_P) \ ++d_desc_count; \ TAILQ_INSERT_TAIL(&d_desc_table, (IDATA_P), link); /* * Software version * * XXX: We encode it as a U16 so that we may bundle it * with the file data as binary. */ #define RDAR_VERSION VERSION_U16(1, 0) /* v1.0 */ /* Fallback path if '-o' is not specified */ #define FALLBACK_OUTPUT_PATH "out.rdar" /* Globals */ static const char *input_dir = NULL; static const char *output_path = NULL; static TAILQ_HEAD(, idata_desc) d_desc_table; static size_t d_desc_count = 0; /* * The root data region (RDR) contains information about the * entire archive. * * @desc_count: Number of descriptors * @revision: Format revision */ struct root_data_region { size_t desc_count; uint16_t revision; }; /* * Represents a file that has been mapped into memory * * @data: File data * @length: Length of data */ struct file_view { char *data; size_t length; }; /* * A data descriptor holds information about a specific * file along with an offset to which it is located at * in the offset. * * @magic: Format magic * @path: File path * @off: File data offset * @flags: File flags * @length: File length in bytes */ struct data_desc { char magic[RDAR_MAGIC_LEN]; char path[RDAR_MAX_PATHLEN]; off_t off; uint16_t flags; size_t length; }; /* * Internal data descriptor, used to hold data * before being flushed to the archive * * @data: Actual data descriptor * @file: File view this descriptor references * @link: TAILQ link */ struct idata_desc { struct data_desc data; struct file_view file; TAILQ_ENTRY(idata_desc) link; }; static void help(void) { printf("usage: ./rdar <... flags>\n"); printf("[-h] Display this help menu\n"); printf("[-v] Display the program version\n"); printf("[-i] Input directory to pack\n"); printf("[-o] Output path for archive file\n"); } static void version(void) { uint16_t Major, Minor; Major = VERSION_MAJOR(RDAR_VERSION); Minor = VERSION_MINOR(RDAR_VERSION); printf("Version v%d.%d\n", Major, Minor); } /* * Destroy a file view * * @view: View to destory */ static void view_destroy(struct file_view *view) { munmap(view->data, view->length); view->data = NULL; view->length = 0; } /* * Create a mapped file view from a file path */ static int view_from_path(const char *path, struct file_view *result) { struct stat sb; int fd; if (path == NULL || result == NULL) { return -1; } fd = open(path, O_RDONLY); if (fd < 0) { printf("fatal: failed to open '%s'\n", path); perror("open"); return -1; } if (stat(path, &sb) < 0) { printf("fatal: failed to stat '%s'\n", path); perror("stat"); close(fd); return -1; } result->data = mmap( NULL, sb.st_size, PROT_READ, MAP_SHARED, fd, 0 ); if (result->data == NULL) { printf("fatal: failed to map '%s'\n", path); perror("mmap"); close(fd); return -1; } result->length = sb.st_size; close(fd); return 0; } /* * Allocate an internal data descriptor * * @path: Path of file * @is_root: If set, don't create a view */ static struct idata_desc * idata_from_file(const char *path, bool is_root) { struct idata_desc *idata_desc; struct data_desc *data_desc; size_t path_len; const char *p; path_len = strlen(path); if (path_len >= RDAR_MAX_PATHLEN - 1) { printf("fatal: path '%s' is too long!\n", path); return NULL; } idata_desc = malloc(sizeof(*idata_desc)); if (idata_desc == NULL) { printf("fatal: failed to allocate idata_desc\n"); printf("* last file processed: %s\n", path); return NULL; } if (!is_root) { if (view_from_path(path, &idata_desc->file) < 0) { free(idata_desc); return NULL; } } p = path; /* Strip leading path stuff */ while (*p == '.' && *p != '\0') ++p; while (*p == '/' && *p != '\0') ++p; /* Initialize the data descriptor */ data_desc = &idata_desc->data; memcpy(data_desc->magic, RDAR_MAGIC, RDAR_MAGIC_LEN); memcpy(data_desc->path, p, strlen(p)); data_desc->path[path_len] = '\0'; data_desc->flags = 0; data_desc->off = 0; return idata_desc; } /* * Push the root descriptor to the table. * * The root descriptor is to contain an offset into a special * %root% file entry which contains data (e.g., version, file count) * encoded within. */ static int idata_push_root(void) { struct idata_desc *root; struct data_desc *desc; root = idata_from_file("%root%", true); if (root == NULL) { return -1; } desc = &root->data; desc->length = sizeof(struct root_data_region); IDATA_PUSH(root); return 0; } static void idata_table_destroy(void) { struct idata_desc *desc, *next; desc = TAILQ_FIRST(&d_desc_table); while (desc != NULL) { view_destroy(&desc->file); next = TAILQ_NEXT(desc, link); TAILQ_REMOVE(&d_desc_table, desc, link); desc = next; } } static void flush_root_desc(struct data_desc *d_desc, int out_fd) { struct root_data_region rdr = { .desc_count = d_desc_count, .revision = RDAR_VERSION }; d_desc->off = lseek(out_fd, 0, SEEK_CUR); write(out_fd, &rdr, sizeof(rdr)); } static void idata_table_flush(void) { const struct file_view *view; struct idata_desc *desc_iter; struct data_desc *d_desc; off_t new_off; int fd; fd = open(output_path, O_WRONLY | O_TRUNC | O_CREAT, 0666); if (fd < 0) { printf("fatal: failed to open output path\n"); perror("open"); return; } new_off = d_desc_count * sizeof(struct data_desc); lseek(fd, new_off, SEEK_SET); /* First pass write the file data */ TAILQ_FOREACH(desc_iter, &d_desc_table, link) { view = &desc_iter->file; d_desc = &desc_iter->data; /* * Check if we need to flush the root descriptor, we ignore any files that start * with the '%' modifier. */ if (d_desc->path[0] == '%') { if (strcmp(d_desc->path, "%root%") == 0) flush_root_desc(d_desc, fd); continue; } d_desc->off = lseek(fd, 0, SEEK_CUR); d_desc->length = view->length; write(fd, view->data, view->length); } /* Data descriptors go at the start */ lseek(fd, 0, SEEK_SET); /* Second pass write the data descriptors */ TAILQ_FOREACH(desc_iter, &d_desc_table, link) { view = &desc_iter->file; d_desc = &desc_iter->data; write(fd, d_desc, sizeof(*d_desc)); } close(fd); } static void recurse_dir(const char *dirpath) { DIR *dir; struct dirent *dirent; struct idata_desc *idata_desc; char pathbuf[264]; dir = opendir(dirpath); if (dir == NULL) { printf("fatal: failed to open '%s'\n", input_dir); perror("opendir"); return; } while ((dirent = readdir(dir)) != NULL) { if (dirent->d_name[0] == '.') { continue; } switch (dirent->d_type) { case DT_REG: snprintf(pathbuf, sizeof(pathbuf), "%s/%s", dirpath, dirent->d_name); printf("[f] %s\n", pathbuf); idata_desc = idata_from_file(pathbuf, false); if (idata_desc == NULL) { printf("warn: ignoring '%s'\n", pathbuf); break; } IDATA_PUSH(idata_desc); break; case DT_DIR: snprintf(pathbuf, sizeof(pathbuf), "%s/%s", dirpath, dirent->d_name); printf("[d] %s\n", pathbuf); recurse_dir(pathbuf); break; } } closedir(dir); } int main(int argc, char **argv) { int opt; if (argc < 2) { printf("fatal: too few arguments\n"); help(); return -1; } while ((opt = getopt(argc, argv, "hvi:o:")) != -1) { switch (opt) { case 'h': help(); return -1; case 'v': version(); return -1; case 'i': input_dir = strdup(optarg); if (input_dir == NULL) { printf("fatal: failed to allocate input_dir\n"); return -1; } break; case 'o': output_path = strdup(optarg); if (output_path == NULL) { printf("fatal: failed to allocate output dir\n"); return -1; } break; } } if (input_dir == NULL) { printf("fatal: input path not specified\n"); help(); return -1; } if (output_path == NULL) { output_path = FALLBACK_OUTPUT_PATH; } TAILQ_INIT(&d_desc_table); idata_push_root(); recurse_dir(input_dir); idata_table_flush(); idata_table_destroy(); return 0; }