container_device_interface/
container_edits_unix.rs

1use std::{
2    fmt,
3    io::{Error, ErrorKind},
4    os::unix::fs::{FileTypeExt, MetadataExt},
5    path::Path,
6};
7
8use anyhow::Result;
9
10pub enum DeviceType {
11    Block,
12    Char,
13    Fifo,
14}
15
16impl fmt::Display for DeviceType {
17    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18        let s = match self {
19            DeviceType::Block => "b",
20            DeviceType::Char => "c",
21            DeviceType::Fifo => "p",
22        };
23        write!(f, "{}", s)
24    }
25}
26
27// deviceInfoFromPath takes the path to a device and returns its type, major and minor device numbers.
28// It was adapted from https://github.com/opencontainers/runc/blob/v1.1.9/libcontainer/devices/device_unix.go#L30-L69
29pub fn device_info_from_path<P: AsRef<Path>>(path: P) -> Result<(String, i64, i64)> {
30    let major = |dev: u64| -> i64 { (dev >> 8) as i64 & 0xff };
31    let minor = |dev: u64| -> i64 { dev as i64 & 0xff };
32
33    let metadata = std::fs::metadata(path)?;
34    let file_type = metadata.file_type();
35
36    let (dev_type, major, minor) = if file_type.is_block_device() {
37        (
38            DeviceType::Block.to_string(),
39            major(metadata.rdev()),
40            minor(metadata.rdev()),
41        )
42    } else if file_type.is_char_device() {
43        (
44            DeviceType::Char.to_string(),
45            major(metadata.rdev()),
46            minor(metadata.rdev()),
47        )
48    } else if file_type.is_fifo() {
49        (DeviceType::Fifo.to_string(), 0, 0)
50    } else {
51        return Err(Error::new(ErrorKind::InvalidInput, "It's not a device node").into());
52    };
53
54    Ok((dev_type, major, minor))
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    use std::ffi::CString;
62
63    use anyhow::Result;
64    use nix::libc::{self, dev_t, mknodat, mode_t, S_IFBLK, S_IFCHR, S_IFIFO};
65    use tempfile::TempDir;
66
67    use crate::{container_edits::DeviceNode, specs::config::DeviceNode as CDIDeviceNode};
68
69    fn is_root() -> bool {
70        unsafe { libc::geteuid() == 0 }
71    }
72
73    fn create_device(path: &str, mode: mode_t, dev: u64, dev_type: &str) -> Result<()> {
74        let path_c = CString::new(path)?;
75
76        // Set the appropriate mode for block or char or fifo device
77        let mode = match dev_type {
78            "b" => mode | S_IFBLK,
79            "c" => mode | S_IFCHR,
80            "p" => mode | S_IFIFO,
81            _ => 0,
82        };
83
84        // Create the device
85        let res = unsafe { mknodat(libc::AT_FDCWD, path_c.as_ptr(), mode, dev as dev_t) };
86        if res < 0 {
87            println!("create device with path: {:?} failed", path);
88            return Err(nix::Error::last().into());
89        }
90
91        println!("create device with path: {:?} successfully", path);
92        Ok(())
93    }
94
95    #[test]
96    fn test_fill_missing_info_block_device() {
97        if !is_root() {
98            println!("INFO: skipping, needs root");
99            return;
100        }
101
102        let temp_dir = TempDir::new().unwrap();
103        let block_device_path = temp_dir.path().join("block_device").display().to_string();
104
105        // Create a block device
106        let res = create_device(&block_device_path, 0o666, 0x0101, "b");
107        assert!(res.is_ok(), "Failed to create block device: {:?}", res);
108
109        let mut dev_node = DeviceNode {
110            node: CDIDeviceNode {
111                path: block_device_path,
112                ..Default::default()
113            },
114        };
115
116        assert!(dev_node.fill_missing_info().is_ok());
117
118        assert_eq!(dev_node.node.r#type, Some(DeviceType::Block.to_string()));
119        assert_eq!(dev_node.node.major, Some(1));
120        assert_eq!(dev_node.node.minor, Some(1));
121    }
122
123    #[test]
124    fn test_fill_missing_info_char_device() {
125        if !is_root() {
126            println!("INFO: skipping, needs root");
127            return;
128        }
129
130        let temp_dir = TempDir::new().unwrap();
131        let char_device_path = temp_dir.path().join("char_device").display().to_string();
132
133        // Create a character device
134        let res = create_device(&char_device_path, 0o666, 0x0202, "c");
135        assert!(res.is_ok(), "Failed to create char device: {:?}", res);
136
137        let mut dev_node = DeviceNode {
138            node: CDIDeviceNode {
139                path: char_device_path,
140                ..Default::default()
141            },
142        };
143
144        assert!(dev_node.fill_missing_info().is_ok());
145
146        assert_eq!(dev_node.node.r#type, Some(DeviceType::Char.to_string()));
147        assert_eq!(dev_node.node.major, Some(1));
148        assert_eq!(dev_node.node.minor, Some(2));
149    }
150
151    #[test]
152    fn test_fill_missing_info_fifo() {
153        if !is_root() {
154            println!("INFO: skipping which needs root");
155            return;
156        }
157
158        let temp_dir = TempDir::new().unwrap();
159        let fifo_device_path = temp_dir.path().join("fifo_device").display().to_string();
160
161        // Create a character device
162        let res = create_device(&fifo_device_path, 0o666, 0x0, "p");
163        assert!(res.is_ok(), "Failed to create fifo device: {:?}", res);
164
165        let mut dev_node = DeviceNode {
166            node: CDIDeviceNode {
167                path: fifo_device_path,
168                ..Default::default()
169            },
170        };
171
172        dev_node.fill_missing_info().unwrap();
173
174        assert_eq!(dev_node.node.r#type, Some(DeviceType::Fifo.to_string()));
175        assert_eq!(dev_node.node.major, None);
176        assert_eq!(dev_node.node.minor, None);
177    }
178
179    #[test]
180    fn test_fill_missing_info_regular_file() {
181        let temp_dir = TempDir::new().unwrap();
182        let file_path = temp_dir.path().join("regular_file");
183        std::fs::File::create(&file_path).unwrap();
184
185        let mut dev_node = DeviceNode {
186            node: CDIDeviceNode {
187                path: file_path.to_string_lossy().to_string(),
188                ..Default::default()
189            },
190        };
191
192        let result = dev_node.fill_missing_info();
193        assert!(result.is_err());
194    }
195}