blob: 7500c1a75f7c25b11f617376729f292e6845dd47 [file] [log] [blame]
// Copyright 2019 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 std::collections::HashSet;
use std::convert::TryFrom;
use std::fs::File;
use std::io::Read;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use flate2::read::GzDecoder;
use flate2::write::GzEncoder;
use flate2::Compression;
use tar::{Archive, Builder, EntryType, Header};
use tempfile::TempDir;
use sdk_metadata::JsonObject;
use crate::app::{Error, Result};
/// A trait for types representing tarball contents.
pub trait TarballContent {
/// Returns true if this piece of content is identical to a given piece of content.
fn is_identical(&self, other: &Self) -> Result<bool>;
}
impl TarballContent for File {
fn is_identical(&self, other: &File) -> Result<bool> {
// Compare file sizes to avoid reading the full file contents.
Ok(self.metadata()?.len() == other.metadata()?.len())
}
}
/// A tarball that can be read from.
pub trait InputTarball<F: TarballContent> {
/// Reads a file from the tarball.
fn get_file<R>(&self, path: &str, reader: R) -> Result<()>
where
R: FnOnce(&mut F) -> Result<()>;
/// Reads a metadata object from the tarball.
fn get_metadata<T: JsonObject>(&self, path: &str) -> Result<T>;
}
/// Implementation of |InputTarball| for the |File| type.
pub struct SourceTarball {
base: TempDir,
}
impl SourceTarball {
/// Opens the tarball the given path.
pub fn new(path: &str) -> Result<SourceTarball> {
let tar_gz = File::open(path)?;
let tar = GzDecoder::new(tar_gz);
let mut archive = Archive::new(tar);
let tempdir = tempfile::Builder::new().prefix("merge_sdk").tempdir()?;
archive.unpack(tempdir.path())?;
Ok(SourceTarball { base: tempdir })
}
}
impl InputTarball<File> for SourceTarball {
fn get_file<R>(&self, path: &str, reader: R) -> Result<()>
where
R: FnOnce(&mut File) -> Result<()>,
{
let mut file =
File::open(self.base.path().join(path)).or(Err(Error::ArchiveFileNotFound {
name: path.to_string(),
}))?;
// Pass the file as a reference to a callback to ensure the File object is being properly
// disposed of. This ensures the temporary base directory gets removed when the program is
// done.
reader(&mut file)
}
fn get_metadata<T: JsonObject>(&self, path: &str) -> Result<T> {
let mut contents = String::new();
self.get_file(path, |file| {
file.read_to_string(&mut contents)?;
Ok(())
})?;
T::new(contents.as_bytes())
}
}
/// A tarball that can be written into.
pub trait OutputTarball<F: TarballContent> {
/// Writes the given content to the given path in the tarball.
///
/// It is an error to write to the same path twice.
fn write_json<T: JsonObject>(&mut self, path: &str, content: &T) -> Result<()>;
/// Writes the given file to the given path in the tarball.
///
/// It is an error to write to the same path twice.
fn write_file(&mut self, path: &str, file: &mut F) -> Result<()>;
}
/// Implementation of |OutputTarball| for the |File| type.
pub struct ResultTarball {
paths: HashSet<String>,
builder: Builder<GzEncoder<File>>,
}
impl ResultTarball {
/// Creates a new tarball.
pub fn new(path: &str) -> Result<ResultTarball> {
let output_path = Path::new(&path);
if output_path.exists() {
std::fs::remove_file(output_path)?;
}
let tar = File::create(&path)?;
let tar_gz = GzEncoder::new(tar, Compression::default());
Ok(ResultTarball {
paths: HashSet::new(),
builder: Builder::new(tar_gz),
})
}
/// Registers a path in the tarball, erroring out if the path already exists.
fn add_path(&mut self, path: &str) -> Result<()> {
if self.paths.insert(path.to_owned()) {
return Ok(());
}
Err(Error::PathAlreadyExists {
path: path.to_string(),
})?
}
/// Wraps up the creation of the tarball.
pub fn export(&mut self) -> Result<()> {
Ok(self.builder.finish()?)
}
}
impl OutputTarball<File> for ResultTarball {
fn write_json<T: JsonObject>(&mut self, path: &str, content: &T) -> Result<()> {
self.add_path(path)?;
let string = content.to_string()?;
let bytes = string.as_bytes();
let mut header = Header::new_gnu();
header.set_path(path)?;
header.set_size(u64::try_from(bytes.len())?);
header.set_entry_type(EntryType::Regular);
// Make the file readable.
header.set_mode(0o444);
// Add a timestamp.
let start = SystemTime::now();
let epoch = start.duration_since(UNIX_EPOCH)?.as_secs();
header.set_mtime(epoch);
Ok(self.builder.append_data(&mut header, path, bytes)?)
}
fn write_file(&mut self, path: &str, file: &mut File) -> Result<()> {
self.add_path(path)?;
Ok(self.builder.append_file(path, file)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_packing_unpacking() {
let content_path = "some/thing.txt";
let content = "yay it worked!";
let tempdir = tempfile::Builder::new()
.prefix("test_tarballs")
.tempdir()
.unwrap();
// Write the file to add to a tarball.
let file_path = tempdir.path().join("file.txt");
let file_path_str = file_path.to_str().unwrap();
std::fs::write(&file_path_str, content).unwrap();
let tarball_path = tempdir.path().join("tarball.tar.gz");
let tarball_path_str = tarball_path.to_str().unwrap();
// Generate the tarball.
{
// The scope is needed in order for the tarball to get properly written...
let mut output = ResultTarball::new(tarball_path_str).unwrap();
let mut content_file = File::open(&file_path_str).unwrap();
output.write_file(content_path, &mut content_file).unwrap();
output.export().unwrap();
}
// Unpack the tarball.
let input = SourceTarball::new(&tarball_path_str).unwrap();
let mut read_content = String::new();
input
.get_file(content_path, |file| {
file.read_to_string(&mut read_content)?;
Ok(())
})
.unwrap();
assert_eq!(content, read_content);
}
}