From 470b1b1eb7338e7ee8bea581f3569b20321c7849 Mon Sep 17 00:00:00 2001 From: Alok Kumar Mishra <152197161+IndAlok@users.noreply.github.com> Date: Wed, 18 Mar 2026 01:21:18 +0530 Subject: [PATCH] rz-ar: add archive extraction utility (#6036) --- README.md | 1 + binrz/blob/meson_install_links.sh | 2 +- binrz/man/rz-ar.1 | 67 +++++++ binrz/meson.build | 2 + binrz/rz-ar/meson.build | 10 ++ binrz/rz-ar/rz-ar.c | 11 ++ binrz/rz-test/load.c | 1 + librz/include/rz_main.h | 1 + librz/main/main.c | 1 + librz/main/meson.build | 2 + librz/main/rz-ar.c | 284 ++++++++++++++++++++++++++++++ test/README.md | 2 +- test/db/tools/rz_ar | 63 +++++++ 13 files changed, 445 insertions(+), 2 deletions(-) create mode 100644 binrz/man/rz-ar.1 create mode 100644 binrz/rz-ar/meson.build create mode 100644 binrz/rz-ar/rz-ar.c create mode 100644 librz/main/rz-ar.c create mode 100644 test/db/tools/rz_ar diff --git a/README.md b/README.md index 70189eb9ed..f6f8f34340 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,7 @@ Apart from the main tool `rizin`, there are also other tools tailored for specif useful for shell scripting or as separate standalone tools: - `rz-bin` - provides all kind of information about binary formats +- `rz-ar` - list and extract members from static archives (.a and .lib) - `rz-asm` - a command-line assembler and disassemblers - `rz-diff` - a tool to compare two binaries as raw data or analyzed executables - `rz-hash` - allows to calculate different hashes or even encrypt data diff --git a/binrz/blob/meson_install_links.sh b/binrz/blob/meson_install_links.sh index a1180bb232..cc47dc7c38 100644 --- a/binrz/blob/meson_install_links.sh +++ b/binrz/blob/meson_install_links.sh @@ -7,7 +7,7 @@ set -e mkdir -p "${DESTDIR}/${MESON_INSTALL_PREFIX}/bin" cd "${DESTDIR}/${MESON_INSTALL_PREFIX}/bin" -TOOLS="rz-hash rz-run rz-asm rz-bin rz-gg rz-diff rz-find rz-sign rz-ax" +TOOLS="rz-ar rz-hash rz-run rz-asm rz-bin rz-gg rz-diff rz-find rz-sign rz-ax" for TOOL in $TOOLS ; do ln -sf rizin "$TOOL" ; diff --git a/binrz/man/rz-ar.1 b/binrz/man/rz-ar.1 new file mode 100644 index 0000000000..8b1cbdd4d8 --- /dev/null +++ b/binrz/man/rz-ar.1 @@ -0,0 +1,67 @@ +.Dd March 14, 2026 +.Dt RZ_AR 1 +.Os +.Sh NAME +.Nm rz-ar +.Nd List and extract members from .a and .lib archives +.Sh SYNOPSIS +.Nm rz-ar +.Op Fl hlqv +.Op Fl o Ar outdir +.Ar archive +.Op Ar member ... +.Sh DESCRIPTION +This command is part of the Rizin project. +.Pp +.Nm rz-ar +lists or extracts archive members from +.Pa .a +and +.Pa .lib +files. +It creates missing directories in the output path automatically. +.Pp +When no member filters are provided, all members are processed. +When one or more +.Ar member +arguments are provided, only matching members are processed. +.Pp +The options are: +.Bl -tag -width Fl +.It Fl h +Show usage help message. +.It Fl l +List matching members only. +.It Fl o Ar outdir +Set extraction output directory (default: current directory). +.It Fl q +Quiet mode. +.It Fl v +Show version information. +.El +.Sh EXIT STATUS +.Bl -tag -width Ds +.It 0 +Operation completed successfully. +.It 1 +Invalid arguments, open failure, no matches, or extraction failures. +.El +.Sh EXAMPLES +List all members: +.Bd -literal -offset indent +rz-ar -l libfoo.a +.Ed +.Pp +Extract all members into current directory: +.Bd -literal -offset indent +rz-ar libfoo.a +.Ed +.Pp +Extract selected members into output directory: +.Bd -literal -offset indent +rz-ar -o out libfoo.a foo.o bar.o +.Ed +.Sh SEE ALSO +.Xr rizin 1 , +.Xr rz-bin 1 , +.Xr rz-hash 1 diff --git a/binrz/meson.build b/binrz/meson.build index 4efe66b7e6..d8e167f6c1 100644 --- a/binrz/meson.build +++ b/binrz/meson.build @@ -1,6 +1,7 @@ if get_option('blob') subdir('blob') else + subdir('rz-ar') subdir('rz-hash') subdir('rz-run') subdir('rz-asm') @@ -17,6 +18,7 @@ if get_option('enable_rz_test') endif install_man( + 'man/rz-ar.1', 'man/rz-bin.1', 'man/rizin.1', 'man/rz-diff.1', diff --git a/binrz/rz-ar/meson.build b/binrz/rz-ar/meson.build new file mode 100644 index 0000000000..91c696a5e3 --- /dev/null +++ b/binrz/rz-ar/meson.build @@ -0,0 +1,10 @@ +executable('rz-ar', 'rz-ar.c', + include_directories: [platform_inc], + dependencies: [ + rz_main_dep, + rz_util_dep, + ], + install: true, + install_rpath: rpath_exe, + implicit_include_directories: false, +) diff --git a/binrz/rz-ar/rz-ar.c b/binrz/rz-ar/rz-ar.c new file mode 100644 index 0000000000..35f17eb786 --- /dev/null +++ b/binrz/rz-ar/rz-ar.c @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2026 RizinOrg +// SPDX-License-Identifier: LGPL-3.0-only + +#include + +int MAIN_NAME(int argc, const ARGV_TYPE **argv) { + char **utf8_argv = ARGV_TYPE_TO_UTF8(argc, argv); + int ret = rz_main_rz_ar(argc, (const char **)utf8_argv); + FREE_UTF8_ARGV(argc, utf8_argv); + return ret; +} diff --git a/binrz/rz-test/load.c b/binrz/rz-test/load.c index 4a613f6e17..8327975e71 100644 --- a/binrz/rz-test/load.c +++ b/binrz/rz-test/load.c @@ -104,6 +104,7 @@ static char *read_string_val(char **nextline, const char *val, ut64 *linenum) { static const char *rz_test_tools[] = { "rizin", + "rz-ar", "rz-asm", "rz-ax", "rz-bin", diff --git a/librz/include/rz_main.h b/librz/include/rz_main.h index 35cbb1cd47..3af11879eb 100644 --- a/librz/include/rz_main.h +++ b/librz/include/rz_main.h @@ -40,6 +40,7 @@ RZ_API int rz_main_rz_find(int argc, const char **argv); RZ_API int rz_main_rz_diff(int argc, const char **argv); RZ_API int rz_main_rz_gg(int argc, const char **argv); RZ_API int rz_main_rz_sign(int argc, const char **argv); +RZ_API int rz_main_rz_ar(int argc, const char **argv); #ifdef __cplusplus } diff --git a/librz/main/main.c b/librz/main/main.c index 3aa879b85c..95cdcd5c67 100644 --- a/librz/main/main.c +++ b/librz/main/main.c @@ -16,6 +16,7 @@ typedef struct main_entry_t { static MainEntry main_prog[] = { { "rizin", rz_main_rizin }, + { "rz-ar", rz_main_rz_ar }, { "rz-asm", rz_main_rz_asm }, { "rz-ax", rz_main_rz_ax }, { "rz-bin", rz_main_rz_bin }, diff --git a/librz/main/meson.build b/librz/main/meson.build index 2e560e626f..bc14b81e68 100644 --- a/librz/main/meson.build +++ b/librz/main/meson.build @@ -1,5 +1,6 @@ rz_main_sources = [ 'main.c', + 'rz-ar.c', 'rz-bin.c', 'rizin.c', 'rz-diff.c', @@ -34,6 +35,7 @@ rz_main_deps = [ rz_sign_dep, rz_core_dep, rz_diff_dep, + dependency('rzar'), ] rz_main = library('rz_main', rz_main_sources, diff --git a/librz/main/rz-ar.c b/librz/main/rz-ar.c new file mode 100644 index 0000000000..494c498252 --- /dev/null +++ b/librz/main/rz-ar.c @@ -0,0 +1,284 @@ +// SPDX-FileCopyrightText: 2026 RizinOrg +// SPDX-License-Identifier: LGPL-3.0-only + +#include +#include +#include +#include +#include "ar.h" + +typedef struct { + const char *outdir; + bool list_only; + bool quiet; +} RzArOptions; + +#define RZ_AR_EXTRACT_CHUNK_SIZE 0x10000 + +static void rz_ar_show_help(void) { + printf("%s%s%s", Color_CYAN, "Usage: ", Color_RESET); + printf("rz-ar [-hlqv] [-o outdir] archive [member ...]\n"); + // clang-format off + const char *options[] = { + "-h", "", "Show this help", + "-l", "", "List matching members", + "-o", "dir", "Set output directory (default .)", + "-q", "", "Quiet mode", + "-v", "", "Show version information", + }; + // clang-format on + rz_print_colored_help(options, RZ_ARRAY_SIZE(options), false); +} + +static char *rz_ar_normalize_member_path(const char *name) { + rz_return_val_if_fail(name, NULL); + char *tmp = rz_file_path_local_to_unix(name); + if (!tmp) { + return NULL; + } + rz_str_replace_char(tmp, '\\', '/'); + RzList *parts = rz_str_split_duplist(tmp, "/", false); + if (!parts) { + free(tmp); + return NULL; + } + RzList *stack = rz_list_newf(free); + if (!stack) { + rz_list_free(parts); + free(tmp); + return NULL; + } + RzListIter *it; + char *part; + rz_list_foreach (parts, it, part) { + if (RZ_STR_ISEMPTY(part) || RZ_STR_EQ(part, ".")) { + continue; + } + if (RZ_STR_EQ(part, "..")) { + char *prev = rz_list_pop(stack); + free(prev); + continue; + } + char *tok = rz_str_dup(part); + if (!tok) { + rz_list_free(stack); + rz_list_free(parts); + free(tmp); + return NULL; + } + for (char *p = tok; *p; p++) { + if (*p == ':' || !IS_PRINTABLE(*p)) { + *p = '_'; + } + } + if (!rz_list_append(stack, tok)) { + free(tok); + rz_list_free(stack); + rz_list_free(parts); + free(tmp); + return NULL; + } + } + RzStrBuf sb; + rz_strbuf_init(&sb); + bool first = true; + rz_list_foreach (stack, it, part) { + if (!first) { + rz_strbuf_append(&sb, "/"); + } + rz_strbuf_append(&sb, part); + first = false; + } + char *out = NULL; + if (!rz_strbuf_is_empty(&sb)) { + out = rz_strbuf_drain_nofree(&sb); + } + rz_strbuf_fini(&sb); + rz_list_free(stack); + rz_list_free(parts); + free(tmp); + return out; +} + +static bool rz_ar_member_matches(const char *member_name, const char *normalized_name, const char **filters, int nfilters) { + rz_return_val_if_fail(member_name && normalized_name, false); + if (!nfilters || !filters) { + return true; + } + const char *member_base = rz_file_basename(member_name); + const char *normalized_base = rz_file_basename(normalized_name); + for (int i = 0; i < nfilters; i++) { + const char *f = filters[i]; + if (RZ_STR_EQ(f, member_name) || RZ_STR_EQ(f, normalized_name) || RZ_STR_EQ(f, member_base) || RZ_STR_EQ(f, normalized_base)) { + return true; + } + } + return false; +} + +static char *rz_ar_unique_output_path(const char *path) { + rz_return_val_if_fail(path, NULL); + if (!rz_file_exists(path) && !rz_file_is_directory(path)) { + return rz_str_dup(path); + } + for (int i = 1;; i++) { + char *candidate = rz_str_newf("%s.%d", path, i); + if (!candidate) { + return NULL; + } + if (!rz_file_exists(candidate) && !rz_file_is_directory(candidate)) { + return candidate; + } + free(candidate); + } +} + +static bool rz_ar_extract_member(RzArFp *member, const char *dst_path) { + rz_return_val_if_fail(member && dst_path, false); + const ut64 size = member->end - member->start; + if (size == 0) { + return rz_file_dump(dst_path, NULL, 0, false); + } + ut8 *buf = malloc(RZ_AR_EXTRACT_CHUNK_SIZE); + if (!buf) { + return false; + } + ut64 off = 0; + bool append = false; + while (off < size) { + const ut64 left = size - off; + const int want = (int)RZ_MIN(left, (ut64)RZ_AR_EXTRACT_CHUNK_SIZE); + const int got = ar_read_at(member, off, buf, want); + if (got <= 0) { + break; + } + if (!rz_file_dump(dst_path, buf, got, append)) { + off = 0; + break; + } + append = true; + off += got; + } + free(buf); + return off == size; +} + +RZ_API int rz_main_rz_ar(int argc, const char **argv) { + RzGetopt opt; + RzArOptions options = { + .outdir = ".", + .list_only = false, + .quiet = false, + }; + int c; + rz_getopt_init(&opt, argc, argv, "hlo:qv"); + while ((c = rz_getopt_next(&opt)) != -1) { + switch (c) { + case 'h': + rz_ar_show_help(); + return 0; + case 'l': + options.list_only = true; + break; + case 'o': + options.outdir = opt.arg; + break; + case 'q': + options.quiet = true; + break; + case 'v': { + RzPath *sys_path = rz_path_new(); + if (!sys_path) { + return 1; + } + int ret = rz_main_version_print(sys_path, "rz-ar"); + rz_path_free(sys_path); + return ret; + } + default: + RZ_LOG_ERROR("rz-ar: invalid option -%c\n", c); + rz_ar_show_help(); + return 1; + } + } + if (opt.ind >= argc) { + RZ_LOG_ERROR("rz-ar: missing archive path\n"); + rz_ar_show_help(); + return 1; + } + const char *archive_path = argv[opt.ind++]; + const char **filters = argc > opt.ind ? &argv[opt.ind] : NULL; + const int nfilters = argc - opt.ind; + RzList *members = ar_open_all(archive_path, O_RDONLY | O_BINARY); + if (!members) { + RZ_LOG_ERROR("rz-ar: cannot open archive '%s'\n", archive_path); + return 1; + } + if (!options.list_only && !rz_sys_mkdirp(options.outdir)) { + RZ_LOG_ERROR("rz-ar: cannot create output directory '%s'\n", options.outdir); + rz_list_free(members); + return 1; + } + int matched = 0; + int extracted = 0; + int failed = 0; + RzListIter *it; + RzArFp *member; + rz_list_foreach (members, it, member) { + char *normalized_name = rz_ar_normalize_member_path(member->name); + if (!normalized_name) { + failed++; + continue; + } + if (!rz_ar_member_matches(member->name, normalized_name, filters, nfilters)) { + free(normalized_name); + continue; + } + matched++; + if (options.list_only) { + printf("%s\n", normalized_name); + free(normalized_name); + continue; + } + char *dst = rz_file_path_join(options.outdir, normalized_name); + free(normalized_name); + if (!dst) { + failed++; + continue; + } + char *unique_dst = rz_ar_unique_output_path(dst); + free(dst); + if (!unique_dst) { + failed++; + continue; + } + char *dir = rz_file_dirname(unique_dst); + if (!dir || !rz_sys_mkdirp(dir)) { + free(dir); + free(unique_dst); + failed++; + continue; + } + free(dir); + if (!rz_ar_extract_member(member, unique_dst)) { + RZ_LOG_ERROR("rz-ar: failed to extract '%s'\n", unique_dst); + free(unique_dst); + failed++; + continue; + } + extracted++; + if (!options.quiet) { + printf("%s\n", unique_dst); + } + free(unique_dst); + } + rz_list_free(members); + if (matched == 0) { + RZ_LOG_ERROR("rz-ar: no matching members\n"); + return 1; + } + if (!options.list_only && !options.quiet) { + printf("extracted %d file(s)%s\n", extracted, failed ? " with failures" : ""); + } + return failed ? 1 : 0; +} diff --git a/test/README.md b/test/README.md index 1c047ab910..995c6faf63 100644 --- a/test/README.md +++ b/test/README.md @@ -190,7 +190,7 @@ Without the regex that filtered out the non-deterministic file path and addresse * **NAME** is the name of the test, it must be unique * **FILE** (optional when `TOOL` is set) is the file or input used for the test -* **TOOL** (optional) allows you to override the tool to test (supports only `rizin`, `rz-asm`, `rz-ax`, `rz-bin`, `rz-diff`, `rz-find`, `rz-gg`, `rz-hash`, `rz-run`, `rz-sign`, `rz-test`) +* **TOOL** (optional) allows you to override the tool to test (supports only `rizin`, `rz-ar`, `rz-asm`, `rz-ax`, `rz-bin`, `rz-diff`, `rz-find`, `rz-gg`, `rz-hash`, `rz-run`, `rz-sign`, `rz-test`) * **ARGS** (optional, unless `TOOL` is set) are the command line argument passed to rizin (e.g -b 16) * **CMDS** are the commands to be executed by the test * **ENVS** (optional) allows to set a custom environment variable (example `FOO=bar`) diff --git a/test/db/tools/rz_ar b/test/db/tools/rz_ar new file mode 100644 index 0000000000..d14b05e9cf --- /dev/null +++ b/test/db/tools/rz_ar @@ -0,0 +1,63 @@ +NAME=rz-ar -h +TOOL=rz-ar +ARGS=-h +REGEXP_FILTER_OUT=(Usage:|Show this help|List matching members|Set output directory \(default \.\)|Quiet mode|Show version information) +EXPECT=<