1use crate::format::InodeMode;
2use crate::oci::Image;
3use crate::reader::{PuzzleFS, WalkPuzzleFS};
4use log::info;
5use nix::sys::stat::{makedev, mknod, Mode, SFlag};
6use nix::unistd::{chown, mkfifo, symlinkat, Gid, Uid};
7use std::collections::HashMap;
8use std::ffi::OsStr;
9use std::fs::Permissions;
10use std::os::unix::ffi::OsStrExt;
11use std::os::unix::fs::PermissionsExt;
12use std::path::{Component, Path, PathBuf};
13use std::{fs, io};
14
15fn runs_privileged() -> bool {
16 Uid::effective().is_root()
17}
18
19fn safe_path(dir: &Path, image_path: &Path) -> anyhow::Result<PathBuf> {
20 let mut buf = PathBuf::new();
26 buf.push(dir);
27 let mut level = 1;
28
29 for component in image_path.components() {
30 match component {
31 Component::Prefix(..) => bail!("Path prefix not understood"), Component::RootDir => {}
33 Component::CurDir => {}
34 Component::Normal(c) => {
35 buf.push(c);
36 level += 1;
37
38 match fs::symlink_metadata(&buf) {
40 Ok(md) => {
41 if md.file_type().is_symlink() {
42 bail!("symlink prefixes are not allowed: {:#?}", buf)
43 }
44 }
45 Err(e) => {
46 if e.kind() != io::ErrorKind::NotFound {
47 bail!("problem accessing path component {:#?}: {}", buf, e)
48 }
49
50 return Ok(buf);
53 }
54 }
55 }
56 Component::ParentDir => {
57 level -= 1;
58 if level <= 0 {
59 bail!("image path escapes extract dir: {:#?}", image_path)
60 }
61 buf.pop();
62 }
63 }
64 }
65
66 Ok(buf)
67}
68
69pub fn extract_rootfs(oci_dir: &str, tag: &str, extract_dir: &str) -> anyhow::Result<()> {
70 let oci_dir = Path::new(oci_dir);
71 let image = Image::open(oci_dir)?;
72 let dir = Path::new(extract_dir);
73 fs::create_dir_all(dir)?;
74 let mut pfs = PuzzleFS::open(image, tag, None)?;
75 let mut walker = WalkPuzzleFS::walk(&mut pfs)?;
76 let mut host_to_pfs = HashMap::<crate::format::Ino, PathBuf>::new();
77
78 walker.try_for_each(|de| -> anyhow::Result<()> {
79 let dir_entry = de?;
80 let path = safe_path(dir, &dir_entry.path)?;
81 let mut is_symlink = false;
82 info!("extracting {:#?}", path);
83 if let Some(existing_path) = host_to_pfs.get(&dir_entry.inode.ino) {
84 fs::hard_link(existing_path, &path)?;
85 return Ok(());
86 }
87 host_to_pfs.insert(dir_entry.inode.ino, path.clone());
88
89 match dir_entry.inode.mode {
90 InodeMode::File { .. } => {
91 let mut reader = dir_entry.open()?;
92 let mut f = fs::File::create(&path)?;
93 io::copy(&mut reader, &mut f)?;
94 }
95 InodeMode::Dir { .. } => fs::create_dir_all(&path)?,
96 InodeMode::Fifo => {
98 mkfifo(&path, Mode::S_IRWXU)?;
99 }
100 InodeMode::Chr { major, minor } => {
101 mknod(&path, SFlag::S_IFCHR, Mode::S_IRWXU, makedev(major, minor))?;
102 }
103 InodeMode::Blk { major, minor } => {
104 mknod(&path, SFlag::S_IFBLK, Mode::S_IRWXU, makedev(major, minor))?;
105 }
106 InodeMode::Lnk => {
107 let target = dir_entry.inode.symlink_target()?;
108 is_symlink = true;
109 symlinkat(target, None, &path)?;
110 }
111 InodeMode::Sock => {
112 todo!();
113 }
114 InodeMode::Wht => {
115 todo!();
116 }
117 _ => {
118 bail!("bad inode mode {:#?}", dir_entry.inode.mode)
119 }
120 }
121 if let Some(x) = dir_entry.inode.additional {
122 for x in &x.xattrs {
123 xattr::set(&path, OsStr::from_bytes(&x.key), &x.val)?;
124 }
125 }
126
127 if !is_symlink {
130 std::fs::set_permissions(
131 &path,
132 Permissions::from_mode(dir_entry.inode.permissions.into()),
133 )?;
134 }
135
136 if runs_privileged() {
137 chown(
138 &path,
139 Some(Uid::from_raw(dir_entry.inode.uid)),
140 Some(Gid::from_raw(dir_entry.inode.gid)),
141 )?;
142 }
143
144 Ok(())
145 })?;
146 Ok(())
147}
148
149#[cfg(test)]
150mod tests {
151 use tempfile::{tempdir, TempDir};
152
153 use std::fs::File;
154
155 use crate::builder::build_test_fs;
156 use std::os::unix::fs::MetadataExt;
157 use walkdir::WalkDir;
158
159 use super::*;
160
161 #[test]
162 fn test_extracted_xattrs() {
163 let dir = TempDir::new_in(".").unwrap();
164 let oci_dir = dir.path().join("oci");
165 let image = Image::new(&oci_dir).unwrap();
166 let rootfs = dir.path().join("rootfs");
167 let extract_dir = TempDir::new_in(".").unwrap();
168
169 let foo = rootfs.join("foo");
170 let bar = rootfs.join("bar");
171
172 let mut file_attributes = HashMap::<String, Vec<u8>>::new();
173 file_attributes.insert("user.meshuggah".to_string(), b"rocks".to_vec());
174 file_attributes.insert("user.nothing".to_string(), b"".to_vec());
175
176 fs::create_dir_all(&foo).unwrap();
180 fs::write(&bar, b"bar").unwrap();
181
182 for f in [&foo, &bar] {
184 for (key, val) in &file_attributes {
185 xattr::set(f, key, val).unwrap();
186 xattr::set(f, key, val).unwrap();
187 }
188 }
189
190 build_test_fs(&rootfs, &image, "test").unwrap();
191
192 extract_rootfs(
193 oci_dir.to_str().unwrap(),
194 "test",
195 extract_dir.path().to_str().unwrap(),
196 )
197 .unwrap();
198
199 let ents = WalkDir::new(&extract_dir)
200 .contents_first(false)
201 .follow_links(false)
202 .same_file_system(true)
203 .sort_by(|a, b| a.file_name().cmp(b.file_name()))
204 .into_iter()
205 .collect::<Result<Vec<walkdir::DirEntry>, walkdir::Error>>()
206 .unwrap();
207
208 for ent in ents.into_iter().skip(1) {
210 for (key, val) in &file_attributes {
211 let attribute = xattr::get(ent.path(), key);
212 println!(
213 "path: {:?} key: {:?} attribute: {:?}",
214 ent.path(),
215 key,
216 attribute
217 );
218 assert!(attribute.unwrap().as_ref().unwrap() == val);
219 }
220 }
221 }
222
223 #[test]
224 fn test_permissions() {
225 let dir = tempdir().unwrap();
226 let oci_dir = dir.path().join("oci");
227 let image = Image::new(&oci_dir).unwrap();
228 let rootfs = dir.path().join("rootfs");
229 let extract_dir = tempdir().unwrap();
230 const TESTED_PERMISSION: u32 = 0o7777;
231
232 let foo = rootfs.join("foo");
233
234 fs::create_dir_all(&rootfs).unwrap();
235 fs::write(&foo, b"foo").unwrap();
236
237 std::fs::set_permissions(foo, Permissions::from_mode(TESTED_PERMISSION)).unwrap();
238
239 build_test_fs(&rootfs, &image, "test").unwrap();
240
241 extract_rootfs(
242 oci_dir.to_str().unwrap(),
243 "test",
244 extract_dir.path().to_str().unwrap(),
245 )
246 .unwrap();
247
248 let extracted_path = extract_dir.path().join("foo");
249 let f = File::open(extracted_path).unwrap();
250 let metadata = f.metadata().unwrap();
251
252 assert_eq!(metadata.permissions().mode() & 0xFFF, TESTED_PERMISSION);
253 }
254
255 #[test]
256 fn test_hardlink_extraction() {
257 let dir = tempdir().unwrap();
258 let oci_dir = dir.path().join("oci");
259 let image = Image::new(&oci_dir).unwrap();
260 let rootfs = dir.path().join("rootfs");
261 let extract_dir = tempdir().unwrap();
262
263 let foo = rootfs.join("foo");
264 let bar = rootfs.join("bar");
265
266 fs::create_dir_all(&rootfs).unwrap();
267 fs::write(&foo, b"foo").unwrap();
268
269 fs::hard_link(&foo, &bar).unwrap();
270
271 assert_eq!(
272 fs::metadata(&foo).unwrap().ino(),
273 fs::metadata(&bar).unwrap().ino()
274 );
275
276 build_test_fs(&rootfs, &image, "test").unwrap();
277
278 extract_rootfs(
279 oci_dir.to_str().unwrap(),
280 "test",
281 extract_dir.path().to_str().unwrap(),
282 )
283 .unwrap();
284
285 let foo = extract_dir.path().join("foo");
286 let bar = extract_dir.path().join("bar");
287
288 assert_eq!(
289 fs::metadata(foo).unwrap().ino(),
290 fs::metadata(bar).unwrap().ino()
291 );
292 }
293
294 #[test]
295 fn test_empty_file() {
296 let dir = tempdir().unwrap();
297 let oci_dir = dir.path().join("oci");
298 let image = Image::new(&oci_dir).unwrap();
299 let rootfs = dir.path().join("rootfs");
300 let foo = rootfs.join("foo");
301 let extract_dir = tempdir().unwrap();
302
303 fs::create_dir_all(&rootfs).unwrap();
304 std::fs::File::create(foo).unwrap();
305
306 build_test_fs(&rootfs, &image, "test").unwrap();
307
308 extract_rootfs(
309 oci_dir.to_str().unwrap(),
310 "test",
311 extract_dir.path().to_str().unwrap(),
312 )
313 .unwrap();
314 let extracted_foo = extract_dir.path().join("foo");
315 assert_eq!(extracted_foo.metadata().unwrap().len(), 0);
316 }
317}