summaryrefslogtreecommitdiff
path: root/src/archive.rs
blob: 00763640f1df4abde2b1fead4d1c86028774aff8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
use std::fs::{self, File};
use std::path::Path;

use anyhow::{bail, Context, Result};
use tar::{Builder, EntryType, Header};

/// Writes a deterministic rootfs tar archive using numeric ownership metadata.
///
/// Extended ACLs and xattrs are intentionally not represented by this initial
/// portable archive format. Callers that require them must use a future archive
/// policy which can verify their availability before packaging.
#[derive(Debug, Default, Clone, Copy)]
pub struct NativeTarWriter;

impl NativeTarWriter {
    pub fn new() -> Self {
        Self
    }

    pub fn write(&self, rootfs: impl AsRef<Path>, output: impl AsRef<Path>) -> Result<()> {
        let rootfs = rootfs.as_ref();
        if !rootfs.is_dir() {
            bail!("rootfs is not a directory: {}", rootfs.display());
        }

        let output = output.as_ref();
        let file = File::create(output).with_context(|| format!("create archive {}", output.display()))?;
        let mut archive = Builder::new(file);
        append_tree(&mut archive, rootfs, Path::new(""))?;
        archive
            .finish()
            .with_context(|| format!("finish archive {}", output.display()))?;
        Ok(())
    }
}

fn append_tree(archive: &mut Builder<File>, rootfs: &Path, relative: &Path) -> Result<()> {
    let directory = rootfs.join(relative);
    let mut entries = fs::read_dir(&directory)
        .with_context(|| format!("read rootfs directory {}", directory.display()))?
        .collect::<std::result::Result<Vec<_>, _>>()?;
    entries.sort_by_key(|entry| entry.file_name());

    for entry in entries {
        let name = entry.file_name();
        if name == ".host" || name == ".fakedata" {
            continue;
        }
        let path = entry.path();
        let archive_path = relative.join(&name);
        let metadata = fs::symlink_metadata(&path)
            .with_context(|| format!("inspect rootfs entry {}", path.display()))?;
        let file_type = metadata.file_type();

        if file_type.is_dir() {
            append_directory(archive, &archive_path, &metadata)?;
            append_tree(archive, rootfs, &archive_path)?;
        } else if file_type.is_file() {
            append_file(archive, &path, &archive_path, &metadata)?;
        } else if file_type.is_symlink() {
            append_symlink(archive, &path, &archive_path, &metadata)?;
        } else {
            bail!("unsupported rootfs entry type: {}", path.display());
        }
    }
    Ok(())
}

fn deterministic_header(metadata: &fs::Metadata, entry_type: EntryType, size: u64) -> Header {
    use std::os::unix::fs::PermissionsExt;

    let mut header = Header::new_gnu();
    header.set_entry_type(entry_type);
    header.set_mode(metadata.permissions().mode());
    header.set_uid(0);
    header.set_gid(0);
    header.set_mtime(0);
    header.set_size(size);
    header
}

fn append_directory(archive: &mut Builder<File>, path: &Path, metadata: &fs::Metadata) -> Result<()> {
    let mut header = deterministic_header(metadata, EntryType::Directory, 0);
    archive
        .append_data(&mut header, path, std::io::empty())
        .with_context(|| format!("append directory {}", path.display()))
}

fn append_file(
    archive: &mut Builder<File>,
    source: &Path,
    path: &Path,
    metadata: &fs::Metadata,
) -> Result<()> {
    let mut header = deterministic_header(metadata, EntryType::Regular, metadata.len());
    let input = File::open(source).with_context(|| format!("open rootfs file {}", source.display()))?;
    archive
        .append_data(&mut header, path, input)
        .with_context(|| format!("append file {}", path.display()))
}

fn append_symlink(
    archive: &mut Builder<File>,
    source: &Path,
    path: &Path,
    metadata: &fs::Metadata,
) -> Result<()> {
    let target = fs::read_link(source).with_context(|| format!("read rootfs symlink {}", source.display()))?;
    let mut header = deterministic_header(metadata, EntryType::Symlink, 0);
    archive
        .append_link(&mut header, path, target)
        .with_context(|| format!("append symlink {}", path.display()))
}