1pub mod modification;
2
3use std::{borrow::Cow, cmp::Ordering, ffi::OsStr, fmt::Debug, path::Path};
4
5#[cfg(not(windows))]
6use std::fmt::Write;
7#[cfg(not(windows))]
8use std::num::ParseIntError;
9#[cfg(not(windows))]
10use std::os::unix::ffi::OsStrExt;
11
12use derive_more::Constructor;
13use jiff::Timestamp;
14use serde_aux::prelude::*;
15use serde_derive::{Deserialize, Serialize};
16use serde_with::{
17 DefaultOnNull,
18 base64::{Base64, Standard},
19 formats::Padded,
20 serde_as, skip_serializing_none,
21};
22
23use crate::blob::{DataId, tree::TreeId};
24use crate::repofile::RusticTime;
25
26#[cfg(not(windows))]
27#[derive(thiserror::Error, Debug, displaydoc::Display)]
29#[non_exhaustive]
30pub enum NodeErrorKind<'a> {
31 #[cfg(not(windows))]
33 UnexpectedEOF {
34 file_name: String,
36 },
37 #[cfg(not(windows))]
39 InvalidUnicode {
40 file_name: String,
42 },
43 #[cfg(not(windows))]
45 UnrecognizedEscape {
46 file_name: String,
48 },
49 #[cfg(not(windows))]
51 ParsingHexFailed {
52 file_name: String,
54 hex: String,
56 chars: std::str::Chars<'a>,
58 source: ParseIntError,
60 },
61 #[cfg(not(windows))]
63 ParsingUnicodeFailed {
64 file_name: String,
66 target: String,
68 chars: std::str::Chars<'a>,
70 source: ParseIntError,
72 },
73}
74
75#[cfg(not(windows))]
76pub(crate) type NodeResult<'a, T> = Result<T, NodeErrorKind<'a>>;
77
78#[derive(
79 Default, Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Constructor, PartialOrd, Ord,
80)]
81pub struct Node {
83 pub name: String,
90 #[serde(flatten)]
91 pub node_type: NodeType,
93 #[serde(flatten)]
94 pub meta: Metadata,
96 #[serde(default, deserialize_with = "deserialize_default_from_null")]
97 pub content: Option<Vec<DataId>>,
103 #[serde(default, skip_serializing_if = "Option::is_none")]
104 pub subtree: Option<TreeId>,
110}
111
112#[serde_as]
113#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, strum::Display)]
114#[serde(tag = "type", rename_all = "lowercase")]
115#[derive(Default)]
117pub enum NodeType {
118 #[strum(to_string = "file")]
120 #[default]
121 File,
122 #[strum(to_string = "dir")]
124 Dir,
125 #[strum(to_string = "symlink:{linktarget}")]
127 Symlink {
128 linktarget: String,
135 #[serde_as(as = "DefaultOnNull<Option<Base64::<Standard,Padded>>>")]
136 #[serde(default, skip_serializing_if = "Option::is_none")]
137 linktarget_raw: Option<Vec<u8>>,
141 },
142 #[strum(to_string = "dev:{device}")]
144 Dev {
145 #[serde(default)]
146 device: u64,
148 },
149 #[strum(to_string = "chardev:{device}")]
151 Chardev {
152 #[serde(default)]
153 device: u64,
155 },
156 #[strum(to_string = "fifo")]
158 Fifo,
159 #[strum(to_string = "socket")]
161 Socket,
162}
163
164impl NodeType {
165 #[cfg(not(windows))]
166 #[must_use]
168 pub fn from_link(target: &Path) -> Self {
169 let (linktarget, linktarget_raw) = target.to_str().map_or_else(
170 || {
171 (
172 target.as_os_str().to_string_lossy().to_string(),
173 Some(target.as_os_str().as_bytes().to_vec()),
174 )
175 },
176 |t| (t.to_string(), None),
177 );
178 Self::Symlink {
179 linktarget,
180 linktarget_raw,
181 }
182 }
183
184 #[cfg(windows)]
185 #[must_use]
189 pub fn from_link(target: &Path) -> Self {
190 Self::Symlink {
191 linktarget: target.as_os_str().to_string_lossy().to_string(),
192 linktarget_raw: None,
193 }
194 }
195
196 #[cfg(not(windows))]
203 #[must_use]
204 pub fn to_link(&self) -> &Path {
205 match self {
206 Self::Symlink {
207 linktarget,
208 linktarget_raw,
209 } => linktarget_raw.as_ref().map_or_else(
210 || Path::new(linktarget),
211 |t| Path::new(OsStr::from_bytes(t)),
212 ),
213 _ => panic!("called method to_link on non-symlink!"),
214 }
215 }
216
217 #[cfg(windows)]
229 #[must_use]
230 pub fn to_link(&self) -> &Path {
231 match self {
232 Self::Symlink { linktarget, .. } => Path::new(linktarget),
233 _ => panic!("called method to_link on non-symlink!"),
234 }
235 }
236}
237
238#[serde_as]
240#[skip_serializing_none]
241#[serde_with::apply(
242 u64 => #[serde(default, skip_serializing_if = "is_default")],
243)]
244#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
245pub struct Metadata {
246 pub mode: Option<u32>,
248 #[serde_as(as = "Option<RusticTime>")]
250 pub mtime: Option<Timestamp>,
251 #[serde_as(as = "Option<RusticTime>")]
253 pub atime: Option<Timestamp>,
254 #[serde_as(as = "Option<RusticTime>")]
256 pub ctime: Option<Timestamp>,
257 pub uid: Option<u32>,
259 pub gid: Option<u32>,
261 pub user: Option<String>,
263 pub group: Option<String>,
265 pub inode: u64,
267 pub device_id: u64,
269 pub size: u64,
271 pub links: u64,
273 #[serde(default, skip_serializing_if = "Vec::is_empty")]
275 pub extended_attributes: Vec<ExtendedAttribute>,
276}
277
278pub(crate) fn is_default<T: Default + PartialEq>(t: &T) -> bool {
279 t == &T::default()
280}
281
282#[serde_as]
284#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)]
285pub struct ExtendedAttribute {
286 pub name: String,
288 #[serde_as(as = "DefaultOnNull<Option<Base64::<Standard,Padded>>>")]
290 pub value: Option<Vec<u8>>,
291}
292
293impl Node {
294 #[must_use]
306 pub fn new_node(name: &OsStr, node_type: NodeType, meta: Metadata) -> Self {
307 Self {
308 name: escape_filename(name),
309 node_type,
310 content: None,
311 subtree: None,
312 meta,
313 }
314 }
315 #[must_use]
316 pub const fn is_dir(&self) -> bool {
318 matches!(self.node_type, NodeType::Dir)
319 }
320
321 #[must_use]
322 pub const fn is_symlink(&self) -> bool {
324 matches!(self.node_type, NodeType::Symlink { .. })
325 }
326
327 #[must_use]
328 pub const fn is_file(&self) -> bool {
330 matches!(self.node_type, NodeType::File)
331 }
332
333 #[must_use]
334 pub const fn is_special(&self) -> bool {
336 matches!(
337 self.node_type,
338 NodeType::Symlink { .. }
339 | NodeType::Dev { .. }
340 | NodeType::Chardev { .. }
341 | NodeType::Fifo
342 | NodeType::Socket
343 )
344 }
345
346 #[must_use]
347 pub fn name(&self) -> Cow<'_, OsStr> {
353 unescape_filename(&self.name).unwrap_or_else(|_| Cow::Borrowed(OsStr::new(&self.name)))
354 }
355}
356
357#[must_use]
368pub fn last_modified_node(n1: &Node, n2: &Node) -> Ordering {
369 n1.meta.mtime.cmp(&n2.meta.mtime)
370}
371
372#[cfg(windows)]
376fn escape_filename(name: &OsStr) -> String {
377 name.to_string_lossy().to_string()
378}
379
380#[cfg(windows)]
386fn unescape_filename(s: &str) -> Result<Cow<'_, OsStr>, core::convert::Infallible> {
387 Ok(Cow::Borrowed(OsStr::new(s)))
388}
389
390#[cfg(not(windows))]
391fn escape_filename(name: &OsStr) -> String {
401 let mut input = name.as_bytes();
402 let mut s = String::with_capacity(name.len());
403
404 let push = |s: &mut String, p: &str| {
405 for c in p.chars() {
406 match c {
407 '\\' => s.push_str("\\\\"),
408 '\"' => s.push_str("\\\""),
409 '\u{7}' => s.push_str("\\a"),
410 '\u{8}' => s.push_str("\\b"),
411 '\u{c}' => s.push_str("\\f"),
412 '\n' => s.push_str("\\n"),
413 '\r' => s.push_str("\\r"),
414 '\t' => s.push_str("\\t"),
415 '\u{b}' => s.push_str("\\v"),
416 c => s.push(c),
417 }
418 }
419 };
420
421 loop {
422 match std::str::from_utf8(input) {
423 Ok(valid) => {
424 push(&mut s, valid);
425 break;
426 }
427 Err(error) => {
428 let (valid, after_valid) = input.split_at(error.valid_up_to());
429 push(&mut s, std::str::from_utf8(valid).unwrap());
430
431 if let Some(invalid_sequence_length) = error.error_len() {
432 for b in &after_valid[..invalid_sequence_length] {
433 write!(s, "\\x{b:02x}").unwrap();
434 }
435 input = &after_valid[invalid_sequence_length..];
436 } else {
437 for b in after_valid {
438 write!(s, "\\x{b:02x}").unwrap();
439 }
440 break;
441 }
442 }
443 }
444 }
445 s
446}
447
448#[cfg(not(windows))]
449fn unescape_filename(s: &str) -> NodeResult<'_, Cow<'_, OsStr>> {
456 if !s.contains('\\') {
457 return Ok(Cow::Borrowed(OsStr::new(s)));
458 }
459
460 let mut chars = s.chars();
461 let mut u = Vec::with_capacity(s.len());
462 loop {
463 match chars.next() {
464 None => break,
465 Some(c) => {
466 if c == '\\' {
467 match chars.next() {
468 None => {
469 return Err(NodeErrorKind::UnexpectedEOF {
470 file_name: s.to_string(),
471 });
472 }
473 Some(c) => match c {
474 '\\' => u.push(b'\\'),
475 '"' => u.push(b'"'),
476 '\'' => u.push(b'\''),
477 '`' => u.push(b'`'),
478 'a' => u.push(b'\x07'),
479 'b' => u.push(b'\x08'),
480 'f' => u.push(b'\x0c'),
481 'n' => u.push(b'\n'),
482 'r' => u.push(b'\r'),
483 't' => u.push(b'\t'),
484 'v' => u.push(b'\x0b'),
485 'x' => {
487 let hex = take(&mut chars, 2);
488 u.push(u8::from_str_radix(&hex, 16).map_err(|err| {
489 NodeErrorKind::ParsingHexFailed {
490 file_name: s.to_string(),
491 hex: hex.clone(),
492 chars: chars.clone(),
493 source: err,
494 }
495 })?);
496 }
497 'u' => {
499 let n = u32::from_str_radix(&take(&mut chars, 4), 16).map_err(
500 |err| NodeErrorKind::ParsingUnicodeFailed {
501 file_name: s.to_string(),
502 target: "u32".to_string(),
503 chars: chars.clone(),
504 source: err,
505 },
506 )?;
507 let c = std::char::from_u32(n).ok_or_else(|| {
508 NodeErrorKind::InvalidUnicode {
509 file_name: s.to_string(),
510 }
511 })?;
512 let mut bytes = vec![0u8; c.len_utf8()];
513 _ = c.encode_utf8(&mut bytes);
514 u.extend_from_slice(&bytes);
515 }
516 'U' => {
517 let n = u32::from_str_radix(&take(&mut chars, 8), 16).map_err(
518 |err| NodeErrorKind::ParsingUnicodeFailed {
519 file_name: s.to_string(),
520 target: "u32".to_string(),
521 chars: chars.clone(),
522 source: err,
523 },
524 )?;
525 let c = std::char::from_u32(n).ok_or_else(|| {
526 NodeErrorKind::InvalidUnicode {
527 file_name: s.to_string(),
528 }
529 })?;
530 let mut bytes = vec![0u8; c.len_utf8()];
531 _ = c.encode_utf8(&mut bytes);
532 u.extend_from_slice(&bytes);
533 }
534 _ => {
535 return Err(NodeErrorKind::UnrecognizedEscape {
536 file_name: s.to_string(),
537 });
538 }
539 },
540 }
541 } else {
542 let mut bytes = vec![0u8; c.len_utf8()];
543 _ = c.encode_utf8(&mut bytes);
544 u.extend_from_slice(&bytes);
545 }
546 }
547 }
548 }
549
550 Ok(Cow::Owned(OsStr::from_bytes(&u).to_os_string()))
551}
552
553#[cfg(not(windows))]
554#[inline]
555fn take<I: Iterator<Item = char>>(iterator: &mut I, n: usize) -> String {
557 let mut s = String::with_capacity(n);
558 for _ in 0..n {
559 s.push(iterator.next().unwrap_or_default());
560 }
561 s
562}
563
564#[cfg(not(windows))]
565#[cfg(test)]
566mod tests {
567 use super::*;
568
569 use proptest::prelude::*;
570 use rstest::rstest;
571
572 proptest! {
573 #[test]
574 fn escape_unescape_is_identity(bytes in prop::collection::vec(prop::num::u8::ANY, 0..65536)) {
575 let name = OsStr::from_bytes(&bytes);
576 let escaped = escape_filename(name);
577 prop_assert_eq!(name, unescape_filename(escaped.as_ref()).unwrap());
578 }
579 }
580
581 #[rstest]
582 #[case(b"\\", r#"\\"#)]
583 #[case(b"\"", r#"\""#)]
584 #[case(b"'", r#"'"#)]
585 #[case(b"`", r#"`"#)]
586 #[case(b"\x07", r#"\a"#)]
587 #[case(b"\x08", r#"\b"#)]
588 #[case(b"\x0b", r#"\v"#)]
589 #[case(b"\x0c", r#"\f"#)]
590 #[case(b"\n", r#"\n"#)]
591 #[case(b"\r", r#"\r"#)]
592 #[case(b"\t", r#"\t"#)]
593 #[case(b"\xab", r#"\xab"#)]
594 #[case(b"\xc2", r#"\xc2"#)]
595 #[case(b"\xff", r#"\xff"#)]
596 #[case(b"\xc3\x9f", "\u{00df}")]
597 #[case(b"\xe2\x9d\xa4", "\u{2764}")]
598 #[case(b"\xf0\x9f\x92\xaf", "\u{01f4af}")]
599 fn escape_cases(#[case] input: &[u8], #[case] expected: &str) {
600 let name = OsStr::from_bytes(input);
601 assert_eq!(expected, escape_filename(name));
602 }
603
604 #[rstest]
605 #[case(r#"\\"#, b"\\")]
606 #[case(r#"\""#, b"\"")]
607 #[case(r#"\'"#, b"\'")]
608 #[case(r#"\`"#, b"`")]
609 #[case(r#"\a"#, b"\x07")]
610 #[case(r#"\b"#, b"\x08")]
611 #[case(r#"\v"#, b"\x0b")]
612 #[case(r#"\f"#, b"\x0c")]
613 #[case(r#"\n"#, b"\n")]
614 #[case(r#"\r"#, b"\r")]
615 #[case(r#"\t"#, b"\t")]
616 #[case(r#"\xab"#, b"\xab")]
617 #[case(r#"\xAB"#, b"\xab")]
618 #[case(r#"\xFF"#, b"\xff")]
619 #[case(r#"\u00df"#, b"\xc3\x9f")]
620 #[case(r#"\u00DF"#, b"\xc3\x9f")]
621 #[case(r#"\u2764"#, b"\xe2\x9d\xa4")]
622 #[case(r#"\U0001f4af"#, b"\xf0\x9f\x92\xaf")]
623 fn unescape_cases(#[case] input: &str, #[case] expected: &[u8]) {
624 let expected = OsStr::from_bytes(expected);
625 assert_eq!(expected, unescape_filename(input).unwrap());
626 }
627
628 proptest! {
629 #[test]
630 fn from_link_to_link_is_identity(bytes in prop::collection::vec(prop::num::u8::ANY, 0..65536)) {
631 let path = Path::new(OsStr::from_bytes(&bytes));
632 let node = NodeType::from_link(path);
633 prop_assert_eq!(path, node.to_link());
634 }
635 }
636}