1use crate::stream::{StreamCommand, Timespec};
2use anyhow::{Context, Result, anyhow, bail};
3use btrfs_uapi::{
4 inode::subvolid_resolve,
5 send_receive::{
6 clone_range, encoded_write, received_subvol_set,
7 subvolume_search_by_received_uuid, subvolume_search_by_uuid,
8 },
9 subvolume::{
10 SubvolumeFlags, snapshot_create, subvolume_create, subvolume_flags_get,
11 subvolume_flags_set, subvolume_info,
12 },
13};
14use std::{
15 ffi::CString,
16 fs::{self, File, OpenOptions},
17 io,
18 os::{
19 fd::{AsFd, AsRawFd},
20 unix::fs::PermissionsExt,
21 },
22 path::{Path, PathBuf},
23};
24
25fn find_mount_root(path: &Path) -> Result<PathBuf> {
32 use std::os::unix::fs::MetadataExt;
33
34 let path = path
35 .canonicalize()
36 .with_context(|| format!("cannot canonicalize '{}'", path.display()))?;
37 let dev = path
38 .metadata()
39 .with_context(|| format!("cannot stat '{}'", path.display()))?
40 .dev();
41
42 let mut root = path.clone();
43 while let Some(parent) = root.parent() {
44 let parent_dev = parent
45 .metadata()
46 .with_context(|| format!("cannot stat '{}'", parent.display()))?
47 .dev();
48 if parent_dev != dev {
49 break;
50 }
51 root = parent.to_path_buf();
52 }
53
54 Ok(root)
55}
56
57pub struct ReceiveContext {
97 mnt_fd: File,
99 mount_root: PathBuf,
101 dest_dir: PathBuf,
103 cur_subvol: Option<String>,
105 cur_subvol_path: Option<PathBuf>,
107 received_uuid: Option<uuid::Uuid>,
109 stransid: u64,
111 write_fd: Option<File>,
114 write_path: PathBuf,
115}
116
117impl ReceiveContext {
118 pub fn new(dest_dir: &Path) -> Result<Self> {
129 let mount_root = find_mount_root(dest_dir)?;
130 let mnt_fd = File::open(&mount_root).with_context(|| {
131 format!("cannot open mount root '{}'", mount_root.display())
132 })?;
133
134 Ok(Self {
135 mnt_fd,
136 mount_root,
137 dest_dir: dest_dir.to_path_buf(),
138 cur_subvol: None,
139 cur_subvol_path: None,
140 received_uuid: None,
141 stransid: 0,
142 write_fd: None,
143 write_path: PathBuf::new(),
144 })
145 }
146
147 #[allow(clippy::too_many_lines)]
159 pub fn process_command(&mut self, cmd: &StreamCommand) -> Result<()> {
160 match cmd {
161 StreamCommand::Subvol {
162 path,
163 uuid,
164 ctransid,
165 } => self.process_subvol(path, uuid, *ctransid),
166 StreamCommand::Snapshot {
167 path,
168 uuid,
169 ctransid,
170 clone_uuid,
171 clone_ctransid,
172 } => self.process_snapshot(
173 path,
174 uuid,
175 *ctransid,
176 clone_uuid,
177 *clone_ctransid,
178 ),
179 StreamCommand::Mkfile { path } => self.process_mkfile(path),
180 StreamCommand::Mkdir { path } => self.process_mkdir(path),
181 StreamCommand::Mknod { path, mode, rdev } => {
182 self.process_mknod(path, *mode, *rdev)
183 }
184 StreamCommand::Mkfifo { path } => self.process_mkfifo(path),
185 StreamCommand::Mksock { path } => self.process_mksock(path),
186 StreamCommand::Symlink { path, target } => {
187 self.process_symlink(path, target)
188 }
189 StreamCommand::Rename { from, to } => self.process_rename(from, to),
190 StreamCommand::Link { path, target } => {
191 self.process_link(path, target)
192 }
193 StreamCommand::Unlink { path } => self.process_unlink(path),
194 StreamCommand::Rmdir { path } => self.process_rmdir(path),
195 StreamCommand::Write { path, offset, data } => {
196 self.process_write(path, *offset, data)
197 }
198 StreamCommand::Clone {
199 path,
200 offset,
201 len,
202 clone_uuid,
203 clone_ctransid,
204 clone_path,
205 clone_offset,
206 } => self.process_clone(
207 path,
208 *offset,
209 *len,
210 clone_uuid,
211 *clone_ctransid,
212 clone_path,
213 *clone_offset,
214 ),
215 StreamCommand::SetXattr { path, name, data } => {
216 self.process_set_xattr(path, name, data)
217 }
218 StreamCommand::RemoveXattr { path, name } => {
219 self.process_remove_xattr(path, name)
220 }
221 StreamCommand::Truncate { path, size } => {
222 self.process_truncate(path, *size)
223 }
224 StreamCommand::Chmod { path, mode } => {
225 self.process_chmod(path, *mode)
226 }
227 StreamCommand::Chown { path, uid, gid } => {
228 self.process_chown(path, *uid, *gid)
229 }
230 StreamCommand::Utimes {
231 path, atime, mtime, ..
232 } => self.process_utimes(path, atime, mtime),
233 StreamCommand::UpdateExtent { .. } => Ok(()),
234 StreamCommand::EncodedWrite {
235 path,
236 offset,
237 unencoded_file_len,
238 unencoded_len,
239 unencoded_offset,
240 compression,
241 encryption,
242 data,
243 } => self.process_encoded_write(
244 path,
245 *offset,
246 *unencoded_file_len,
247 *unencoded_len,
248 *unencoded_offset,
249 *compression,
250 *encryption,
251 data,
252 ),
253 StreamCommand::Fallocate {
254 path,
255 mode,
256 offset,
257 len,
258 } => self.process_fallocate(path, *mode, *offset, *len),
259 StreamCommand::Fileattr { .. } => {
260 Ok(())
264 }
265 StreamCommand::EnableVerity {
266 path,
267 algorithm,
268 block_size,
269 salt,
270 sig,
271 } => self.process_enable_verity(
272 path,
273 *algorithm,
274 *block_size,
275 salt,
276 sig,
277 ),
278 StreamCommand::End => unreachable!("End is handled by the caller"),
279 }
280 }
281
282 pub fn finish_subvol(&mut self) -> Result<()> {
294 self.close_write_fd();
295
296 let subvol_path = match &self.cur_subvol_path {
297 Some(p) => p.clone(),
298 None => return Ok(()),
299 };
300 let uuid = match &self.received_uuid {
301 Some(u) => *u,
302 None => return Ok(()),
303 };
304
305 let subvol_file = File::open(&subvol_path).with_context(|| {
306 format!("cannot open subvolume '{}'", subvol_path.display())
307 })?;
308 let fd = subvol_file.as_fd();
309
310 received_subvol_set(fd, &uuid, self.stransid).with_context(|| {
311 format!(
312 "failed to set received subvol on '{}'",
313 subvol_path.display()
314 )
315 })?;
316
317 let flags = subvolume_flags_get(fd).with_context(|| {
319 format!("failed to get flags for '{}'", subvol_path.display())
320 })?;
321 subvolume_flags_set(fd, flags | SubvolumeFlags::RDONLY).with_context(
322 || {
323 format!(
324 "failed to set read-only on '{}'",
325 subvol_path.display()
326 )
327 },
328 )?;
329
330 self.cur_subvol = None;
331 self.cur_subvol_path = None;
332 self.received_uuid = None;
333 self.stransid = 0;
334
335 Ok(())
336 }
337
338 pub fn close_write_fd(&mut self) {
345 self.write_fd = None;
346 self.write_path = PathBuf::new();
347 }
348
349 fn full_path(&self, relative: &str) -> Result<PathBuf> {
350 let subvol_path = self
351 .cur_subvol_path
352 .as_ref()
353 .ok_or_else(|| anyhow!("no current subvolume"))?;
354 Ok(subvol_path.join(relative))
355 }
356
357 fn process_subvol(
358 &mut self,
359 path: &str,
360 uuid: &uuid::Uuid,
361 ctransid: u64,
362 ) -> Result<()> {
363 self.finish_subvol()?;
364
365 let subvol_path = self.dest_dir.join(path);
366
367 let parent_dir = File::open(&self.dest_dir).with_context(|| {
369 format!("cannot open '{}'", self.dest_dir.display())
370 })?;
371 let c_name = CString::new(path).with_context(|| {
372 format!("subvolume name contains null byte: {path}")
373 })?;
374 subvolume_create(parent_dir.as_fd(), &c_name, &[]).with_context(
375 || {
376 format!(
377 "failed to create subvolume '{}'",
378 subvol_path.display()
379 )
380 },
381 )?;
382
383 self.cur_subvol = Some(path.to_string());
384 self.cur_subvol_path = Some(subvol_path);
385 self.received_uuid = Some(*uuid);
386 self.stransid = ctransid;
387
388 Ok(())
389 }
390
391 fn process_snapshot(
392 &mut self,
393 path: &str,
394 uuid: &uuid::Uuid,
395 ctransid: u64,
396 clone_uuid: &uuid::Uuid,
397 clone_ctransid: u64,
398 ) -> Result<()> {
399 self.finish_subvol()?;
400
401 let subvol_path = self.dest_dir.join(path);
402
403 let parent_root_id = subvolume_search_by_received_uuid(
405 self.mnt_fd.as_fd(),
406 clone_uuid,
407 )
408 .or_else(|_| subvolume_search_by_uuid(self.mnt_fd.as_fd(), clone_uuid))
409 .with_context(|| {
410 format!(
411 "cannot find parent subvolume with UUID {} for snapshot '{}'",
412 clone_uuid.as_hyphenated(),
413 path
414 )
415 })?;
416
417 let parent_path = subvolid_resolve(self.mnt_fd.as_fd(), parent_root_id)
419 .with_context(|| {
420 format!(
421 "cannot resolve path for parent subvolume {parent_root_id}"
422 )
423 })?;
424
425 let parent_full = self.mount_root.join(&parent_path);
429 let parent_file = File::open(&parent_full).with_context(|| {
430 format!("cannot open parent subvolume '{}'", parent_full.display())
431 })?;
432
433 let parent_info =
434 subvolume_info(parent_file.as_fd()).with_context(|| {
435 format!(
436 "failed to get info for parent '{}'",
437 parent_full.display()
438 )
439 })?;
440
441 if parent_info.ctransid != clone_ctransid
444 && parent_info.stransid != clone_ctransid
445 {
446 bail!(
447 "parent subvolume '{}' ctransid mismatch: stream expects {}, found ctransid={} stransid={}",
448 parent_path,
449 clone_ctransid,
450 parent_info.ctransid,
451 parent_info.stransid,
452 );
453 }
454
455 let dest_dir_file = File::open(&self.dest_dir).with_context(|| {
457 format!("cannot open '{}'", self.dest_dir.display())
458 })?;
459 let c_name = CString::new(path).with_context(|| {
460 format!("snapshot name contains null byte: {path}")
461 })?;
462 snapshot_create(
463 dest_dir_file.as_fd(),
464 parent_file.as_fd(),
465 &c_name,
466 false,
467 &[],
468 )
469 .with_context(|| {
470 format!("failed to create snapshot '{}'", subvol_path.display())
471 })?;
472
473 let snap_file = File::open(&subvol_path).with_context(|| {
475 format!("cannot open snapshot '{}'", subvol_path.display())
476 })?;
477 let snap_flags =
478 subvolume_flags_get(snap_file.as_fd()).with_context(|| {
479 format!("failed to get flags for '{}'", subvol_path.display())
480 })?;
481 if snap_flags.contains(SubvolumeFlags::RDONLY) {
482 subvolume_flags_set(
483 snap_file.as_fd(),
484 snap_flags & !SubvolumeFlags::RDONLY,
485 )
486 .with_context(|| {
487 format!(
488 "failed to make snapshot '{}' writable",
489 subvol_path.display()
490 )
491 })?;
492 }
493
494 self.cur_subvol = Some(path.to_string());
495 self.cur_subvol_path = Some(subvol_path);
496 self.received_uuid = Some(*uuid);
497 self.stransid = ctransid;
498
499 Ok(())
500 }
501
502 fn process_mkfile(&mut self, path: &str) -> Result<()> {
503 let full = self.full_path(path)?;
504 File::create(&full).with_context(|| {
505 format!("failed to create file '{}'", full.display())
506 })?;
507 Ok(())
508 }
509
510 fn process_mkdir(&mut self, path: &str) -> Result<()> {
511 let full = self.full_path(path)?;
512 fs::create_dir(&full).with_context(|| {
513 format!("failed to create directory '{}'", full.display())
514 })?;
515 Ok(())
516 }
517
518 #[allow(clippy::cast_possible_truncation)] fn process_mknod(
520 &mut self,
521 path: &str,
522 mode: u64,
523 rdev: u64,
524 ) -> Result<()> {
525 self.do_mknod(path, mode as nix::libc::mode_t, rdev as nix::libc::dev_t)
526 }
527
528 fn process_mkfifo(&mut self, path: &str) -> Result<()> {
529 self.do_mknod(path, nix::libc::S_IFIFO | 0o600, 0)
530 }
531
532 fn process_mksock(&mut self, path: &str) -> Result<()> {
533 self.do_mknod(path, nix::libc::S_IFSOCK | 0o600, 0)
534 }
535
536 fn do_mknod(
537 &mut self,
538 path: &str,
539 mode: nix::libc::mode_t,
540 rdev: nix::libc::dev_t,
541 ) -> Result<()> {
542 let full = self.full_path(path)?;
543 let c_path = path_to_cstring(&full)?;
544 let ret = unsafe { nix::libc::mknod(c_path.as_ptr(), mode, rdev) };
545 if ret < 0 {
546 return Err(io::Error::last_os_error()).with_context(|| {
547 format!("mknod failed for '{}'", full.display())
548 });
549 }
550 Ok(())
551 }
552
553 fn process_symlink(&mut self, path: &str, target: &str) -> Result<()> {
554 let full = self.full_path(path)?;
555 std::os::unix::fs::symlink(target, &full).with_context(|| {
556 format!("failed to create symlink '{}'", full.display())
557 })?;
558 Ok(())
559 }
560
561 fn process_rename(&mut self, from: &str, to: &str) -> Result<()> {
562 let full_from = self.full_path(from)?;
563 let full_to = self.full_path(to)?;
564 fs::rename(&full_from, &full_to).with_context(|| {
565 format!(
566 "failed to rename '{}' to '{}'",
567 full_from.display(),
568 full_to.display()
569 )
570 })?;
571 Ok(())
572 }
573
574 fn process_link(&mut self, path: &str, target: &str) -> Result<()> {
575 let full_path = self.full_path(path)?;
576 let full_target = self.full_path(target)?;
577 fs::hard_link(&full_target, &full_path).with_context(|| {
578 format!(
579 "failed to hard link '{}' -> '{}'",
580 full_path.display(),
581 full_target.display()
582 )
583 })?;
584 Ok(())
585 }
586
587 fn process_unlink(&mut self, path: &str) -> Result<()> {
588 let full = self.full_path(path)?;
589 if self.write_path == full {
591 self.close_write_fd();
592 }
593 fs::remove_file(&full).with_context(|| {
594 format!("failed to unlink '{}'", full.display())
595 })?;
596 Ok(())
597 }
598
599 fn process_rmdir(&mut self, path: &str) -> Result<()> {
600 let full = self.full_path(path)?;
601 fs::remove_dir(&full)
602 .with_context(|| format!("failed to rmdir '{}'", full.display()))?;
603 Ok(())
604 }
605
606 fn process_write(
607 &mut self,
608 path: &str,
609 offset: u64,
610 data: &[u8],
611 ) -> Result<()> {
612 let full = self.full_path(path)?;
613
614 if self.write_path != full {
616 self.close_write_fd();
617 let file =
618 OpenOptions::new().write(true).open(&full).with_context(
619 || format!("cannot open '{}' for writing", full.display()),
620 )?;
621 self.write_fd = Some(file);
622 self.write_path.clone_from(&full);
623 }
624
625 let fd = self.write_fd.as_ref().unwrap();
626 #[allow(clippy::cast_possible_wrap)] let written = unsafe {
628 nix::libc::pwrite(
629 fd.as_raw_fd(),
630 data.as_ptr().cast::<nix::libc::c_void>(),
631 data.len(),
632 offset as nix::libc::off_t,
633 )
634 };
635 if written < 0 {
636 return Err(io::Error::last_os_error()).with_context(|| {
637 format!("pwrite failed on '{}'", full.display())
638 });
639 }
640 #[allow(clippy::cast_sign_loss)] if (written as usize) != data.len() {
642 bail!(
643 "short pwrite on '{}': wrote {} of {} bytes",
644 full.display(),
645 written,
646 data.len()
647 );
648 }
649
650 Ok(())
651 }
652
653 #[allow(clippy::too_many_arguments)]
654 fn process_clone(
655 &mut self,
656 path: &str,
657 offset: u64,
658 len: u64,
659 clone_uuid: &uuid::Uuid,
660 _clone_ctransid: u64,
661 clone_path: &str,
662 clone_offset: u64,
663 ) -> Result<()> {
664 let full = self.full_path(path)?;
665
666 let clone_subvol_root =
668 subvolume_search_by_received_uuid(self.mnt_fd.as_fd(), clone_uuid)
669 .or_else(|_| {
670 subvolume_search_by_uuid(self.mnt_fd.as_fd(), clone_uuid)
671 })
672 .with_context(|| {
673 format!(
674 "cannot find clone source subvolume with UUID {}",
675 clone_uuid.as_hyphenated()
676 )
677 })?;
678
679 let subvol_path =
680 subvolid_resolve(self.mnt_fd.as_fd(), clone_subvol_root)
681 .with_context(|| {
682 format!("cannot resolve path for clone source subvolume {clone_subvol_root}")
683 })?;
684
685 let clone_full = self.mount_root.join(&subvol_path).join(clone_path);
688 let clone_file = File::open(&clone_full).with_context(|| {
689 format!("cannot open clone source '{}'", clone_full.display())
690 })?;
691
692 if self.write_path == full {
694 self.close_write_fd();
695 }
696
697 let dest_file = OpenOptions::new()
698 .write(true)
699 .open(&full)
700 .with_context(|| {
701 format!("cannot open '{}' for clone", full.display())
702 })?;
703
704 clone_range(
705 dest_file.as_fd(),
706 clone_file.as_fd(),
707 clone_offset,
708 len,
709 offset,
710 )
711 .with_context(|| {
712 format!("clone_range failed on '{}'", full.display())
713 })?;
714
715 Ok(())
716 }
717
718 fn process_set_xattr(
719 &mut self,
720 path: &str,
721 name: &str,
722 data: &[u8],
723 ) -> Result<()> {
724 let full = self.full_path(path)?;
725 let c_path = path_to_cstring(&full)?;
726 let c_name = CString::new(name)
727 .with_context(|| format!("invalid xattr name: {name}"))?;
728 let ret = unsafe {
729 nix::libc::lsetxattr(
730 c_path.as_ptr(),
731 c_name.as_ptr(),
732 data.as_ptr().cast::<nix::libc::c_void>(),
733 data.len(),
734 0,
735 )
736 };
737 if ret < 0 {
738 return Err(io::Error::last_os_error()).with_context(|| {
739 format!("lsetxattr failed on '{}'", full.display())
740 });
741 }
742 Ok(())
743 }
744
745 fn process_remove_xattr(&mut self, path: &str, name: &str) -> Result<()> {
746 let full = self.full_path(path)?;
747 let c_path = path_to_cstring(&full)?;
748 let c_name = CString::new(name)
749 .with_context(|| format!("invalid xattr name: {name}"))?;
750 let ret = unsafe {
751 nix::libc::lremovexattr(c_path.as_ptr(), c_name.as_ptr())
752 };
753 if ret < 0 {
754 return Err(io::Error::last_os_error()).with_context(|| {
755 format!("lremovexattr failed on '{}'", full.display())
756 });
757 }
758 Ok(())
759 }
760
761 #[allow(clippy::cast_possible_wrap)] fn process_truncate(&mut self, path: &str, size: u64) -> Result<()> {
763 let full = self.full_path(path)?;
764 let c_path = path_to_cstring(&full)?;
765 let ret = unsafe {
766 nix::libc::truncate(c_path.as_ptr(), size as nix::libc::off_t)
767 };
768 if ret < 0 {
769 return Err(io::Error::last_os_error()).with_context(|| {
770 format!("truncate failed on '{}'", full.display())
771 });
772 }
773 Ok(())
774 }
775
776 #[allow(clippy::cast_possible_truncation)] fn process_chmod(&mut self, path: &str, mode: u64) -> Result<()> {
778 let full = self.full_path(path)?;
779 fs::set_permissions(&full, fs::Permissions::from_mode(mode as u32))
780 .with_context(|| format!("chmod failed on '{}'", full.display()))?;
781 Ok(())
782 }
783
784 #[allow(clippy::cast_possible_truncation)] fn process_chown(&mut self, path: &str, uid: u64, gid: u64) -> Result<()> {
786 let full = self.full_path(path)?;
787 let c_path = path_to_cstring(&full)?;
788 let ret = unsafe {
789 nix::libc::lchown(
790 c_path.as_ptr(),
791 uid as nix::libc::uid_t,
792 gid as nix::libc::gid_t,
793 )
794 };
795 if ret < 0 {
796 return Err(io::Error::last_os_error()).with_context(|| {
797 format!("lchown failed on '{}'", full.display())
798 });
799 }
800 Ok(())
801 }
802
803 #[allow(clippy::cast_possible_wrap)] fn process_utimes(
805 &mut self,
806 path: &str,
807 atime: &Timespec,
808 mtime: &Timespec,
809 ) -> Result<()> {
810 let full = self.full_path(path)?;
811 let c_path = path_to_cstring(&full)?;
812 let times = [
813 nix::libc::timespec {
814 tv_sec: atime.sec as i64,
815 tv_nsec: i64::from(atime.nsec),
816 },
817 nix::libc::timespec {
818 tv_sec: mtime.sec as i64,
819 tv_nsec: i64::from(mtime.nsec),
820 },
821 ];
822 let ret = unsafe {
823 nix::libc::utimensat(
824 nix::libc::AT_FDCWD,
825 c_path.as_ptr(),
826 times.as_ptr(),
827 nix::libc::AT_SYMLINK_NOFOLLOW,
828 )
829 };
830 if ret < 0 {
831 return Err(io::Error::last_os_error()).with_context(|| {
832 format!("utimensat failed on '{}'", full.display())
833 });
834 }
835 Ok(())
836 }
837
838 #[allow(clippy::cast_possible_wrap)] fn process_fallocate(
840 &mut self,
841 path: &str,
842 mode: u32,
843 offset: u64,
844 len: u64,
845 ) -> Result<()> {
846 let full = self.full_path(path)?;
847 self.close_write_fd();
848 let file =
849 OpenOptions::new()
850 .write(true)
851 .open(&full)
852 .with_context(|| {
853 format!("cannot open '{}' for fallocate", full.display())
854 })?;
855 let ret = unsafe {
856 nix::libc::fallocate(
857 file.as_raw_fd(),
858 mode as i32,
859 offset as nix::libc::off_t,
860 len as nix::libc::off_t,
861 )
862 };
863 if ret < 0 {
864 return Err(io::Error::last_os_error()).with_context(|| {
865 format!("fallocate failed on '{}'", full.display())
866 });
867 }
868 Ok(())
869 }
870
871 fn process_enable_verity(
872 &mut self,
873 path: &str,
874 algorithm: u8,
875 block_size: u32,
876 salt: &[u8],
877 sig: &[u8],
878 ) -> Result<()> {
879 let full = self.full_path(path)?;
880
881 self.close_write_fd();
884
885 let file = File::open(&full).with_context(|| {
887 format!("cannot open '{}' for verity", full.display())
888 })?;
889
890 crate::verity::enable_verity(
891 file.as_fd(),
892 algorithm,
893 block_size,
894 salt,
895 sig,
896 )
897 .with_context(|| {
898 format!("FS_IOC_ENABLE_VERITY failed on '{}'", full.display())
899 })?;
900
901 Ok(())
902 }
903
904 #[allow(clippy::too_many_arguments)]
905 fn process_encoded_write(
906 &mut self,
907 path: &str,
908 offset: u64,
909 unencoded_file_len: u64,
910 unencoded_len: u64,
911 unencoded_offset: u64,
912 compression: u32,
913 encryption: u32,
914 data: &[u8],
915 ) -> Result<()> {
916 use std::os::unix::fs::FileExt;
917
918 if encryption != 0 {
919 bail!(
920 "encrypted encoded writes are not supported (encryption={encryption})"
921 );
922 }
923
924 let full = self.full_path(path)?;
925
926 if self.write_path != full {
928 self.close_write_fd();
929 let file =
930 OpenOptions::new().write(true).open(&full).with_context(
931 || format!("cannot open '{}' for writing", full.display()),
932 )?;
933 self.write_fd = Some(file);
934 self.write_path.clone_from(&full);
935 }
936
937 let fd = self.write_fd.as_ref().unwrap();
940 match encoded_write(
941 fd.as_fd(),
942 data,
943 offset,
944 unencoded_file_len,
945 unencoded_len,
946 unencoded_offset,
947 compression,
948 encryption,
949 ) {
950 Ok(()) => return Ok(()),
951 Err(
952 nix::errno::Errno::ENOTTY
953 | nix::errno::Errno::EINVAL
954 | nix::errno::Errno::ENOSPC,
955 ) => {
956 }
958 Err(e) => {
959 return Err(e).with_context(|| {
960 format!("encoded write failed on '{}'", full.display())
961 });
962 }
963 }
964
965 #[allow(clippy::cast_possible_truncation)] let decompressed =
968 decompress(data, unencoded_len as usize, compression)
969 .with_context(|| {
970 format!("decompression failed for '{}'", full.display())
971 })?;
972
973 #[allow(clippy::cast_possible_truncation)]
974 let write_data = &decompressed[unencoded_offset as usize
976 ..unencoded_offset as usize + unencoded_file_len as usize];
977
978 fd.write_all_at(write_data, offset).with_context(|| {
979 format!("pwrite failed on '{}'", full.display())
980 })?;
981
982 Ok(())
983 }
984}
985
986fn decompress(
989 data: &[u8],
990 output_len: usize,
991 compression: u32,
992) -> Result<Vec<u8>> {
993 match compression {
994 0 => {
995 Ok(data.to_vec())
997 }
998 1 => {
999 use std::io::Read;
1001 let mut decoder = flate2::read::ZlibDecoder::new(data);
1002 let mut out = vec![0u8; output_len];
1003 decoder
1004 .read_exact(&mut out)
1005 .context("zlib decompression failed")?;
1006 Ok(out)
1007 }
1008 2 => {
1009 let out = zstd::bulk::decompress(data, output_len)
1011 .context("zstd decompression failed")?;
1012 Ok(out)
1013 }
1014 3..=7 => {
1015 let sector_size = 1usize << (compression - 3 + 12);
1018 decompress_lzo(data, output_len, sector_size)
1019 }
1020 _ => bail!("unsupported compression type {compression}"),
1021 }
1022}
1023
1024fn decompress_lzo(
1033 data: &[u8],
1034 output_len: usize,
1035 sector_size: usize,
1036) -> Result<Vec<u8>> {
1037 if data.len() < 4 {
1038 bail!("LZO data too short for header");
1039 }
1040 let total_len = u32::from_le_bytes(data[0..4].try_into().unwrap()) as usize;
1041 if total_len > data.len() {
1042 bail!(
1043 "LZO total length {total_len} exceeds data length {}",
1044 data.len()
1045 );
1046 }
1047
1048 let mut out = Vec::with_capacity(output_len);
1049 let mut pos = 4; while pos < total_len && out.len() < output_len {
1052 let sector_remaining = sector_size - (pos % sector_size);
1055 if sector_remaining < 4 {
1056 if total_len - pos <= sector_remaining {
1057 break;
1058 }
1059 pos += sector_remaining;
1060 }
1061
1062 if pos + 4 > total_len {
1063 bail!("LZO segment header truncated at offset {pos}");
1064 }
1065 let seg_len =
1066 u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
1067 pos += 4;
1068
1069 if pos + seg_len > data.len() {
1070 bail!(
1071 "LZO segment data truncated at offset {pos}, need {seg_len} bytes"
1072 );
1073 }
1074
1075 let remaining = (output_len - out.len()).min(sector_size);
1076 let mut segment_out = vec![0u8; remaining];
1077 lzokay::decompress::decompress(
1078 &data[pos..pos + seg_len],
1079 &mut segment_out,
1080 )
1081 .map_err(|e| {
1082 anyhow!("LZO decompression failed at offset {pos}: {e:?}")
1083 })?;
1084 out.extend_from_slice(&segment_out);
1085
1086 pos += seg_len;
1087 }
1088
1089 if out.len() < output_len {
1090 out.resize(output_len, 0);
1091 }
1092
1093 Ok(out)
1094}
1095
1096fn path_to_cstring(path: &Path) -> Result<CString> {
1097 use std::os::unix::ffi::OsStrExt;
1098 CString::new(path.as_os_str().as_bytes()).with_context(|| {
1099 format!("path contains null byte: '{}'", path.display())
1100 })
1101}
1102
1103#[cfg(test)]
1104mod tests {
1105 use super::*;
1106 use std::path::Path;
1107
1108 #[test]
1111 fn decompress_none_passthrough() {
1112 let data = b"hello world";
1113 let result = decompress(data, data.len(), 0).unwrap();
1114 assert_eq!(result, data);
1115 }
1116
1117 #[test]
1120 fn decompress_zlib() {
1121 use flate2::write::ZlibEncoder;
1122 use std::io::Write;
1123
1124 let original = b"the quick brown fox jumps over the lazy dog";
1125 let mut encoder =
1126 ZlibEncoder::new(Vec::new(), flate2::Compression::default());
1127 encoder.write_all(original).unwrap();
1128 let compressed = encoder.finish().unwrap();
1129
1130 let result = decompress(&compressed, original.len(), 1).unwrap();
1131 assert_eq!(result, original);
1132 }
1133
1134 #[test]
1137 fn decompress_zstd() {
1138 let original = b"repeating data repeating data repeating data";
1139 let compressed = zstd::bulk::compress(original, 3).unwrap();
1140
1141 let result = decompress(&compressed, original.len(), 2).unwrap();
1142 assert_eq!(result, original);
1143 }
1144
1145 #[test]
1148 fn decompress_unsupported_type() {
1149 let err = decompress(b"data", 4, 99).unwrap_err();
1150 assert!(
1151 err.to_string().contains("unsupported compression type 99"),
1152 "unexpected error: {err}"
1153 );
1154 }
1155
1156 #[test]
1159 fn decompress_lzo_header_too_short() {
1160 let err = decompress_lzo(&[0, 1, 2], 100, 4096).unwrap_err();
1161 assert!(
1162 err.to_string().contains("too short"),
1163 "unexpected error: {err}"
1164 );
1165 }
1166
1167 #[test]
1170 fn decompress_lzo_total_len_exceeds_data() {
1171 let mut data = vec![0u8; 8];
1173 data[0..4].copy_from_slice(&1000u32.to_le_bytes());
1174 let err = decompress_lzo(&data, 100, 4096).unwrap_err();
1175 assert!(
1176 err.to_string().contains("exceeds data length"),
1177 "unexpected error: {err}"
1178 );
1179 }
1180
1181 fn build_lzo_buffer(segments: &[Vec<u8>], sector_size: usize) -> Vec<u8> {
1184 let mut buf = Vec::new();
1185 buf.extend_from_slice(&[0u8; 4]);
1187
1188 for seg in segments {
1189 buf.extend_from_slice(&(seg.len() as u32).to_le_bytes());
1191 buf.extend_from_slice(seg);
1192 let pos = buf.len();
1196 let sector_rem = sector_size - (pos % sector_size);
1197 if sector_rem < 4 && sector_rem < sector_size {
1198 buf.resize(buf.len() + sector_rem, 0);
1200 }
1201 }
1202
1203 let total = buf.len() as u32;
1205 buf[0..4].copy_from_slice(&total.to_le_bytes());
1206 buf
1207 }
1208
1209 #[test]
1212 fn decompress_lzo_single_segment() {
1213 let original = b"hello lzo compression test data!";
1214 let compressed =
1215 lzo1x::compress(original, lzo1x::CompressLevel::default());
1216
1217 let buf = build_lzo_buffer(&[compressed], 4096);
1218 let result = decompress_lzo(&buf, original.len(), 4096).unwrap();
1219 assert_eq!(&result[..original.len()], original.as_slice());
1220 }
1221
1222 #[test]
1229 fn decompress_lzo_output_zero_fill() {
1230 let original = vec![0xABu8; 4096];
1232 let compressed =
1233 lzo1x::compress(&original, lzo1x::CompressLevel::default());
1234
1235 let output_len = 8192; let buf = build_lzo_buffer(&[compressed], 4096);
1237 let result = decompress_lzo(&buf, output_len, 4096).unwrap();
1238 assert_eq!(&result[..4096], original.as_slice());
1239 assert!(
1241 result[4096..].iter().all(|&b| b == 0),
1242 "expected zero-fill after decompressed data"
1243 );
1244 assert_eq!(result.len(), output_len);
1245 }
1246
1247 #[test]
1250 fn decompress_lzo_segment_truncated() {
1251 let mut data = vec![0u8; 20];
1254 let total_len: u32 = 20;
1255 data[0..4].copy_from_slice(&total_len.to_le_bytes());
1256 let seg_len: u32 = 100;
1258 data[4..8].copy_from_slice(&seg_len.to_le_bytes());
1259 let err = decompress_lzo(&data, 4096, 4096).unwrap_err();
1262 assert!(
1263 err.to_string().contains("truncated"),
1264 "unexpected error: {err}"
1265 );
1266 }
1267
1268 #[test]
1271 fn decompress_lzo_via_compression_type_3() {
1272 let original = b"lzo via decompress entry point";
1273 let compressed =
1274 lzo1x::compress(original, lzo1x::CompressLevel::default());
1275 let buf = build_lzo_buffer(&[compressed], 4096);
1276
1277 let result = decompress(&buf, original.len(), 3).unwrap();
1279 assert_eq!(&result[..original.len()], original.as_slice());
1280 }
1281
1282 #[test]
1285 fn path_to_cstring_valid() {
1286 let path = Path::new("/tmp/test-file");
1287 let cstr = path_to_cstring(path).unwrap();
1288 assert_eq!(cstr.as_bytes(), b"/tmp/test-file");
1289 }
1290
1291 #[test]
1292 fn path_to_cstring_with_null_byte() {
1293 use std::{ffi::OsStr, os::unix::ffi::OsStrExt};
1294 let os_str = OsStr::from_bytes(b"/tmp/bad\x00path");
1295 let path = Path::new(os_str);
1296 let err = path_to_cstring(path).unwrap_err();
1297 assert!(
1298 err.to_string().contains("null byte"),
1299 "unexpected error: {err}"
1300 );
1301 }
1302}