]>
Commit | Line | Data |
---|---|---|
1829ef0d WB |
1 | use std::collections::HashMap; |
2 | use std::io::Read; | |
3 | use std::path::{Path, PathBuf}; | |
13993292 | 4 | |
1829ef0d | 5 | use anyhow::{bail, format_err, Error}; |
13993292 WB |
6 | |
7 | use pxar::decoder::sync as decoder; | |
8 | use pxar::decoder::SeqRead; | |
9 | use pxar::encoder::sync as encoder; | |
10 | use pxar::encoder::{LinkOffset, SeqWrite}; | |
807a4fd6 | 11 | use pxar::format::acl::{self, Permissions}; |
1feb04d7 | 12 | use pxar::format::{mode, Device}; |
1829ef0d | 13 | use pxar::EntryKind as PxarEntryKind; |
13993292 WB |
14 | use pxar::Metadata; |
15 | ||
bb74cbfa | 16 | /// Hardlink information we use while encoding pxar archives. |
1829ef0d WB |
17 | pub struct HardlinkInfo { |
18 | link: LinkOffset, | |
19 | path: PathBuf, | |
20 | } | |
21 | ||
bb74cbfa | 22 | /// Mapping from `Entry.link_key` to its produced `HardlinkInfo`. |
1829ef0d WB |
23 | pub type HardlinkList = HashMap<String, HardlinkInfo>; |
24 | ||
bb74cbfa | 25 | /// Our virtual file system's file entry types. |
ea8ff636 | 26 | #[derive(Debug, Eq, PartialEq)] |
13993292 WB |
27 | pub enum EntryKind { |
28 | Invalid, | |
29 | File(Vec<u8>), | |
30 | Directory(Vec<Entry>), | |
31 | Symlink(PathBuf), | |
1829ef0d | 32 | Hardlink(String), |
650070b0 | 33 | Device(Device), |
13993292 WB |
34 | Socket, |
35 | Fifo, | |
36 | } | |
37 | ||
bb74cbfa | 38 | /// A virtual file entry. |
1829ef0d | 39 | #[derive(Debug, Eq)] |
13993292 | 40 | pub struct Entry { |
bb74cbfa | 41 | /// For now we just use strings as those are easier to write down for test cases. |
13993292 | 42 | pub name: String, |
bb74cbfa WB |
43 | |
44 | /// The metadata is the same we have in pxar archives for ease of use. | |
13993292 | 45 | pub metadata: Metadata, |
bb74cbfa WB |
46 | |
47 | /// The file's kind & contents. | |
13993292 | 48 | pub entry: EntryKind, |
bb74cbfa WB |
49 | |
50 | /// If we want to make a hardlink to a file, we write a "key" in here by which we can refer | |
51 | /// back to it in an `EntryKind::Hardlink`. Note that in order for the tests to work we | |
52 | /// currently have to use the canonical file path as key. (This just made it much easier to | |
53 | /// test things by being able to just derive `PartialEq` on `EntryKind`.) | |
1829ef0d WB |
54 | pub link_key: Option<String>, |
55 | } | |
56 | ||
57 | impl PartialEq for Entry { | |
58 | fn eq(&self, other: &Self) -> bool { | |
59 | // When decoding we don't get hardlink names back, so we skip those in our eq check. | |
60 | self.name == other.name && self.metadata == other.metadata && self.entry == other.entry | |
61 | } | |
13993292 WB |
62 | } |
63 | ||
64 | impl Entry { | |
bb74cbfa WB |
65 | /// Start a new entry. Takes a file *name* (not a path!). We use builder methods for the |
66 | /// remaining data. | |
13993292 WB |
67 | pub fn new(name: &str) -> Self { |
68 | Self { | |
69 | name: name.to_string(), | |
70 | metadata: Metadata::default(), | |
71 | entry: EntryKind::Invalid, | |
1829ef0d | 72 | link_key: None, |
13993292 WB |
73 | } |
74 | } | |
75 | ||
bb74cbfa WB |
76 | /// Add metadata. You can skip this for hardlinks, but note that for other files you'll want to |
77 | /// have metadat with the correct file mode bits at least. | |
78 | /// | |
79 | /// There could be convenience methods for files and symlinks though. | |
13993292 WB |
80 | pub fn metadata(mut self, metadata: impl Into<Metadata>) -> Self { |
81 | self.metadata = metadata.into(); | |
82 | self | |
83 | } | |
84 | ||
bb74cbfa WB |
85 | /// Fill in the entry kind. This is never optional, actually, but since metadata is optional |
86 | /// making this a builder method rather than a paramtere of `new()` makes the test cases read | |
87 | /// nicer, since directory contents are last. | |
13993292 WB |
88 | pub fn entry(mut self, entry: EntryKind) -> Self { |
89 | self.entry = entry; | |
90 | self | |
91 | } | |
92 | ||
1829ef0d WB |
93 | /// NOTE: For test cases to be equal via the `==` comparison link keys must be equal to the full |
94 | /// canonical path, as decoded hardlinks will use the `entry.path()` from the pxar decoder as | |
95 | /// value for `EntryKind::Hardlink`. | |
96 | pub fn link_key(mut self, key: impl Into<String>) -> Self { | |
97 | self.link_key = Some(key.into()); | |
98 | self | |
99 | } | |
100 | ||
bb74cbfa | 101 | /// Internal assertion that this file must not contain a hard link key. |
1829ef0d WB |
102 | fn no_hardlink(&self) -> Result<(), Error> { |
103 | if let Some(key) = &self.link_key { | |
104 | bail!( | |
105 | "hardlink key on entry which may not be hard-linked: {}", | |
106 | key | |
107 | ); | |
108 | } | |
109 | Ok(()) | |
110 | } | |
111 | ||
bb74cbfa WB |
112 | /// Encode this entry (recursively) into a pxar encoder. |
113 | /// | |
114 | /// The path should "point" to the parent. | |
1829ef0d WB |
115 | pub fn encode_into<T>( |
116 | &self, | |
117 | encoder: &mut encoder::Encoder<T>, | |
118 | hardlinks: &mut HardlinkList, | |
119 | path: &Path, | |
120 | ) -> Result<(), Error> | |
13993292 WB |
121 | where |
122 | T: SeqWrite, | |
123 | { | |
124 | match &self.entry { | |
125 | EntryKind::Invalid => bail!("invalid entry encountered"), | |
126 | ||
127 | EntryKind::File(data) => { | |
1829ef0d | 128 | let link: LinkOffset = encoder.add_file( |
13993292 WB |
129 | &self.metadata, |
130 | &self.name, | |
131 | data.len() as u64, | |
132 | &mut &data[..], | |
133 | )?; | |
1829ef0d WB |
134 | if let Some(key) = &self.link_key { |
135 | let info = HardlinkInfo { | |
136 | link, | |
137 | path: path.join(&self.name), | |
138 | }; | |
139 | if hardlinks.insert(key.clone(), info).is_some() { | |
140 | bail!("duplicate hardlink key: {}", key); | |
141 | } | |
142 | } | |
13993292 WB |
143 | } |
144 | ||
145 | EntryKind::Directory(entries) => { | |
1829ef0d | 146 | self.no_hardlink()?; |
13993292 | 147 | let mut dir = encoder.create_directory(&self.name, &self.metadata)?; |
1829ef0d | 148 | let path = path.join(&self.name); |
13993292 | 149 | for entry in entries { |
1829ef0d | 150 | entry.encode_into(&mut dir, hardlinks, &path)?; |
13993292 WB |
151 | } |
152 | dir.finish()?; | |
153 | } | |
154 | ||
155 | EntryKind::Symlink(path) => { | |
1829ef0d | 156 | self.no_hardlink()?; |
13993292 WB |
157 | let _: () = encoder.add_symlink(&self.metadata, &self.name, path)?; |
158 | } | |
159 | ||
1829ef0d WB |
160 | EntryKind::Hardlink(key) => { |
161 | self.no_hardlink()?; | |
162 | if let Some(info) = hardlinks.get(key) { | |
163 | let _: () = encoder.add_hardlink(&self.name, &info.path, info.link)?; | |
164 | } else { | |
165 | bail!("missing hardlink target key: {}", key); | |
166 | } | |
167 | } | |
168 | ||
650070b0 WB |
169 | EntryKind::Device(device) => { |
170 | self.no_hardlink()?; | |
171 | let _: () = encoder.add_device(&self.metadata, &self.name, device.clone())?; | |
172 | } | |
173 | ||
174 | EntryKind::Fifo => { | |
175 | self.no_hardlink()?; | |
176 | let _: () = encoder.add_fifo(&self.metadata, &self.name)?; | |
177 | } | |
178 | ||
179 | EntryKind::Socket => { | |
180 | self.no_hardlink()?; | |
181 | let _: () = encoder.add_socket(&self.metadata, &self.name)?; | |
182 | } | |
13993292 WB |
183 | } |
184 | Ok(()) | |
185 | } | |
186 | ||
bb74cbfa WB |
187 | /// Decode an entry from a pxar decoder. |
188 | /// | |
189 | /// Note that you CANNOT re-encode it afterwards currently, as for now we do not synthesize | |
190 | /// `link_key` properties! | |
1829ef0d | 191 | pub fn decode_from<T: SeqRead>(decoder: &mut decoder::Decoder<T>) -> Result<Entry, Error> { |
bb74cbfa WB |
192 | // The decoder linearly goes through the entire archive recursiveley, we need to know when |
193 | // a directory ends: | |
1829ef0d | 194 | decoder.enable_goodbye_entries(true); |
bb74cbfa | 195 | |
1829ef0d WB |
196 | Self::decode_root(decoder) |
197 | } | |
198 | ||
199 | fn decode_root<T: SeqRead>(decoder: &mut decoder::Decoder<T>) -> Result<Entry, Error> { | |
200 | let item = decoder | |
201 | .next() | |
202 | .ok_or_else(|| format_err!("empty archive?"))??; | |
203 | ||
204 | match item.kind() { | |
205 | PxarEntryKind::Directory => (), | |
206 | other => bail!("invalid entry kind for root node: {:?}", other), | |
207 | } | |
208 | ||
209 | let mut root = Entry::new("/").metadata(item.metadata().clone()); | |
210 | root.decode_directory(decoder)?; | |
211 | Ok(root) | |
212 | } | |
213 | ||
214 | fn decode_directory<T: SeqRead>( | |
215 | &mut self, | |
216 | decoder: &mut decoder::Decoder<T>, | |
217 | ) -> Result<(), Error> { | |
218 | let mut contents = Vec::new(); | |
219 | ||
220 | while let Some(item) = decoder.next().transpose()? { | |
221 | let make_entry = | |
222 | || -> Result<Entry, Error> { | |
223 | Ok(Entry::new(item.file_name().to_str().ok_or_else(|| { | |
224 | format_err!("non-utf8 name in test: {:?}", item.file_name()) | |
225 | })?) | |
226 | .metadata(item.metadata().clone()) | |
227 | .link_key(item.path().as_os_str().to_str().ok_or_else(|| { | |
228 | format_err!("non-utf8 path in test: {:?}", item.file_name()) | |
229 | })?)) | |
230 | }; | |
231 | match item.kind() { | |
232 | PxarEntryKind::GoodbyeTable => break, | |
233 | PxarEntryKind::File { size, .. } => { | |
234 | let mut data = Vec::new(); | |
235 | decoder | |
236 | .contents() | |
237 | .ok_or_else(|| { | |
238 | format_err!("failed to get contents for file entry: {:?}", item.path()) | |
239 | })? | |
240 | .read_to_end(&mut data)?; | |
1feb04d7 WB |
241 | if data.len() as u64 != *size { |
242 | bail!( | |
243 | "file {:?} was advertised to be of size {} but we read {} bytes", | |
244 | item.path(), | |
245 | size, | |
246 | data.len(), | |
247 | ); | |
248 | } | |
1829ef0d WB |
249 | contents.push(make_entry()?.entry(EntryKind::File(data))); |
250 | } | |
251 | PxarEntryKind::Directory => { | |
252 | let mut dir = make_entry()?; | |
253 | dir.decode_directory(decoder)?; | |
254 | contents.push(dir); | |
255 | } | |
256 | PxarEntryKind::Symlink(link) => { | |
257 | contents.push(make_entry()?.entry(EntryKind::Symlink(link.into()))); | |
258 | } | |
259 | PxarEntryKind::Hardlink(link) => { | |
260 | contents.push( | |
261 | make_entry()?.entry(EntryKind::Hardlink( | |
262 | link.as_os_str() | |
263 | .to_str() | |
264 | .ok_or_else(|| { | |
265 | format_err!("non-utf8 hardlink entry: {:?}", link.as_os_str()) | |
266 | })? | |
267 | .to_string(), | |
268 | )), | |
269 | ); | |
270 | } | |
650070b0 WB |
271 | PxarEntryKind::Device(device) => { |
272 | contents.push(make_entry()?.entry(EntryKind::Device(device.clone()))); | |
273 | } | |
274 | PxarEntryKind::Fifo => { | |
275 | contents.push(make_entry()?.entry(EntryKind::Fifo)); | |
276 | } | |
277 | PxarEntryKind::Socket => { | |
278 | contents.push(make_entry()?.entry(EntryKind::Socket)); | |
279 | } | |
1829ef0d WB |
280 | } |
281 | } | |
282 | ||
283 | self.entry = EntryKind::Directory(contents); | |
284 | Ok(()) | |
13993292 WB |
285 | } |
286 | } | |
287 | ||
8fe60382 | 288 | #[rustfmt::skip] |
13993292 WB |
289 | pub fn test_fs() -> Entry { |
290 | Entry::new("/") | |
291 | .metadata(Metadata::dir_builder(0o755)) | |
292 | .entry(EntryKind::Directory(vec![ | |
293 | Entry::new("home") | |
294 | .metadata(Metadata::dir_builder(0o755)) | |
295 | .entry(EntryKind::Directory(vec![ | |
296 | Entry::new("user") | |
297 | .metadata(Metadata::dir_builder(0o700).owner(1000, 1000)) | |
298 | .entry(EntryKind::Directory(vec![ | |
299 | Entry::new(".profile") | |
300 | .metadata(Metadata::file_builder(0o644).owner(1000, 1000)) | |
301 | .entry(EntryKind::File(b"#umask 022".to_vec())), | |
302 | Entry::new("data") | |
303 | .metadata(Metadata::file_builder(0o644).owner(1000, 1000)) | |
304 | .entry(EntryKind::File(b"a file from a user".to_vec())), | |
305 | ])), | |
306 | ])), | |
307 | Entry::new("bin") | |
308 | .metadata(Metadata::builder(mode::IFLNK | 0o777)) | |
309 | .entry(EntryKind::Symlink(PathBuf::from("usr/bin"))), | |
1829ef0d WB |
310 | Entry::new("usr") |
311 | .metadata(Metadata::dir_builder(0o755)) | |
312 | .entry(EntryKind::Directory(vec![ | |
313 | Entry::new("bin") | |
314 | .metadata(Metadata::dir_builder(0o755)) | |
315 | .entry(EntryKind::Directory(vec![ | |
316 | Entry::new("bzip2") | |
317 | .metadata(Metadata::file_builder(0o755)) | |
318 | .entry(EntryKind::File(b"This is an executable".to_vec())) | |
319 | .link_key("/usr/bin/bzip2"), | |
320 | Entry::new("cat") | |
321 | .metadata(Metadata::file_builder(0o755)) | |
322 | .entry(EntryKind::File(b"This is another executable".to_vec())), | |
323 | Entry::new("bunzip2") | |
324 | .entry(EntryKind::Hardlink("/usr/bin/bzip2".to_string())), | |
325 | ])), | |
326 | ])), | |
650070b0 WB |
327 | Entry::new("dev") |
328 | .metadata(Metadata::dir_builder(0o755)) | |
329 | .entry(EntryKind::Directory(vec![ | |
330 | Entry::new("null") | |
331 | .metadata(Metadata::builder(mode::IFCHR | 0o666)) | |
332 | .entry(EntryKind::Device(Device { major: 1, minor: 3 })), | |
333 | Entry::new("zero") | |
334 | .metadata(Metadata::builder(mode::IFCHR | 0o666)) | |
335 | .entry(EntryKind::Device(Device { major: 1, minor: 5 })), | |
336 | Entry::new("loop0") | |
337 | .metadata(Metadata::builder(mode::IFBLK | 0o666)) | |
338 | .entry(EntryKind::Device(Device { major: 7, minor: 0 })), | |
339 | Entry::new("loop1") | |
807a4fd6 WB |
340 | .metadata( |
341 | Metadata::builder(mode::IFBLK | 0o666) | |
342 | .acl_user(acl::User::new(1000, 6)) | |
343 | .acl_group(acl::Group::new(1000, 6)) | |
344 | ) | |
650070b0 WB |
345 | .entry(EntryKind::Device(Device { major: 7, minor: 1 })), |
346 | ])), | |
347 | Entry::new("run") | |
807a4fd6 WB |
348 | .metadata( |
349 | Metadata::dir_builder(0o755) | |
350 | .default_acl(Some(acl::Default { | |
351 | user_obj_permissions: Permissions(acl::READ | acl::WRITE), | |
352 | group_obj_permissions: Permissions(acl::READ), | |
353 | other_permissions: Permissions(acl::READ), | |
354 | mask_permissions: Permissions::NO_MASK, | |
355 | })) | |
356 | .default_acl_user(acl::User::new(1001, acl::READ)) | |
357 | ) | |
650070b0 WB |
358 | .entry(EntryKind::Directory(vec![ |
359 | Entry::new("fifo0") | |
360 | .metadata(Metadata::builder(mode::IFIFO | 0o666)) | |
361 | .entry(EntryKind::Fifo), | |
362 | Entry::new("sock0") | |
363 | .metadata(Metadata::builder(mode::IFSOCK | 0o600)) | |
364 | .entry(EntryKind::Socket), | |
365 | ])), | |
13993292 WB |
366 | ])) |
367 | } |