Skip to main content

rustic_core/backend/
node.rs

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/// [`NodeErrorKind`] describes the errors that can be returned by an action utilizing a node in Backends
28#[derive(thiserror::Error, Debug, displaydoc::Display)]
29#[non_exhaustive]
30pub enum NodeErrorKind<'a> {
31    /// Unexpected EOF while parsing filename: `{file_name}`
32    #[cfg(not(windows))]
33    UnexpectedEOF {
34        /// The filename
35        file_name: String,
36    },
37    /// Invalid unicode while parsing filename: `{file_name}`
38    #[cfg(not(windows))]
39    InvalidUnicode {
40        /// The filename
41        file_name: String,
42    },
43    /// Unrecognized Escape while parsing filename: `{file_name}`
44    #[cfg(not(windows))]
45    UnrecognizedEscape {
46        /// The filename
47        file_name: String,
48    },
49    /// Parsing hex chars {chars:?} failed for `{hex}` in filename: `{file_name}` : `{source}`
50    #[cfg(not(windows))]
51    ParsingHexFailed {
52        /// The filename
53        file_name: String,
54        /// The hex string
55        hex: String,
56        /// The remaining chars
57        chars: std::str::Chars<'a>,
58        /// The error that occurred
59        source: ParseIntError,
60    },
61    /// Parsing unicode chars {chars:?} failed for `{target}` in filename: `{file_name}` : `{source}`
62    #[cfg(not(windows))]
63    ParsingUnicodeFailed {
64        /// The filename
65        file_name: String,
66        /// The target type
67        target: String,
68        /// The remaining chars
69        chars: std::str::Chars<'a>,
70        /// The error that occurred
71        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)]
81/// A node within the tree hierarchy
82pub struct Node {
83    /// Name of the node: filename or dirname.
84    ///
85    /// # Warning
86    ///
87    /// * This contains an escaped variant of the name in order to handle non-unicode filenames.
88    /// * Don't access this field directly, use the [`Node::name()`] method instead!
89    pub name: String,
90    #[serde(flatten)]
91    /// Information about node type
92    pub node_type: NodeType,
93    #[serde(flatten)]
94    /// Node Metadata
95    pub meta: Metadata,
96    #[serde(default, deserialize_with = "deserialize_default_from_null")]
97    /// Contents of the Node
98    ///
99    /// # Note
100    ///
101    /// This should be only set for regular files.
102    pub content: Option<Vec<DataId>>,
103    #[serde(default, skip_serializing_if = "Option::is_none")]
104    /// Subtree of the Node.
105    ///
106    /// # Note
107    ///
108    /// This should be only set for directories. (TODO: Check if this is correct)
109    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/// Types a [`Node`] can have with type-specific additional information
116#[derive(Default)]
117pub enum NodeType {
118    /// Node is a regular file
119    #[strum(to_string = "file")]
120    #[default]
121    File,
122    /// Node is a directory
123    #[strum(to_string = "dir")]
124    Dir,
125    /// Node is a symlink
126    #[strum(to_string = "symlink:{linktarget}")]
127    Symlink {
128        /// The target of the symlink
129        ///
130        /// # Warning
131        ///
132        /// * This contains the target only if it is a valid unicode target.
133        /// * Don't access this field directly, use the [`NodeType::to_link()`] method instead!
134        linktarget: String,
135        #[serde_as(as = "DefaultOnNull<Option<Base64::<Standard,Padded>>>")]
136        #[serde(default, skip_serializing_if = "Option::is_none")]
137        /// The raw link target saved as bytes.
138        ///
139        /// This is only filled (and mandatory) if the link target is non-unicode.
140        linktarget_raw: Option<Vec<u8>>,
141    },
142    /// Node is a block device file
143    #[strum(to_string = "dev:{device}")]
144    Dev {
145        #[serde(default)]
146        /// Device id
147        device: u64,
148    },
149    /// Node is a char device file
150    #[strum(to_string = "chardev:{device}")]
151    Chardev {
152        #[serde(default)]
153        /// Device id
154        device: u64,
155    },
156    /// Node is a fifo
157    #[strum(to_string = "fifo")]
158    Fifo,
159    /// Node is a socket
160    #[strum(to_string = "socket")]
161    Socket,
162}
163
164impl NodeType {
165    #[cfg(not(windows))]
166    /// Get a [`NodeType`] from a linktarget path
167    #[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    // Windows doesn't support non-unicode link targets, so we assume unicode here.
186    // TODO: Test and check this!
187    /// Get a [`NodeType`] from a linktarget path
188    #[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    // Must be only called on NodeType::Symlink!
197    /// Get the link path from a `NodeType::Symlink`.
198    ///
199    /// # Panics
200    ///
201    /// * If called on a non-symlink node
202    #[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    /// Convert a `NodeType::Symlink` to a `Path`.
218    ///
219    /// # Warning
220    ///
221    /// * Must be only called on `NodeType::Symlink`!
222    ///
223    /// # Panics
224    ///
225    /// * If called on a non-symlink node
226    /// * If the link target is not valid unicode
227    // TODO: Implement non-unicode link targets correctly for windows
228    #[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/// Metadata of a [`Node`]
239#[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    /// Unix file mode
247    pub mode: Option<u32>,
248    /// Unix mtime (last modification time)
249    #[serde_as(as = "Option<RusticTime>")]
250    pub mtime: Option<Timestamp>,
251    /// Unix atime (last access time)
252    #[serde_as(as = "Option<RusticTime>")]
253    pub atime: Option<Timestamp>,
254    /// Unix ctime (last status change time)
255    #[serde_as(as = "Option<RusticTime>")]
256    pub ctime: Option<Timestamp>,
257    /// Unix uid (user id)
258    pub uid: Option<u32>,
259    /// Unix gid (group id)
260    pub gid: Option<u32>,
261    /// Unix user name
262    pub user: Option<String>,
263    /// Unix group name
264    pub group: Option<String>,
265    /// Unix inode number
266    pub inode: u64,
267    /// Unix device id
268    pub device_id: u64,
269    /// Size of the node
270    pub size: u64,
271    /// Number of hardlinks to this node
272    pub links: u64,
273    /// Extended attributes of the node
274    #[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/// Extended attribute of a [`Node`]
283#[serde_as]
284#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)]
285pub struct ExtendedAttribute {
286    /// Name of the extended attribute
287    pub name: String,
288    /// Value of the extended attribute
289    #[serde_as(as = "DefaultOnNull<Option<Base64::<Standard,Padded>>>")]
290    pub value: Option<Vec<u8>>,
291}
292
293impl Node {
294    /// Create a new [`Node`] with the given name, type and metadata
295    ///
296    /// # Arguments
297    ///
298    /// * `name` - Name of the node
299    /// * `node_type` - Type of the node
300    /// * `meta` - Metadata of the node
301    ///
302    /// # Returns
303    ///
304    /// The created [`Node`]
305    #[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    /// Evaluates if this node is a directory
317    pub const fn is_dir(&self) -> bool {
318        matches!(self.node_type, NodeType::Dir)
319    }
320
321    #[must_use]
322    /// Evaluates if this node is a symlink
323    pub const fn is_symlink(&self) -> bool {
324        matches!(self.node_type, NodeType::Symlink { .. })
325    }
326
327    #[must_use]
328    /// Evaluates if this node is a regular file
329    pub const fn is_file(&self) -> bool {
330        matches!(self.node_type, NodeType::File)
331    }
332
333    #[must_use]
334    /// Evaluates if this node is a special file
335    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    /// Get the node name as `OsString`, handling name ecaping
348    ///
349    /// # Panics
350    ///
351    /// * If the name is not valid unicode
352    pub fn name(&self) -> Cow<'_, OsStr> {
353        unescape_filename(&self.name).unwrap_or_else(|_| Cow::Borrowed(OsStr::new(&self.name)))
354    }
355}
356
357/// An ordering function returning the latest node by mtime
358///
359/// # Arguments
360///
361/// * `n1` - First node
362/// * `n2` - Second node
363///
364/// # Returns
365///
366/// The ordering of the two nodes
367#[must_use]
368pub fn last_modified_node(n1: &Node, n2: &Node) -> Ordering {
369    n1.meta.mtime.cmp(&n2.meta.mtime)
370}
371
372// TODO: Should be probably called `_lossy`
373// TODO(Windows): This is not able to handle non-unicode filenames and
374// doesn't treat filenames which need and escape (like `\`, `"`, ...) correctly
375#[cfg(windows)]
376fn escape_filename(name: &OsStr) -> String {
377    name.to_string_lossy().to_string()
378}
379
380/// Unescape a filename
381///
382/// # Arguments
383///
384/// * `s` - The escaped filename
385#[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))]
391/// Escape a filename
392///
393/// # Arguments
394///
395/// * `name` - The filename to escape
396// This escapes the filename in a way that *should* be compatible to golangs
397// stconv.Quote, see https://pkg.go.dev/strconv#Quote
398// However, so far there was no specification what Quote really does, so this
399// is some kind of try-and-error and maybe does not cover every case.
400fn 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))]
449/// Unescape a filename
450///
451/// # Arguments
452///
453/// * `s` - The escaped filename
454// inspired by the enquote crate
455fn 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                            // hex
486                            '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                            // unicode
498                            '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]
555// Iterator#take cannot be used because it consumes the iterator
556fn 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}