|
| 1 | +//! Copyright (C) Microsoft Corporation. |
| 2 | +//! |
| 3 | +//! This program is free software: you can redistribute it and/or modify |
| 4 | +//! it under the terms of the GNU General Public License as published by |
| 5 | +//! the Free Software Foundation, either version 3 of the License, or |
| 6 | +//! (at your option) any later version. |
| 7 | +//! |
| 8 | +//! This program is distributed in the hope that it will be useful, |
| 9 | +//! but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 10 | +//! MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 11 | +//! GNU General Public License for more details. |
| 12 | +//! |
| 13 | +//! You should have received a copy of the GNU General Public License |
| 14 | +//! along with this program. If not, see <https://www.gnu.org/licenses/>. |
| 15 | +use anyhow::{Context, Result}; |
| 16 | +use std::{ |
| 17 | + collections::{hash_map::Entry, HashMap}, |
| 18 | + io::Write, |
| 19 | + os::unix::{ |
| 20 | + fs::MetadataExt, |
| 21 | + prelude::{FileTypeExt, OsStrExt}, |
| 22 | + }, |
| 23 | + path::{Path, PathBuf}, |
| 24 | +}; |
| 25 | +use walkdir::WalkDir; |
| 26 | + |
| 27 | +// https://mgorny.pl/articles/portability-of-tar-features.html#id25 |
| 28 | +const PAX_SCHILY_XATTR: &[u8; 13] = b"SCHILY.xattr."; |
| 29 | + |
| 30 | +/// custom implementation of tar-rs's append_dir_all that: |
| 31 | +/// - works around https://github.com/alexcrichton/tar-rs/issues/102 so that security capabilities are preserved |
| 32 | +/// - emulates tar's `--clamp-mtime` option so that any file/dir/symlink mtimes are no later than a specific value |
| 33 | +/// - supports hardlinks |
| 34 | +pub(super) fn append_dir_all_with_xattrs( |
| 35 | + builder: &mut tar::Builder<impl Write>, |
| 36 | + src_path: impl AsRef<Path>, |
| 37 | + clamp_mtime: i64, |
| 38 | +) -> Result<()> { |
| 39 | + let src_path = src_path.as_ref(); |
| 40 | + // Map (dev, inode) -> path for hardlinks |
| 41 | + let mut hardlinks: HashMap<(u64, u64), PathBuf> = HashMap::new(); |
| 42 | + |
| 43 | + for entry in WalkDir::new(src_path) |
| 44 | + .follow_links(false) |
| 45 | + .sort_by_file_name() |
| 46 | + .into_iter() |
| 47 | + { |
| 48 | + let entry = entry?; |
| 49 | + let meta = entry.metadata()?; |
| 50 | + // skip sockets as tar-rs errors when trying to archive them. |
| 51 | + // For comparison, umoci also errors, whereas docker skips them |
| 52 | + if meta.file_type().is_socket() { |
| 53 | + continue; |
| 54 | + } |
| 55 | + |
| 56 | + let rel_path = pathdiff::diff_paths(entry.path(), src_path) |
| 57 | + .expect("walkdir returns path inside of search root"); |
| 58 | + if rel_path == Path::new("") { |
| 59 | + continue; |
| 60 | + } |
| 61 | + |
| 62 | + if entry.file_type().is_symlink() { |
| 63 | + if meta.mtime() > clamp_mtime { |
| 64 | + // Setting the mtime on a symlink is fiddly with tar-rs, so we use filetime to change |
| 65 | + // the mtime before adding the symlink to the tar archive |
| 66 | + let mtime = filetime::FileTime::from_unix_time(clamp_mtime, 0); |
| 67 | + filetime::set_symlink_file_times(entry.path(), mtime, mtime)?; |
| 68 | + } |
| 69 | + add_pax_extension_header(entry.path(), builder)?; |
| 70 | + builder.append_path_with_name(entry.path(), rel_path)?; |
| 71 | + } else if entry.file_type().is_file() || entry.file_type().is_dir() { |
| 72 | + add_pax_extension_header(entry.path(), builder)?; |
| 73 | + |
| 74 | + // If this is a hardlink, add a link header instead of the file |
| 75 | + // if this isn't the first time we've seen this inode |
| 76 | + if meta.nlink() > 1 { |
| 77 | + match hardlinks.entry((meta.dev(), meta.ino())) { |
| 78 | + Entry::Occupied(e) => { |
| 79 | + // Add link header and continue to next entry |
| 80 | + let mut header = tar::Header::new_gnu(); |
| 81 | + header.set_metadata(&meta); |
| 82 | + if meta.mtime() > clamp_mtime { |
| 83 | + header.set_mtime(clamp_mtime as u64); |
| 84 | + } |
| 85 | + header.set_entry_type(tar::EntryType::Link); |
| 86 | + header.set_cksum(); |
| 87 | + builder.append_link(&mut header, &rel_path, e.get())?; |
| 88 | + continue; |
| 89 | + } |
| 90 | + Entry::Vacant(e) => { |
| 91 | + // This is the first time we've seen this inode |
| 92 | + e.insert(rel_path.clone()); |
| 93 | + } |
| 94 | + } |
| 95 | + } |
| 96 | + |
| 97 | + let mut header = tar::Header::new_gnu(); |
| 98 | + header.set_size(meta.len()); |
| 99 | + header.set_metadata(&meta); |
| 100 | + if meta.mtime() > clamp_mtime { |
| 101 | + header.set_mtime(clamp_mtime as u64); |
| 102 | + } |
| 103 | + if entry.file_type().is_file() { |
| 104 | + builder.append_data( |
| 105 | + &mut header, |
| 106 | + rel_path, |
| 107 | + &mut std::fs::File::open(entry.path())?, |
| 108 | + )?; |
| 109 | + } else { |
| 110 | + builder.append_data(&mut header, rel_path, &mut std::io::empty())?; |
| 111 | + }; |
| 112 | + } |
| 113 | + } |
| 114 | + |
| 115 | + Ok(()) |
| 116 | +} |
| 117 | + |
| 118 | +// Convert any extended attributes on the specified path to a tar PAX extension header, and add it to the tar archive |
| 119 | +fn add_pax_extension_header( |
| 120 | + path: impl AsRef<Path>, |
| 121 | + builder: &mut tar::Builder<impl Write>, |
| 122 | +) -> Result<(), anyhow::Error> { |
| 123 | + let path = path.as_ref(); |
| 124 | + let xattrs = xattr::list(path) |
| 125 | + .with_context(|| format!("Failed to list xattrs from `{}`", path.display()))?; |
| 126 | + let mut pax_header = tar::Header::new_gnu(); |
| 127 | + let mut pax_data = Vec::new(); |
| 128 | + for key in xattrs { |
| 129 | + let value = xattr::get(path, &key) |
| 130 | + .with_context(|| { |
| 131 | + format!( |
| 132 | + "Failed to get xattr `{}` from `{}`", |
| 133 | + key.to_string_lossy(), |
| 134 | + path.display() |
| 135 | + ) |
| 136 | + })? |
| 137 | + .unwrap_or_default(); |
| 138 | + |
| 139 | + // each entry is "<len> <key>=<value>\n": https://www.ibm.com/docs/en/zos/2.3.0?topic=SSLTBW_2.3.0/com.ibm.zos.v2r3.bpxa500/paxex.html |
| 140 | + let data_len = PAX_SCHILY_XATTR.len() + key.as_bytes().len() + value.len() + 3; |
| 141 | + // Calculate the total length, including the length of the length field |
| 142 | + let mut len_len = 1; |
| 143 | + while data_len + len_len >= 10usize.pow(len_len.try_into().unwrap()) { |
| 144 | + len_len += 1; |
| 145 | + } |
| 146 | + write!(pax_data, "{} ", data_len + len_len)?; |
| 147 | + pax_data.write_all(PAX_SCHILY_XATTR)?; |
| 148 | + pax_data.write_all(key.as_bytes())?; |
| 149 | + pax_data.write_all("=".as_bytes())?; |
| 150 | + pax_data.write_all(&value)?; |
| 151 | + pax_data.write_all("\n".as_bytes())?; |
| 152 | + } |
| 153 | + if !pax_data.is_empty() { |
| 154 | + pax_header.set_size(pax_data.len() as u64); |
| 155 | + pax_header.set_entry_type(tar::EntryType::XHeader); |
| 156 | + pax_header.set_cksum(); |
| 157 | + builder.append(&pax_header, &*pax_data)?; |
| 158 | + } |
| 159 | + Ok(()) |
| 160 | +} |
0 commit comments