Skip to content

Commit 864b24d

Browse files
committed
feat: add list API, CLI subcommand, and WASM binding
1 parent 9bfb7df commit 864b24d

5 files changed

Lines changed: 102 additions & 0 deletions

File tree

cli/src/args.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,11 @@ pub enum CliSubcommand {
3838
#[arg(long)]
3939
delete_source: bool,
4040
},
41+
#[command(about = "List entry names in a .brarchive file")]
42+
List {
43+
path: PathBuf,
44+
/// Given a directory, list entries in all .brarchive files under __brarchive/
45+
#[arg(short, long)]
46+
recursive: bool,
47+
},
4148
}

cli/src/main.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,18 @@ fn main() {
3434
info!("Successfully encoded archive in {}!", humantime::format_duration(start_time.elapsed()));
3535
}
3636
}
37+
CliSubcommand::List { path, recursive } => {
38+
if recursive {
39+
let archive_root = path.join("__brarchive");
40+
if !archive_root.exists() {
41+
error!("No __brarchive/ directory found in \"{}\"", path.display());
42+
exit(1);
43+
}
44+
list_recursive(&archive_root, &archive_root);
45+
} else {
46+
list_single(&path);
47+
}
48+
}
3749
CliSubcommand::Decode { path, out, recursive, delete_source } => {
3850
let start_time = Instant::now();
3951

@@ -265,6 +277,38 @@ fn decode_recursive(archive_root: &Path, current: &Path, out_root: &Path, delete
265277
}
266278
}
267279

280+
fn list_single(path: &Path) {
281+
let data = fs::read(path).unwrap_or_else(|err| {
282+
error!("Failed to read \"{}\": {}", path.display(), err);
283+
exit(1);
284+
});
285+
let names = brarchive::list(&data).unwrap_or_else(|err| {
286+
error!("Failed to list \"{}\": {}", path.display(), err);
287+
exit(1);
288+
});
289+
for name in names {
290+
println!("{}", name);
291+
}
292+
}
293+
294+
fn list_recursive(archive_root: &Path, current: &Path) {
295+
let read_dir = fs::read_dir(current).unwrap_or_else(|err| {
296+
error!("Failed to read \"{}\": {}", current.display(), err);
297+
exit(1);
298+
});
299+
for entry in read_dir {
300+
let entry = entry.unwrap_or_else(|err| { error!("{}", err); exit(1); });
301+
let p = entry.path();
302+
if p.is_dir() {
303+
list_recursive(archive_root, &p);
304+
} else if p.is_file() && p.extension().and_then(OsStr::to_str) == Some("brarchive") {
305+
let relative = p.strip_prefix(archive_root).unwrap_or(&p);
306+
println!("{}:", relative.display());
307+
list_single(&p);
308+
}
309+
}
310+
}
311+
268312
fn extract_file_name(path: &Path) -> Option<PathBuf> {
269313
path.file_stem().and_then(OsStr::to_str).map(PathBuf::from)
270314
}

lib/src/lib.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,15 @@ where
9797
Ok(buf)
9898
}
9999

100+
/// List entry names in a .brarchive file without reading content.
101+
pub fn list(data: &[u8]) -> Result<Vec<String>, BrArchiveError> {
102+
let mut buf = Cursor::new(data);
103+
let header = v1::read_header(&mut buf)?;
104+
(0..header.entries)
105+
.map(|_| v1::read_entry_descriptor(&mut buf).map(|e| e.name.to_string()))
106+
.collect()
107+
}
108+
100109
/// Deserialize a .brarchive file into any collection constructible from (String, String) pairs.
101110
///
102111
/// Use a type annotation to select the output type:

lib/tests/api.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,36 @@ fn serialize_with_no_dedup_matches_serialize() {
6767
let b = brarchive::serialize_with(data, SerializeOptions { dedup: false }).unwrap();
6868
assert_eq!(a, b);
6969
}
70+
71+
#[test]
72+
fn list_returns_entry_names() {
73+
let bytes = brarchive::serialize([
74+
("a.json", "1"),
75+
("b.json", "2"),
76+
("c.json", "3"),
77+
]).unwrap();
78+
let names = brarchive::list(&bytes).unwrap();
79+
assert_eq!(names, vec!["a.json", "b.json", "c.json"]);
80+
}
81+
82+
#[test]
83+
fn list_empty_archive() {
84+
let bytes = brarchive::serialize::<Vec<(&str, &str)>, _, _>(vec![]).unwrap();
85+
let names = brarchive::list(&bytes).unwrap();
86+
assert!(names.is_empty());
87+
}
88+
89+
#[test]
90+
fn list_does_not_require_reading_content() {
91+
// list should work even when content bytes are not present (header + descriptors only).
92+
// Verify that list() and deserialize() agree on names.
93+
let data = vec![
94+
("x.json".to_string(), "hello".to_string()),
95+
("y.json".to_string(), "world".to_string()),
96+
];
97+
let bytes = brarchive::serialize(data).unwrap();
98+
let names = brarchive::list(&bytes).unwrap();
99+
let map: std::collections::BTreeMap<String, String> = brarchive::deserialize(&bytes).unwrap();
100+
let map_keys: Vec<&str> = map.keys().map(String::as_str).collect();
101+
assert_eq!(names, map_keys);
102+
}

wasm/src/lib.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@ pub fn serialize_with_options(entries: JsValue, dedup: bool) -> Result<Uint8Arra
2727
Ok(Uint8Array::from(bytes.as_slice()))
2828
}
2929

30+
/// List entry names in a .brarchive file without reading content. Returns a `string[]`.
31+
#[wasm_bindgen]
32+
pub fn list(data: &[u8]) -> Result<JsValue, JsError> {
33+
let names = brarchive::list(data)
34+
.map_err(|e| JsError::new(&e.to_string()))?;
35+
serde_wasm_bindgen::to_value(&names)
36+
.map_err(|e| JsError::new(&e.to_string()))
37+
}
38+
3039
/// Deserialize .brarchive bytes into a plain JS object `{ [key: string]: string }`.
3140
#[wasm_bindgen]
3241
pub fn deserialize(data: &[u8]) -> Result<JsValue, JsError> {

0 commit comments

Comments
 (0)