blob: 444312f36e9e2b940833801f16a1a39877a43773 [file] [log] [blame]
// Copyright 2025 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use crate::types::{
Capability, ComponentContents, ElfContents, FileInfo, FileMetadata, OtherContents,
OutputSummary, PackageContents, PackageFile, ProtocolToClientMap,
};
use anyhow::{Context, Result};
use argh::FromArgs;
use assembled_system::{AssembledSystem, Image, PackageSetMetadata, PackagesMetadata};
use camino::{Utf8Path, Utf8PathBuf};
use fidl_fuchsia_component_decl as fdecl;
use fuchsia_archive::Reader as FARReader;
use fuchsia_pkg::PackageManifest;
use fuchsia_repo::repo_client::RepoClient;
use fuchsia_repo::repository::PmRepository;
use fuchsia_url::{Hash, UnpinnedAbsolutePackageUrl};
use log::debug;
use rayon::ThreadPoolBuilder;
use rayon::prelude::*;
use serde::Deserialize;
use std::collections::{BTreeMap, HashMap};
use std::env::current_exe;
use std::fmt::Debug;
use std::fs::File;
use std::io::Read;
use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Instant;
#[derive(FromArgs)]
#[argh(subcommand, name = "process")]
/// process an out directory into a JSON representation
pub struct ProcessCommand {
/// the path to the assembly manifest file.
#[argh(option)]
assembly_manifest: Utf8PathBuf,
/// the path to the TUF repository for universe packages.
#[argh(option)]
tuf_repository: Option<Utf8PathBuf>,
/// the path to save the output json file
#[argh(option)]
out: Utf8PathBuf,
/// if set, process manifests one at a time, for debugging.
#[argh(switch)]
debug_no_parallel: bool,
}
impl ProcessCommand {
pub async fn execute(self) -> Result<()> {
let manifest = AssembledSystem::from_relative_config_path(&self.assembly_manifest)?;
if self.debug_no_parallel {
ThreadPoolBuilder::new().num_threads(1).build_global().expect("make thread pool");
}
let errors = Errors::default();
let manifest_count = AtomicUsize::new(0);
let names = Mutex::new(HashMap::new());
let content_hash_to_path = Mutex::new(HashMap::new());
let start = Instant::now();
let assembly_manifest_dir_path = self.assembly_manifest.parent().unwrap();
manifest
.images
.into_par_iter()
.flat_map_iter(|image_manifest| -> Box<dyn Iterator<Item = Utf8PathBuf>> {
// TODO(https://fxbug.dev/401590492): we must extend this with Bootfs packages too.
let packages = match image_manifest {
Image::Dtbo(_)
| Image::FVM(_)
| Image::FVMSparse(_)
| Image::FVMFastboot(_)
| Image::Fxfs(_)
| Image::QemuKernel(_)
| Image::TestRamdisk(_)
| Image::VBMeta(_)
| Image::ZBI { path: _, signed: _ } => return Box::new(std::iter::empty()),
// We skip this one, its contents are listed in the blobfs and fxfs contents.
Image::BasePackage(_) => return Box::new(std::iter::empty()),
Image::BlobFS { path: _, contents }
| Image::FxfsSparse { path: _, contents } => contents.packages,
};
let PackagesMetadata {
base: PackageSetMetadata { metadata: base_packages },
cache: PackageSetMetadata { metadata: cache_packages },
} = packages;
Box::new(base_packages.into_iter().chain(cache_packages.into_iter()).map(
|metadata| {
// This path is relative to the assembly manifest path.
absolute_path_for(&assembly_manifest_dir_path, &metadata.manifest)
},
))
})
.for_each(|manifest_path| {
manifest_count.fetch_add(1, Ordering::Relaxed);
let package_errors = errors.for_package(PackageContext::Manifest(&manifest_path));
let manifest = match PackageManifest::try_load_from(&manifest_path) {
Ok(m) => m,
Err(err) => {
package_errors.log_manifest_error(err, "loading manifest");
return;
}
};
let hash = manifest.hash();
if let Some(contents) =
process_package_manifest(manifest, package_errors, &content_hash_to_path)
{
names.lock().unwrap().insert(hash, contents);
}
});
if let Some(tuf_repository) = self.tuf_repository {
process_universe(
tuf_repository,
&errors,
&names,
&content_hash_to_path,
&manifest_count,
)
.await?;
}
let file_infos = Mutex::new(HashMap::new());
let elf_count = AtomicUsize::new(0);
let other_count = AtomicUsize::new(0);
let interner = InternEnumerator::new();
let debugdump_path = current_exe().expect("get current path").with_file_name("debugdump");
if !debugdump_path.exists() {
panic!(
"Expected to find debugdump binary adjacent to pkgstats here: {:?}",
debugdump_path
);
}
content_hash_to_path.lock().unwrap().par_iter().for_each(|(hash, path)| {
// Checks for empty paths, Path::new("").parent is always None.
if path.parent().is_none() {
debug!("Skipping, no path");
return;
}
let path = Utf8PathBuf::from(path);
let alt_path = path
.parent()
.map(|v| v.join("exe.unstripped").join(path.file_name().unwrap_or_default()));
let path = if let Some(alt_path) = alt_path {
if alt_path.is_file() { alt_path } else { path }
} else {
path
};
debug!("Found canonical path at {path}");
if !path.exists() {
eprintln!("The file '{path}' doesn't exist. Skipping.");
return;
}
let mut f = match File::open(&path) {
Ok(f) => f,
Err(err) => {
eprintln!("Failed to open {path}, skipping: {err:?}");
return;
}
};
let mut header_buf = [0u8; 4];
// Check if this looks like an ELF file, starting with 0x7F 'E' 'L' 'F'
if f.read_exact(&mut header_buf).is_ok()
&& header_buf == [0x7fu8, 0x45u8, 0x4cu8, 0x46u8]
{
// process
elf_count.fetch_add(1, Ordering::Relaxed);
debug!("Looks like ELF, dumping headers");
let mut elf_contents = ElfContents::new(path.to_string());
let proc = std::process::Command::new(&debugdump_path)
.arg(path.as_os_str())
.output()
.expect("running debugdump");
let output = serde_json::from_slice::<DebugDumpOutput>(&proc.stdout);
let files = match output {
Ok(output) => {
if output.status != *"OK" {
debug!("Dumping failed, {}", output.error);
eprintln!("Debug info error: {}", output.error);
vec![]
} else {
debug!("Dumping succeeded, found {} files", output.files.len());
output.files
}
}
Err(e) => {
eprintln!("Error parsing debugdump output: {:?}", e);
vec![]
}
};
for line in files.iter() {
elf_contents.source_file_references.insert(interner.intern(line));
}
file_infos.lock().unwrap().insert(hash.clone(), FileInfo::Elf(elf_contents));
} else {
debug!("Looks like some other kind of file");
file_infos
.lock()
.unwrap()
.insert(hash.clone(), FileInfo::Other(OtherContents { source_path: path }));
other_count.fetch_add(1, Ordering::Relaxed);
}
});
let duration = Instant::now() - start;
println!(
"Loaded in {:?}. {} manifests, {} valid, {} manifest errors, {} file errors. {} ELF / {} Other files found. Contents processed: {}",
duration,
manifest_count.load(Ordering::Relaxed),
names.lock().unwrap().len(),
errors.manifest_errors.load(Ordering::Relaxed),
errors.manifest_file_errors.load(Ordering::Relaxed),
elf_count.load(Ordering::Relaxed),
other_count.load(Ordering::Relaxed),
content_hash_to_path.lock().unwrap().len(),
);
let start = Instant::now();
let mut packages = names.lock().unwrap().drain().collect::<BTreeMap<_, _>>();
let contents = file_infos.lock().unwrap().drain().collect::<BTreeMap<_, _>>();
let files = interner
.intern_set
.lock()
.unwrap()
.drain()
.map(|(k, v)| (v, FileMetadata { source_path: k }))
.collect::<BTreeMap<_, _>>();
// Populate a Protocol->(Package, component) client mapping.
let mut protocol_to_client: ProtocolToClientMap = HashMap::new();
for (hash, package) in packages.iter_mut() {
for (component_name, component) in package.components.iter_mut() {
for Capability::Protocol(protocol) in component
.used_from_parent
.iter()
.chain(component.used_from_child.iter().map(|(c, _)| c))
{
let protocol_to_packages =
protocol_to_client.entry(protocol.clone()).or_default();
let package_to_components =
protocol_to_packages.entry(hash.clone()).or_default();
package_to_components.insert(component_name.clone());
}
}
}
let output = OutputSummary { packages, contents, files, protocol_to_client };
let mut file = std::fs::File::create(self.out)?;
serde_json::to_writer(&mut file, &output)?;
let dur = Instant::now() - start;
println!("Output JSON in {:?}", dur);
Ok(())
}
}
// Given a file at `../../some/path/file.ext` and a root directory: `/the/root/path/` returns the
// absolute path for: `/the/root/path/../../some/path/file.ext
fn absolute_path_for(root_path: &Utf8Path, relative_path: &Utf8Path) -> Utf8PathBuf {
Utf8PathBuf::try_from(root_path.join(relative_path).canonicalize().expect("path exists"))
.expect("assembly related path must be utf8")
}
enum PackageContext<'a> {
Manifest(&'a Utf8PathBuf),
Universe(&'a str),
}
impl std::fmt::Display for PackageContext<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PackageContext::Manifest(m) => write!(f, "{}", m),
PackageContext::Universe(p) => write!(f, "{}", p),
}
}
}
#[derive(Default)]
struct Errors {
manifest_errors: AtomicUsize,
manifest_file_errors: AtomicUsize,
}
impl Errors {
fn for_package<'a>(&'a self, package_context: PackageContext<'a>) -> PackageErrors<'a> {
PackageErrors { errors: &self, package_context }
}
}
struct PackageErrors<'a> {
errors: &'a Errors,
package_context: PackageContext<'a>,
}
impl PackageErrors<'_> {
fn log_no_package_url(&self) {
debug!("[{}] No package URL", self.package_context);
self.errors.manifest_errors.fetch_add(1, Ordering::Relaxed);
}
fn log_manifest_error<E>(&self, err: E, step: &str)
where
E: Debug,
{
self.errors.manifest_errors.fetch_add(1, Ordering::Relaxed);
debug!(status = "Failed", step; "");
eprintln!("[{}] Failed {}: {:?}", self.package_context, step, err);
}
fn log_manifest_file_error<E>(&self, err: E, step: &str, context: impl AsRef<str>)
where
E: Debug,
{
self.errors.manifest_file_errors.fetch_add(1, Ordering::Relaxed);
debug!(status = "Failed", step; "");
eprintln!("[{}] Failed {} for {}: {:?}", self.package_context, step, context.as_ref(), err);
}
}
fn process_package_manifest(
manifest: PackageManifest,
errors: PackageErrors<'_>,
content_hash_to_path: &Mutex<HashMap<Hash, Utf8PathBuf>>,
) -> Option<PackageContents> {
let url = match manifest.package_url() {
Err(err) => {
errors.log_manifest_error(err, "formatting URL");
return None;
}
Ok(None) => {
// Package does not have a URL, skip.
errors.log_no_package_url();
return None;
}
Ok(Some(url)) => url,
};
let mut contents = PackageContents::new(url);
debug!("Have {} blobs", manifest.blobs().len());
for blob in manifest.blobs() {
let blob_source_path = Utf8PathBuf::from(&blob.source_path);
if blob.path == "meta/" {
process_far(&blob_source_path, &mut contents, &errors);
} else {
content_hash_to_path.lock().unwrap().insert(blob.merkle, blob_source_path);
contents
.files
.push(PackageFile { name: blob.path.to_string(), hash: blob.merkle.to_string() });
}
}
Some(contents)
}
fn process_far(far_path: &Utf8PathBuf, contents: &mut PackageContents, errors: &PackageErrors<'_>) {
// Handle meta
let meta_file = match File::open(far_path) {
Ok(meta_file) => meta_file,
Err(err) => {
errors.log_manifest_file_error(err, "opening file", &far_path);
return;
}
};
let mut reader = match FARReader::new(meta_file) {
Ok(r) => r,
Err(err) => {
errors.log_manifest_file_error(err, "opening as FAR file", &far_path);
return;
}
};
let mut component_manifest_paths = vec![];
debug!("Loaded manifest, have {} entries", reader.list().len());
for entry in reader.list() {
let path = String::from_utf8_lossy(entry.path());
if path.ends_with(".cm") {
debug!("Found a component manifest, {}", path);
component_manifest_paths.push(entry.path().to_owned());
}
}
for component_manifest_path in component_manifest_paths {
let data = match reader.read_file(&component_manifest_path) {
Ok(d) => d,
Err(err) => {
errors.log_manifest_file_error(
err,
"reading component manifest",
String::from_utf8_lossy(&component_manifest_path),
);
break;
}
};
let manifest: fdecl::Component = match fidl::unpersist(&data) {
Ok(m) => m,
Err(err) => {
errors.log_manifest_file_error(
err,
"parsing component manifest",
String::from_utf8_lossy(&component_manifest_path),
);
break;
}
};
let mut component = ComponentContents::default();
for cap in manifest.uses.into_iter().flatten() {
match cap {
fdecl::Use::Protocol(p) => {
let (name, from) = match (p.source_name, p.source) {
(Some(s), Some(r)) => (s, r),
_ => continue,
};
match from {
fdecl::Ref::Parent(_) => {
component.used_from_parent.insert(Capability::Protocol(name));
}
fdecl::Ref::Child(c) => {
component.used_from_child.insert((Capability::Protocol(name), c.name));
}
// TODO(https://fxbug.dev/347290357): Handle different types of refs
e => {
debug!("Unknown use from ref: {:?}", e);
}
}
}
// TODO(https://fxbug.dev/347290357): Handle different types of entries
e => {
debug!("Unknown use entry: {:?}", e)
// Skip all else for now
}
}
}
for cap in manifest.exposes.into_iter().flatten() {
match cap {
fdecl::Expose::Protocol(p) => {
let (name, from) = match (p.source_name, p.source) {
(Some(s), Some(r)) => (s, r),
_ => continue,
};
match from {
fdecl::Ref::Self_(_) => {
component.exposed_from_self.insert(Capability::Protocol(name));
}
fdecl::Ref::Child(c) => {
component
.exposed_from_child
.insert((Capability::Protocol(name), c.name));
}
e => {
// TODO(https://fxbug.dev/347290357): Handle different types of refs
debug!("Unknown expose from ref: {:?}", e);
}
}
}
// TODO(https://fxbug.dev/347290357): Handle different types of entries
e => {
debug!("Unknown exposes entry: {:?}", e)
// Skip all else for now
}
}
}
for cap in manifest.offers.into_iter().flatten() {
if let fdecl::Offer::Protocol(p) = cap {
if let (Some(name), Some(from)) = (p.source_name, p.source) {
match from {
fdecl::Ref::Self_(_) => {
component.offered_from_self.insert(Capability::Protocol(name));
}
fdecl::Ref::Child(_) => {
// Do not handle yet
}
e => {
debug!("Unknown offer from ref: {:?}", e);
}
}
}
}
}
let path = String::from_utf8_lossy(&component_manifest_path);
let last_segment = path.rfind("/");
let name = match last_segment {
Some(i) => &path[i + 1..],
None => &path,
};
contents.components.insert(name.to_string(), component);
}
}
async fn process_universe(
repo_path: Utf8PathBuf,
errors: &Errors,
names: &Mutex<HashMap<Hash, PackageContents>>,
content_hash_to_path: &Mutex<HashMap<Hash, Utf8PathBuf>>,
manifest_count: &AtomicUsize,
) -> Result<()> {
let repo_path = repo_path
.canonicalize_utf8()
.with_context(|| format!("canonicalizing repo path {:?}", repo_path))?;
let blobs_path = repo_path.join("repository/blobs");
let repository = PmRepository::new(repo_path);
let mut repo_client = RepoClient::from_trusted_remote(Box::new(repository) as Box<_>)
.await
.with_context(|| format!("Creating repo client using default trusted root"))?;
repo_client.update().await.context("updating the repository metadata")?;
for package in repo_client.list_packages().await? {
manifest_count.fetch_add(1, Ordering::Relaxed);
let package_errors = errors.for_package(PackageContext::Universe(&package.name));
let Ok(url) = UnpinnedAbsolutePackageUrl::parse(&format!(
"fuchsia-pkg://fuchsia.com/{}",
package.name
)) else {
package_errors.log_no_package_url();
continue;
};
let mut contents = PackageContents::new(url);
let entries = repo_client.show_package(&package.name, true).await?;
if let Some(entries) = entries {
for entry in entries {
// If hash=None, it's part of the meta.far which we process on its
// own to be able to extract the CM contents.
if let Some(hash) = entry.hash {
let path = blobs_path.join(hash.to_string());
if entry.path == "meta.far" {
process_far(
&path,
&mut contents,
&errors.for_package(PackageContext::Universe(&package.name)),
);
names.lock().unwrap().insert(hash, contents);
break;
} else {
content_hash_to_path.lock().unwrap().insert(hash, path);
}
}
}
}
}
Ok(())
}
#[derive(Clone)]
struct InternEnumerator {
intern_set: Arc<Mutex<HashMap<String, u32>>>,
next_id: Arc<AtomicU32>,
}
impl InternEnumerator {
pub fn new() -> Self {
Self {
intern_set: Arc::new(Mutex::new(HashMap::new())),
next_id: Arc::new(AtomicU32::new(0)),
}
}
pub fn intern(&self, value: &str) -> u32 {
let mut set = self.intern_set.lock().unwrap();
if let Some(val) = set.get(value) {
*val
} else {
let next = self.next_id.fetch_add(1, Ordering::Relaxed);
set.insert(value.to_string(), next);
next
}
}
}
#[derive(Deserialize)]
struct DebugDumpOutput {
status: String,
error: String,
files: Vec<String>,
}