-
-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy pathreflection.rs
More file actions
150 lines (136 loc) · 5.66 KB
/
reflection.rs
File metadata and controls
150 lines (136 loc) · 5.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
use super::{HardlinkList, InodeKey, Value};
use crate::{hardlink::LinkPathListReflection, inode::InodeNumber};
use dashmap::DashMap;
use derive_more::{Display, Error, Into, IntoIterator};
use into_sorted::IntoSortedUnstable;
use pipe_trait::Pipe;
#[cfg(feature = "json")]
use serde::{Deserialize, Serialize};
/// Intermediate format used for construction and inspection of [`HardlinkList`]'s
/// internal content.
///
/// **Guarantees:**
/// * Every `(device, inode)` pair is unique within the scope of a single scan, but inode
/// numbers alone are **not** guaranteed to be unique: when scanning multiple filesystems,
/// two unrelated files on different devices can share the same inode number and will each
/// produce a separate entry. The reflection stores only the inode number (the JSON format
/// does not carry device information), so round-tripping a multi-filesystem scan through
/// JSON is an unsupported edge case.
/// * The internal list is always sorted by inode numbers (and by device number as a
/// tie-breaker when two entries share the same inode number).
///
/// **Equality:** `Reflection` implements `PartialEq` and `Eq` traits.
///
/// **Serialization and deserialization:** _(feature: `json`)_ `Reflection` implements
/// `Serialize` and `Deserialize` traits, this allows functions in `serde_json` to convert
/// a `Reflection` into/from JSON.
#[derive(Debug, Clone, PartialEq, Eq, Into, IntoIterator)]
#[cfg_attr(feature = "json", derive(Deserialize, Serialize))]
pub struct Reflection<Size>(Vec<ReflectionEntry<Size>>);
impl<Size> Reflection<Size> {
/// Get the number of entries inside the reflection.
#[inline]
pub fn len(&self) -> usize {
self.0.len()
}
/// Check whether the reflection has any entry.
#[inline]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
/// Iterate over the entries.
#[inline]
pub fn iter(&self) -> impl Iterator<Item = &ReflectionEntry<Size>> + Clone {
self.0.iter()
}
}
/// An entry in [`Reflection`].
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "json", derive(Deserialize, Serialize))]
pub struct ReflectionEntry<Size> {
/// The inode number of the file.
pub ino: InodeNumber,
/// Size of the file.
pub size: Size,
/// Total number of links of the file, both listed (in [`Self::paths`]) and unlisted.
pub links: u64,
/// Paths to the detected links of the file.
pub paths: LinkPathListReflection,
}
impl<Size> ReflectionEntry<Size> {
/// Create a new entry.
#[inline]
fn new(ino: InodeNumber, Value { size, links, paths }: Value<Size>) -> Self {
let paths = paths.into();
ReflectionEntry {
ino,
size,
links,
paths,
}
}
/// Dissolve [`ReflectionEntry`] into a pair of [`InodeNumber`] and [`Value`].
#[inline]
fn dissolve(self) -> (InodeNumber, Value<Size>) {
let ReflectionEntry {
ino,
size,
links,
paths,
} = self;
let paths = paths.into();
(ino, Value { size, links, paths })
}
}
impl<Size> From<Vec<ReflectionEntry<Size>>> for Reflection<Size> {
/// Sort the list by inode numbers, then create the reflection.
fn from(list: Vec<ReflectionEntry<Size>>) -> Self {
list.into_sorted_unstable_by_key(|entry| u64::from(entry.ino))
.pipe(Reflection)
}
}
impl<Size> From<HardlinkList<Size>> for Reflection<Size> {
fn from(HardlinkList(list): HardlinkList<Size>) -> Self {
// Collect to a vec, sort by (ino, dev) for a stable, deterministic order, then
// strip dev before wrapping. Sorting here (with dev still available) avoids the
// nondeterminism that would arise from an unstable sort on ino alone when two
// entries from different filesystems share the same inode number.
let mut pairs: Vec<(InodeKey, Value<Size>)> = list.into_iter().collect();
pairs.sort_unstable_by_key(|(key, _)| (u64::from(key.ino), key.dev));
pairs
.into_iter()
.map(|(key, value)| ReflectionEntry::new(key.ino, value))
.collect::<Vec<_>>()
.pipe(Reflection)
}
}
/// Error that occurs when an attempt to convert a [`Reflection`] into a
/// [`HardlinkList`] fails.
#[derive(Debug, Display, Error, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ConversionError {
/// When the source has duplicated inode numbers.
#[display("Inode number {_0} is duplicated")]
DuplicatedInode(#[error(not(source))] InodeNumber),
}
impl<Size> TryFrom<Reflection<Size>> for HardlinkList<Size> {
type Error = ConversionError;
fn try_from(Reflection(entries): Reflection<Size>) -> Result<Self, Self::Error> {
let map = DashMap::with_capacity(entries.len());
for entry in entries {
let (ino, value) = entry.dissolve();
// Device number is unknown when loading from a reflection (e.g. JSON input);
// use dev=0 as a placeholder. This means that when reloading JSON output that
// was produced by scanning multiple filesystems, files from different devices
// sharing the same inode number cannot be distinguished and therefore cannot
// all be represented. Such duplicates cause a ConversionError::DuplicatedInode
// and are treated as an unsupported edge case, since the JSON format does not
// carry device information.
let key = InodeKey { dev: 0, ino };
if map.insert(key, value).is_some() {
return ino.pipe(ConversionError::DuplicatedInode).pipe(Err);
}
}
map.pipe(HardlinkList).pipe(Ok)
}
}