1use std::collections::HashSet;
22use std::io::Write;
23use std::path::Path;
24use std::{fs::File, io::Read, path::PathBuf};
25
26use anyhow::{Context, Result};
27use clap::{Parser, Subcommand};
28
29use composefs::{
30 dumpfile::write_dumpfile,
31 erofs::reader::erofs_to_filesystem,
32 fsverity::{FsVerityHashValue, Sha256HashValue, measure_verity_with_fallback},
33 generic_tree::{Inode, LeafContent, LeafId},
34 tree::{FileSystem, RegularFile},
35};
36
37#[derive(Parser, Debug)]
39#[command(
40 name = "composefs-info",
41 version,
42 about = "Query information from composefs images"
43)]
44struct Cli {
45 #[command(subcommand)]
47 command: Command,
48}
49
50#[derive(Subcommand, Debug)]
52enum Command {
53 Ls {
55 #[arg(long = "filter", action = clap::ArgAction::Append)]
57 filter: Vec<String>,
58 images: Vec<PathBuf>,
60 },
61
62 Dump {
64 #[arg(long = "filter", action = clap::ArgAction::Append)]
66 filter: Vec<String>,
67 images: Vec<PathBuf>,
69 },
70
71 Objects {
73 images: Vec<PathBuf>,
75 },
76
77 MissingObjects {
79 #[arg(long = "basedir", required = true)]
81 basedir: PathBuf,
82 images: Vec<PathBuf>,
84 },
85
86 MeasureFile {
88 files: Vec<PathBuf>,
90 },
91}
92
93pub fn run() -> Result<()> {
95 let cli = Cli::parse();
96 run_with_cli(cli)
97}
98
99pub fn run_from_args(extra_args: Vec<std::ffi::OsString>) -> Result<()> {
106 let mut argv = vec![std::ffi::OsString::from("composefs-info")];
107 argv.extend(extra_args);
108 let cli = Cli::parse_from(argv);
109 run_with_cli(cli)
110}
111
112fn run_with_cli(cli: Cli) -> Result<()> {
113 match &cli.command {
114 Command::Ls { filter, images } => cmd_ls(filter, images),
115 Command::Dump { filter, images } => cmd_dump(filter, images),
116 Command::Objects { images } => cmd_objects(images),
117 Command::MissingObjects { basedir, images } => cmd_missing_objects(basedir, images),
118 Command::MeasureFile { files } => cmd_measure_file(files),
119 }
120}
121
122fn print_escaped<W: Write>(out: &mut W, s: &[u8]) -> std::io::Result<()> {
124 for &c in s {
125 match c {
126 b'\\' => write!(out, "\\\\")?,
127 b'\n' => write!(out, "\\n")?,
128 b'\r' => write!(out, "\\r")?,
129 b'\t' => write!(out, "\\t")?,
130 c if !c.is_ascii_graphic() && c != b' ' => write!(out, "\\x{c:02x}")?,
132 c => out.write_all(&[c])?,
133 }
134 }
135 Ok(())
136}
137
138fn ls_print<W: Write>(
140 out: &mut W,
141 fs: &FileSystem<Sha256HashValue>,
142 dir: &composefs::tree::Directory<Sha256HashValue>,
143 path: &[u8],
144 seen_leaf_ids: &mut HashSet<LeafId>,
145 filter: Option<&[String]>,
146) -> Result<()> {
147 for (name, child) in dir.sorted_entries() {
148 let name_bytes = name.as_encoded_bytes();
149
150 if let Some(filter) = filter
151 && !filter.is_empty()
152 {
153 let name_str = name.to_string_lossy();
154 if !filter.iter().any(|f| f == name_str.as_ref()) {
155 continue;
156 }
157 }
158
159 let mut child_path = path.to_vec();
160 child_path.push(b'/');
161 child_path.extend_from_slice(name_bytes);
162
163 match child {
164 Inode::Directory(child_dir) => {
165 print_escaped(out, &child_path)?;
167 write!(out, "/\t")?;
168 writeln!(out)?;
169 ls_print(out, fs, child_dir, &child_path, seen_leaf_ids, None)?;
171 }
172 Inode::Leaf(leaf_id, _) => {
173 let leaf = fs.leaf(*leaf_id);
174
175 print_escaped(out, &child_path)?;
176
177 match &leaf.content {
178 LeafContent::Regular(regular) => {
179 let is_hardlink = !seen_leaf_ids.insert(*leaf_id);
180 if !is_hardlink && let RegularFile::External(id, _) = regular {
181 write!(out, "\t@ ")?;
182 print_escaped(out, id.to_object_pathname().as_bytes())?;
183 }
184 }
185 LeafContent::Symlink(target) => {
186 write!(out, "\t-> ")?;
187 print_escaped(out, target.as_encoded_bytes())?;
188 }
189 _ => {}
190 }
191
192 writeln!(out)?;
193 }
194 }
195 }
196 Ok(())
197}
198
199fn cmd_ls(filter: &[String], images: &[PathBuf]) -> Result<()> {
201 let stdout = std::io::stdout();
202 let mut out = stdout.lock();
203
204 for image_path in images {
205 let image_data = read_image(image_path)?;
206 let fs = erofs_to_filesystem::<Sha256HashValue>(&image_data)
207 .with_context(|| format!("Failed to parse image: {image_path:?}"))?;
208
209 let mut seen_leaf_ids = HashSet::new();
210 ls_print(
211 &mut out,
212 &fs,
213 &fs.root,
214 b"",
215 &mut seen_leaf_ids,
216 Some(filter),
217 )?;
218 }
219
220 Ok(())
221}
222
223fn cmd_dump(_filter: &[String], images: &[PathBuf]) -> Result<()> {
228 let stdout = std::io::stdout();
229 let mut out = stdout.lock();
230
231 for image_path in images {
232 let image_data = read_image(image_path)?;
233 let fs = erofs_to_filesystem::<Sha256HashValue>(&image_data)
234 .with_context(|| format!("Failed to parse image: {image_path:?}"))?;
235 write_dumpfile(&mut out, &fs)
236 .with_context(|| format!("Failed to dump image: {image_path:?}"))?;
237 }
238
239 Ok(())
240}
241
242fn collect_objects_from_fs(fs: &FileSystem<Sha256HashValue>) -> HashSet<Sha256HashValue> {
249 fs.leaves
250 .iter()
251 .filter_map(|leaf| match &leaf.content {
252 LeafContent::Regular(RegularFile::External(id, _)) => Some(id.clone()),
253 _ => None,
254 })
255 .collect()
256}
257
258fn cmd_objects(images: &[PathBuf]) -> Result<()> {
260 for image_path in images {
261 let image_data = read_image(image_path)?;
262 let fs = erofs_to_filesystem::<Sha256HashValue>(&image_data)
263 .with_context(|| format!("Failed to parse image: {image_path:?}"))?;
264
265 let mut objects: Vec<Sha256HashValue> = collect_objects_from_fs(&fs).into_iter().collect();
266 objects.sort_by_key(|id| id.to_hex());
267
268 for obj in objects {
269 println!("{}", obj.to_object_pathname());
270 }
271 }
272 Ok(())
273}
274
275fn cmd_missing_objects(basedir: &Path, images: &[PathBuf]) -> Result<()> {
277 let mut all_objects: HashSet<Sha256HashValue> = HashSet::new();
278
279 for image_path in images {
280 let image_data = read_image(image_path)?;
281 let fs = erofs_to_filesystem::<Sha256HashValue>(&image_data)
282 .with_context(|| format!("Failed to parse image: {image_path:?}"))?;
283 all_objects.extend(collect_objects_from_fs(&fs));
284 }
285
286 let mut missing: Vec<Sha256HashValue> = all_objects
287 .into_iter()
288 .filter(|obj| !basedir.join(obj.to_object_pathname()).exists())
289 .collect();
290
291 missing.sort_by_key(|a| a.to_hex());
292
293 for obj in missing {
294 println!("{}", obj.to_object_pathname());
295 }
296
297 Ok(())
298}
299
300fn cmd_measure_file(files: &[PathBuf]) -> Result<()> {
302 for path in files {
303 let file = File::open(path).with_context(|| format!("Failed to open file: {path:?}"))?;
304 let digest = measure_verity_with_fallback::<Sha256HashValue>(file)
305 .with_context(|| format!("Failed to measure verity for {path:?}"))?;
306 println!("{}", digest.to_hex());
307 }
308 Ok(())
309}
310
311fn read_image(path: &PathBuf) -> Result<Vec<u8>> {
313 let mut file = File::open(path).with_context(|| format!("Failed to open image: {path:?}"))?;
314 let mut data = Vec::new();
315 file.read_to_end(&mut data)
316 .with_context(|| format!("Failed to read image: {path:?}"))?;
317 Ok(data)
318}