Skip to main content

netget/server/usb/msc/
image_dir.rs

1//! The one directory a disk image may live in.
2//!
3//! `mount_disk` is offered to the model on every event a USB/IP peer provokes, and its
4//! `disk_image` named any path on the host: the file was opened read/write (created if
5//! absent), memory-mapped, and from then on the peer read it as SCSI sectors and, with
6//! `write_protect: false`, wrote it in place. A peer that talked the model into
7//! `{"disk_image": "/home/op/.ssh/authorized_keys"}` read the file; with write protection
8//! off it rewrote it. The startup `disk_image` parameter, model-settable through
9//! `open_server`, was the same path with the same open.
10//!
11//! An [`ImageDir`] is the boundary: the `image_dir` startup parameter, defaulting to
12//! NetGet's own `usb-images` directory under the platform's local-data dir — neither
13//! `$HOME` nor the working directory, which are what is being protected. A path's parent is
14//! canonicalised and must sit under the root; the final component is a plain file name;
15//! an existing target must be a regular file (a symlink is refused, and the open itself
16//! uses `O_NOFOLLOW`).
17
18use std::path::{Component, Path, PathBuf};
19
20use anyhow::{bail, Context, Result};
21
22/// Startup parameter naming the directory.
23pub const IMAGE_DIR_PARAM: &str = "image_dir";
24
25/// Where images live when no `image_dir` is configured.
26pub fn default_image_dir() -> PathBuf {
27    if let Some(data) = dirs::data_local_dir() {
28        return data.join("netget").join("usb-images");
29    }
30    if let Some(home) = dirs::home_dir() {
31        return home.join(".netget-usb-images");
32    }
33    std::env::temp_dir().join("netget-usb-images")
34}
35
36/// One server's image boundary: a canonical directory that exists.
37#[derive(Debug, Clone)]
38pub struct ImageDir {
39    root: PathBuf,
40}
41
42impl ImageDir {
43    /// Build the boundary from the `image_dir` startup parameter, creating the directory so
44    /// the default is usable without preparation.
45    pub fn new(configured: Option<&str>) -> Result<Self> {
46        let requested = match configured {
47            Some(raw) if !raw.trim().is_empty() => PathBuf::from(raw.trim()),
48            _ => default_image_dir(),
49        };
50        std::fs::create_dir_all(&requested).with_context(|| {
51            format!(
52                "could not create the {IMAGE_DIR_PARAM} directory {}",
53                requested.display()
54            )
55        })?;
56        let root = requested.canonicalize().with_context(|| {
57            format!(
58                "could not canonicalise {IMAGE_DIR_PARAM} {}",
59                requested.display()
60            )
61        })?;
62        if !root.is_dir() {
63            bail!("{IMAGE_DIR_PARAM} ({}) is not a directory", root.display());
64        }
65        Ok(Self { root })
66    }
67
68    /// The canonical root.
69    pub fn root(&self) -> &Path {
70        &self.root
71    }
72
73    /// The image file `raw` names, which may not exist yet: its directory must exist under
74    /// the root, its name must be a plain file name, and if it exists it must be a regular
75    /// file. A relative path is resolved against the root.
76    pub fn resolve(&self, raw: &str, context: &str) -> Result<PathBuf> {
77        let trimmed = raw.trim();
78        if trimmed.is_empty() {
79            bail!("{context} is empty");
80        }
81        let requested = Path::new(trimmed);
82        let absolute = if requested.is_absolute() {
83            requested.to_path_buf()
84        } else {
85            self.root.join(requested)
86        };
87        let name = match absolute.components().next_back() {
88            Some(Component::Normal(name)) => name.to_os_string(),
89            _ => bail!("{context} ({trimmed}) does not end in a file name"),
90        };
91        let parent = absolute
92            .parent()
93            .with_context(|| format!("{context} ({trimmed}) has no directory"))?;
94        let parent = parent
95            .canonicalize()
96            .with_context(|| format!("{context} ({trimmed}): its directory does not exist"))?;
97        if !parent.starts_with(&self.root) {
98            bail!(
99                "{context} ({trimmed}) is in {} which is outside this server's \
100                 {IMAGE_DIR_PARAM} ({}). Only images under that directory are served; put the \
101                 image there, or set the `{IMAGE_DIR_PARAM}` startup parameter to a directory \
102                 that contains it.",
103                parent.display(),
104                self.root.display()
105            );
106        }
107        let resolved = parent.join(name);
108        if let Ok(meta) = std::fs::symlink_metadata(&resolved) {
109            if meta.file_type().is_symlink() {
110                bail!("{context} ({trimmed}) is a symlink; an image must be a regular file",);
111            }
112            if !meta.is_file() {
113                bail!("{context} ({trimmed}) exists and is not a regular file");
114            }
115        }
116        Ok(resolved)
117    }
118}