oci_unpack/unpacker/
mod.rs

1mod event_handler;
2mod images;
3mod layers;
4
5use std::collections::BTreeMap;
6use std::io;
7use std::os::unix::ffi::OsStrExt;
8use std::path::{Path, PathBuf};
9
10use crate::{digest::DigestError, reference::Reference, MediaType};
11
12pub use event_handler::{EventHandler, NoEventHandler};
13
14/// Errors from [`Unpacker::unpack`].
15#[derive(thiserror::Error, Debug)]
16pub enum UnpackError {
17    #[error("I/O error: {1}: {0}")]
18    Io(io::Error, PathBuf),
19
20    #[cfg(feature = "sandbox")]
21    #[error("Failed to create a sandbox: {0}")]
22    Sandbox(#[from] landlock::RulesetError),
23
24    #[error("Operation interrupted.")]
25    Interrupted,
26
27    #[error("HTTP request failed: {0}")]
28    HttpRequest(#[from] crate::http::HttpError),
29
30    #[error("Invalid JSON: {0}")]
31    Json(#[from] serde_json::Error),
32
33    #[error("Invalid digest: {0}")]
34    InvalidDigest(#[from] DigestError),
35
36    #[error("Missing or invalid Content-Type.")]
37    MissingContentType,
38
39    #[error("Invalid Content-Type: {0}")]
40    InvalidContentType(MediaType),
41
42    #[error("No image for the architecture.")]
43    MissingArchitecture,
44}
45
46/// Wrap a [std::io::Error] with the path related to the I/O operation.
47///
48/// The second argument can be either a single expression, or a block.
49macro_rules! try_io {
50    ($path:expr, $b:block) => {
51        match (|| -> Result<_, io::Error> { Ok($b) })() {
52            Ok(ok) => ok,
53            Err(err) => return Err(UnpackError::Io(io::Error::from(err), $path.into())),
54        }
55    };
56
57    ($path:expr, $e:expr $(,)?) => {
58        $e.map_err(|e| UnpackError::Io(io::Error::from(e), $path.into()))?
59    };
60}
61
62// Make visible to mods.
63use try_io;
64
65/// Track directory metadata, to be applied when all files are written.
66///
67/// `mode` can't be set during the unpack, because if a directory has no
68/// `write` or `execute` permission, the program can't create files.
69///
70/// `mtime` could be set, but it is replaced by the kernel when new files
71/// are unpacked.
72///
73/// The `usize` field in the key is the length, in bytes, of the path. It
74/// is needed to guarantee that child directories are updated before their
75/// parents.
76type DirectoryMetadata = BTreeMap<(usize, PathBuf), DirectoryMetadataEntry>;
77
78struct DirectoryMetadataEntry {
79    mode: rustix::fs::Mode,
80    mtime: u64,
81    uid: Option<u32>,
82    gid: Option<u32>,
83}
84
85impl DirectoryMetadataEntry {
86    /// Return a key to use with `DirectoryMetadata`
87    fn key(path: PathBuf) -> (usize, PathBuf) {
88        let path_len = path.as_os_str().as_bytes().len();
89        (usize::MAX - path_len, path)
90    }
91}
92
93/// Download an image and unpack its contents to a new directory.
94pub struct Unpacker<'a, E> {
95    reference: Reference<'a>,
96    architecture: Option<&'a str>,
97    os: Option<&'a str>,
98    event_handler: E,
99    require_sandbox: bool,
100}
101
102impl<'a> Unpacker<'a, NoEventHandler> {
103    /// Create a new unpacker for the given reference.
104    ///
105    /// Sandbox is required by default.
106    pub fn new(reference: Reference<'a>) -> Self {
107        Self {
108            reference,
109            architecture: None,
110            os: None,
111            event_handler: NoEventHandler,
112            require_sandbox: true,
113        }
114    }
115
116    /// Set a handler to receive events during the operation.
117    pub fn event_handler<E: EventHandler>(self, event_handler: E) -> Unpacker<'a, E> {
118        Unpacker {
119            event_handler,
120            reference: self.reference,
121            architecture: self.architecture,
122            os: self.os,
123            require_sandbox: self.require_sandbox,
124        }
125    }
126}
127
128impl<'a, E: EventHandler> Unpacker<'a, E> {
129    /// Set sandbox requirement.
130    ///
131    /// If `require_sandbox` is `false`, the unpacker ignores errors if
132    /// it can't create a sandbox to restrict filesystem access.
133    pub fn require_sandbox(mut self, require_sandbox: bool) -> Self {
134        self.require_sandbox = require_sandbox;
135        self
136    }
137
138    /// Set the expected CPU architecture of the image.
139    ///
140    /// If omitted, it uses the architecture currently in use.
141    pub fn architecture(mut self, architecture: &'a str) -> Self {
142        self.architecture = Some(architecture);
143        self
144    }
145
146    /// Set the expected operating system  the image.
147    ///
148    /// If omitted, it uses the operating system currently in use.
149    pub fn os(mut self, os: &'a str) -> Self {
150        self.os = Some(os);
151        self
152    }
153
154    /// Download the image of `reference`, and unpack its contents to the
155    /// directory `target`.
156    ///
157    /// If `target` exists, it must be empty.
158    ///
159    /// Before unpacking the layers, it tries to create a sandbox to restrict
160    /// the write access to the `target` directory. If the sandbox can't be
161    /// created, and `require_sandbox` is `true`, the process is interrupted.
162    pub fn unpack(self, target: impl AsRef<Path>) -> Result<(), UnpackError> {
163        let target = target.as_ref();
164
165        Self::check_empty_dir(target).map_err(|e| UnpackError::Io(e, target.to_owned()))?;
166
167        let mut client = crate::http::Client::new(&self.reference, &self.event_handler);
168
169        let manifest =
170            crate::manifests::get(&self.reference, self.architecture, self.os, &mut client)?;
171
172        // Create sandbox after downloading the manifest, but before writing any
173        // file. Thus, we don't need to gran read-access to the files needed to
174        // make HTTPS requests (like `/etc/resolv.conf` or `/etc/ssl`).
175        #[cfg(feature = "sandbox")]
176        if let Err(err) = Self::sandbox(target, &self.event_handler) {
177            if self.require_sandbox {
178                return Err(UnpackError::Sandbox(err));
179            }
180        }
181
182        images::get(client, manifest, target, &self.event_handler)
183    }
184
185    /// Check if the `target` directory is empty.
186    ///
187    /// The directory is created if it does not exist.
188    fn check_empty_dir(path: &Path) -> io::Result<()> {
189        if !path.exists() {
190            return std::fs::create_dir_all(path);
191        }
192
193        if std::fs::read_dir(path)?.next().is_some() {
194            // Use `ErrorKind::DirectoryNotEmpty` when the
195            // feature `io_error_more` is stabilized.
196            return Err(io::Error::from_raw_os_error(libc::ENOTEMPTY));
197        }
198
199        Ok(())
200    }
201
202    /// Restrict filesystem access to the `target` directory.
203    ///
204    /// The sandbox must be created after initializing the HTTP client,
205    /// since the rules don't allow access to other files in the system,
206    /// like `/etc/resolv.conf` or `/etc/ssl`.
207    #[cfg(feature = "sandbox")]
208    fn sandbox(
209        target: &Path,
210        event_handler: &impl EventHandler,
211    ) -> Result<(), landlock::RulesetError> {
212        use landlock::*;
213
214        let abi = ABI::V2;
215
216        let status = Ruleset::default()
217            .set_compatibility(CompatLevel::HardRequirement)
218            .handle_access(AccessFs::from_all(abi))?
219            .create()?
220            .add_rules(path_beneath_rules(&[target], AccessFs::from_all(abi)))?
221            .restrict_self()?;
222
223        event_handler.sandbox_status(status);
224
225        Ok(())
226    }
227}