1use std::fmt;
35use std::fs::{self, File};
36use std::io::{self, Read};
37use std::path::{Component, Path, PathBuf};
38
39use rightkit_fs::{AtomicWriteOptions, AtomicWriter, FsError, ReplaceMode};
40
41use crate::discovery::{try_lock_exclusive, LockedFile};
42use crate::suite::{make_private_dir, Suite, SuiteError};
43
44pub const MAX_APP_NAME_LEN: usize = 64;
46pub const MIGRATION_MARKER: &str = ".rightkit-migrated.json";
49pub const MIGRATION_STATE: &str = ".rightkit-migration-state.json";
53const STATE_VERSION: u64 = 2;
55const SELECTED_STATE_VERSION: u64 = 3;
56const WRITER_LOCK: &str = ".writer.lock";
57
58const WINDOWS_RESERVED: [&str; 22] = [
59 "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8",
60 "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
61];
62
63pub fn valid_app_name(s: &str) -> bool {
69 let b = s.as_bytes();
70 if b.is_empty() || b.len() > MAX_APP_NAME_LEN {
71 return false;
72 }
73 if !b
74 .iter()
75 .all(|c| c.is_ascii_alphanumeric() || matches!(c, b'-' | b'_' | b'.'))
76 {
77 return false;
78 }
79 if !b[0].is_ascii_alphanumeric() || !b[b.len() - 1].is_ascii_alphanumeric() || s.contains("..")
80 {
81 return false;
82 }
83 if s.eq_ignore_ascii_case("shared") {
85 return false;
86 }
87 let stem = s.split('.').next().unwrap_or(s).to_ascii_uppercase();
88 !WINDOWS_RESERVED.contains(&stem.as_str())
89}
90
91#[derive(Debug)]
92pub enum AppRootError {
93 InvalidName(String),
94 PathEscape(String),
96 CaseConflict {
98 requested: String,
99 existing: String,
100 },
101 WriterBusy(PathBuf),
103 UnsupportedSource(PathBuf),
108 Collision(PathBuf),
110 Overlap(PathBuf),
112 VerifyFailed(PathBuf),
114 PeerDirMismatch,
118 Suite(SuiteError),
119 Io(io::Error),
120}
121
122impl fmt::Display for AppRootError {
123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124 match self {
125 Self::InvalidName(n) => write!(f, "invalid app name: {n:?}"),
126 Self::PathEscape(p) => write!(f, "path escapes the app root: {p}"),
127 Self::CaseConflict {
128 requested,
129 existing,
130 } => write!(
131 f,
132 "app root {requested:?} conflicts with existing {existing:?} (differs only by case)"
133 ),
134 Self::WriterBusy(p) => write!(f, "another writer holds {}", p.display()),
135 Self::UnsupportedSource(p) => {
136 write!(
137 f,
138 "refusing symlink or special file in source: {}",
139 p.display()
140 )
141 }
142 Self::Collision(p) => write!(f, "destination differs from source: {}", p.display()),
143 Self::Overlap(p) => write!(f, "source and destination overlap: {}", p.display()),
144 Self::VerifyFailed(p) => write!(f, "verification failed: {}", p.display()),
145 Self::PeerDirMismatch => {
146 write!(
147 f,
148 "the peer process uses a different data folder than this one"
149 )
150 }
151 Self::Suite(e) => write!(f, "{e}"),
152 Self::Io(e) => write!(f, "{e}"),
153 }
154 }
155}
156impl std::error::Error for AppRootError {}
157
158impl AppRootError {
159 pub fn link_not_created(&self) -> Option<&LinkNotCreated> {
164 match self {
165 Self::Io(e) => e.get_ref()?.downcast_ref::<LinkNotCreated>(),
166 _ => None,
167 }
168 }
169
170 pub fn is_retryable(&self) -> bool {
173 self.link_not_created().is_some() || matches!(self, Self::WriterBusy(_))
174 }
175}
176
177#[derive(Debug, Clone, PartialEq, Eq)]
184#[non_exhaustive]
185pub struct LinkNotCreated {
186 pub link: PathBuf,
188 pub target: PathBuf,
190 pub resolved: PathBuf,
192 pub reason: String,
194}
195
196impl fmt::Display for LinkNotCreated {
197 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198 write!(
199 f,
200 "cannot create symlink {} -> {} in the app root: {}",
201 self.link.display(),
202 self.target.display(),
203 self.reason
204 )
205 }
206}
207impl std::error::Error for LinkNotCreated {}
208
209impl From<io::Error> for AppRootError {
210 fn from(e: io::Error) -> Self {
211 Self::Io(e)
212 }
213}
214impl From<SuiteError> for AppRootError {
215 fn from(e: SuiteError) -> Self {
216 Self::Suite(e)
217 }
218}
219
220pub struct WriterLock {
223 _file: LockedFile,
224}
225
226#[derive(Debug, Clone, PartialEq, Eq)]
228pub enum MigrationOutcome {
229 Migrated { files: u64, bytes: u64 },
231 AlreadyMigrated,
233 NothingToMigrate,
235}
236
237#[derive(Debug, Clone, Copy, PartialEq, Eq)]
241#[non_exhaustive]
242pub enum MigrationState {
243 Ready,
246 ReadyWithOmissions,
251 Incomplete,
255}
256
257impl MigrationState {
258 pub fn as_str(self) -> &'static str {
260 match self {
261 Self::Ready => "ready",
262 Self::ReadyWithOmissions => "ready_with_omissions",
263 Self::Incomplete => "incomplete",
264 }
265 }
266
267 pub fn is_usable(self) -> bool {
270 !matches!(self, Self::Incomplete)
271 }
272
273 fn parse(s: &str) -> Option<Self> {
274 match s {
275 "ready" => Some(Self::Ready),
276 "ready_with_omissions" => Some(Self::ReadyWithOmissions),
277 "incomplete" => Some(Self::Incomplete),
278 _ => None,
279 }
280 }
281}
282
283#[derive(Debug, Clone, PartialEq, Eq)]
285#[non_exhaustive]
286pub enum MigrationScope {
287 All,
289 Roots(Vec<PathBuf>),
291}
292
293#[derive(Debug, Clone, PartialEq, Eq)]
295#[non_exhaustive]
296pub struct MigrationStatus {
297 state: MigrationState,
298 scope: MigrationScope,
299}
300
301impl MigrationStatus {
302 pub fn state(&self) -> MigrationState {
304 self.state
305 }
306 pub fn scope(&self) -> &MigrationScope {
308 &self.scope
309 }
310}
311
312#[derive(Debug, Clone, PartialEq, Eq)]
319#[non_exhaustive]
320pub struct MigrationReport {
321 outcome: MigrationOutcome,
322 migrated_links: Vec<MigratedLink>,
323 skipped_specials: Vec<SkippedSpecial>,
324 omitted_optional_links: Vec<OmittedLink>,
325 skipped_reparse_points: Vec<SkippedReparsePoint>,
326 state: MigrationState,
327}
328
329impl MigrationReport {
330 fn bare(outcome: MigrationOutcome, state: MigrationState) -> Self {
331 Self {
332 outcome,
333 migrated_links: Vec::new(),
334 skipped_specials: Vec::new(),
335 omitted_optional_links: Vec::new(),
336 skipped_reparse_points: Vec::new(),
337 state,
338 }
339 }
340
341 pub fn state(&self) -> MigrationState {
344 self.state
345 }
346
347 pub fn skipped_reparse_points(&self) -> &[SkippedReparsePoint] {
350 &self.skipped_reparse_points
351 }
352
353 pub fn outcome(&self) -> &MigrationOutcome {
355 &self.outcome
356 }
357
358 pub fn into_outcome(self) -> MigrationOutcome {
359 self.outcome
360 }
361
362 pub fn migrated_links(&self) -> &[MigratedLink] {
364 &self.migrated_links
365 }
366
367 pub fn skipped_specials(&self) -> &[SkippedSpecial] {
369 &self.skipped_specials
370 }
371
372 pub fn omitted_optional_links(&self) -> &[OmittedLink] {
375 &self.omitted_optional_links
376 }
377
378 pub fn complete(&self) -> bool {
382 self.state == MigrationState::Ready
383 }
384}
385
386#[derive(Debug, Clone, PartialEq, Eq)]
388pub struct MigratedLink {
389 pub path: PathBuf,
391 pub original_target: PathBuf,
393 pub written_target: PathBuf,
395 pub rewritten: bool,
398}
399
400#[derive(Debug, Clone, Copy, PartialEq, Eq)]
402pub enum SpecialKind {
403 Socket,
404 Fifo,
405}
406
407impl SpecialKind {
408 fn as_str(self) -> &'static str {
409 match self {
410 Self::Socket => "socket",
411 Self::Fifo => "fifo",
412 }
413 }
414}
415
416#[derive(Debug, Clone, PartialEq, Eq)]
418pub struct SkippedSpecial {
419 pub path: PathBuf,
421 pub kind: SpecialKind,
422}
423
424#[derive(Debug, Clone, Copy, PartialEq, Eq)]
426#[non_exhaustive]
427pub enum ReparseKind {
428 Junction,
431 Other,
434}
435
436impl ReparseKind {
437 fn as_str(self) -> &'static str {
438 match self {
439 Self::Junction => "junction",
440 Self::Other => "other",
441 }
442 }
443}
444
445#[derive(Debug, Clone, PartialEq, Eq)]
448#[non_exhaustive]
449pub struct SkippedReparsePoint {
450 pub path: PathBuf,
452 pub kind: ReparseKind,
453 pub tag: u32,
455 pub reason: String,
457}
458
459#[derive(Debug, Clone, PartialEq, Eq)]
461pub struct OmittedLink {
462 pub path: PathBuf,
464 pub original_target: PathBuf,
466 pub resolved_target: PathBuf,
468 pub reason: String,
470}
471
472type OptionalLinkPolicy = std::sync::Arc<dyn Fn(&Path) -> bool + Send + Sync>;
473
474#[derive(Clone, Default)]
479#[non_exhaustive]
480pub struct MigrateOptions {
481 links: bool,
482 ephemeral_specials: bool,
483 reparse_points: bool,
484 optional_link: Option<OptionalLinkPolicy>,
485 excluded: Vec<PathBuf>,
486 pub(crate) included: Option<Vec<PathBuf>>,
487}
488
489impl fmt::Debug for MigrateOptions {
490 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
491 f.debug_struct("MigrateOptions")
492 .field("links", &self.links)
493 .field("ephemeral_specials", &self.ephemeral_specials)
494 .field("reparse_points", &self.reparse_points)
495 .field("optional_link_policy", &self.optional_link.is_some())
496 .field("excluded", &self.excluded)
497 .field("included", &self.included)
498 .finish()
499 }
500}
501
502impl MigrateOptions {
503 pub fn include_roots(mut self, roots: Vec<PathBuf>) -> Result<Self, AppRootError> {
509 self.included = Some(normalize_roots(roots)?);
510 Ok(self)
511 }
512
513 fn selection(&self, rel: &Path) -> Result<bool, AppRootError> {
514 let Some(roots) = &self.included else {
515 return Ok(true);
516 };
517 for root in roots {
518 if rel.starts_with(root) || root.starts_with(rel) {
519 return Ok(true);
520 }
521 let a: Vec<_> = rel.components().collect();
522 let b: Vec<_> = root.components().collect();
523 if a.iter()
524 .zip(&b)
525 .all(|(x, y)| folded_component(*x) == folded_component(*y))
526 {
527 return Err(AppRootError::UnsupportedSource(rel.to_path_buf()));
528 }
529 }
530 Ok(false)
531 }
532
533 fn scope_state(&self, mut body: serde_json::Value) -> serde_json::Value {
534 if let Some(roots) = &self.included {
535 body["version"] = serde_json::json!(SELECTED_STATE_VERSION);
536 body["selected_roots"] = serde_json::json!(roots);
537 }
538 body
539 }
540
541 pub fn strict() -> Self {
543 Self::default()
544 }
545
546 pub fn preserve_links(mut self, on: bool) -> Self {
553 self.links = on;
554 self
555 }
556
557 pub fn skip_ephemeral_specials(mut self, on: bool) -> Self {
561 self.ephemeral_specials = on;
562 self
563 }
564
565 pub fn skip_reparse_points(mut self, on: bool) -> Self {
575 self.reparse_points = on;
576 self
577 }
578
579 pub fn optional_links(
585 mut self,
586 is_optional: impl Fn(&Path) -> bool + Send + Sync + 'static,
587 ) -> Self {
588 self.optional_link = Some(std::sync::Arc::new(is_optional));
589 self
590 }
591
592 pub fn exclude(mut self, rel: impl Into<PathBuf>) -> Self {
602 let rel = rel.into();
603 if rel.components().next().is_some()
604 && rel.components().all(|c| matches!(c, Component::Normal(_)))
605 {
606 self.excluded.push(rel);
607 }
608 self
609 }
610
611 pub fn links(&self) -> bool {
612 self.links
613 }
614
615 pub fn ephemeral_specials(&self) -> bool {
616 self.ephemeral_specials
617 }
618
619 pub fn reparse_points(&self) -> bool {
620 self.reparse_points
621 }
622
623 fn is_strict(&self) -> bool {
624 !self.links
625 && !self.ephemeral_specials
626 && !self.reparse_points
627 && self.optional_link.is_none()
628 }
629
630 fn is_optional(&self, rel: &Path) -> bool {
631 self.optional_link.as_ref().is_some_and(|f| f(rel))
632 }
633
634 fn is_excluded(&self, rel: &Path) -> Result<bool, AppRootError> {
637 for ex in &self.excluded {
638 if rel.starts_with(ex) {
639 return Ok(true);
640 }
641 let mut have = rel.components();
642 let ambiguous = ex.components().all(|want| match (want, have.next()) {
643 (Component::Normal(w), Some(Component::Normal(h))) => {
644 w.to_string_lossy().to_lowercase() == h.to_string_lossy().to_lowercase()
645 }
646 _ => false,
647 });
648 if ambiguous {
649 return Err(AppRootError::UnsupportedSource(rel.to_path_buf()));
650 }
651 }
652 Ok(false)
653 }
654}
655
656pub(crate) fn normalize_roots(mut roots: Vec<PathBuf>) -> Result<Vec<PathBuf>, AppRootError> {
657 for root in &roots {
658 if root.as_os_str().is_empty()
659 || root.to_str().is_none()
660 || !root.components().all(|c| matches!(c, Component::Normal(_)))
661 || root
662 .components()
663 .any(|c| c.as_os_str().to_string_lossy().contains(['\\', ':', '\0']))
664 || root
665 .to_string_lossy()
666 .split(['/', '\\'])
667 .any(|part| part.is_empty() || part == "." || part == "..")
668 || [MIGRATION_STATE, MIGRATION_MARKER, WRITER_LOCK]
669 .iter()
670 .any(|reserved| roots_overlap(root, Path::new(reserved)))
671 {
672 return Err(AppRootError::PathEscape(root.display().to_string()));
673 }
674 }
675 roots.sort();
676 roots.dedup();
677 for (i, root) in roots.iter().enumerate() {
678 for other in &roots[..i] {
679 let a: Vec<_> = root.components().collect();
680 let b: Vec<_> = other.components().collect();
681 for (x, y) in a.iter().zip(&b) {
682 if x == y {
683 continue;
684 }
685 if folded_component(*x) == folded_component(*y) {
686 return Err(AppRootError::PathEscape(root.display().to_string()));
687 }
688 break;
689 }
690 }
691 }
692 let all = roots.clone();
693 roots.retain(|root| {
694 !all.iter()
695 .any(|other| root != other && root.starts_with(other))
696 });
697 Ok(roots)
698}
699
700fn folded_component(component: Component<'_>) -> String {
701 component.as_os_str().to_string_lossy().to_lowercase()
702}
703
704pub(crate) fn roots_overlap(a: &Path, b: &Path) -> bool {
705 a.components()
706 .zip(b.components())
707 .all(|(x, y)| folded_component(x) == folded_component(y))
708}
709
710#[derive(Debug, Clone)]
712pub struct AppRoot {
713 name: String,
714 root: PathBuf,
715}
716
717impl Suite {
718 pub fn app_root(&self, app: &str) -> Result<AppRoot, AppRootError> {
720 AppRoot::new(self, app)
721 }
722}
723
724impl AppRoot {
725 pub fn new(suite: &Suite, app: &str) -> Result<AppRoot, AppRootError> {
726 if !valid_app_name(app) {
727 return Err(AppRootError::InvalidName(app.to_string()));
728 }
729 let parent = suite.app_parent();
730 if let Ok(rd) = fs::read_dir(&parent) {
731 for e in rd.flatten() {
732 let n = e.file_name().to_string_lossy().into_owned();
733 if n != app && n.eq_ignore_ascii_case(app) {
734 return Err(AppRootError::CaseConflict {
735 requested: app.to_string(),
736 existing: n,
737 });
738 }
739 }
740 }
741 let root = parent.join(app);
742 make_private_dir(&root)?;
743 Ok(AppRoot {
744 name: app.to_string(),
745 root,
746 })
747 }
748
749 pub fn name(&self) -> &str {
750 &self.name
751 }
752
753 pub fn root(&self) -> &Path {
754 &self.root
755 }
756
757 pub fn verify_peer_dir(&self, peer: &Path) -> Result<(), AppRootError> {
765 let ours = fs::canonicalize(&self.root)?;
766 let theirs = fs::canonicalize(peer).map_err(|_| AppRootError::PeerDirMismatch)?;
767 if same_directory(&ours, &theirs) {
768 Ok(())
769 } else {
770 Err(AppRootError::PeerDirMismatch)
771 }
772 }
773
774 pub fn path(&self, rel: impl AsRef<Path>) -> Result<PathBuf, AppRootError> {
778 self.path_checked(rel.as_ref(), true)
779 }
780
781 fn path_checked(&self, rel: &Path, follow_leaf: bool) -> Result<PathBuf, AppRootError> {
784 let bad = || AppRootError::PathEscape(rel.display().to_string());
785 let mut any = false;
786 for c in rel.components() {
787 match c {
788 Component::Normal(n) => {
789 any = true;
790 if n.to_string_lossy().contains(['\\', ':', '\0']) {
792 return Err(bad());
793 }
794 }
795 _ => return Err(bad()),
796 }
797 }
798 if !any {
799 return Err(bad());
800 }
801 let full = self.root.join(rel);
802 let canon_root = fs::canonicalize(&self.root)?;
803 let mut probe = if follow_leaf {
804 full.as_path()
805 } else {
806 full.parent().unwrap_or(&self.root)
807 };
808 loop {
809 match fs::canonicalize(probe) {
810 Ok(c) => {
811 if !c.starts_with(&canon_root) {
812 return Err(bad());
813 }
814 break;
815 }
816 Err(e) if e.kind() == io::ErrorKind::NotFound => {
817 if fs::symlink_metadata(probe).is_ok() {
819 return Err(bad());
820 }
821 match probe.parent() {
822 Some(p) if p.starts_with(&self.root) => probe = p,
823 _ => return Err(bad()),
824 }
825 }
826 Err(e) => return Err(e.into()),
827 }
828 }
829 Ok(full)
830 }
831
832 pub fn data_dir(&self, rel: impl AsRef<Path>) -> Result<PathBuf, AppRootError> {
834 let d = self.path(rel)?;
835 make_private_dir(&d)?;
836 self.path(d.strip_prefix(&self.root).unwrap_or(&d))?;
838 Ok(d)
839 }
840
841 pub fn lock_writer(&self) -> Result<WriterLock, AppRootError> {
843 let p = self.root.join(WRITER_LOCK);
844 match try_lock_exclusive(&p)? {
845 Some(f) => Ok(WriterLock { _file: f }),
846 None => Err(AppRootError::WriterBusy(p)),
847 }
848 }
849
850 pub fn migrate_from(&self, src: &Path) -> Result<MigrationOutcome, AppRootError> {
861 Ok(self
862 .migrate_from_with(src, MigrateOptions::strict())?
863 .into_outcome())
864 }
865
866 pub fn migrate_from_with(
879 &self,
880 src: &Path,
881 opts: MigrateOptions,
882 ) -> Result<MigrationReport, AppRootError> {
883 self.migrate_impl(src, &opts, &make_link)
884 }
885
886 pub fn migration_state(&self, src: &Path) -> Result<Option<MigrationState>, AppRootError> {
894 Ok(self.migration_status(src)?.map(|status| status.state()))
895 }
896
897 pub fn migration_status(&self, src: &Path) -> Result<Option<MigrationStatus>, AppRootError> {
902 if !src.is_absolute() || src.components().any(|c| c == Component::ParentDir) {
903 return Err(AppRootError::PathEscape(src.display().to_string()));
904 }
905 let csrc = match fs::canonicalize(src) {
906 Ok(c) => c,
907 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
908 Err(e) => return Err(e.into()),
909 };
910 let invalid = |detail: &str| {
911 AppRootError::Io(io::Error::new(
912 io::ErrorKind::InvalidData,
913 detail.to_string(),
914 ))
915 };
916 let valid_source = |source: &str| {
917 let path = Path::new(source);
918 path.is_absolute() && !path.components().any(|part| part == Component::ParentDir)
919 };
920 let read = |path: &Path| -> Result<Option<serde_json::Value>, AppRootError> {
921 let text = match fs::read_to_string(path) {
922 Ok(text) => text,
923 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
924 Err(error) => return Err(error.into()),
925 };
926 serde_json::from_str(&text)
927 .map(Some)
928 .map_err(|_| invalid("invalid migration metadata JSON"))
929 };
930 if let Some(value) = read(&self.root.join(MIGRATION_STATE))? {
931 let source = value
932 .get("source")
933 .and_then(serde_json::Value::as_str)
934 .ok_or_else(|| invalid("missing migration source"))?;
935 if !valid_source(source) {
936 return Err(invalid("invalid migration source"));
937 }
938 let state = value
939 .get("state")
940 .and_then(serde_json::Value::as_str)
941 .and_then(MigrationState::parse)
942 .ok_or_else(|| invalid("unknown migration state"))?;
943 let scope = match value.get("version").and_then(serde_json::Value::as_u64) {
944 Some(1) | Some(STATE_VERSION) if value.get("selected_roots").is_none() => {
945 MigrationScope::All
946 }
947 Some(SELECTED_STATE_VERSION) => {
948 let roots: Vec<PathBuf> = serde_json::from_value(
949 value
950 .get("selected_roots")
951 .cloned()
952 .ok_or_else(|| invalid("missing selected scope"))?,
953 )
954 .map_err(|_| invalid("invalid selected scope"))?;
955 if normalize_roots(roots.clone())
956 .map_err(|_| invalid("invalid selected scope"))?
957 != roots
958 {
959 return Err(invalid("noncanonical selected scope"));
960 }
961 MigrationScope::Roots(roots)
962 }
963 _ => return Err(invalid("unknown migration version or scope")),
964 };
965 return Ok((Some(source) == csrc.to_str()).then_some(MigrationStatus { state, scope }));
966 }
967 let Some(marker) = read(&self.root.join(MIGRATION_MARKER))? else {
968 return Ok(None);
969 };
970 let source = marker
971 .get("source")
972 .and_then(serde_json::Value::as_str)
973 .ok_or_else(|| invalid("missing historical migration source"))?;
974 if !valid_source(source) {
975 return Err(invalid("invalid historical migration source"));
976 }
977 let complete = match marker.get("complete") {
978 None => true,
979 Some(value) => value
980 .as_bool()
981 .ok_or_else(|| invalid("invalid historical completion flag"))?,
982 };
983 Ok((Some(source) == csrc.to_str()).then_some(MigrationStatus {
984 state: if complete {
985 MigrationState::Ready
986 } else {
987 MigrationState::ReadyWithOmissions
988 },
989 scope: MigrationScope::All,
990 }))
991 }
992
993 fn read_state(
997 &self,
998 csrc: &Path,
999 ) -> Result<Option<(MigrationState, serde_json::Value)>, AppRootError> {
1000 let path = self.root.join(MIGRATION_STATE);
1001 let text = match fs::read_to_string(&path) {
1002 Ok(t) => t,
1003 Err(e)
1004 if matches!(
1005 e.kind(),
1006 io::ErrorKind::NotFound | io::ErrorKind::InvalidData
1007 ) =>
1008 {
1009 return Ok(None)
1010 }
1011 Err(e) => return Err(e.into()),
1012 };
1013 let Ok(v) = serde_json::from_str::<serde_json::Value>(&text) else {
1014 return Ok(None);
1015 };
1016 let Some(version) = v.get("version").and_then(|n| n.as_u64()) else {
1017 return Ok(None);
1018 };
1019 if v.get("source").and_then(|s| s.as_str()) != csrc.to_str() {
1020 return Ok(None);
1021 }
1022 let state = v
1023 .get("state")
1024 .and_then(|s| s.as_str())
1025 .and_then(MigrationState::parse);
1026 match state {
1027 Some(state) if version <= SELECTED_STATE_VERSION => {
1028 if version < SELECTED_STATE_VERSION && v.get("selected_roots").is_some() {
1029 return Err(AppRootError::PathEscape("legacy state cannot carry selected scope".into()));
1030 }
1031 if version == SELECTED_STATE_VERSION {
1032 let roots: Vec<PathBuf> = serde_json::from_value(v.get("selected_roots").cloned().ok_or_else(|| AppRootError::PathEscape("missing selected scope".into()))?)
1033 .map_err(|_| AppRootError::PathEscape("invalid selected scope".into()))?;
1034 if normalize_roots(roots.clone())? != roots {
1035 return Err(AppRootError::PathEscape("noncanonical selected scope".into()));
1036 }
1037 }
1038 Ok(Some((state, v)))
1039 },
1040 _ => Err(AppRootError::Io(io::Error::new(
1041 io::ErrorKind::InvalidData,
1042 format!(
1043 "{} was written by a newer version or holds an unknown state; refusing to guess",
1044 path.display()
1045 ),
1046 ))),
1047 }
1048 }
1049
1050 fn legacy_marker(&self, csrc: &Path) -> Option<bool> {
1052 let text = fs::read_to_string(self.root.join(MIGRATION_MARKER)).ok()?;
1053 let v = serde_json::from_str::<serde_json::Value>(&text).ok()?;
1054 if v.get("source").and_then(|s| s.as_str()) != csrc.to_str() {
1055 return None;
1056 }
1057 Some(v.get("complete").and_then(|c| c.as_bool()).unwrap_or(true))
1058 }
1059
1060 fn write_state(&self, body: &serde_json::Value) -> Result<(), AppRootError> {
1061 write_json_atomic(&self.root.join(MIGRATION_STATE), body)?;
1062 Ok(())
1063 }
1064
1065 fn migrate_impl(
1066 &self,
1067 src: &Path,
1068 opts: &MigrateOptions,
1069 maker: LinkMaker<'_>,
1070 ) -> Result<MigrationReport, AppRootError> {
1071 self.migrate_current(src, opts, maker, false)
1072 }
1073
1074 pub(crate) fn migrate_for_cutover(
1076 &self,
1077 src: &Path,
1078 opts: &MigrateOptions,
1079 ) -> Result<MigrationReport, AppRootError> {
1080 self.migrate_current(src, opts, &make_link, true)
1081 }
1082
1083 fn migrate_current(
1084 &self,
1085 src: &Path,
1086 opts: &MigrateOptions,
1087 maker: LinkMaker<'_>,
1088 revalidate: bool,
1089 ) -> Result<MigrationReport, AppRootError> {
1090 if !src.is_absolute() || src.components().any(|c| c == Component::ParentDir) {
1091 return Err(AppRootError::PathEscape(src.display().to_string()));
1092 }
1093 let md = match fs::symlink_metadata(src) {
1094 Ok(m) => m,
1095 Err(e) if e.kind() == io::ErrorKind::NotFound => {
1096 return Ok(MigrationReport::bare(
1097 MigrationOutcome::NothingToMigrate,
1098 MigrationState::Ready,
1099 ))
1100 }
1101 Err(e) => return Err(e.into()),
1102 };
1103 if !md.is_dir() {
1104 return Err(AppRootError::UnsupportedSource(src.to_path_buf()));
1105 }
1106 let csrc = fs::canonicalize(src)?;
1107 let cdst = fs::canonicalize(&self.root)?;
1108 if csrc.starts_with(&cdst) || cdst.starts_with(&csrc) {
1109 return Err(AppRootError::Overlap(src.to_path_buf()));
1110 }
1111 let _lock = self.lock_writer()?;
1112 let prior = self.read_state(&csrc)?;
1113 let prior_selected = prior
1114 .as_ref()
1115 .is_some_and(|(_, state)| state.get("selected_roots").is_some());
1116 let mut effective = opts.clone();
1117 if let (Some(roots), Some((_, state))) = (&mut effective.included, &prior) {
1118 if let Some(previous) = state.get("selected_roots") {
1119 let previous: Vec<PathBuf> = serde_json::from_value(previous.clone())
1120 .map_err(|_| AppRootError::PathEscape("invalid selected scope".into()))?;
1121 roots.extend(previous);
1122 *roots = normalize_roots(std::mem::take(roots))?;
1123 }
1124 }
1125 let opts = &effective;
1126 let revalidate = revalidate || opts.included.is_some() || prior_selected;
1127 match prior {
1128 Some((MigrationState::Ready, state)) if !revalidate => {
1129 self.repair_marker(&csrc, &state);
1130 return Ok(MigrationReport::bare(
1131 MigrationOutcome::AlreadyMigrated,
1132 MigrationState::Ready,
1133 ));
1134 }
1135 Some((MigrationState::ReadyWithOmissions, state)) if !revalidate => {
1136 return self.retry_unresolved(&csrc, &state, maker);
1137 }
1138 Some(_) => {}
1140 None => {
1141 if let Some(complete) = self.legacy_marker(&csrc).filter(|_| !revalidate) {
1142 let state = if complete {
1143 MigrationState::Ready
1144 } else {
1145 MigrationState::ReadyWithOmissions
1146 };
1147 return Ok(MigrationReport::bare(
1148 MigrationOutcome::AlreadyMigrated,
1149 state,
1150 ));
1151 }
1152 }
1153 }
1154 let mut entries = Vec::new();
1156 collect(&csrc, Path::new(""), opts, &mut entries)?;
1157 if revalidate {
1158 let current: std::collections::BTreeSet<_> = entries.iter().map(Entry::rel).collect();
1161 let mut destination = Vec::new();
1162 let destination_opts = opts.clone().exclude(WRITER_LOCK);
1163 collect(&cdst, Path::new(""), &destination_opts, &mut destination)?;
1164 for entry in &destination {
1165 if !current.contains(entry.rel()) {
1166 return Err(AppRootError::Collision(cdst.join(entry.rel())));
1167 }
1168 }
1169 }
1170 self.write_state(&opts.scope_state(state_json(
1173 MigrationState::Incomplete,
1174 csrc.to_str(),
1175 &Lists::default(),
1176 None,
1177 )))?;
1178 match self.copy_entries(&csrc, &entries, opts, maker) {
1179 Ok(done) => self.commit(&csrc, opts, done),
1180 Err(e) => {
1181 self.record_failure(&csrc, opts, &e);
1182 Err(e)
1183 }
1184 }
1185 }
1186
1187 fn copy_entries(
1189 &self,
1190 csrc: &Path,
1191 entries: &[Entry],
1192 opts: &MigrateOptions,
1193 maker: LinkMaker<'_>,
1194 ) -> Result<Lists, AppRootError> {
1195 let mut done = Lists::default();
1196 let mut created: Vec<(PathBuf, PathBuf, bool)> = Vec::new();
1198 for entry in entries {
1199 match entry {
1200 Entry::Dir { rel } => {
1201 let to = self.path_checked(rel, false)?;
1204 match fs::symlink_metadata(&to) {
1205 Ok(m) if m.file_type().is_dir() => make_private_dir(&to)?,
1206 Ok(_) => return Err(AppRootError::Collision(to)),
1207 Err(e) if e.kind() == io::ErrorKind::NotFound => make_private_dir(&to)?,
1208 Err(e) => return Err(e.into()),
1209 }
1210 }
1211 Entry::File { rel, len } => {
1212 done.files += 1;
1213 done.bytes += len;
1214 let from = csrc.join(rel);
1215 let to = self.path(rel)?;
1216 if let Some(parent) = to.parent() {
1217 make_private_dir(parent)?;
1218 }
1219 match fs::symlink_metadata(&to) {
1220 Ok(m) if regular_entry(&m) && same_content(&from, &to)? => {
1221 continue;
1222 }
1223 Ok(_) => return Err(AppRootError::Collision(to)),
1224 Err(e) if e.kind() == io::ErrorKind::NotFound => {}
1225 Err(e) => return Err(e.into()),
1226 }
1227 let mut writer =
1228 AtomicWriter::create_new_private(&to).map_err(io::Error::other)?;
1229 let mut source =
1230 rightkit_fs::open_read_nofollow(&from, &rightkit_fs::ReadOptions::new())
1231 .map_err(io::Error::other)?;
1232 io::copy(&mut source, writer.file_mut())?;
1233 commit_migration_file(writer, &from, &to)?;
1234 }
1235 Entry::Link { rel, target, dir } => {
1236 let (written, rewritten) = plan_link(csrc, rel, target);
1237 let to = self.path_checked(rel, false)?;
1239 if let Some(parent) = to.parent() {
1240 make_private_dir(parent)?;
1241 }
1242 match fs::symlink_metadata(&to) {
1243 Ok(m) => {
1244 if !(m.file_type().is_symlink()
1245 && same_link_target(&fs::read_link(&to)?, &written))
1246 {
1247 return Err(AppRootError::Collision(to));
1248 }
1249 }
1250 Err(e) if e.kind() == io::ErrorKind::NotFound => {
1251 if let Err(e) = maker(&written, &to, *dir) {
1252 if !link_unavailable(&e) {
1253 return Err(e.into());
1254 }
1255 if opts.is_optional(rel) {
1256 done.omitted.push(OmittedLink {
1257 path: rel.clone(),
1258 original_target: target.clone(),
1259 resolved_target: written,
1260 reason: e.to_string(),
1261 });
1262 continue;
1263 }
1264 return Err(AppRootError::Io(io::Error::other(LinkNotCreated {
1265 link: rel.clone(),
1266 target: target.clone(),
1267 resolved: written,
1268 reason: e.to_string(),
1269 })));
1270 }
1271 }
1272 Err(e) => return Err(e.into()),
1273 }
1274 created.push((rel.clone(), written.clone(), *dir));
1275 done.links.push(MigratedLink {
1276 path: rel.clone(),
1277 original_target: target.clone(),
1278 written_target: written,
1279 rewritten,
1280 });
1281 }
1282 Entry::Special { rel, kind } => done.specials.push(SkippedSpecial {
1283 path: rel.clone(),
1284 kind: *kind,
1285 }),
1286 Entry::Reparse { rel, kind, tag } => done.reparse.push(SkippedReparsePoint {
1287 path: rel.clone(),
1288 kind: *kind,
1289 tag: *tag,
1290 reason: reparse_reason(*kind, *tag),
1291 }),
1292 }
1293 }
1294 for entry in entries {
1295 match entry {
1296 Entry::File { rel, .. } => {
1297 let to = self.path(rel)?;
1298 if !same_content(&csrc.join(rel), &to)? {
1299 return Err(AppRootError::VerifyFailed(to));
1300 }
1301 }
1302 Entry::Dir { rel } => {
1303 let to = self.path_checked(rel, false)?;
1304 if !fs::symlink_metadata(&to).is_ok_and(|m| m.file_type().is_dir()) {
1305 return Err(AppRootError::VerifyFailed(to));
1306 }
1307 }
1308 _ => {}
1309 }
1310 }
1311 for (rel, written, dir) in &created {
1312 let to = self.path_checked(rel, false)?;
1314 if !link_ok(&to, written, *dir) {
1315 return Err(AppRootError::VerifyFailed(to));
1316 }
1317 }
1318 Ok(done)
1319 }
1320
1321 fn commit(
1325 &self,
1326 csrc: &Path,
1327 opts: &MigrateOptions,
1328 done: Lists,
1329 ) -> Result<MigrationReport, AppRootError> {
1330 let state = done.state();
1331 self.write_state(&opts.scope_state(state_json(state, csrc.to_str(), &done, None)))?;
1332 if state == MigrationState::Ready && opts.included.is_none() {
1333 let body = marker_json(
1334 csrc.to_str(),
1335 done.files,
1336 done.bytes,
1337 opts,
1338 &done.links,
1339 &done.specials,
1340 &done.omitted,
1341 );
1342 write_json_atomic(&self.root.join(MIGRATION_MARKER), &body)?;
1343 }
1344 Ok(MigrationReport {
1345 outcome: MigrationOutcome::Migrated {
1346 files: done.files,
1347 bytes: done.bytes,
1348 },
1349 migrated_links: done.links,
1350 skipped_specials: done.specials,
1351 omitted_optional_links: done.omitted,
1352 skipped_reparse_points: done.reparse,
1353 state,
1354 })
1355 }
1356
1357 fn record_failure(&self, csrc: &Path, opts: &MigrateOptions, e: &AppRootError) {
1359 let mut body = opts.scope_state(state_json(
1360 MigrationState::Incomplete,
1361 csrc.to_str(),
1362 &Lists::default(),
1363 Some(&e.to_string()),
1364 ));
1365 if let (Some(l), Some(obj)) = (e.link_not_created(), body.as_object_mut()) {
1366 obj.insert(
1367 "unresolved".into(),
1368 serde_json::json!([{"path": lossy(&l.link), "kind": "link", "reason": l.reason}]),
1369 );
1370 }
1371 let _ = self.write_state(&body);
1372 }
1373
1374 fn repair_marker(&self, csrc: &Path, state: &serde_json::Value) {
1376 let path = self.root.join(MIGRATION_MARKER);
1377 if path.exists() {
1378 return;
1379 }
1380 let l = Lists::from_state(state);
1381 let opts = MigrateOptions::strict().preserve_links(true);
1382 let body = marker_json(
1383 csrc.to_str(),
1384 l.files,
1385 l.bytes,
1386 &opts,
1387 &l.links,
1388 &l.specials,
1389 &l.omitted,
1390 );
1391 let _ = write_json_atomic(&path, &body);
1392 }
1393
1394 fn retry_unresolved(
1400 &self,
1401 csrc: &Path,
1402 state: &serde_json::Value,
1403 maker: LinkMaker<'_>,
1404 ) -> Result<MigrationReport, AppRootError> {
1405 let prior = Lists::from_state(state);
1406 let mut next = Lists {
1407 files: prior.files,
1408 bytes: prior.bytes,
1409 links: prior.links,
1410 specials: prior.specials,
1411 omitted: Vec::new(),
1412 reparse: prior.reparse,
1413 };
1414 let mut created = Vec::new();
1415 for o in prior.omitted {
1416 match self.retry_link(csrc, &o, maker)? {
1417 Ok(link) => {
1418 created.push(link.clone());
1419 next.links.push(link);
1420 }
1421 Err(reason) => next.omitted.push(OmittedLink { reason, ..o }),
1422 }
1423 }
1424 let now = next.state();
1425 if !created.is_empty() || now == MigrationState::Ready {
1426 self.write_state(&state_json(now, csrc.to_str(), &next, None))?;
1427 if now == MigrationState::Ready {
1428 let opts = MigrateOptions::strict().preserve_links(true);
1429 let body = marker_json(
1430 csrc.to_str(),
1431 next.files,
1432 next.bytes,
1433 &opts,
1434 &next.links,
1435 &next.specials,
1436 &next.omitted,
1437 );
1438 write_json_atomic(&self.root.join(MIGRATION_MARKER), &body)?;
1439 }
1440 }
1441 Ok(MigrationReport {
1442 outcome: MigrationOutcome::AlreadyMigrated,
1443 migrated_links: created,
1444 skipped_specials: Vec::new(),
1445 omitted_optional_links: next.omitted,
1446 skipped_reparse_points: next.reparse,
1447 state: now,
1448 })
1449 }
1450
1451 fn retry_link(
1454 &self,
1455 csrc: &Path,
1456 o: &OmittedLink,
1457 maker: LinkMaker<'_>,
1458 ) -> Result<Result<MigratedLink, String>, AppRootError> {
1459 let from = csrc.join(&o.path);
1460 let md = match fs::symlink_metadata(&from) {
1461 Ok(m) if m.file_type().is_symlink() => m,
1462 Ok(_) => return Ok(Err("the source entry is no longer a link".to_string())),
1463 Err(e) if e.kind() == io::ErrorKind::NotFound => {
1464 return Ok(Err("the source link no longer exists".to_string()))
1465 }
1466 Err(e) => return Err(e.into()),
1467 };
1468 let target = fs::read_link(&from)?;
1469 let dir = link_is_dir(&md.file_type());
1470 let (written, rewritten) = plan_link(csrc, &o.path, &target);
1471 let to = self.path_checked(&o.path, false)?;
1472 match fs::symlink_metadata(&to) {
1473 Ok(m) => {
1474 let same = m.file_type().is_symlink()
1475 && fs::read_link(&to).is_ok_and(|t| same_link_target(&t, &written));
1476 if !same {
1477 return Ok(Err("the destination path is occupied".to_string()));
1478 }
1479 }
1480 Err(e) if e.kind() == io::ErrorKind::NotFound => {
1481 if let Some(parent) = to.parent() {
1482 make_private_dir(parent)?;
1483 }
1484 if let Err(e) = maker(&written, &to, dir) {
1485 if link_unavailable(&e) {
1486 return Ok(Err(e.to_string()));
1487 }
1488 return Err(e.into());
1489 }
1490 }
1491 Err(e) => return Err(e.into()),
1492 }
1493 if !link_ok(&to, &written, dir) {
1494 return Err(AppRootError::VerifyFailed(to));
1495 }
1496 Ok(Ok(MigratedLink {
1497 path: o.path.clone(),
1498 original_target: target,
1499 written_target: written,
1500 rewritten,
1501 }))
1502 }
1503}
1504
1505type LinkMaker<'a> = &'a dyn Fn(&Path, &Path, bool) -> io::Result<()>;
1506
1507enum Entry {
1509 Dir {
1511 rel: PathBuf,
1512 },
1513 File {
1514 rel: PathBuf,
1515 len: u64,
1516 },
1517 Link {
1519 rel: PathBuf,
1520 target: PathBuf,
1521 dir: bool,
1522 },
1523 Special {
1524 rel: PathBuf,
1525 kind: SpecialKind,
1526 },
1527 #[cfg_attr(not(windows), allow(dead_code))]
1530 Reparse {
1531 rel: PathBuf,
1532 kind: ReparseKind,
1533 tag: u32,
1534 },
1535}
1536
1537impl Entry {
1538 fn rel(&self) -> &Path {
1539 match self {
1540 Self::Dir { rel }
1541 | Self::File { rel, .. }
1542 | Self::Link { rel, .. }
1543 | Self::Special { rel, .. }
1544 | Self::Reparse { rel, .. } => rel,
1545 }
1546 }
1547}
1548
1549#[derive(Default)]
1552struct Lists {
1553 files: u64,
1554 bytes: u64,
1555 links: Vec<MigratedLink>,
1556 specials: Vec<SkippedSpecial>,
1557 omitted: Vec<OmittedLink>,
1558 reparse: Vec<SkippedReparsePoint>,
1559}
1560
1561impl Lists {
1562 fn state(&self) -> MigrationState {
1565 if self.omitted.is_empty() && self.reparse.is_empty() {
1566 MigrationState::Ready
1567 } else {
1568 MigrationState::ReadyWithOmissions
1569 }
1570 }
1571
1572 fn from_state(v: &serde_json::Value) -> Self {
1574 let text =
1575 |o: &serde_json::Value, k: &str| o.get(k).and_then(|s| s.as_str()).map(PathBuf::from);
1576 let items = |k: &str| {
1577 v.get(k)
1578 .and_then(|a| a.as_array())
1579 .cloned()
1580 .unwrap_or_default()
1581 };
1582 Self {
1583 files: v.get("files").and_then(|n| n.as_u64()).unwrap_or(0),
1584 bytes: v.get("bytes").and_then(|n| n.as_u64()).unwrap_or(0),
1585 links: items("links")
1586 .iter()
1587 .filter_map(|o| {
1588 Some(MigratedLink {
1589 path: text(o, "path")?,
1590 original_target: text(o, "original_target")?,
1591 written_target: text(o, "written_target")?,
1592 rewritten: o
1593 .get("rewritten")
1594 .and_then(|b| b.as_bool())
1595 .unwrap_or(false),
1596 })
1597 })
1598 .collect(),
1599 specials: items("skipped_specials")
1600 .iter()
1601 .filter_map(|o| {
1602 let kind = match o.get("kind").and_then(|k| k.as_str())? {
1603 "socket" => SpecialKind::Socket,
1604 "fifo" => SpecialKind::Fifo,
1605 _ => return None,
1606 };
1607 Some(SkippedSpecial {
1608 path: text(o, "path")?,
1609 kind,
1610 })
1611 })
1612 .collect(),
1613 omitted: items("omitted_optional_links")
1614 .iter()
1615 .filter_map(|o| {
1616 Some(OmittedLink {
1617 path: text(o, "path")?,
1618 original_target: text(o, "original_target")?,
1619 resolved_target: text(o, "resolved_target")?,
1620 reason: o.get("reason").and_then(|r| r.as_str())?.to_string(),
1621 })
1622 })
1623 .collect(),
1624 reparse: items("skipped_reparse_points")
1625 .iter()
1626 .filter_map(|o| {
1627 let kind = match o.get("kind").and_then(|k| k.as_str())? {
1628 "junction" => ReparseKind::Junction,
1629 _ => ReparseKind::Other,
1630 };
1631 Some(SkippedReparsePoint {
1632 path: text(o, "path")?,
1633 kind,
1634 tag: o.get("tag").and_then(|t| t.as_u64()).unwrap_or(0) as u32,
1635 reason: o.get("reason").and_then(|r| r.as_str())?.to_string(),
1636 })
1637 })
1638 .collect(),
1639 }
1640 }
1641}
1642
1643fn is_reserved_root_name(name: &std::ffi::OsStr) -> bool {
1646 let n = name.to_string_lossy();
1647 n == MIGRATION_MARKER || n == MIGRATION_STATE
1648}
1649
1650fn collect(
1656 base: &Path,
1657 rel: &Path,
1658 opts: &MigrateOptions,
1659 out: &mut Vec<Entry>,
1660) -> Result<(), AppRootError> {
1661 let mut entries: Vec<_> = fs::read_dir(base.join(rel))?.collect::<Result<_, _>>()?;
1662 entries.sort_by_key(|e| e.file_name());
1663 for e in entries {
1664 let name = e.file_name();
1665 if rel.as_os_str().is_empty() && is_reserved_root_name(&name) {
1666 continue;
1667 }
1668 let r = rel.join(&name);
1669 if !opts.selection(&r)? || opts.is_excluded(&r)? {
1670 continue;
1671 }
1672 #[cfg(windows)]
1676 {
1677 if let Some(kind) = reparse_of(&e, &base.join(&r))? {
1678 if opts
1679 .included
1680 .as_ref()
1681 .is_some_and(|roots| !roots.iter().any(|root| r.starts_with(root)))
1682 {
1683 return Err(AppRootError::UnsupportedSource(base.join(&r)));
1684 }
1685 match kind {
1686 Reparse::Symlink => {}
1687 Reparse::Socket => {
1688 if opts.ephemeral_specials {
1689 out.push(Entry::Special {
1690 rel: r,
1691 kind: SpecialKind::Socket,
1692 });
1693 continue;
1694 }
1695 return Err(AppRootError::UnsupportedSource(base.join(&r)));
1696 }
1697 Reparse::Junction | Reparse::Other(_) => {
1698 if opts.reparse_points {
1699 let (kind, tag) = match kind {
1700 Reparse::Other(tag) => (ReparseKind::Other, tag),
1701 _ => (ReparseKind::Junction, IO_REPARSE_TAG_MOUNT_POINT),
1702 };
1703 out.push(Entry::Reparse { rel: r, kind, tag });
1704 continue;
1705 }
1706 return Err(AppRootError::UnsupportedSource(base.join(&r)));
1707 }
1708 }
1709 }
1710 }
1711 let ft = e.file_type()?;
1713 if opts
1714 .included
1715 .as_ref()
1716 .is_some_and(|roots| !roots.iter().any(|root| r.starts_with(root)))
1717 && !ft.is_dir()
1718 {
1719 return Err(AppRootError::UnsupportedSource(base.join(&r)));
1720 }
1721 if ft.is_symlink() {
1722 if !opts.links {
1723 return Err(AppRootError::UnsupportedSource(base.join(&r)));
1724 }
1725 let target = fs::read_link(base.join(&r))?;
1726 out.push(Entry::Link {
1727 dir: link_is_dir(&ft),
1728 rel: r,
1729 target,
1730 });
1731 } else if ft.is_dir() {
1732 out.push(Entry::Dir { rel: r.clone() });
1733 collect(base, &r, opts, out)?;
1734 } else if ft.is_file() {
1735 out.push(Entry::File {
1736 len: e.metadata()?.len(),
1737 rel: r,
1738 });
1739 } else {
1740 match special_kind(&ft) {
1741 Some(kind) if opts.ephemeral_specials => out.push(Entry::Special { rel: r, kind }),
1743 _ => return Err(AppRootError::UnsupportedSource(base.join(&r))),
1744 }
1745 }
1746 }
1747 Ok(())
1748}
1749
1750#[cfg(windows)]
1752#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1753enum Reparse {
1754 Symlink,
1756 Junction,
1758 Socket,
1760 Other(u32),
1762}
1763
1764#[cfg(windows)]
1765const IO_REPARSE_TAG_SYMLINK: u32 = 0xA000_000C;
1766#[cfg(windows)]
1767const IO_REPARSE_TAG_MOUNT_POINT: u32 = 0xA000_0003;
1768#[cfg(windows)]
1769const IO_REPARSE_TAG_AF_UNIX: u32 = 0x8000_0023;
1770
1771#[cfg(windows)]
1772fn classify_reparse_tag(tag: u32) -> Reparse {
1773 match tag {
1774 IO_REPARSE_TAG_SYMLINK => Reparse::Symlink,
1775 IO_REPARSE_TAG_MOUNT_POINT => Reparse::Junction,
1776 IO_REPARSE_TAG_AF_UNIX => Reparse::Socket,
1777 other => Reparse::Other(other),
1778 }
1779}
1780
1781#[cfg(windows)]
1784fn reparse_of(e: &fs::DirEntry, full: &Path) -> io::Result<Option<Reparse>> {
1785 use std::os::windows::fs::MetadataExt;
1786 const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400;
1787 if e.metadata()?.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT == 0 {
1788 return Ok(None);
1789 }
1790 Ok(Some(classify_reparse_tag(reparse_tag(full)?)))
1791}
1792
1793#[cfg(windows)]
1796fn reparse_tag(path: &Path) -> io::Result<u32> {
1797 use std::os::windows::ffi::OsStrExt;
1798 use windows::core::PCWSTR;
1799 use windows::Win32::Storage::FileSystem::{FindClose, FindFirstFileW, WIN32_FIND_DATAW};
1800 let wide: Vec<u16> = path
1801 .as_os_str()
1802 .encode_wide()
1803 .chain(std::iter::once(0))
1804 .collect();
1805 let mut data = WIN32_FIND_DATAW::default();
1806 let handle =
1808 unsafe { FindFirstFileW(PCWSTR(wide.as_ptr()), &mut data) }.map_err(io::Error::other)?;
1809 let _ = unsafe { FindClose(handle) };
1811 Ok(data.dwReserved0)
1812}
1813
1814fn reparse_reason(kind: ReparseKind, tag: u32) -> String {
1815 match kind {
1816 ReparseKind::Junction => {
1817 "junction or volume mount point: never followed, copied or recreated".to_string()
1818 }
1819 ReparseKind::Other => {
1820 format!("reparse point (tag {tag:#010x}): never followed, opened, copied or recreated")
1821 }
1822 }
1823}
1824
1825#[cfg(unix)]
1826fn special_kind(ft: &fs::FileType) -> Option<SpecialKind> {
1827 use std::os::unix::fs::FileTypeExt;
1828 if ft.is_socket() {
1829 Some(SpecialKind::Socket)
1830 } else if ft.is_fifo() {
1831 Some(SpecialKind::Fifo)
1832 } else {
1833 None
1834 }
1835}
1836
1837#[cfg(not(unix))]
1839fn special_kind(_ft: &fs::FileType) -> Option<SpecialKind> {
1840 None
1841}
1842
1843#[cfg(windows)]
1844fn link_is_dir(ft: &fs::FileType) -> bool {
1845 use std::os::windows::fs::FileTypeExt;
1846 ft.is_symlink_dir()
1847}
1848
1849#[cfg(not(windows))]
1850fn link_is_dir(_ft: &fs::FileType) -> bool {
1851 false
1852}
1853
1854#[cfg(windows)]
1855fn link_kind_matches(ft: &fs::FileType, dir: bool) -> bool {
1856 use std::os::windows::fs::FileTypeExt;
1857 ft.is_symlink_dir() == dir
1858}
1859
1860#[cfg(not(windows))]
1861fn link_kind_matches(_ft: &fs::FileType, _dir: bool) -> bool {
1862 true
1863}
1864
1865fn plan_link(csrc: &Path, rel: &Path, target: &Path) -> (PathBuf, bool) {
1873 if target
1874 .components()
1875 .any(|c| matches!(c, Component::Prefix(_) | Component::RootDir))
1876 {
1877 return (target.to_path_buf(), false);
1878 }
1879 let parent_rel = rel.parent().unwrap_or_else(|| Path::new(""));
1880 let mut depth = parent_rel.components().count();
1881 let mut inside = true;
1882 let mut abs = csrc.to_path_buf();
1883 if depth > 0 {
1884 abs.push(parent_rel);
1885 }
1886 for c in target.components() {
1887 match c {
1888 Component::Normal(n) => {
1889 abs.push(n);
1890 depth += 1;
1891 }
1892 Component::ParentDir => {
1893 if depth == 0 {
1894 inside = false;
1895 } else {
1896 depth -= 1;
1897 }
1898 abs.pop();
1899 }
1900 _ => {}
1901 }
1902 }
1903 if inside {
1904 (target.to_path_buf(), false)
1905 } else {
1906 (strip_verbatim(abs), true)
1907 }
1908}
1909
1910#[cfg(windows)]
1913fn strip_verbatim(p: PathBuf) -> PathBuf {
1914 let s = p.to_string_lossy().into_owned();
1915 if let Some(rest) = s.strip_prefix(r"\\?\UNC\") {
1916 PathBuf::from(format!(r"\\{rest}"))
1917 } else if let Some(rest) = s.strip_prefix(r"\\?\") {
1918 if rest.len() >= 2 && rest.as_bytes()[1] == b':' {
1919 return PathBuf::from(rest);
1920 }
1921 p
1922 } else {
1923 p
1924 }
1925}
1926
1927#[cfg(not(windows))]
1928fn strip_verbatim(p: PathBuf) -> PathBuf {
1929 p
1930}
1931
1932fn same_link_target(a: &Path, b: &Path) -> bool {
1935 if a == b {
1936 return true;
1937 }
1938 #[cfg(windows)]
1939 {
1940 fn norm(p: &Path) -> String {
1941 let s = p.to_string_lossy().replace('/', "\\");
1942 let s = s
1943 .strip_prefix(r"\\?\UNC\")
1944 .map(|r| format!(r"\\{r}"))
1945 .or_else(|| s.strip_prefix(r"\\?\").map(str::to_string))
1946 .or_else(|| s.strip_prefix(r"\??\").map(str::to_string))
1947 .unwrap_or(s);
1948 s.to_lowercase()
1949 }
1950 norm(a) == norm(b)
1951 }
1952 #[cfg(not(windows))]
1953 {
1954 false
1955 }
1956}
1957
1958fn make_link(target: &Path, to: &Path, dir: bool) -> io::Result<()> {
1960 #[cfg(unix)]
1961 {
1962 let _ = dir;
1963 std::os::unix::fs::symlink(target, to)
1964 }
1965 #[cfg(windows)]
1966 {
1967 if dir {
1968 std::os::windows::fs::symlink_dir(target, to)
1969 } else {
1970 std::os::windows::fs::symlink_file(target, to)
1971 }
1972 }
1973 #[cfg(not(any(unix, windows)))]
1974 {
1975 let _ = (target, to, dir);
1976 Err(io::Error::from(io::ErrorKind::Unsupported))
1977 }
1978}
1979
1980fn link_unavailable(e: &io::Error) -> bool {
1984 #[cfg(windows)]
1985 {
1986 e.kind() != io::ErrorKind::AlreadyExists
1987 }
1988 #[cfg(not(windows))]
1989 {
1990 matches!(
1991 e.kind(),
1992 io::ErrorKind::PermissionDenied | io::ErrorKind::Unsupported
1993 )
1994 }
1995}
1996
1997fn lossy(p: &Path) -> String {
1998 p.to_string_lossy().into_owned()
1999}
2000
2001fn links_json(links: &[MigratedLink]) -> serde_json::Value {
2002 serde_json::Value::Array(
2003 links
2004 .iter()
2005 .map(|l| {
2006 serde_json::json!({
2007 "path": lossy(&l.path),
2008 "original_target": lossy(&l.original_target),
2009 "written_target": lossy(&l.written_target),
2010 "rewritten": l.rewritten,
2011 })
2012 })
2013 .collect(),
2014 )
2015}
2016
2017fn specials_json(specials: &[SkippedSpecial]) -> serde_json::Value {
2018 serde_json::Value::Array(
2019 specials
2020 .iter()
2021 .map(|s| serde_json::json!({"path": lossy(&s.path), "kind": s.kind.as_str()}))
2022 .collect(),
2023 )
2024}
2025
2026fn omitted_json(omitted: &[OmittedLink]) -> serde_json::Value {
2027 serde_json::Value::Array(
2028 omitted
2029 .iter()
2030 .map(|o| {
2031 serde_json::json!({
2032 "path": lossy(&o.path),
2033 "original_target": lossy(&o.original_target),
2034 "resolved_target": lossy(&o.resolved_target),
2035 "reason": o.reason,
2036 })
2037 })
2038 .collect(),
2039 )
2040}
2041
2042fn reparse_json(reparse: &[SkippedReparsePoint]) -> serde_json::Value {
2043 serde_json::Value::Array(
2044 reparse
2045 .iter()
2046 .map(|r| {
2047 serde_json::json!({
2048 "path": lossy(&r.path),
2049 "kind": r.kind.as_str(),
2050 "tag": r.tag,
2051 "reason": r.reason,
2052 })
2053 })
2054 .collect(),
2055 )
2056}
2057
2058fn marker_json(
2065 source: Option<&str>,
2066 files: u64,
2067 bytes: u64,
2068 opts: &MigrateOptions,
2069 links: &[MigratedLink],
2070 specials: &[SkippedSpecial],
2071 omitted: &[OmittedLink],
2072) -> serde_json::Value {
2073 let mut body = serde_json::json!({
2074 "source": source,
2075 "files": files,
2076 "bytes": bytes,
2077 });
2078 if opts.is_strict() {
2079 return body;
2080 }
2081 let obj = body.as_object_mut().expect("object");
2082 obj.insert("links".into(), links_json(links));
2083 obj.insert("skipped_specials".into(), specials_json(specials));
2084 obj.insert("omitted_optional_links".into(), omitted_json(omitted));
2085 obj.insert(
2086 "complete".into(),
2087 serde_json::Value::Bool(omitted.is_empty()),
2088 );
2089 body
2090}
2091
2092fn state_json(
2096 state: MigrationState,
2097 source: Option<&str>,
2098 l: &Lists,
2099 error: Option<&str>,
2100) -> serde_json::Value {
2101 let mut unresolved: Vec<serde_json::Value> = l
2102 .omitted
2103 .iter()
2104 .map(|o| serde_json::json!({"path": lossy(&o.path), "kind": "link", "reason": o.reason}))
2105 .collect();
2106 unresolved.extend(l.reparse.iter().map(|r| {
2107 serde_json::json!({"path": lossy(&r.path), "kind": "reparse_point", "reason": r.reason})
2108 }));
2109 let mut body = serde_json::json!({
2110 "version": STATE_VERSION,
2111 "state": state.as_str(),
2112 "source": source,
2113 "files": l.files,
2114 "bytes": l.bytes,
2115 "links": links_json(&l.links),
2116 "skipped_specials": specials_json(&l.specials),
2117 "omitted_optional_links": omitted_json(&l.omitted),
2118 "skipped_reparse_points": reparse_json(&l.reparse),
2119 "unresolved": unresolved,
2120 });
2121 if let (Some(e), Some(obj)) = (error, body.as_object_mut()) {
2122 obj.insert("error".into(), e.into());
2123 }
2124 body
2125}
2126
2127fn commit_migration_file(
2129 writer: AtomicWriter,
2130 source: &Path,
2131 target: &Path,
2132) -> Result<(), AppRootError> {
2133 match writer.commit() {
2134 Ok(()) => Ok(()),
2135 Err(FsError::AlreadyExists(_)) => {
2136 if regular_entry(&fs::symlink_metadata(target)?) && same_content(source, target)? {
2137 Ok(())
2138 } else {
2139 Err(AppRootError::Collision(target.to_path_buf()))
2140 }
2141 }
2142 Err(e) => Err(io::Error::other(e).into()),
2143 }
2144}
2145
2146fn regular_entry(metadata: &fs::Metadata) -> bool {
2147 #[cfg(windows)]
2148 {
2149 use std::os::windows::fs::MetadataExt;
2150 if metadata.file_attributes() & 0x400 != 0 {
2151 return false;
2152 }
2153 }
2154 metadata.file_type().is_file()
2155}
2156
2157fn validate_metadata_target(path: &Path) -> io::Result<bool> {
2160 match fs::symlink_metadata(path) {
2161 Ok(m) if regular_entry(&m) => Ok(true),
2162 Ok(_) => Err(io::Error::new(
2163 io::ErrorKind::InvalidData,
2164 format!(
2165 "migration metadata is not a regular file: {}",
2166 path.display()
2167 ),
2168 )),
2169 Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false),
2170 Err(e) => Err(e),
2171 }
2172}
2173
2174pub(crate) fn write_json_atomic(path: &Path, body: &serde_json::Value) -> io::Result<()> {
2176 let replacing = validate_metadata_target(path)?;
2177 let mut writer = AtomicWriter::create_with(
2178 path,
2179 AtomicWriteOptions {
2180 private: true,
2181 replace: if replacing {
2182 ReplaceMode::Rename
2183 } else {
2184 ReplaceMode::NoReplace
2185 },
2186 ..Default::default()
2187 },
2188 )
2189 .map_err(io::Error::other)?;
2190 writer
2191 .write_all(body.to_string().as_bytes())
2192 .map_err(io::Error::other)?;
2193 writer
2194 .commit_checked(|_| validate_metadata_target(path).map(|_| ()))
2195 .map_err(io::Error::other)
2196}
2197
2198fn link_ok(to: &Path, written: &Path, dir: bool) -> bool {
2201 match fs::symlink_metadata(to) {
2202 Ok(m) => {
2203 m.file_type().is_symlink()
2204 && fs::read_link(to).is_ok_and(|t| same_link_target(&t, written))
2205 && link_kind_matches(&m.file_type(), dir)
2206 }
2207 Err(_) => false,
2208 }
2209}
2210
2211pub(crate) fn same_content(a: &Path, b: &Path) -> Result<bool, AppRootError> {
2213 let mut fa = rightkit_fs::open_read_nofollow(a, &rightkit_fs::ReadOptions::new())
2214 .map_err(io::Error::other)?;
2215 let mut fb = rightkit_fs::open_read_nofollow(b, &rightkit_fs::ReadOptions::new())
2216 .map_err(io::Error::other)?;
2217 if fa.metadata()?.len() != fb.metadata()?.len() {
2218 return Ok(false);
2219 }
2220 let (mut ba, mut bb) = (vec![0u8; 64 * 1024], vec![0u8; 64 * 1024]);
2221 loop {
2222 let na = read_full(&mut fa, &mut ba)?;
2223 let nb = read_full(&mut fb, &mut bb)?;
2224 if na != nb || ba[..na] != bb[..nb] {
2225 return Ok(false);
2226 }
2227 if na == 0 {
2228 return Ok(true);
2229 }
2230 }
2231}
2232
2233fn read_full(f: &mut File, buf: &mut [u8]) -> io::Result<usize> {
2234 let mut n = 0;
2235 while n < buf.len() {
2236 match f.read(&mut buf[n..])? {
2237 0 => break,
2238 k => n += k,
2239 }
2240 }
2241 Ok(n)
2242}
2243
2244fn same_directory(a: &Path, b: &Path) -> bool {
2247 #[cfg(unix)]
2248 {
2249 use std::os::unix::fs::MetadataExt;
2250 match (fs::metadata(a), fs::metadata(b)) {
2251 (Ok(x), Ok(y)) => x.dev() == y.dev() && x.ino() == y.ino(),
2252 _ => false,
2253 }
2254 }
2255 #[cfg(not(unix))]
2256 {
2257 a.to_string_lossy()
2258 .eq_ignore_ascii_case(&b.to_string_lossy())
2259 }
2260}