rz-ar: add archive extraction utility (#6036)

This commit is contained in:
Alok Kumar Mishra 2026-03-18 01:21:18 +05:30 committed by GitHub
parent 6e016eee9c
commit 470b1b1eb7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 445 additions and 2 deletions

View file

@ -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

View file

@ -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" ;

67
binrz/man/rz-ar.1 Normal file
View file

@ -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

View file

@ -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',

10
binrz/rz-ar/meson.build Normal file
View file

@ -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,
)

11
binrz/rz-ar/rz-ar.c Normal file
View file

@ -0,0 +1,11 @@
// SPDX-FileCopyrightText: 2026 RizinOrg <info@rizin.re>
// SPDX-License-Identifier: LGPL-3.0-only
#include <rz_main.h>
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;
}

View file

@ -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",

View file

@ -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
}

View file

@ -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 },

View file

@ -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,

284
librz/main/rz-ar.c Normal file
View file

@ -0,0 +1,284 @@
// SPDX-FileCopyrightText: 2026 RizinOrg <info@rizin.re>
// SPDX-License-Identifier: LGPL-3.0-only
#include <rz_main.h>
#include <rz_cons.h>
#include <rz_util.h>
#include <rz_util/rz_print.h>
#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;
}

View file

@ -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`)

63
test/db/tools/rz_ar Normal file
View file

@ -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=<<EOF
Usage:
Show this help
List matching members
Set output directory (default .)
Quiet mode
Show version information
EOF
RUN
NAME=rz-ar list fpu.lib
TOOL=rz-ar
ARGS=-l bins/ar/fpu.lib
REGEXP_FILTER_OUT=(FpuXexpY\.obj|FpuTrunc\.obj)
EXPECT=<<EOF
FpuXexpY.obj
FpuTrunc.obj
EOF
RUN
NAME=rz-ar list ucrtd.osmode.lib
TOOL=rz-ar
ARGS=-l bins/ar/ucrtd.osmode.lib __setusermatherr_stub.obj
REGEXP_FILTER_OUT=(__setusermatherr_stub\.obj)
EXPECT=<<EOF
__setusermatherr_stub.obj
EOF
RUN
NAME=rz-ar list normalized parent path
TOOL=rz-ar
ARGS=-l bins/ar/ucrtd.osmode.lib __setusermatherr.obj
EXPECT=<<EOF
d_/os/obj/x86fre/minkernel/crts/ucrt/src/appcrt/dll/not_callable_by_os/objfre/i386/__setusermatherr.obj
EOF
RUN
NAME=rz-ar extract one member
TOOL=rz-ar
ARGS=-o .tmp/rz-ar bins/ar/fpu.lib FpuXexpY.obj
REGEXP_FILTER_OUT=(FpuXexpY\.obj)
EXPECT=<<EOF
FpuXexpY.obj
EOF
RUN
NAME=rz-ar no matching members
TOOL=rz-ar
ARGS=bins/ar/fpu.lib not_existing_member.obj
EXIT_STATUS=1
EXPECT=
RUN
NAME=rz-ar invalid archive
TOOL=rz-ar
ARGS=bins/elf/analysis/hello-linux-x86_64
EXIT_STATUS=1
EXPECT=
RUN