]>
Commit | Line | Data |
---|---|---|
55c0b3cc | 1 | use std::ffi::{CStr, CString, OsStr}; |
c443f58b | 2 | use std::fmt; |
42c2b5be | 3 | use std::io::{Read, Seek, SeekFrom, Write}; |
c443f58b | 4 | use std::os::unix::ffi::OsStrExt; |
9d135fe6 | 5 | |
c443f58b | 6 | use anyhow::{bail, format_err, Error}; |
013b1e8b | 7 | use serde::{Deserialize, Serialize}; |
9d135fe6 | 8 | |
c443f58b | 9 | use pathpatterns::{MatchList, MatchType}; |
6ef1b649 WB |
10 | |
11 | use proxmox_io::ReadExt; | |
12 | use proxmox_schema::api; | |
9d135fe6 | 13 | |
f323e906 WB |
14 | use crate::file_formats::PROXMOX_CATALOG_FILE_MAGIC_1_0; |
15 | ||
16 | /// Trait for writing file list catalogs. | |
17 | /// | |
18 | /// A file list catalog simply stores a directory tree. Such catalogs may be used as index to do a | |
19 | /// fast search for files. | |
20 | pub trait BackupCatalogWriter { | |
21 | fn start_directory(&mut self, name: &CStr) -> Result<(), Error>; | |
22 | fn end_directory(&mut self) -> Result<(), Error>; | |
23 | fn add_file(&mut self, name: &CStr, size: u64, mtime: i64) -> Result<(), Error>; | |
24 | fn add_symlink(&mut self, name: &CStr) -> Result<(), Error>; | |
25 | fn add_hardlink(&mut self, name: &CStr) -> Result<(), Error>; | |
26 | fn add_block_device(&mut self, name: &CStr) -> Result<(), Error>; | |
27 | fn add_char_device(&mut self, name: &CStr) -> Result<(), Error>; | |
28 | fn add_fifo(&mut self, name: &CStr) -> Result<(), Error>; | |
29 | fn add_socket(&mut self, name: &CStr) -> Result<(), Error>; | |
30 | } | |
9d135fe6 | 31 | |
dc9596de | 32 | #[repr(u8)] |
1be05037 | 33 | #[derive(Copy, Clone, PartialEq, Eq)] |
f323e906 | 34 | pub enum CatalogEntryType { |
dc9596de DM |
35 | Directory = b'd', |
36 | File = b'f', | |
37 | Symlink = b'l', | |
38 | Hardlink = b'h', | |
39 | BlockDevice = b'b', | |
40 | CharDevice = b'c', | |
41 | Fifo = b'p', // Fifo,Pipe | |
42 | Socket = b's', | |
43 | } | |
44 | ||
45 | impl TryFrom<u8> for CatalogEntryType { | |
42c2b5be | 46 | type Error = Error; |
dc9596de DM |
47 | |
48 | fn try_from(value: u8) -> Result<Self, Error> { | |
49 | Ok(match value { | |
50 | b'd' => CatalogEntryType::Directory, | |
51 | b'f' => CatalogEntryType::File, | |
52 | b'l' => CatalogEntryType::Symlink, | |
53 | b'h' => CatalogEntryType::Hardlink, | |
54 | b'b' => CatalogEntryType::BlockDevice, | |
55 | b'c' => CatalogEntryType::CharDevice, | |
56 | b'p' => CatalogEntryType::Fifo, | |
57 | b's' => CatalogEntryType::Socket, | |
58 | _ => bail!("invalid CatalogEntryType value '{}'", char::from(value)), | |
59 | }) | |
60 | } | |
61 | } | |
62 | ||
05d18b90 DC |
63 | impl From<&DirEntryAttribute> for CatalogEntryType { |
64 | fn from(value: &DirEntryAttribute) -> Self { | |
65 | match value { | |
66 | DirEntryAttribute::Directory { .. } => CatalogEntryType::Directory, | |
67 | DirEntryAttribute::File { .. } => CatalogEntryType::File, | |
68 | DirEntryAttribute::Symlink => CatalogEntryType::Symlink, | |
69 | DirEntryAttribute::Hardlink => CatalogEntryType::Hardlink, | |
70 | DirEntryAttribute::BlockDevice => CatalogEntryType::BlockDevice, | |
71 | DirEntryAttribute::CharDevice => CatalogEntryType::CharDevice, | |
72 | DirEntryAttribute::Fifo => CatalogEntryType::Fifo, | |
73 | DirEntryAttribute::Socket => CatalogEntryType::Socket, | |
74 | } | |
75 | } | |
76 | } | |
77 | ||
dc9596de DM |
78 | impl fmt::Display for CatalogEntryType { |
79 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | |
80 | write!(f, "{}", char::from(*self as u8)) | |
81 | } | |
82 | } | |
83 | ||
780dd2b0 DM |
84 | /// Represents a named directory entry |
85 | /// | |
86 | /// The ``attr`` property contain the exact type with type specific | |
87 | /// attributes. | |
1be05037 | 88 | #[derive(Clone, PartialEq, Eq)] |
7d017123 DM |
89 | pub struct DirEntry { |
90 | pub name: Vec<u8>, | |
91 | pub attr: DirEntryAttribute, | |
55c0b3cc DM |
92 | } |
93 | ||
780dd2b0 | 94 | /// Used to specific additional attributes inside DirEntry |
1be05037 | 95 | #[derive(Clone, Debug, PartialEq, Eq)] |
780dd2b0 DM |
96 | pub enum DirEntryAttribute { |
97 | Directory { start: u64 }, | |
5499bd3d | 98 | File { size: u64, mtime: i64 }, |
780dd2b0 DM |
99 | Symlink, |
100 | Hardlink, | |
101 | BlockDevice, | |
102 | CharDevice, | |
103 | Fifo, | |
104 | Socket, | |
105 | } | |
106 | ||
7d017123 | 107 | impl DirEntry { |
5499bd3d | 108 | fn new(etype: CatalogEntryType, name: Vec<u8>, start: u64, size: u64, mtime: i64) -> Self { |
7d017123 | 109 | match etype { |
42c2b5be TL |
110 | CatalogEntryType::Directory => DirEntry { |
111 | name, | |
112 | attr: DirEntryAttribute::Directory { start }, | |
113 | }, | |
114 | CatalogEntryType::File => DirEntry { | |
115 | name, | |
116 | attr: DirEntryAttribute::File { size, mtime }, | |
117 | }, | |
118 | CatalogEntryType::Symlink => DirEntry { | |
119 | name, | |
120 | attr: DirEntryAttribute::Symlink, | |
121 | }, | |
122 | CatalogEntryType::Hardlink => DirEntry { | |
123 | name, | |
124 | attr: DirEntryAttribute::Hardlink, | |
125 | }, | |
126 | CatalogEntryType::BlockDevice => DirEntry { | |
127 | name, | |
128 | attr: DirEntryAttribute::BlockDevice, | |
129 | }, | |
130 | CatalogEntryType::CharDevice => DirEntry { | |
131 | name, | |
132 | attr: DirEntryAttribute::CharDevice, | |
133 | }, | |
134 | CatalogEntryType::Fifo => DirEntry { | |
135 | name, | |
136 | attr: DirEntryAttribute::Fifo, | |
137 | }, | |
138 | CatalogEntryType::Socket => DirEntry { | |
139 | name, | |
140 | attr: DirEntryAttribute::Socket, | |
141 | }, | |
7d017123 DM |
142 | } |
143 | } | |
c9f00221 | 144 | |
c443f58b WB |
145 | /// Get file mode bits for this entry to be used with the `MatchList` api. |
146 | pub fn get_file_mode(&self) -> Option<u32> { | |
42c2b5be TL |
147 | Some(match self.attr { |
148 | DirEntryAttribute::Directory { .. } => pxar::mode::IFDIR, | |
149 | DirEntryAttribute::File { .. } => pxar::mode::IFREG, | |
150 | DirEntryAttribute::Symlink => pxar::mode::IFLNK, | |
151 | DirEntryAttribute::Hardlink => return None, | |
152 | DirEntryAttribute::BlockDevice => pxar::mode::IFBLK, | |
153 | DirEntryAttribute::CharDevice => pxar::mode::IFCHR, | |
154 | DirEntryAttribute::Fifo => pxar::mode::IFIFO, | |
155 | DirEntryAttribute::Socket => pxar::mode::IFSOCK, | |
156 | } as u32) | |
c443f58b WB |
157 | } |
158 | ||
4145c367 | 159 | /// Check if DirEntry is a directory |
c9f00221 | 160 | pub fn is_directory(&self) -> bool { |
a6bd6698 | 161 | matches!(self.attr, DirEntryAttribute::Directory { .. }) |
c9f00221 | 162 | } |
c2f91494 CE |
163 | |
164 | /// Check if DirEntry is a symlink | |
165 | pub fn is_symlink(&self) -> bool { | |
a6bd6698 | 166 | matches!(self.attr, DirEntryAttribute::Symlink { .. }) |
c2f91494 | 167 | } |
7d017123 DM |
168 | } |
169 | ||
bf6e3217 DM |
170 | struct DirInfo { |
171 | name: CString, | |
172 | entries: Vec<DirEntry>, | |
9d135fe6 DM |
173 | } |
174 | ||
bf6e3217 | 175 | impl DirInfo { |
bf6e3217 | 176 | fn new(name: CString) -> Self { |
42c2b5be TL |
177 | DirInfo { |
178 | name, | |
179 | entries: Vec::new(), | |
180 | } | |
bf6e3217 DM |
181 | } |
182 | ||
183 | fn new_rootdir() -> Self { | |
184 | DirInfo::new(CString::new(b"/".to_vec()).unwrap()) | |
185 | } | |
186 | ||
42c2b5be | 187 | fn encode_entry<W: Write>(writer: &mut W, entry: &DirEntry, pos: u64) -> Result<(), Error> { |
bf6e3217 | 188 | match entry { |
42c2b5be TL |
189 | DirEntry { |
190 | name, | |
191 | attr: DirEntryAttribute::Directory { start }, | |
192 | } => { | |
12a1975a DM |
193 | writer.write_all(&[CatalogEntryType::Directory as u8])?; |
194 | catalog_encode_u64(writer, name.len() as u64)?; | |
195 | writer.write_all(name)?; | |
196 | catalog_encode_u64(writer, pos - start)?; | |
bf6e3217 | 197 | } |
42c2b5be TL |
198 | DirEntry { |
199 | name, | |
200 | attr: DirEntryAttribute::File { size, mtime }, | |
201 | } => { | |
12a1975a DM |
202 | writer.write_all(&[CatalogEntryType::File as u8])?; |
203 | catalog_encode_u64(writer, name.len() as u64)?; | |
204 | writer.write_all(name)?; | |
205 | catalog_encode_u64(writer, *size)?; | |
5499bd3d | 206 | catalog_encode_i64(writer, *mtime)?; |
bf6e3217 | 207 | } |
42c2b5be TL |
208 | DirEntry { |
209 | name, | |
210 | attr: DirEntryAttribute::Symlink, | |
211 | } => { | |
12a1975a DM |
212 | writer.write_all(&[CatalogEntryType::Symlink as u8])?; |
213 | catalog_encode_u64(writer, name.len() as u64)?; | |
214 | writer.write_all(name)?; | |
bf6e3217 | 215 | } |
42c2b5be TL |
216 | DirEntry { |
217 | name, | |
218 | attr: DirEntryAttribute::Hardlink, | |
219 | } => { | |
12a1975a DM |
220 | writer.write_all(&[CatalogEntryType::Hardlink as u8])?; |
221 | catalog_encode_u64(writer, name.len() as u64)?; | |
222 | writer.write_all(name)?; | |
bf6e3217 | 223 | } |
42c2b5be TL |
224 | DirEntry { |
225 | name, | |
226 | attr: DirEntryAttribute::BlockDevice, | |
227 | } => { | |
12a1975a DM |
228 | writer.write_all(&[CatalogEntryType::BlockDevice as u8])?; |
229 | catalog_encode_u64(writer, name.len() as u64)?; | |
230 | writer.write_all(name)?; | |
bf6e3217 | 231 | } |
42c2b5be TL |
232 | DirEntry { |
233 | name, | |
234 | attr: DirEntryAttribute::CharDevice, | |
235 | } => { | |
12a1975a DM |
236 | writer.write_all(&[CatalogEntryType::CharDevice as u8])?; |
237 | catalog_encode_u64(writer, name.len() as u64)?; | |
238 | writer.write_all(name)?; | |
bf6e3217 | 239 | } |
42c2b5be TL |
240 | DirEntry { |
241 | name, | |
242 | attr: DirEntryAttribute::Fifo, | |
243 | } => { | |
12a1975a DM |
244 | writer.write_all(&[CatalogEntryType::Fifo as u8])?; |
245 | catalog_encode_u64(writer, name.len() as u64)?; | |
246 | writer.write_all(name)?; | |
bf6e3217 | 247 | } |
42c2b5be TL |
248 | DirEntry { |
249 | name, | |
250 | attr: DirEntryAttribute::Socket, | |
251 | } => { | |
12a1975a DM |
252 | writer.write_all(&[CatalogEntryType::Socket as u8])?; |
253 | catalog_encode_u64(writer, name.len() as u64)?; | |
254 | writer.write_all(name)?; | |
bf6e3217 DM |
255 | } |
256 | } | |
12a1975a | 257 | Ok(()) |
9d135fe6 | 258 | } |
bf6e3217 DM |
259 | |
260 | fn encode(self, start: u64) -> Result<(CString, Vec<u8>), Error> { | |
261 | let mut table = Vec::new(); | |
12a1975a | 262 | catalog_encode_u64(&mut table, self.entries.len() as u64)?; |
bf6e3217 | 263 | for entry in self.entries { |
12a1975a | 264 | Self::encode_entry(&mut table, &entry, start)?; |
bf6e3217 DM |
265 | } |
266 | ||
12a1975a DM |
267 | let mut data = Vec::new(); |
268 | catalog_encode_u64(&mut data, table.len() as u64)?; | |
269 | data.extend_from_slice(&table); | |
270 | ||
271 | Ok((self.name, data)) | |
9d135fe6 | 272 | } |
26566451 | 273 | |
5499bd3d | 274 | fn parse<C: FnMut(CatalogEntryType, &[u8], u64, u64, i64) -> Result<bool, Error>>( |
26566451 DM |
275 | data: &[u8], |
276 | mut callback: C, | |
277 | ) -> Result<(), Error> { | |
26566451 DM |
278 | let mut cursor = data; |
279 | ||
280 | let entries = catalog_decode_u64(&mut cursor)?; | |
281 | ||
55c0b3cc DM |
282 | let mut name_buf = vec![0u8; 4096]; |
283 | ||
26566451 | 284 | for _ in 0..entries { |
42c2b5be | 285 | let mut buf = [0u8]; |
26566451 DM |
286 | cursor.read_exact(&mut buf)?; |
287 | let etype = CatalogEntryType::try_from(buf[0])?; | |
288 | ||
55c0b3cc DM |
289 | let name_len = catalog_decode_u64(&mut cursor)? as usize; |
290 | if name_len >= name_buf.len() { | |
42c2b5be TL |
291 | bail!( |
292 | "directory entry name too long ({} >= {})", | |
293 | name_len, | |
294 | name_buf.len() | |
295 | ); | |
55c0b3cc DM |
296 | } |
297 | let name = &mut name_buf[0..name_len]; | |
298 | cursor.read_exact(name)?; | |
26566451 | 299 | |
8f24a9ea | 300 | let cont = match etype { |
26566451 DM |
301 | CatalogEntryType::Directory => { |
302 | let offset = catalog_decode_u64(&mut cursor)?; | |
8f24a9ea | 303 | callback(etype, name, offset, 0, 0)? |
26566451 DM |
304 | } |
305 | CatalogEntryType::File => { | |
306 | let size = catalog_decode_u64(&mut cursor)?; | |
5499bd3d | 307 | let mtime = catalog_decode_i64(&mut cursor)?; |
8f24a9ea | 308 | callback(etype, name, 0, size, mtime)? |
26566451 | 309 | } |
42c2b5be | 310 | _ => callback(etype, name, 0, 0, 0)?, |
8f24a9ea DM |
311 | }; |
312 | if !cont { | |
313 | return Ok(()); | |
26566451 DM |
314 | } |
315 | } | |
316 | ||
317 | if !cursor.is_empty() { | |
318 | bail!("unable to parse whole catalog data block"); | |
319 | } | |
320 | ||
321 | Ok(()) | |
322 | } | |
bf6e3217 DM |
323 | } |
324 | ||
780dd2b0 DM |
325 | /// Write small catalog files |
326 | /// | |
327 | /// A Catalogs simply contains list of files and directories | |
328 | /// (directory tree). They are use to find content without having to | |
329 | /// search the real archive (which may be large). For files, they | |
330 | /// include the last modification time and file size. | |
bf6e3217 DM |
331 | pub struct CatalogWriter<W> { |
332 | writer: W, | |
333 | dirstack: Vec<DirInfo>, | |
334 | pos: u64, | |
335 | } | |
336 | ||
42c2b5be | 337 | impl<W: Write> CatalogWriter<W> { |
780dd2b0 | 338 | /// Create a new CatalogWriter instance |
bf6e3217 | 339 | pub fn new(writer: W) -> Result<Self, Error> { |
42c2b5be TL |
340 | let mut me = Self { |
341 | writer, | |
342 | dirstack: vec![DirInfo::new_rootdir()], | |
343 | pos: 0, | |
344 | }; | |
c74c074b DM |
345 | me.write_all(&PROXMOX_CATALOG_FILE_MAGIC_1_0)?; |
346 | Ok(me) | |
9d135fe6 | 347 | } |
bf6e3217 | 348 | |
f89359c2 DM |
349 | fn write_all(&mut self, data: &[u8]) -> Result<(), Error> { |
350 | self.writer.write_all(data)?; | |
351 | self.pos += u64::try_from(data.len())?; | |
352 | Ok(()) | |
353 | } | |
354 | ||
780dd2b0 DM |
355 | /// Finish writing, flush all data |
356 | /// | |
357 | /// This need to be called before drop. | |
bf6e3217 DM |
358 | pub fn finish(&mut self) -> Result<(), Error> { |
359 | if self.dirstack.len() != 1 { | |
360 | bail!("unable to finish catalog at level {}", self.dirstack.len()); | |
361 | } | |
362 | ||
363 | let dir = self.dirstack.pop().unwrap(); | |
364 | ||
365 | let start = self.pos; | |
366 | let (_, data) = dir.encode(start)?; | |
367 | self.write_all(&data)?; | |
368 | ||
369 | self.write_all(&start.to_le_bytes())?; | |
370 | ||
371 | self.writer.flush()?; | |
372 | ||
373 | Ok(()) | |
9d135fe6 DM |
374 | } |
375 | } | |
376 | ||
42c2b5be | 377 | impl<W: Write> BackupCatalogWriter for CatalogWriter<W> { |
9d135fe6 | 378 | fn start_directory(&mut self, name: &CStr) -> Result<(), Error> { |
bf6e3217 DM |
379 | let new = DirInfo::new(name.to_owned()); |
380 | self.dirstack.push(new); | |
9d135fe6 DM |
381 | Ok(()) |
382 | } | |
383 | ||
384 | fn end_directory(&mut self) -> Result<(), Error> { | |
bf6e3217 DM |
385 | let (start, name) = match self.dirstack.pop() { |
386 | Some(dir) => { | |
387 | let start = self.pos; | |
388 | let (name, data) = dir.encode(start)?; | |
389 | self.write_all(&data)?; | |
390 | (start, name) | |
391 | } | |
392 | None => { | |
393 | bail!("got unexpected end_directory level 0"); | |
394 | } | |
395 | }; | |
396 | ||
42c2b5be TL |
397 | let current = self |
398 | .dirstack | |
399 | .last_mut() | |
400 | .ok_or_else(|| format_err!("outside root"))?; | |
bf6e3217 | 401 | let name = name.to_bytes().to_vec(); |
42c2b5be TL |
402 | current.entries.push(DirEntry { |
403 | name, | |
404 | attr: DirEntryAttribute::Directory { start }, | |
405 | }); | |
bf6e3217 | 406 | |
9d135fe6 DM |
407 | Ok(()) |
408 | } | |
409 | ||
5499bd3d | 410 | fn add_file(&mut self, name: &CStr, size: u64, mtime: i64) -> Result<(), Error> { |
42c2b5be TL |
411 | let dir = self |
412 | .dirstack | |
413 | .last_mut() | |
414 | .ok_or_else(|| format_err!("outside root"))?; | |
bf6e3217 | 415 | let name = name.to_bytes().to_vec(); |
42c2b5be TL |
416 | dir.entries.push(DirEntry { |
417 | name, | |
418 | attr: DirEntryAttribute::File { size, mtime }, | |
419 | }); | |
9d135fe6 DM |
420 | Ok(()) |
421 | } | |
422 | ||
423 | fn add_symlink(&mut self, name: &CStr) -> Result<(), Error> { | |
42c2b5be TL |
424 | let dir = self |
425 | .dirstack | |
426 | .last_mut() | |
427 | .ok_or_else(|| format_err!("outside root"))?; | |
bf6e3217 | 428 | let name = name.to_bytes().to_vec(); |
42c2b5be TL |
429 | dir.entries.push(DirEntry { |
430 | name, | |
431 | attr: DirEntryAttribute::Symlink, | |
432 | }); | |
9d135fe6 DM |
433 | Ok(()) |
434 | } | |
435 | ||
436 | fn add_hardlink(&mut self, name: &CStr) -> Result<(), Error> { | |
42c2b5be TL |
437 | let dir = self |
438 | .dirstack | |
439 | .last_mut() | |
440 | .ok_or_else(|| format_err!("outside root"))?; | |
bf6e3217 | 441 | let name = name.to_bytes().to_vec(); |
42c2b5be TL |
442 | dir.entries.push(DirEntry { |
443 | name, | |
444 | attr: DirEntryAttribute::Hardlink, | |
445 | }); | |
9d135fe6 DM |
446 | Ok(()) |
447 | } | |
448 | ||
449 | fn add_block_device(&mut self, name: &CStr) -> Result<(), Error> { | |
42c2b5be TL |
450 | let dir = self |
451 | .dirstack | |
452 | .last_mut() | |
453 | .ok_or_else(|| format_err!("outside root"))?; | |
bf6e3217 | 454 | let name = name.to_bytes().to_vec(); |
42c2b5be TL |
455 | dir.entries.push(DirEntry { |
456 | name, | |
457 | attr: DirEntryAttribute::BlockDevice, | |
458 | }); | |
55c0b3cc | 459 | Ok(()) |
9d135fe6 DM |
460 | } |
461 | ||
462 | fn add_char_device(&mut self, name: &CStr) -> Result<(), Error> { | |
42c2b5be TL |
463 | let dir = self |
464 | .dirstack | |
465 | .last_mut() | |
466 | .ok_or_else(|| format_err!("outside root"))?; | |
bf6e3217 | 467 | let name = name.to_bytes().to_vec(); |
42c2b5be TL |
468 | dir.entries.push(DirEntry { |
469 | name, | |
470 | attr: DirEntryAttribute::CharDevice, | |
471 | }); | |
9d135fe6 DM |
472 | Ok(()) |
473 | } | |
474 | ||
475 | fn add_fifo(&mut self, name: &CStr) -> Result<(), Error> { | |
42c2b5be TL |
476 | let dir = self |
477 | .dirstack | |
478 | .last_mut() | |
479 | .ok_or_else(|| format_err!("outside root"))?; | |
bf6e3217 | 480 | let name = name.to_bytes().to_vec(); |
42c2b5be TL |
481 | dir.entries.push(DirEntry { |
482 | name, | |
483 | attr: DirEntryAttribute::Fifo, | |
484 | }); | |
9d135fe6 DM |
485 | Ok(()) |
486 | } | |
487 | ||
488 | fn add_socket(&mut self, name: &CStr) -> Result<(), Error> { | |
42c2b5be TL |
489 | let dir = self |
490 | .dirstack | |
491 | .last_mut() | |
492 | .ok_or_else(|| format_err!("outside root"))?; | |
bf6e3217 | 493 | let name = name.to_bytes().to_vec(); |
42c2b5be TL |
494 | dir.entries.push(DirEntry { |
495 | name, | |
496 | attr: DirEntryAttribute::Socket, | |
497 | }); | |
9d135fe6 DM |
498 | Ok(()) |
499 | } | |
500 | } | |
501 | ||
780dd2b0 | 502 | /// Read Catalog files |
bf6e3217 DM |
503 | pub struct CatalogReader<R> { |
504 | reader: R, | |
505 | } | |
9d135fe6 | 506 | |
42c2b5be | 507 | impl<R: Read + Seek> CatalogReader<R> { |
780dd2b0 | 508 | /// Create a new CatalogReader instance |
bf6e3217 DM |
509 | pub fn new(reader: R) -> Self { |
510 | Self { reader } | |
9d135fe6 DM |
511 | } |
512 | ||
780dd2b0 | 513 | /// Print whole catalog to stdout |
bf6e3217 | 514 | pub fn dump(&mut self) -> Result<(), Error> { |
2ec208ae DM |
515 | let root = self.root()?; |
516 | match root { | |
42c2b5be TL |
517 | DirEntry { |
518 | attr: DirEntryAttribute::Directory { start }, | |
519 | .. | |
520 | } => self.dump_dir(std::path::Path::new("./"), start), | |
48efb526 | 521 | _ => bail!("unexpected root entry type, not a directory!"), |
2ec208ae | 522 | } |
bf6e3217 | 523 | } |
9d135fe6 | 524 | |
7d017123 | 525 | /// Get the root DirEntry |
42c2b5be | 526 | pub fn root(&mut self) -> Result<DirEntry, Error> { |
7d017123 | 527 | // Root dir is special |
6aa906b5 | 528 | self.reader.seek(SeekFrom::Start(0))?; |
42c2b5be | 529 | let mut magic = [0u8; 8]; |
6aa906b5 DM |
530 | self.reader.read_exact(&mut magic)?; |
531 | if magic != PROXMOX_CATALOG_FILE_MAGIC_1_0 { | |
532 | bail!("got unexpected magic number for catalog"); | |
533 | } | |
7d017123 DM |
534 | self.reader.seek(SeekFrom::End(-8))?; |
535 | let start = unsafe { self.reader.read_le_value::<u64>()? }; | |
42c2b5be TL |
536 | Ok(DirEntry { |
537 | name: b"".to_vec(), | |
538 | attr: DirEntryAttribute::Directory { start }, | |
539 | }) | |
7d017123 | 540 | } |
9d135fe6 | 541 | |
7d017123 | 542 | /// Read all directory entries |
42c2b5be | 543 | pub fn read_dir(&mut self, parent: &DirEntry) -> Result<Vec<DirEntry>, Error> { |
7d017123 DM |
544 | let start = match parent.attr { |
545 | DirEntryAttribute::Directory { start } => start, | |
546 | _ => bail!("parent is not a directory - internal error"), | |
547 | }; | |
9d135fe6 | 548 | |
7d017123 | 549 | let data = self.read_raw_dirinfo_block(start)?; |
9d135fe6 | 550 | |
7d017123 DM |
551 | let mut entry_list = Vec::new(); |
552 | ||
553 | DirInfo::parse(&data, |etype, name, offset, size, mtime| { | |
b423958d | 554 | let entry = DirEntry::new(etype, name.to_vec(), start - offset, size, mtime); |
7d017123 | 555 | entry_list.push(entry); |
8f24a9ea | 556 | Ok(true) |
7d017123 DM |
557 | })?; |
558 | ||
559 | Ok(entry_list) | |
560 | } | |
561 | ||
227501c0 | 562 | /// Lookup a DirEntry from an absolute path |
42c2b5be | 563 | pub fn lookup_recursive(&mut self, path: &[u8]) -> Result<DirEntry, Error> { |
227501c0 DC |
564 | let mut current = self.root()?; |
565 | if path == b"/" { | |
566 | return Ok(current); | |
567 | } | |
568 | ||
569 | let components = if !path.is_empty() && path[0] == b'/' { | |
570 | &path[1..] | |
571 | } else { | |
572 | path | |
42c2b5be TL |
573 | } |
574 | .split(|c| *c == b'/'); | |
227501c0 DC |
575 | |
576 | for comp in components { | |
577 | if let Some(entry) = self.lookup(¤t, comp)? { | |
578 | current = entry; | |
579 | } else { | |
42c2b5be TL |
580 | bail!( |
581 | "path {:?} not found in catalog", | |
582 | String::from_utf8_lossy(path) | |
583 | ); | |
227501c0 DC |
584 | } |
585 | } | |
586 | Ok(current) | |
587 | } | |
588 | ||
7d017123 DM |
589 | /// Lockup a DirEntry inside a parent directory |
590 | pub fn lookup( | |
591 | &mut self, | |
592 | parent: &DirEntry, | |
593 | filename: &[u8], | |
42c2b5be | 594 | ) -> Result<Option<DirEntry>, Error> { |
7d017123 DM |
595 | let start = match parent.attr { |
596 | DirEntryAttribute::Directory { start } => start, | |
597 | _ => bail!("parent is not a directory - internal error"), | |
598 | }; | |
599 | ||
600 | let data = self.read_raw_dirinfo_block(start)?; | |
601 | ||
602 | let mut item = None; | |
603 | DirInfo::parse(&data, |etype, name, offset, size, mtime| { | |
604 | if name != filename { | |
8f24a9ea | 605 | return Ok(true); |
7d017123 DM |
606 | } |
607 | ||
b423958d | 608 | let entry = DirEntry::new(etype, name.to_vec(), start - offset, size, mtime); |
7d017123 | 609 | item = Some(entry); |
8f24a9ea | 610 | Ok(false) // stop parsing |
7d017123 DM |
611 | })?; |
612 | ||
c443f58b | 613 | Ok(item) |
7d017123 DM |
614 | } |
615 | ||
616 | /// Read the raw directory info block from current reader position. | |
42c2b5be | 617 | fn read_raw_dirinfo_block(&mut self, start: u64) -> Result<Vec<u8>, Error> { |
7d017123 DM |
618 | self.reader.seek(SeekFrom::Start(start))?; |
619 | let size = catalog_decode_u64(&mut self.reader)?; | |
42c2b5be TL |
620 | if size < 1 { |
621 | bail!("got small directory size {}", size) | |
622 | }; | |
12a1975a | 623 | let data = self.reader.read_exact_allocated(size as usize)?; |
7d017123 DM |
624 | Ok(data) |
625 | } | |
626 | ||
780dd2b0 | 627 | /// Print the content of a directory to stdout |
7d017123 | 628 | pub fn dump_dir(&mut self, prefix: &std::path::Path, start: u64) -> Result<(), Error> { |
7d017123 | 629 | let data = self.read_raw_dirinfo_block(start)?; |
9d135fe6 | 630 | |
26566451 | 631 | DirInfo::parse(&data, |etype, name, offset, size, mtime| { |
bf6e3217 | 632 | let mut path = std::path::PathBuf::from(prefix); |
55c0b3cc DM |
633 | let name: &OsStr = OsStrExt::from_bytes(name); |
634 | path.push(name); | |
9d135fe6 | 635 | |
9d135fe6 DM |
636 | match etype { |
637 | CatalogEntryType::Directory => { | |
dce4b540 | 638 | log::info!("{} {:?}", etype, path); |
bf6e3217 DM |
639 | if offset > start { |
640 | bail!("got wrong directory offset ({} > {})", offset, start); | |
641 | } | |
642 | let pos = start - offset; | |
643 | self.dump_dir(&path, pos)?; | |
9d135fe6 DM |
644 | } |
645 | CatalogEntryType::File => { | |
6a7be83e | 646 | let mut mtime_string = mtime.to_string(); |
a57413a5 | 647 | if let Ok(s) = proxmox_time::strftime_local("%FT%TZ", mtime) { |
6a7be83e DM |
648 | mtime_string = s; |
649 | } | |
bf6e3217 | 650 | |
dce4b540 | 651 | log::info!("{} {:?} {} {}", etype, path, size, mtime_string,); |
9d135fe6 | 652 | } |
bf6e3217 | 653 | _ => { |
dce4b540 | 654 | log::info!("{} {:?}", etype, path); |
9d135fe6 DM |
655 | } |
656 | } | |
5d92935e | 657 | |
8f24a9ea | 658 | Ok(true) |
26566451 | 659 | }) |
9d135fe6 | 660 | } |
90dfd0a7 CE |
661 | |
662 | /// Finds all entries matching the given match patterns and calls the | |
663 | /// provided callback on them. | |
e9c18590 | 664 | pub fn find<'a>( |
90dfd0a7 | 665 | &mut self, |
c443f58b WB |
666 | parent: &DirEntry, |
667 | file_path: &mut Vec<u8>, | |
e9c18590 | 668 | match_list: &'a impl MatchList<'a>, //&[MatchEntry], |
c443f58b | 669 | callback: &mut dyn FnMut(&[u8]) -> Result<(), Error>, |
90dfd0a7 | 670 | ) -> Result<(), Error> { |
c443f58b | 671 | let file_len = file_path.len(); |
42c2b5be | 672 | for e in self.read_dir(parent)? { |
c443f58b WB |
673 | let is_dir = e.is_directory(); |
674 | file_path.truncate(file_len); | |
675 | if !e.name.starts_with(b"/") { | |
676 | file_path.reserve(e.name.len() + 1); | |
677 | file_path.push(b'/'); | |
678 | } | |
679 | file_path.extend(&e.name); | |
680 | match match_list.matches(&file_path, e.get_file_mode()) { | |
b65908fa GG |
681 | Ok(Some(MatchType::Exclude)) => continue, |
682 | Ok(Some(MatchType::Include)) => callback(file_path)?, | |
683 | _ => (), | |
c443f58b WB |
684 | } |
685 | if is_dir { | |
686 | self.find(&e, file_path, match_list, callback)?; | |
90dfd0a7 | 687 | } |
90dfd0a7 | 688 | } |
c443f58b | 689 | file_path.truncate(file_len); |
38d9a698 | 690 | |
90dfd0a7 CE |
691 | Ok(()) |
692 | } | |
86582454 WB |
693 | |
694 | /// Returns the list of content of the given path | |
695 | pub fn list_dir_contents(&mut self, path: &[u8]) -> Result<Vec<ArchiveEntry>, Error> { | |
696 | let dir = self.lookup_recursive(path)?; | |
697 | let mut res = vec![]; | |
698 | let mut path = path.to_vec(); | |
699 | if !path.is_empty() && path[0] == b'/' { | |
700 | path.remove(0); | |
701 | } | |
702 | ||
703 | for direntry in self.read_dir(&dir)? { | |
704 | let mut components = path.clone(); | |
705 | components.push(b'/'); | |
706 | components.extend(&direntry.name); | |
707 | let mut entry = ArchiveEntry::new(&components, Some(&direntry.attr)); | |
a7646fe4 TL |
708 | if let DirEntryAttribute::File { size, mtime } = direntry.attr { |
709 | entry.size = size.into(); | |
710 | entry.mtime = mtime.into(); | |
86582454 WB |
711 | } |
712 | res.push(entry); | |
713 | } | |
714 | ||
715 | Ok(res) | |
716 | } | |
9d135fe6 | 717 | } |
12a1975a | 718 | |
5499bd3d DC |
719 | /// Serialize i64 as short, variable length byte sequence |
720 | /// | |
721 | /// Stores 7 bits per byte, Bit 8 indicates the end of the sequence (when not set). | |
722 | /// If the value is negative, we end with a zero byte (0x00). | |
ea368a06 | 723 | #[allow(clippy::neg_multiply)] |
5499bd3d DC |
724 | pub fn catalog_encode_i64<W: Write>(writer: &mut W, v: i64) -> Result<(), Error> { |
725 | let mut enc = Vec::new(); | |
726 | ||
727 | let mut d = if v < 0 { | |
728 | (-1 * (v + 1)) as u64 + 1 // also handles i64::MIN | |
729 | } else { | |
730 | v as u64 | |
731 | }; | |
732 | ||
733 | loop { | |
734 | if d < 128 { | |
735 | if v < 0 { | |
736 | enc.push(128 | d as u8); | |
737 | enc.push(0u8); | |
738 | } else { | |
739 | enc.push(d as u8); | |
740 | } | |
741 | break; | |
742 | } | |
743 | enc.push((128 | (d & 127)) as u8); | |
a3775bb4 | 744 | d >>= 7; |
5499bd3d DC |
745 | } |
746 | writer.write_all(&enc)?; | |
747 | ||
748 | Ok(()) | |
749 | } | |
750 | ||
751 | /// Deserialize i64 from variable length byte sequence | |
752 | /// | |
753 | /// We currently read maximal 11 bytes, which give a maximum of 70 bits + sign. | |
754 | /// this method is compatible with catalog_encode_u64 iff the | |
755 | /// value encoded is <= 2^63 (values > 2^63 cannot be represented in an i64) | |
ea368a06 | 756 | #[allow(clippy::neg_multiply)] |
5499bd3d | 757 | pub fn catalog_decode_i64<R: Read>(reader: &mut R) -> Result<i64, Error> { |
5499bd3d DC |
758 | let mut v: u64 = 0; |
759 | let mut buf = [0u8]; | |
760 | ||
42c2b5be TL |
761 | for i in 0..11 { |
762 | // only allow 11 bytes (70 bits + sign marker) | |
5499bd3d DC |
763 | if buf.is_empty() { |
764 | bail!("decode_i64 failed - unexpected EOB"); | |
765 | } | |
766 | reader.read_exact(&mut buf)?; | |
767 | ||
768 | let t = buf[0]; | |
769 | ||
770 | if t == 0 { | |
771 | if v == 0 { | |
772 | return Ok(0); | |
773 | } | |
774 | return Ok(((v - 1) as i64 * -1) - 1); // also handles i64::MIN | |
775 | } else if t < 128 { | |
42c2b5be | 776 | v |= (t as u64) << (i * 7); |
5499bd3d DC |
777 | return Ok(v as i64); |
778 | } else { | |
42c2b5be | 779 | v |= ((t & 127) as u64) << (i * 7); |
5499bd3d DC |
780 | } |
781 | } | |
782 | ||
783 | bail!("decode_i64 failed - missing end marker"); | |
784 | } | |
785 | ||
12a1975a DM |
786 | /// Serialize u64 as short, variable length byte sequence |
787 | /// | |
788 | /// Stores 7 bits per byte, Bit 8 indicates the end of the sequence (when not set). | |
12a1975a DM |
789 | pub fn catalog_encode_u64<W: Write>(writer: &mut W, v: u64) -> Result<(), Error> { |
790 | let mut enc = Vec::new(); | |
791 | ||
12a1975a DM |
792 | let mut d = v; |
793 | loop { | |
794 | if d < 128 { | |
795 | enc.push(d as u8); | |
796 | break; | |
797 | } | |
798 | enc.push((128 | (d & 127)) as u8); | |
a3775bb4 | 799 | d >>= 7; |
12a1975a DM |
800 | } |
801 | writer.write_all(&enc)?; | |
802 | ||
803 | Ok(()) | |
804 | } | |
805 | ||
806 | /// Deserialize u64 from variable length byte sequence | |
807 | /// | |
5499bd3d DC |
808 | /// We currently read maximal 10 bytes, which give a maximum of 70 bits, |
809 | /// but we currently only encode up to 64 bits | |
12a1975a | 810 | pub fn catalog_decode_u64<R: Read>(reader: &mut R) -> Result<u64, Error> { |
12a1975a DM |
811 | let mut v: u64 = 0; |
812 | let mut buf = [0u8]; | |
813 | ||
42c2b5be TL |
814 | for i in 0..10 { |
815 | // only allow 10 bytes (70 bits) | |
12a1975a DM |
816 | if buf.is_empty() { |
817 | bail!("decode_u64 failed - unexpected EOB"); | |
818 | } | |
819 | reader.read_exact(&mut buf)?; | |
820 | let t = buf[0]; | |
821 | if t < 128 { | |
42c2b5be | 822 | v |= (t as u64) << (i * 7); |
12a1975a DM |
823 | return Ok(v); |
824 | } else { | |
42c2b5be | 825 | v |= ((t & 127) as u64) << (i * 7); |
12a1975a DM |
826 | } |
827 | } | |
828 | ||
829 | bail!("decode_u64 failed - missing end marker"); | |
830 | } | |
831 | ||
832 | #[test] | |
833 | fn test_catalog_u64_encoder() { | |
12a1975a | 834 | fn test_encode_decode(value: u64) { |
12a1975a DM |
835 | let mut data = Vec::new(); |
836 | catalog_encode_u64(&mut data, value).unwrap(); | |
837 | ||
838 | //println!("ENCODE {} {:?}", value, data); | |
839 | ||
840 | let slice = &mut &data[..]; | |
841 | let decoded = catalog_decode_u64(slice).unwrap(); | |
842 | ||
843 | //println!("DECODE {}", decoded); | |
844 | ||
845 | assert!(decoded == value); | |
846 | } | |
847 | ||
5499bd3d DC |
848 | test_encode_decode(u64::MIN); |
849 | test_encode_decode(126); | |
42c2b5be TL |
850 | test_encode_decode((1 << 12) - 1); |
851 | test_encode_decode((1 << 20) - 1); | |
852 | test_encode_decode((1 << 50) - 1); | |
5499bd3d DC |
853 | test_encode_decode(u64::MAX); |
854 | } | |
855 | ||
856 | #[test] | |
857 | fn test_catalog_i64_encoder() { | |
5499bd3d | 858 | fn test_encode_decode(value: i64) { |
5499bd3d DC |
859 | let mut data = Vec::new(); |
860 | catalog_encode_i64(&mut data, value).unwrap(); | |
861 | ||
862 | let slice = &mut &data[..]; | |
863 | let decoded = catalog_decode_i64(slice).unwrap(); | |
864 | ||
865 | assert!(decoded == value); | |
866 | } | |
867 | ||
868 | test_encode_decode(0); | |
869 | test_encode_decode(-0); | |
870 | test_encode_decode(126); | |
871 | test_encode_decode(-126); | |
42c2b5be TL |
872 | test_encode_decode((1 << 12) - 1); |
873 | test_encode_decode(-(1 << 12) - 1); | |
874 | test_encode_decode((1 << 20) - 1); | |
875 | test_encode_decode(-(1 << 20) - 1); | |
5499bd3d DC |
876 | test_encode_decode(i64::MIN); |
877 | test_encode_decode(i64::MAX); | |
878 | } | |
879 | ||
880 | #[test] | |
881 | fn test_catalog_i64_compatibility() { | |
5499bd3d | 882 | fn test_encode_decode(value: u64) { |
5499bd3d DC |
883 | let mut data = Vec::new(); |
884 | catalog_encode_u64(&mut data, value).unwrap(); | |
885 | ||
886 | let slice = &mut &data[..]; | |
887 | let decoded = catalog_decode_i64(slice).unwrap() as u64; | |
888 | ||
889 | assert!(decoded == value); | |
890 | } | |
891 | ||
892 | test_encode_decode(u64::MIN); | |
12a1975a | 893 | test_encode_decode(126); |
42c2b5be TL |
894 | test_encode_decode((1 << 12) - 1); |
895 | test_encode_decode((1 << 20) - 1); | |
896 | test_encode_decode((1 << 50) - 1); | |
5499bd3d | 897 | test_encode_decode(u64::MAX); |
12a1975a | 898 | } |
013b1e8b WB |
899 | |
900 | /// An entry in a hierarchy of files for restore and listing. | |
901 | #[api] | |
902 | #[derive(Serialize, Deserialize)] | |
903 | pub struct ArchiveEntry { | |
904 | /// Base64-encoded full path to the file, including the filename | |
905 | pub filepath: String, | |
906 | /// Displayable filename text for UIs | |
907 | pub text: String, | |
908 | /// File or directory type of this entry | |
909 | #[serde(rename = "type")] | |
910 | pub entry_type: String, | |
911 | /// Is this entry a leaf node, or does it have children (i.e. a directory)? | |
912 | pub leaf: bool, | |
a7646fe4 | 913 | /// The file size, if entry_type is 'f' (file) |
42c2b5be | 914 | #[serde(skip_serializing_if = "Option::is_none")] |
013b1e8b WB |
915 | pub size: Option<u64>, |
916 | /// The file "last modified" time stamp, if entry_type is 'f' (file) | |
42c2b5be | 917 | #[serde(skip_serializing_if = "Option::is_none")] |
013b1e8b WB |
918 | pub mtime: Option<i64>, |
919 | } | |
920 | ||
921 | impl ArchiveEntry { | |
922 | pub fn new(filepath: &[u8], entry_type: Option<&DirEntryAttribute>) -> Self { | |
923 | let size = match entry_type { | |
924 | Some(DirEntryAttribute::File { size, .. }) => Some(*size), | |
925 | _ => None, | |
926 | }; | |
927 | Self::new_with_size(filepath, entry_type, size) | |
928 | } | |
929 | ||
930 | pub fn new_with_size( | |
931 | filepath: &[u8], | |
932 | entry_type: Option<&DirEntryAttribute>, | |
933 | size: Option<u64>, | |
934 | ) -> Self { | |
935 | Self { | |
936 | filepath: base64::encode(filepath), | |
937 | text: String::from_utf8_lossy(filepath.split(|x| *x == b'/').last().unwrap()) | |
938 | .to_string(), | |
939 | entry_type: match entry_type { | |
940 | Some(entry_type) => CatalogEntryType::from(entry_type).to_string(), | |
941 | None => "v".to_owned(), | |
942 | }, | |
943 | leaf: !matches!(entry_type, None | Some(DirEntryAttribute::Directory { .. })), | |
944 | size, | |
945 | mtime: match entry_type { | |
946 | Some(DirEntryAttribute::File { mtime, .. }) => Some(*mtime), | |
947 | _ => None, | |
948 | }, | |
949 | } | |
950 | } | |
951 | } |