1use flate2::{
2 read::{DeflateDecoder, GzDecoder},
3 write::{DeflateEncoder, GzEncoder},
4};
5use positioned_io::ReadAt;
6use std::{
7 fmt::{Debug, Formatter},
8 fs::{DirEntry, File, Permissions},
9 io::{Read, Seek, SeekFrom, Write},
10};
11
12mod varint;
13
14pub const FILE_SIGNATURE: &[u8] = b"DDUPBAK\0";
15
16#[derive(Debug, Clone, Copy)]
17pub enum CompressionFormat {
18 None,
19 Gzip,
20 Deflate,
21}
22
23impl CompressionFormat {
24 pub fn encode(&self) -> u8 {
25 match self {
26 CompressionFormat::None => 0,
27 CompressionFormat::Gzip => 1,
28 CompressionFormat::Deflate => 2,
29 }
30 }
31
32 pub fn decode(value: u8) -> Self {
33 match value {
34 0 => CompressionFormat::None,
35 1 => CompressionFormat::Gzip,
36 2 => CompressionFormat::Deflate,
37 _ => panic!("Invalid compression format"),
38 }
39 }
40}
41
42#[inline]
43fn encode_file_permissions(permissions: Permissions) -> u32 {
44 #[cfg(unix)]
45 {
46 use std::os::unix::fs::PermissionsExt;
47
48 permissions.mode()
49 }
50 #[cfg(windows)]
51 {
52 if permissions.readonly() { 1 } else { 0 }
53 }
54}
55#[inline]
56fn decode_file_permissions(mode: u32) -> Permissions {
57 #[cfg(unix)]
58 {
59 use std::os::unix::fs::PermissionsExt;
60
61 Permissions::from_mode(mode)
62 }
63 #[cfg(windows)]
64 {
65 let mut permissions = unsafe { std::mem::zeroed::<Permissions>() };
66 if mode == 1 {
67 permissions.set_readonly(true);
68 } else {
69 permissions.set_readonly(false);
70 }
71
72 permissions
73 }
74}
75
76#[inline]
77fn metadata_owner(_metadata: &std::fs::Metadata) -> (u32, u32) {
78 #[cfg(unix)]
79 {
80 use std::os::unix::fs::MetadataExt;
81
82 (_metadata.uid(), _metadata.gid())
83 }
84 #[cfg(windows)]
85 {
86 (0, 0)
87 }
88}
89#[inline]
90fn metadata_mtime(metadata: &std::fs::Metadata) -> i64 {
91 #[cfg(unix)]
92 {
93 use std::os::unix::fs::MetadataExt;
94
95 metadata.mtime()
96 }
97 #[cfg(windows)]
98 {
99 use std::os::windows::fs::MetadataExt;
100
101 metadata.last_write_time() as i64
102 }
103}
104
105pub struct FileEntry {
106 pub name: String,
107 pub mode: Permissions,
108 pub owner: (u32, u32),
109 pub mtime: i64,
110 pub compression: CompressionFormat,
111 pub size: u64,
112
113 file: File,
114 decoder: Option<Box<dyn Read + Send>>,
115 offset: u64,
116 consumed: u64,
117}
118
119impl Debug for FileEntry {
120 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
121 f.debug_struct("FileEntry")
122 .field("name", &self.name)
123 .field("mode", &self.mode)
124 .field("owner", &self.owner)
125 .field("mtime", &self.mtime)
126 .field("size", &self.size)
127 .field("offset", &self.offset)
128 .field("compression", &self.compression)
129 .finish()
130 }
131}
132
133impl Read for FileEntry {
134 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
135 if self.consumed >= self.size {
136 return Ok(0);
137 }
138
139 let remaining = self.size - self.consumed;
140
141 match self.compression {
142 CompressionFormat::None => {
143 let bytes_read = self.file.read_at(self.offset + self.consumed, buf)?;
144
145 if bytes_read > remaining as usize {
146 self.consumed += remaining;
147 return Ok(remaining as usize);
148 }
149
150 self.consumed += bytes_read as u64;
151 Ok(bytes_read)
152 }
153 CompressionFormat::Gzip => {
154 if self.decoder.is_none() {
155 self.file
156 .seek(SeekFrom::Start(self.offset + self.consumed))?;
157 let decoder = GzDecoder::new(self.file.try_clone()?);
158 self.decoder = Some(Box::new(decoder));
159 }
160
161 let decoder = self.decoder.as_mut().unwrap();
162 let bytes_read = decoder.read(buf)?;
163
164 if bytes_read > remaining as usize {
165 self.decoder = None;
166 self.consumed += remaining;
167 return Ok(remaining as usize);
168 }
169
170 self.consumed += bytes_read as u64;
171 Ok(bytes_read)
172 }
173 CompressionFormat::Deflate => {
174 if self.decoder.is_none() {
175 self.file
176 .seek(SeekFrom::Start(self.offset + self.consumed))?;
177 let decoder = DeflateDecoder::new(self.file.try_clone()?);
178 self.decoder = Some(Box::new(decoder));
179 }
180
181 let decoder = self.decoder.as_mut().unwrap();
182 let bytes_read = decoder.read(buf)?;
183
184 if bytes_read > remaining as usize {
185 self.decoder = None;
186 self.consumed += remaining;
187 return Ok(remaining as usize);
188 }
189
190 self.consumed += bytes_read as u64;
191 Ok(bytes_read)
192 }
193 }
194 }
195}
196
197#[derive(Debug)]
198pub struct DirectoryEntry {
199 pub name: String,
200 pub mode: Permissions,
201 pub owner: (u32, u32),
202 pub mtime: i64,
203 pub entries: Vec<Entry>,
204}
205
206#[derive(Debug)]
207pub struct SymlinkEntry {
208 pub name: String,
209 pub mode: Permissions,
210 pub owner: (u32, u32),
211 pub mtime: i64,
212 pub target: String,
213 pub target_dir: bool,
214}
215
216#[derive(Debug)]
217pub enum Entry {
218 File(Box<FileEntry>),
219 Directory(DirectoryEntry),
220 Symlink(SymlinkEntry),
221}
222
223impl Entry {
224 pub fn name(&self) -> &str {
228 match self {
229 Entry::File(entry) => &entry.name,
230 Entry::Directory(entry) => &entry.name,
231 Entry::Symlink(entry) => &entry.name,
232 }
233 }
234
235 pub fn mode(&self) -> Permissions {
238 match self {
239 Entry::File(entry) => entry.mode.clone(),
240 Entry::Directory(entry) => entry.mode.clone(),
241 Entry::Symlink(entry) => entry.mode.clone(),
242 }
243 }
244
245 pub fn owner(&self) -> (u32, u32) {
248 match self {
249 Entry::File(entry) => entry.owner,
250 Entry::Directory(entry) => entry.owner,
251 Entry::Symlink(entry) => entry.owner,
252 }
253 }
254
255 pub fn mtime(&self) -> i64 {
258 match self {
259 Entry::File(entry) => entry.mtime,
260 Entry::Directory(entry) => entry.mtime,
261 Entry::Symlink(entry) => entry.mtime,
262 }
263 }
264}
265
266type ProgressCallback = Option<fn(&std::path::PathBuf)>;
267type CompressionFormatCallback =
268 Option<fn(&std::path::PathBuf, &std::fs::Metadata) -> CompressionFormat>;
269
270pub struct Archive {
271 file: File,
272 compression_callback: CompressionFormatCallback,
273
274 entries: Vec<Entry>,
275 entries_offset: u64,
276}
277
278impl Debug for Archive {
279 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
280 f.debug_struct("Archive")
281 .field("entries", &self.entries)
282 .finish()
283 }
284}
285
286impl Archive {
287 pub fn new(mut file: File) -> Self {
291 file.set_len(0).unwrap();
292 file.write_all(FILE_SIGNATURE).unwrap();
293 file.sync_all().unwrap();
294
295 Self {
296 file,
297 compression_callback: None,
298 entries: Vec::new(),
299 entries_offset: 8,
300 }
301 }
302
303 pub fn open(path: &str) -> Result<Self, std::io::Error> {
306 let mut file = File::open(path)?;
307 let len = file.metadata()?.len();
308
309 let mut buffer = [0; 8];
310 file.read_exact(&mut buffer)?;
311 if buffer != FILE_SIGNATURE {
312 return Err(std::io::Error::new(
313 std::io::ErrorKind::InvalidData,
314 "Invalid file signature",
315 ));
316 }
317
318 file.read_exact_at(len - 16, &mut buffer)?;
319 let entries_count = u64::from_le_bytes(buffer);
320 file.read_exact_at(len - 8, &mut buffer)?;
321 let entries_offset = u64::from_le_bytes(buffer);
322
323 println!("Entries count: {}", entries_count);
324 println!("Entries offset: {}", entries_offset);
325
326 let mut entries = Vec::with_capacity(entries_count as usize);
327 file.seek(SeekFrom::Start(entries_offset))?;
328
329 let mut decoder = DeflateDecoder::new(file.try_clone()?);
330 for _ in 0..entries_count {
331 let file_clone = file.try_clone()?;
332 let entry = Self::decode_entry(&mut decoder, file_clone)?;
333 entries.push(entry);
334 }
335
336 Ok(Self {
337 file,
338 compression_callback: None,
339 entries,
340 entries_offset,
341 })
342 }
343
344 pub fn set_compression_callback(&mut self, callback: CompressionFormatCallback) -> &mut Self {
348 self.compression_callback = callback;
349
350 self
351 }
352
353 pub fn add_directory(
361 &mut self,
362 path: &str,
363 progress: ProgressCallback,
364 ) -> Result<&mut Self, std::io::Error> {
365 self.trim_end_header()?;
366
367 for entry in std::fs::read_dir(path)?.flatten() {
368 self.encode_entry(None, entry, progress)?;
369 }
370
371 self.write_end_header()?;
372
373 Ok(self)
374 }
375
376 pub fn entries(&self) -> &[Entry] {
378 &self.entries
379 }
380
381 pub fn into_entries(self) -> Vec<Entry> {
383 self.entries
384 }
385
386 pub fn add_entry(
394 &mut self,
395 entry: DirEntry,
396 progress: ProgressCallback,
397 ) -> Result<&mut Self, std::io::Error> {
398 self.trim_end_header()?;
399 self.encode_entry(None, entry, progress)?;
400
401 self.write_end_header()?;
402
403 Ok(self)
404 }
405
406 fn trim_end_header(&mut self) -> Result<(), std::io::Error> {
407 if self.entries_offset == 0 {
408 return Ok(());
409 }
410
411 self.file.set_len(self.entries_offset)?;
412
413 Ok(())
414 }
415
416 fn write_end_header(&mut self) -> Result<(), std::io::Error> {
417 let mut encoder = DeflateEncoder::new(&mut self.file, flate2::Compression::default());
418 for entry in &self.entries {
419 Self::encode_entry_metadata(&mut encoder, entry)?;
420 }
421
422 encoder.finish()?;
423
424 self.file
425 .write_all(&(self.entries.len() as u64).to_le_bytes())?;
426 self.file.write_all(&self.entries_offset.to_le_bytes())?;
427 self.file.sync_all()?;
428
429 Ok(())
430 }
431
432 fn encode_entry_metadata<S: Write>(
433 writer: &mut S,
434 entry: &Entry,
435 ) -> Result<(), std::io::Error> {
436 let name = entry.name();
437 let name_length = name.len() as u8;
438
439 let mut buffer = Vec::with_capacity(1 + name.len() + 4);
440
441 buffer.push(name_length);
442 buffer.extend_from_slice(name.as_bytes());
443
444 let mode = encode_file_permissions(entry.mode());
445 let compression = match entry {
446 Entry::File(file_entry) => file_entry.compression,
447 _ => CompressionFormat::None,
448 };
449 let entry_type = match entry {
450 Entry::File(_) => 0,
451 Entry::Directory(_) => 1,
452 Entry::Symlink(_) => 2,
453 };
454
455 let type_compression_mode =
456 (entry_type << 30) | ((compression.encode() as u32) << 26) | (mode & 0x3FFFFFFF);
457 buffer.extend_from_slice(&type_compression_mode.to_le_bytes()[..4]);
458
459 writer.write_all(&buffer)?;
460
461 let (uid, gid) = entry.owner();
462 writer.write_all(&varint::encode_u32(uid))?;
463 writer.write_all(&varint::encode_u32(gid))?;
464 writer.write_all(&varint::encode_i64(entry.mtime()))?;
465
466 match entry {
467 Entry::File(file_entry) => {
468 writer.write_all(&varint::encode_u64(file_entry.size))?;
469 writer.write_all(&varint::encode_u64(file_entry.offset))?;
470 }
471 Entry::Directory(dir_entry) => {
472 writer.write_all(&varint::encode_u64(dir_entry.entries.len() as u64))?;
473
474 for sub_entry in &dir_entry.entries {
475 Self::encode_entry_metadata(writer, sub_entry)?;
476 }
477 }
478 Entry::Symlink(link_entry) => {
479 writer.write_all(&varint::encode_u64(link_entry.target.len() as u64))?;
480 writer.write_all(link_entry.target.as_bytes())?;
481 writer.write_all(&[link_entry.target_dir as u8])?;
482 }
483 }
484
485 Ok(())
486 }
487
488 fn encode_entry(
489 &mut self,
490 entries: Option<&mut Vec<Entry>>,
491 fs_entry: DirEntry,
492 progress: ProgressCallback,
493 ) -> Result<(), std::io::Error> {
494 let path = fs_entry.path();
495 if let Some(f) = progress {
496 f(&path)
497 }
498
499 let file_name = path.file_name().unwrap().to_string_lossy();
500 let metadata = path.symlink_metadata()?;
501
502 if metadata.is_file() {
503 let mut file = File::open(&path)?;
504 let mut buffer = vec![0; 1024 * 1024];
505 let mut bytes_read = file.read(&mut buffer)?;
506
507 let compression = self.compression_callback.map_or(
508 if metadata.len() > 16 {
509 CompressionFormat::Deflate
510 } else {
511 CompressionFormat::None
512 },
513 |f| f(&path, &metadata),
514 );
515
516 let entry = FileEntry {
517 name: file_name.to_string(),
518 mode: metadata.permissions(),
519 file: self.file.try_clone()?,
520 owner: metadata_owner(&metadata),
521 mtime: metadata_mtime(&metadata),
522 decoder: None,
523 size: metadata.len(),
524 offset: self.entries_offset,
525 consumed: 0,
526 compression,
527 };
528
529 if let Some(entries) = entries {
530 entries.push(Entry::File(Box::new(entry)));
531 } else {
532 self.entries.push(Entry::File(Box::new(entry)));
533 }
534
535 match compression {
536 CompressionFormat::None => loop {
537 self.file.write_all(&buffer[..bytes_read])?;
538 self.entries_offset += bytes_read as u64;
539
540 bytes_read = file.read(&mut buffer)?;
541 if bytes_read == 0 {
542 break;
543 }
544 },
545 CompressionFormat::Gzip => {
546 let mut encoder =
547 GzEncoder::new(&mut self.file, flate2::Compression::default());
548 loop {
549 encoder.write_all(&buffer[..bytes_read])?;
550
551 bytes_read = file.read(&mut buffer)?;
552 if bytes_read == 0 {
553 break;
554 }
555 }
556 encoder.finish()?;
557
558 self.entries_offset = self.file.stream_position()?;
559 }
560 CompressionFormat::Deflate => {
561 let mut encoder =
562 DeflateEncoder::new(&mut self.file, flate2::Compression::default());
563 loop {
564 encoder.write_all(&buffer[..bytes_read])?;
565
566 bytes_read = file.read(&mut buffer)?;
567 if bytes_read == 0 {
568 break;
569 }
570 }
571 encoder.finish()?;
572
573 self.entries_offset = self.file.stream_position()?;
574 }
575 }
576 } else if metadata.is_dir() {
577 let mut dir_entries = Vec::new();
578 for entry in std::fs::read_dir(&path)?.flatten() {
579 self.encode_entry(Some(&mut dir_entries), entry, progress)?;
580 }
581
582 let dir_entry = DirectoryEntry {
583 name: file_name.to_string(),
584 mode: metadata.permissions(),
585 owner: metadata_owner(&metadata),
586 mtime: metadata_mtime(&metadata),
587 entries: dir_entries,
588 };
589
590 if let Some(entries) = entries {
591 entries.push(Entry::Directory(dir_entry));
592 } else {
593 self.entries.push(Entry::Directory(dir_entry));
594 }
595 } else if metadata.is_symlink() {
596 let target = std::fs::read_link(&path)?;
597 let target = target.to_string_lossy().to_string();
598
599 let link_entry = SymlinkEntry {
600 name: file_name.to_string(),
601 mode: metadata.permissions(),
602 owner: metadata_owner(&metadata),
603 mtime: metadata_mtime(&metadata),
604 target,
605 target_dir: std::fs::metadata(&path)?.is_dir(),
606 };
607
608 if let Some(entries) = entries {
609 entries.push(Entry::Symlink(link_entry));
610 } else {
611 self.entries.push(Entry::Symlink(link_entry));
612 }
613 }
614
615 Ok(())
616 }
617
618 fn decode_entry<S: Read>(decoder: &mut S, file: File) -> Result<Entry, std::io::Error> {
619 let mut name_length = [0; 1];
620 decoder.read_exact(&mut name_length)?;
621 let name_length = name_length[0] as usize;
622
623 let mut name_bytes = vec![0; name_length];
624 decoder.read_exact(&mut name_bytes)?;
625 let name = String::from_utf8(name_bytes).unwrap();
626
627 let mut type_mode_bytes = [0; 4];
628 decoder.read_exact(&mut type_mode_bytes)?;
629 let type_compression_mode = u32::from_le_bytes(type_mode_bytes);
630
631 let entry_type = (type_compression_mode >> 30) & 0b11;
632 let compression = CompressionFormat::decode(((type_compression_mode >> 26) & 0b1111) as u8);
633 let mode = decode_file_permissions(type_compression_mode & 0x3FFFFFFF);
634
635 let uid = varint::decode_u32(decoder);
636 let gid = varint::decode_u32(decoder);
637 let mtime = varint::decode_i64(decoder);
638
639 let size = varint::decode_u64(decoder);
640
641 match entry_type {
642 0 => {
643 let offset = varint::decode_u64(decoder);
644
645 Ok(Entry::File(Box::new(FileEntry {
646 name,
647 mode,
648 owner: (uid, gid),
649 mtime,
650 file,
651 decoder: None,
652 size,
653 offset,
654 consumed: 0,
655 compression,
656 })))
657 }
658 1 => {
659 let mut entries: Vec<Entry> = Vec::with_capacity(size as usize);
660 for _ in 0..size {
661 let entry = Self::decode_entry(decoder, file.try_clone()?)?;
662 entries.push(entry);
663 }
664
665 Ok(Entry::Directory(DirectoryEntry {
666 name,
667 mode,
668 owner: (uid, gid),
669 mtime,
670 entries,
671 }))
672 }
673 2 => {
674 let mut target_bytes = vec![0; size as usize];
675 decoder.read_exact(&mut target_bytes)?;
676
677 let target = String::from_utf8(target_bytes).unwrap();
678 let target = std::path::PathBuf::from(target);
679
680 let target = target
681 .canonicalize()
682 .unwrap_or_else(|_| target.clone())
683 .to_string_lossy()
684 .to_string();
685 let metadata = std::fs::metadata(&target)?;
686
687 Ok(Entry::Symlink(SymlinkEntry {
688 name,
689 mode,
690 owner: (uid, gid),
691 mtime,
692 target,
693 target_dir: metadata.is_dir(),
694 }))
695 }
696 _ => panic!("Unsupported entry type"),
697 }
698 }
699}