rz-ar: add archive extraction utility (#6036)
This commit is contained in:
parent
6e016eee9c
commit
470b1b1eb7
13 changed files with 445 additions and 2 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
67
binrz/man/rz-ar.1
Normal 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
|
||||
|
|
@ -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
10
binrz/rz-ar/meson.build
Normal 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
11
binrz/rz-ar/rz-ar.c
Normal 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;
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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
284
librz/main/rz-ar.c
Normal 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;
|
||||
}
|
||||
|
|
@ -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
63
test/db/tools/rz_ar
Normal 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
|
||||
Loading…
Reference in a new issue