blob: e5f60a35c635a7d9d6b5501fcfe19030546c3667 [file] [log] [blame]
// Copyright 2021 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::blobfs::{BlobFsReader, BlobFsReaderBuilder},
anyhow::{anyhow, Context, Result},
log::warn,
pathdiff::diff_paths,
std::{
collections::HashSet,
fs::{self, File},
io::{BufReader, Read, Seek},
path::{Path, PathBuf},
},
};
/// Interface for fetching raw bytes by file path.
pub trait ArtifactReader: Send + Sync {
/// Read the raw bytes stored in filesystem location `path`.
fn read_bytes(&mut self, path: &Path) -> Result<Vec<u8>>;
/// Get the accumulated set of filesystem locations that have been read by
/// this reader.
fn get_deps(&self) -> HashSet<PathBuf>;
}
/// Implementation of `ArtifactReader` for blobfs archive files.
pub struct BlobFsArtifactReader<RS: Read + Seek> {
blobfs_dep_path: PathBuf,
blobfs_reader: BlobFsReader<RS>,
}
impl BlobFsArtifactReader<BufReader<File>> {
/// Try to construct an artifact reader rooted at `build_path` that loads
/// blobfs from the `build_path`-relative path `blobfs_path`.
pub fn try_new<P1: AsRef<Path>, P2: AsRef<Path>>(
build_path: P1,
blobfs_path: P2,
) -> Result<Self> {
let build_path_ref = build_path.as_ref();
let blobfs_path_ref = blobfs_path.as_ref();
let build_path = match build_path_ref.canonicalize() {
Ok(path) => path,
Err(err) => {
warn!(
"Blobfs artifact reader failed to canonicalize build path: {:?}: {}",
build_path_ref,
err.to_string()
);
build_path_ref.to_path_buf()
}
};
let blobfs_path = match blobfs_path_ref.canonicalize() {
Ok(path) => path,
Err(err) => {
warn!(
"File artifact reader failed to canonicalize blobfs archive path: {:?}: {}",
blobfs_path_ref,
err.to_string()
);
blobfs_path_ref.to_path_buf()
}
};
if !blobfs_path.is_absolute() {
return Err(anyhow!("Blobfs archive path {:?} is not absolute", blobfs_path));
}
let blobfs_path_str = blobfs_path.to_str().ok_or_else(|| {
anyhow!("Blobfs archive path {:?} could not be converted to string", blobfs_path)
})?;
let blobfs_dep_path =
dep_from_absolute(&build_path, blobfs_path_str).with_context(|| {
format!(
"Blobfs archive path {:?} could not be made relative to build path {:?}",
blobfs_path, build_path
)
})?;
let blobfs_file = File::open(&blobfs_path)
.map_err(|err| anyhow!("Failed to open blobfs archive {:?}: {}", blobfs_path, err))?;
let blobfs_reader = BlobFsReaderBuilder::new()
.archive(BufReader::new(blobfs_file))
.context("Failed to prepare blobfs archive for artifact reader")?
.build()
.context("Failed to parse blobfs archive metadata for artifact reader")?;
Ok(Self { blobfs_dep_path, blobfs_reader })
}
/// Try to construct a compound artifact reader that consults multiple
/// blobfs archives (in the order specified by `blobfs_paths`) when reading
/// artifacts.
pub fn try_compound<P1: AsRef<Path>, P2: AsRef<Path>>(
build_path: P1,
blobfs_paths: &Vec<P2>,
) -> Result<CompoundArtifactReader> {
Ok(CompoundArtifactReader::new(
blobfs_paths
.into_iter()
.map(|blobfs_path| {
let reader = Self::try_new(&build_path, blobfs_path)?;
let boxed: Box<dyn ArtifactReader> = Box::new(reader);
Ok(boxed)
})
.collect::<Result<Vec<Box<dyn ArtifactReader>>>>()?,
))
}
}
// `BlobfsArtifactReader` cannot be cloned in general, but the
// `<BufReader<File>>` must be clonable for some workflows.
impl Clone for BlobFsArtifactReader<BufReader<File>> {
fn clone(&self) -> Self {
Self {
blobfs_dep_path: self.blobfs_dep_path.clone(),
blobfs_reader: self.blobfs_reader.clone(),
}
}
}
impl<RS: Read + Seek + Send + Sync> ArtifactReader for BlobFsArtifactReader<RS> {
fn read_bytes(&mut self, path: &Path) -> Result<Vec<u8>> {
self.blobfs_reader.read_blob(path).with_context(|| {
format!("Failed to read blob {:?} from blobfs via artifact reader", path)
})
}
fn get_deps(&self) -> HashSet<PathBuf> {
[self.blobfs_dep_path.clone()].into()
}
}
/// An artifact reader that consults a sequence of delegate readers, returning
/// the first non-error result, or else an error describing all error results.
/// The dependencies tracked by this implementation is the union of all
/// delegates' dependencies.
pub struct CompoundArtifactReader {
delegates: Vec<Box<dyn ArtifactReader>>,
}
impl CompoundArtifactReader {
pub fn new(delegates: Vec<Box<dyn ArtifactReader>>) -> Self {
Self { delegates }
}
}
impl ArtifactReader for CompoundArtifactReader {
fn read_bytes(&mut self, path: &Path) -> Result<Vec<u8>> {
let mut errs = vec![];
for delegate in self.delegates.iter_mut() {
match delegate.read_bytes(path) {
Ok(data) => {
return Ok(data);
}
Err(err) => {
errs.push(err);
}
}
}
let mut compound_err = anyhow!("Compound artifact read failed");
for err in errs.into_iter() {
compound_err = compound_err.context("Read failure");
for ctx in err.chain() {
compound_err = compound_err.context(ctx.to_string());
}
}
Err(compound_err)
}
fn get_deps(&self) -> HashSet<PathBuf> {
let mut deps = HashSet::new();
for delegate in self.delegates.iter() {
deps.extend(delegate.get_deps().into_iter());
}
deps
}
}
impl From<Vec<BlobFsArtifactReader<BufReader<File>>>> for CompoundArtifactReader {
fn from(readers: Vec<BlobFsArtifactReader<BufReader<File>>>) -> Self {
Self::new(
readers
.iter()
.map(|reader| Box::new(reader.clone()) as Box<dyn ArtifactReader>)
.collect(),
)
}
}
/// An `ArtifactReader` implementation that reads paths relative to a particular
/// directory.
#[derive(Clone)]
pub struct FileArtifactReader {
build_path: PathBuf,
artifact_path: PathBuf,
deps: HashSet<PathBuf>,
}
impl FileArtifactReader {
/// Construct a new artifact reader that tracks dependencies relative to
/// `build_path` and reads artifacts relative to `artifact_path`.
pub fn new(build_path: &Path, artifact_path: &Path) -> Self {
let build_path = match build_path.canonicalize() {
Ok(path) => path,
Err(err) => {
warn!(
"File artifact reader failed to canonicalize build path: {:?}: {}",
build_path,
err.to_string()
);
build_path.to_path_buf()
}
};
let artifact_path = match artifact_path.canonicalize() {
Ok(path) => path,
Err(err) => {
warn!(
"File artifact reader failed to canonicalize artifact path: {:?}: {}",
artifact_path,
err.to_string()
);
artifact_path.to_path_buf()
}
};
Self { build_path, artifact_path, deps: HashSet::new() }
}
}
impl ArtifactReader for FileArtifactReader {
fn read_bytes(&mut self, path: &Path) -> Result<Vec<u8>> {
let absolute_path_string =
absolute_from_absolute_or_artifact_relative(&self.artifact_path, path)
.context("Absolute path conversion failure during read")?;
let dep_path_string = dep_from_absolute(&self.build_path, &absolute_path_string)
.context("Dep path conversion failed during read")?;
self.deps.insert(dep_path_string);
Ok(fs::read(&absolute_path_string).map_err(|err| {
anyhow!("Artifact read failed ({}): {}", &absolute_path_string, err.to_string())
})?)
}
fn get_deps(&self) -> HashSet<PathBuf> {
self.deps.clone()
}
}
fn absolute_from_absolute_or_artifact_relative<P1: AsRef<Path>, P2: AsRef<Path>>(
artifact_path: P1,
path: P2,
) -> Result<String> {
let artifact_path_ref = artifact_path.as_ref();
let path_ref = path.as_ref();
let artifact_relative_path_buf = if path_ref.is_absolute() {
diff_paths(path_ref, &artifact_path).ok_or_else(|| {
anyhow!(
"Absolute artifact path {:?} cannot be rebased to base artifact path {:?}",
path_ref,
artifact_path_ref,
)
})?
} else {
path_ref.to_path_buf()
};
let absolute_path_buf = artifact_path_ref.join(&artifact_relative_path_buf);
let absolute_path_buf = absolute_path_buf.canonicalize().map_err(|err| {
anyhow!(
"Failed to canonicalize computed path: {:?}: {}",
absolute_path_buf,
err.to_string()
)
})?;
if absolute_path_buf.is_relative() {
return Err(anyhow!(
"Computed artifact path is relative: computed {:?} from path {:?} and artifact base path {:?}",
absolute_path_buf,
path_ref,
artifact_path_ref,
));
}
if absolute_path_buf.is_dir() {
return Err(anyhow!(
"Computed artifact path is directory: computed {:?} from path {:?} and artifact base path {:?}",
absolute_path_buf,
path_ref,
artifact_path_ref,
));
}
let absolute_path_str = absolute_path_buf.to_str();
if absolute_path_str.is_none() {
return Err(anyhow!(
"Computed absolute artifact path {:?} could not be converted to string",
absolute_path_buf
));
};
Ok(absolute_path_str.unwrap().to_string())
}
fn dep_from_absolute<P1: AsRef<Path>, P2: AsRef<Path>>(
build_path: P1,
path: P2,
) -> Result<PathBuf> {
let build_path_ref = build_path.as_ref();
let path_ref = path.as_ref();
let canonical_path_buf = path_ref.canonicalize().map_err(|err| {
anyhow!("Failed to canonicalize absolute path: {:?}: {:?}", path_ref, err.to_string())
})?;
if canonical_path_buf.is_absolute() {
diff_paths(&canonical_path_buf, &build_path).ok_or_else(|| {
anyhow!(
"Artifact path {:?} (from {:?}) cannot be formatted relative to build path {:?}",
canonical_path_buf,
path_ref,
build_path_ref,
)
})
} else {
Err(anyhow!(
"Canonicalized form of {:?} is {:?}, which is not an absolute path",
path_ref,
canonical_path_buf,
))
}
}
#[cfg(test)]
mod tests {
use {
super::{ArtifactReader, FileArtifactReader},
maplit::hashset,
std::{
fs::{create_dir, File},
io::Write,
path::Path,
},
tempfile::tempdir,
};
#[test]
fn test_basic() {
let dir = tempdir().unwrap().into_path();
let mut loader = FileArtifactReader::new(&dir, &dir);
let mut file = File::create(dir.join("foo")).unwrap();
file.write_all(b"test_data").unwrap();
file.sync_all().unwrap();
let result = loader.read_bytes(&Path::new("foo"));
assert_eq!(result.is_ok(), true);
let data = result.unwrap();
assert_eq!(data, b"test_data");
}
#[test]
fn test_deps() {
let build_path = tempdir().unwrap().into_path();
let artifact_path_buf = build_path.join("artifacts");
let artifact_path = artifact_path_buf.as_path();
create_dir(&artifact_path).unwrap();
let mut loader = FileArtifactReader::new(&build_path, artifact_path);
let mut file = File::create(&artifact_path.join("foo")).unwrap();
file.write_all(b"test_data").unwrap();
file.sync_all().unwrap();
let mut file = File::create(&artifact_path.join("bar")).unwrap();
file.write_all(b"test_data").unwrap();
file.sync_all().unwrap();
assert_eq!(loader.read_bytes(&Path::new("foo")).is_ok(), true);
assert_eq!(loader.read_bytes(&Path::new("bar")).is_ok(), true);
assert_eq!(loader.read_bytes(&Path::new("foo")).is_ok(), true);
let deps = loader.get_deps();
assert_eq!(
deps,
hashset! {"artifacts/foo".to_string().into(), "artifacts/bar".to_string().into()}
);
}
}