1use crate::varint;
2use entries::EntryMode;
3use flate2::{
4 read::DeflateDecoder,
5 write::{DeflateEncoder, GzEncoder},
6};
7use positioned_io::ReadAt;
8use std::{
9 ffi::OsStr,
10 fmt::{Debug, Formatter},
11 fs::{DirEntry, File, Metadata},
12 io::{Read, Seek, SeekFrom, Write},
13 path::Path,
14 sync::Arc,
15 time::SystemTime,
16};
17
18pub mod entries;
19
20pub const FILE_SIGNATURE: [u8; 7] = *b"DDUPBAK";
21pub const FILE_VERSION: u8 = 1;
22
23#[repr(u8)]
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25pub enum CompressionFormat {
26 None,
27 Gzip,
28 Deflate,
29 Brotli,
30}
31
32impl CompressionFormat {
33 pub const fn encode(&self) -> u8 {
34 match self {
35 CompressionFormat::None => 0,
36 CompressionFormat::Gzip => 1,
37 CompressionFormat::Deflate => 2,
38 CompressionFormat::Brotli => 3,
39 }
40 }
41
42 pub const fn decode(value: u8) -> Self {
43 match value {
44 0 => CompressionFormat::None,
45 1 => CompressionFormat::Gzip,
46 2 => CompressionFormat::Deflate,
47 3 => CompressionFormat::Brotli,
48 _ => panic!("Invalid compression format"),
49 }
50 }
51}
52
53impl Default for CompressionFormat {
54 #[inline]
55 fn default() -> Self {
56 CompressionFormat::None
57 }
58}
59
60#[inline]
61fn metadata_owner(_metadata: &Metadata) -> (u32, u32) {
62 #[cfg(unix)]
63 {
64 use std::os::unix::fs::MetadataExt;
65
66 (_metadata.uid(), _metadata.gid())
67 }
68 #[cfg(not(unix))]
69 {
70 (0, 0)
71 }
72}
73
74pub type ProgressCallback = Option<Arc<dyn Fn(&Path) + Send + Sync + 'static>>;
75pub type CompressionFormatCallback =
76 Option<Arc<dyn Fn(&Path, &Metadata) -> CompressionFormat + Send + Sync>>;
77type RealSizeCallback = Option<Arc<dyn Fn(&Path) -> u64 + Send + Sync + 'static>>;
78
79pub struct Archive {
80 file: Arc<File>,
81 version: u8,
82 compression_callback: CompressionFormatCallback,
83 real_size_callback: RealSizeCallback,
84
85 pub entries: Vec<entries::Entry>,
86 entries_offset: u64,
87}
88
89impl Debug for Archive {
90 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
91 f.debug_struct("Archive")
92 .field("version", &self.version)
93 .field("entries", &self.entries)
94 .finish()
95 }
96}
97
98impl Archive {
99 pub fn new(mut file: File) -> Self {
103 file.set_len(0).unwrap();
104 file.write_all(&FILE_SIGNATURE).unwrap();
105 file.write_all(&[FILE_VERSION]).unwrap();
106 file.sync_all().unwrap();
107
108 Self {
109 file: Arc::new(file),
110 version: FILE_VERSION,
111 compression_callback: None,
112 real_size_callback: None,
113 entries: Vec::new(),
114 entries_offset: 8,
115 }
116 }
117
118 pub fn open(path: &str) -> std::io::Result<Self> {
121 let file = File::open(path)?;
122
123 Self::open_file(file)
124 }
125
126 pub fn open_file(mut file: File) -> std::io::Result<Self> {
129 let len = file.metadata()?.len();
130
131 let mut buffer = [0; 8];
132 file.read_exact(&mut buffer)?;
133 if !buffer.starts_with(&FILE_SIGNATURE) {
134 return Err(std::io::Error::new(
135 std::io::ErrorKind::InvalidData,
136 "Invalid file signature",
137 ));
138 }
139 let version = buffer[7];
140
141 file.read_exact_at(len - 16, &mut buffer)?;
142 let entries_count = u64::from_le_bytes(buffer);
143 file.read_exact_at(len - 8, &mut buffer)?;
144 let entries_offset = u64::from_le_bytes(buffer);
145
146 let mut entries = Vec::with_capacity(entries_count as usize);
147 file.seek(SeekFrom::Start(entries_offset))?;
148
149 let mut decoder = DeflateDecoder::new(file.try_clone()?);
150 let file = Arc::new(file);
151 for _ in 0..entries_count {
152 let entry = Self::decode_entry(&mut decoder, file.clone())?;
153 entries.push(entry);
154 }
155
156 Ok(Self {
157 file,
158 version,
159 compression_callback: None,
160 real_size_callback: None,
161 entries,
162 entries_offset,
163 })
164 }
165
166 #[inline]
169 pub const fn version(&self) -> u8 {
170 self.version
171 }
172
173 #[inline]
177 pub fn set_compression_callback(&mut self, callback: CompressionFormatCallback) -> &mut Self {
178 self.compression_callback = callback;
179
180 self
181 }
182
183 #[inline]
187 pub fn set_real_size_callback(&mut self, callback: RealSizeCallback) -> &mut Self {
188 self.real_size_callback = callback;
189
190 self
191 }
192
193 pub fn add_directory(
201 &mut self,
202 path: &str,
203 progress: ProgressCallback,
204 ) -> std::io::Result<&mut Self> {
205 self.trim_end_header()?;
206
207 for entry in std::fs::read_dir(path)?.flatten() {
208 self.encode_entry(None, entry, progress.clone())?;
209 }
210
211 self.write_end_header()?;
212
213 Ok(self)
214 }
215
216 #[inline]
218 pub fn entries(&self) -> &[entries::Entry] {
219 &self.entries
220 }
221
222 #[inline]
224 pub fn into_entries(self) -> Vec<entries::Entry> {
225 self.entries
226 }
227
228 #[allow(clippy::too_many_arguments)]
236 pub fn write_file_entry(
237 &mut self,
238 mut reader: impl Read,
239 size_real: Option<u64>,
240 name: impl Into<String>,
241 mode: EntryMode,
242 mtime: SystemTime,
243 owner: (u32, u32),
244 compression: CompressionFormat,
245 ) -> std::io::Result<Box<entries::FileEntry>> {
246 let offset = self.file.stream_position()?;
247
248 let mut buffer = [0; 4096];
249 let mut bytes_read = 0;
250 let mut total_bytes = 0;
251 match compression {
252 CompressionFormat::None => {
253 loop {
254 self.file.write_all(&buffer[..bytes_read])?;
255 total_bytes += bytes_read;
256
257 bytes_read = reader.read(&mut buffer)?;
258 if bytes_read == 0 {
259 break;
260 }
261 }
262
263 self.file.flush()?;
264 }
265 CompressionFormat::Gzip => {
266 let mut encoder = GzEncoder::new(&mut self.file, flate2::Compression::default());
267 loop {
268 encoder.write_all(&buffer[..bytes_read])?;
269 total_bytes += bytes_read;
270
271 bytes_read = reader.read(&mut buffer)?;
272 if bytes_read == 0 {
273 break;
274 }
275 }
276
277 encoder.flush()?;
278 encoder.finish()?;
279 }
280 CompressionFormat::Deflate => {
281 let mut encoder =
282 DeflateEncoder::new(&mut self.file, flate2::Compression::default());
283 loop {
284 encoder.write_all(&buffer[..bytes_read])?;
285 total_bytes += bytes_read;
286
287 bytes_read = reader.read(&mut buffer)?;
288 if bytes_read == 0 {
289 break;
290 }
291 }
292
293 encoder.flush()?;
294 encoder.finish()?;
295 }
296
297 #[cfg(feature = "brotli")]
298 CompressionFormat::Brotli => {
299 let mut encoder = brotli::CompressorWriter::new(&mut self.file, 4096, 11, 22);
300 loop {
301 encoder.write_all(&buffer[..bytes_read])?;
302 total_bytes += bytes_read;
303
304 bytes_read = reader.read(&mut buffer)?;
305 if bytes_read == 0 {
306 break;
307 }
308 }
309 }
310 #[cfg(not(feature = "brotli"))]
311 CompressionFormat::Brotli => {
312 Err(std::io::Error::new(
313 std::io::ErrorKind::Unsupported,
314 "Brotli support is not enabled. Please enable the 'brotli' feature.",
315 ))?;
316 }
317 }
318
319 let size_compressed = match compression {
320 CompressionFormat::None => None,
321 _ => Some(self.file.stream_position()? - offset),
322 };
323 let size_real = size_real.unwrap_or(total_bytes as u64);
324
325 let entry = Box::new(entries::FileEntry {
326 name: name.into(),
327 mode,
328 file: self.file.clone(),
329 owner,
330 mtime,
331 decoder: None,
332 size_compressed,
333 size_real,
334 size: total_bytes as u64,
335 offset,
336 consumed: 0,
337 compression,
338 });
339
340 self.entries_offset = self.file.stream_position()?;
341
342 Ok(entry)
343 }
344
345 pub fn add_entries(
353 &mut self,
354 entries: Vec<DirEntry>,
355 progress: ProgressCallback,
356 ) -> std::io::Result<&mut Self> {
357 self.trim_end_header()?;
358
359 for entry in entries {
360 self.encode_entry(None, entry, progress.clone())?;
361 }
362
363 self.write_end_header()?;
364
365 Ok(self)
366 }
367
368 fn recursive_find_archive_entry<'a>(
369 entry: &'a entries::Entry,
370 entry_parts: &[&OsStr],
371 current_depth: usize,
372 ) -> Option<&'a entries::Entry> {
373 if entry_parts.len() > current_depth + 1 {
374 return None;
375 }
376
377 let current_part = entry_parts.first()?;
378 let entry_name: &OsStr = entry.name().as_ref();
379 if entry_name != *current_part {
380 return None;
381 }
382
383 if entry_parts.len() == 1 {
384 return Some(entry);
385 }
386
387 if let entries::Entry::Directory(dir_entry) = entry {
388 let remaining_parts = &entry_parts[1..];
389
390 for sub_entry in &dir_entry.entries {
391 if let Some(found) = Self::recursive_find_archive_entry(
392 sub_entry,
393 remaining_parts,
394 current_depth - 1,
395 ) {
396 return Some(found);
397 }
398 }
399 }
400
401 None
402 }
403
404 fn recursive_find_archive_entry_mut<'a>(
405 entry: &'a mut entries::Entry,
406 entry_parts: &[&OsStr],
407 current_depth: usize,
408 ) -> Option<&'a mut entries::Entry> {
409 if entry_parts.len() > current_depth + 1 {
410 return None;
411 }
412
413 let current_part = entry_parts.first()?;
414 let entry_name: &OsStr = entry.name().as_ref();
415 if entry_name != *current_part {
416 return None;
417 }
418
419 if entry_parts.len() == 1 {
420 return Some(entry);
421 }
422
423 if let entries::Entry::Directory(dir_entry) = entry {
424 let remaining_parts = &entry_parts[1..];
425
426 for sub_entry in &mut dir_entry.entries {
427 if let Some(found) = Self::recursive_find_archive_entry_mut(
428 sub_entry,
429 remaining_parts,
430 current_depth - 1,
431 ) {
432 return Some(found);
433 }
434 }
435 }
436
437 None
438 }
439
440 #[inline]
445 pub fn find_archive_entry(&self, entry_name: &Path) -> Option<&entries::Entry> {
446 let entry_parts = entry_name
447 .components()
448 .map(|c| c.as_os_str())
449 .collect::<Vec<&OsStr>>();
450 for entry in self.entries() {
451 if let Some(found) =
452 Self::recursive_find_archive_entry(entry, &entry_parts, entry_parts.len())
453 {
454 return Some(found);
455 }
456 }
457
458 None
459 }
460
461 #[inline]
466 pub fn find_archive_entry_mut(&mut self, entry_name: &Path) -> Option<&mut entries::Entry> {
467 let entry_parts = entry_name
468 .components()
469 .map(|c| c.as_os_str())
470 .collect::<Vec<&OsStr>>();
471 for entry in &mut self.entries {
472 if let Some(found) =
473 Self::recursive_find_archive_entry_mut(entry, &entry_parts, entry_parts.len())
474 {
475 return Some(found);
476 }
477 }
478
479 None
480 }
481
482 pub fn trim_end_header(&mut self) -> std::io::Result<()> {
483 if self.entries_offset == 0 {
484 return Ok(());
485 }
486
487 self.file.set_len(self.entries_offset)?;
488 self.file.flush()?;
489 self.file.seek(SeekFrom::Start(self.entries_offset))?;
490
491 Ok(())
492 }
493
494 pub fn write_end_header(&mut self) -> std::io::Result<()> {
495 let mut encoder = DeflateEncoder::new(&mut self.file, flate2::Compression::default());
496 for entry in &self.entries {
497 Self::encode_entry_metadata(&mut encoder, entry)?;
498 }
499
500 encoder.flush()?;
501 encoder.finish()?;
502 self.file.flush()?;
503
504 self.file
505 .write_all(&(self.entries.len() as u64).to_le_bytes())?;
506 self.file.write_all(&self.entries_offset.to_le_bytes())?;
507 self.file.flush()?;
508 self.file.sync_all()?;
509
510 Ok(())
511 }
512
513 fn encode_entry_metadata<S: Write>(
514 writer: &mut S,
515 entry: &entries::Entry,
516 ) -> std::io::Result<()> {
517 let name = entry.name();
518 let name_length = name.len() as u8;
519
520 writer.write_all(&varint::encode_u32(name_length as u32))?;
521
522 let mut buffer = Vec::with_capacity(name.len() + 4);
523 buffer.extend_from_slice(name.as_bytes());
524
525 let mode = entry.mode().bits();
526 let compression = match entry {
527 entries::Entry::File(file_entry) => file_entry.compression,
528 _ => CompressionFormat::None,
529 };
530 let entry_type = match entry {
531 entries::Entry::File(_) => 0,
532 entries::Entry::Directory(_) => 1,
533 entries::Entry::Symlink(_) => 2,
534 };
535
536 let type_compression_mode =
537 (entry_type << 30) | ((compression.encode() as u32) << 26) | (mode & 0x3FFFFFFF);
538 buffer.extend_from_slice(&type_compression_mode.to_le_bytes()[..4]);
539
540 writer.write_all(&buffer)?;
541
542 let (uid, gid) = entry.owner();
543 writer.write_all(&varint::encode_u32(uid))?;
544 writer.write_all(&varint::encode_u32(gid))?;
545
546 let mtime = entry
547 .mtime()
548 .duration_since(SystemTime::UNIX_EPOCH)
549 .unwrap();
550 writer.write_all(&varint::encode_u64(mtime.as_secs()))?;
551
552 match entry {
553 entries::Entry::File(file_entry) => {
554 writer.write_all(&varint::encode_u64(file_entry.size))?;
555
556 if let Some(size_compressed) = file_entry.size_compressed {
557 writer.write_all(&varint::encode_u64(size_compressed))?;
558 }
559 writer.write_all(&varint::encode_u64(file_entry.size_real))?;
560 writer.write_all(&varint::encode_u64(file_entry.offset))?;
561 }
562 entries::Entry::Directory(dir_entry) => {
563 writer.write_all(&varint::encode_u64(dir_entry.entries.len() as u64))?;
564
565 for sub_entry in &dir_entry.entries {
566 Self::encode_entry_metadata(writer, sub_entry)?;
567 }
568 }
569 entries::Entry::Symlink(link_entry) => {
570 writer.write_all(&varint::encode_u64(link_entry.target.len() as u64))?;
571 writer.write_all(link_entry.target.as_bytes())?;
572 writer.write_all(&[link_entry.target_dir as u8])?;
573 }
574 }
575
576 Ok(())
577 }
578
579 fn encode_entry(
580 &mut self,
581 entries: Option<&mut Vec<entries::Entry>>,
582 fs_entry: DirEntry,
583 progress: ProgressCallback,
584 ) -> std::io::Result<()> {
585 let path = fs_entry.path();
586
587 let file_name = path.file_name().unwrap().to_string_lossy().to_string();
588 let metadata = path.symlink_metadata()?;
589
590 if metadata.is_file() {
591 let mut file = File::open(&path)?;
592 let mut buffer = [0; 4096];
593 let mut bytes_read = file.read(&mut buffer)?;
594
595 let compression = match self.compression_callback {
596 Some(ref f) => f(&path, &metadata),
597 None => {
598 if metadata.len() > 16 {
599 CompressionFormat::Deflate
600 } else {
601 CompressionFormat::None
602 }
603 }
604 };
605
606 match compression {
607 CompressionFormat::None => {
608 loop {
609 self.file.write_all(&buffer[..bytes_read])?;
610
611 bytes_read = file.read(&mut buffer)?;
612 if bytes_read == 0 {
613 break;
614 }
615 }
616
617 self.file.flush()?;
618 }
619 CompressionFormat::Gzip => {
620 let mut encoder =
621 GzEncoder::new(&mut self.file, flate2::Compression::default());
622 loop {
623 encoder.write_all(&buffer[..bytes_read])?;
624
625 bytes_read = file.read(&mut buffer)?;
626 if bytes_read == 0 {
627 break;
628 }
629 }
630
631 encoder.flush()?;
632 encoder.finish()?;
633 }
634 CompressionFormat::Deflate => {
635 let mut encoder =
636 DeflateEncoder::new(&mut self.file, flate2::Compression::default());
637 loop {
638 encoder.write_all(&buffer[..bytes_read])?;
639
640 bytes_read = file.read(&mut buffer)?;
641 if bytes_read == 0 {
642 break;
643 }
644 }
645
646 encoder.flush()?;
647 encoder.finish()?;
648 }
649
650 #[cfg(feature = "brotli")]
651 CompressionFormat::Brotli => {
652 let mut encoder = brotli::CompressorWriter::new(&mut self.file, 4096, 11, 22);
653 loop {
654 encoder.write_all(&buffer[..bytes_read])?;
655
656 bytes_read = file.read(&mut buffer)?;
657 if bytes_read == 0 {
658 break;
659 }
660 }
661 }
662 #[cfg(not(feature = "brotli"))]
663 CompressionFormat::Brotli => {
664 Err(std::io::Error::new(
665 std::io::ErrorKind::Unsupported,
666 "Brotli support is not enabled. Please enable the 'brotli' feature.",
667 ))?;
668 }
669 }
670
671 let entry = entries::FileEntry {
672 name: file_name,
673 mode: metadata.permissions().into(),
674 file: self.file.clone(),
675 owner: metadata_owner(&metadata),
676 mtime: metadata.modified()?,
677 decoder: None,
678 size_compressed: match compression {
679 CompressionFormat::None => None,
680 _ => Some(self.file.stream_position()? - self.entries_offset),
681 },
682 size_real: match self.real_size_callback {
683 Some(ref f) => f(&path),
684 None => metadata.len(),
685 },
686 size: metadata.len(),
687 offset: self.entries_offset,
688 consumed: 0,
689 compression,
690 };
691
692 self.entries_offset = self.file.stream_position()?;
693
694 if let Some(entries) = entries {
695 entries.push(entries::Entry::File(Box::new(entry)));
696 } else {
697 self.entries.push(entries::Entry::File(Box::new(entry)));
698 }
699 } else if metadata.is_dir() {
700 let mut dir_entries = Vec::new();
701 for entry in std::fs::read_dir(&path)?.flatten() {
702 self.encode_entry(Some(&mut dir_entries), entry, progress.clone())?;
703 }
704
705 let dir_entry = entries::DirectoryEntry {
706 name: file_name,
707 mode: metadata.permissions().into(),
708 owner: metadata_owner(&metadata),
709 mtime: metadata.modified()?,
710 entries: dir_entries,
711 };
712
713 if let Some(entries) = entries {
714 entries.push(entries::Entry::Directory(Box::new(dir_entry)));
715 } else {
716 self.entries
717 .push(entries::Entry::Directory(Box::new(dir_entry)));
718 }
719 } else if metadata.is_symlink() {
720 if let Ok(Ok(target)) = std::fs::read_link(&path).map(|p| p.canonicalize()) {
721 let target = target.to_string_lossy().to_string();
722
723 let link_entry = entries::SymlinkEntry {
724 name: file_name,
725 mode: metadata.permissions().into(),
726 owner: metadata_owner(&metadata),
727 mtime: metadata.modified()?,
728 target,
729 target_dir: std::fs::metadata(&path)?.is_dir(),
730 };
731
732 if let Some(entries) = entries {
733 entries.push(entries::Entry::Symlink(Box::new(link_entry)));
734 } else {
735 self.entries
736 .push(entries::Entry::Symlink(Box::new(link_entry)));
737 }
738 }
739 }
740
741 if let Some(f) = progress {
742 f(&path)
743 }
744
745 Ok(())
746 }
747
748 fn decode_entry<S: Read>(decoder: &mut S, file: Arc<File>) -> std::io::Result<entries::Entry> {
749 let name_length = varint::decode_u32(decoder) as usize;
750
751 let mut name_bytes = vec![0; name_length];
752 decoder.read_exact(&mut name_bytes)?;
753 let name = String::from_utf8(name_bytes).unwrap();
754
755 let mut type_mode_bytes = [0; 4];
756 decoder.read_exact(&mut type_mode_bytes)?;
757 let type_compression_mode = u32::from_le_bytes(type_mode_bytes);
758
759 let entry_type = (type_compression_mode >> 30) & 0b11;
760 let compression = CompressionFormat::decode(((type_compression_mode >> 26) & 0b1111) as u8);
761 let mode = EntryMode::from(type_compression_mode & 0x3FFFFFFF);
762
763 let uid = varint::decode_u32(decoder);
764 let gid = varint::decode_u32(decoder);
765
766 let mtime = varint::decode_u64(decoder);
767 let mtime = SystemTime::UNIX_EPOCH + std::time::Duration::new(mtime, 0);
768
769 let size = varint::decode_u64(decoder);
770
771 match entry_type {
772 0 => {
773 let size_compressed = match compression {
774 CompressionFormat::None => None,
775 _ => Some(varint::decode_u64(decoder)),
776 };
777 let size_real = varint::decode_u64(decoder);
778 let offset = varint::decode_u64(decoder);
779
780 Ok(entries::Entry::File(Box::new(entries::FileEntry {
781 name,
782 mode,
783 owner: (uid, gid),
784 mtime,
785 file,
786 decoder: None,
787 size_compressed,
788 size_real,
789 size,
790 offset,
791 consumed: 0,
792 compression,
793 })))
794 }
795 1 => {
796 let mut entries: Vec<entries::Entry> = Vec::with_capacity(size as usize);
797 for _ in 0..size {
798 let entry = Self::decode_entry(decoder, file.clone())?;
799 entries.push(entry);
800 }
801
802 Ok(entries::Entry::Directory(Box::new(
803 entries::DirectoryEntry {
804 name,
805 mode,
806 owner: (uid, gid),
807 mtime,
808 entries,
809 },
810 )))
811 }
812 2 => {
813 let mut target_bytes = vec![0; size as usize];
814 decoder.read_exact(&mut target_bytes)?;
815
816 let target = String::from_utf8(target_bytes).unwrap();
817
818 let mut target_dir_bytes = [0; 1];
819 decoder.read_exact(&mut target_dir_bytes)?;
820 let target_dir = target_dir_bytes[0] != 0;
821
822 Ok(entries::Entry::Symlink(Box::new(entries::SymlinkEntry {
823 name,
824 mode,
825 owner: (uid, gid),
826 mtime,
827 target,
828 target_dir,
829 })))
830 }
831 _ => panic!("Unsupported entry type"),
832 }
833 }
834}