blob: c76a6cdac8ea25e6315a9ea3fd66df8ceb98cf0f [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::HashMap;
use std::convert::TryFrom;
use std::fs::File;
use std::io::Read;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use flate2::Compression;
use flate2::write::GzEncoder;
use flate2::read::GzDecoder;
use tar::{Archive, Builder, EntryType, Header};
use sdk_metadata::JsonObject;
use crate::app::{Error, Result};
type SdkArchive = Archive<GzDecoder<File>>;
/// A tarball that can be read from.
pub struct SourceTarball {
archive: SdkArchive,
}
impl SourceTarball {
/// Creates a new tarball at the given path.
pub fn new(path: String) -> Result<SourceTarball> {
let tar_gz = File::open(path)?;
let tar = GzDecoder::new(tar_gz)?;
Ok(SourceTarball { archive: Archive::new(tar) })
}
/// Reads a file from the tarball.
fn get_file<'a>(&'a mut self, path: &str) -> Result<impl Read + 'a> {
let archive = &mut self.archive;
let entries = archive.entries()?;
Ok(entries
.filter_map(|entry| { entry.ok() })
.find(|entry| {
if let Ok(entry_path) = entry.path() {
return entry_path.to_str() == Some(path)
}
false
})
.ok_or(Error::ArchiveFileNotFound { name: path.to_string() })?)
}
/// Reads a metadata object from the tarball.
pub fn get_metadata<T: JsonObject>(&mut self, path: &str) -> Result<T> {
T::new(self.get_file(path)?)
}
}
/// The types of content that can be written to a tarball.
enum TarballContent {
/// Plain string content, exported as a read-only file.
Plain(String),
}
/// A tarball that can be written into.
pub struct OutputTarball {
contents: HashMap<String, TarballContent>,
}
impl OutputTarball {
/// Creates a new tarball.
pub fn new() -> OutputTarball {
OutputTarball { contents: HashMap::new() }
}
/// Writes the given content to the given path in the tarball.
///
/// It is an error to write to the same path twice.
pub fn write(&mut self, path: String, content: String) -> Result<()> {
if let Some(_) = self.contents.insert(path.clone(), TarballContent::Plain(content)) {
return Err(Error::PathAlreadyExists { path })?;
}
Ok(())
}
/// Creates the tarball at the given path.
///
/// This method will obliterate any file that already exists at that path.
pub fn export(&self, path: String) -> Result<()> {
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());
let mut builder = Builder::new(tar_gz);
for (file_path, content) in &self.contents {
match content {
TarballContent::Plain(s) => {
let bytes = s.as_bytes();
let mut header = Header::new_gnu();
header.set_path(file_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);
builder.append_data(&mut header, file_path, bytes)?;
},
}
}
Ok(builder.finish()?)
}
}