Skip to main content

swh_graph/
labels.rs

1// Copyright (C) 2023-2026  The Software Heritage developers
2// See the AUTHORS file at the top-level directory of this distribution
3// License: GNU General Public License version 3, or any later version
4// See top-level LICENSE file for more information
5
6//! Labels on graph arcs
7
8use thiserror::Error;
9
10use crate::NodeType;
11
12/// Intermediary type that needs to be casted into one of the [`EdgeLabel`] variants
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
14pub struct UntypedEdgeLabel(pub(crate) u64);
15
16impl From<u64> for UntypedEdgeLabel {
17    #[inline(always)]
18    fn from(n: u64) -> UntypedEdgeLabel {
19        UntypedEdgeLabel(n)
20    }
21}
22
23#[derive(Error, Debug, Clone, PartialEq, Eq, Hash)]
24pub enum EdgeTypingError {
25    #[error("{src} -> {dst} arcs cannot have labels")]
26    NodeTypes { src: NodeType, dst: NodeType },
27}
28
29impl UntypedEdgeLabel {
30    pub fn for_edge_type(
31        &self,
32        src: NodeType,
33        dst: NodeType,
34        transpose_graph: bool,
35    ) -> Result<EdgeLabel, EdgeTypingError> {
36        use crate::NodeType::*;
37
38        let (src, dst) = if transpose_graph {
39            (dst, src)
40        } else {
41            (src, dst)
42        };
43
44        match (src, dst) {
45            (Snapshot, _) => Ok(EdgeLabel::Branch(self.0.into())),
46            (Directory, _) => Ok(EdgeLabel::DirEntry(self.0.into())),
47            (Origin, Snapshot) => Ok(EdgeLabel::Visit(self.0.into())),
48            _ => Err(EdgeTypingError::NodeTypes { src, dst }),
49        }
50    }
51}
52
53impl From<EdgeLabel> for UntypedEdgeLabel {
54    #[inline(always)]
55    fn from(label: EdgeLabel) -> Self {
56        UntypedEdgeLabel(match label {
57            EdgeLabel::Branch(branch) => branch.0,
58            EdgeLabel::DirEntry(dir_entry) => dir_entry.0,
59            EdgeLabel::Visit(visit) => visit.0,
60        })
61    }
62}
63
64#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
65pub enum EdgeLabel {
66    /// `snp -> *` branches (or `* -> snp` on the transposed graph)
67    Branch(Branch),
68    /// `dir -> *` branches (or `* -> dir` on the transposed graph)
69    DirEntry(DirEntry),
70    /// `ori -> snp` branches (or `snp -> ori` on the transposed graph)
71    Visit(Visit),
72}
73
74macro_rules! impl_edgelabel_convert {
75    ( $variant:ident ( $inner:ty ) ) => {
76        impl From<$inner> for EdgeLabel {
77            fn from(v: $inner) -> EdgeLabel {
78                EdgeLabel::$variant(v)
79            }
80        }
81
82        impl TryFrom<EdgeLabel> for $inner {
83            type Error = ();
84
85            fn try_from(label: EdgeLabel) -> Result<$inner, Self::Error> {
86                match label {
87                    EdgeLabel::$variant(v) => Ok(v),
88                    _ => Err(()),
89                }
90            }
91        }
92
93        impl From<UntypedEdgeLabel> for $inner {
94            fn from(label: UntypedEdgeLabel) -> $inner {
95                <$inner>::from(label.0)
96            }
97        }
98    };
99}
100
101impl_edgelabel_convert!(Branch(Branch));
102impl_edgelabel_convert!(DirEntry(DirEntry));
103impl_edgelabel_convert!(Visit(Visit));
104
105#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
106pub enum VisitStatus {
107    Full,
108    Partial,
109}
110
111#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
112pub struct Visit(pub(crate) u64);
113
114impl From<u64> for Visit {
115    #[inline(always)]
116    fn from(n: u64) -> Visit {
117        Visit(n)
118    }
119}
120
121impl Visit {
122    /// Returns a new [`Visit`]
123    ///
124    /// or `None` if `timestamp` is 2^59 or greater
125    pub fn new(status: VisitStatus, timestamp: u64) -> Option<Visit> {
126        let is_full = match status {
127            VisitStatus::Full => 1u64,
128            VisitStatus::Partial => 0,
129        };
130        let reserved_bits = 0b1000u64;
131        timestamp
132            .checked_shl(5)
133            .map(|shifted_timestamp| Visit(shifted_timestamp | (is_full << 4) | reserved_bits))
134    }
135
136    #[inline(always)]
137    pub fn timestamp(&self) -> u64 {
138        self.0 >> 5
139    }
140
141    #[inline(always)]
142    pub fn status(&self) -> VisitStatus {
143        if self.0 & 0b10000 != 0 {
144            VisitStatus::Full
145        } else {
146            VisitStatus::Partial
147        }
148    }
149}
150
151#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
152pub struct Branch(pub(crate) u64);
153
154impl From<u64> for Branch {
155    #[inline(always)]
156    fn from(n: u64) -> Branch {
157        Branch(n)
158    }
159}
160
161impl Branch {
162    /// Returns a new [`Branch`]
163    ///
164    /// or `None` if `label_name_id` is 2^61 or greater
165    pub fn new(label_name_id: LabelNameId) -> Option<Branch> {
166        label_name_id.0.checked_shl(3).map(Branch)
167    }
168
169    #[deprecated(since = "7.0.0", note = "filename_id was renamed label_name_id")]
170    /// Deprecated alias for [`label_name_id`](Self::label_name_id)
171    #[inline(always)]
172    pub fn filename_id(self) -> LabelNameId {
173        self.label_name_id()
174    }
175
176    /// Returns an id of the label name of the entry.
177    ///
178    /// The id can be resolved to the label name through graph properties.
179    #[inline(always)]
180    pub fn label_name_id(self) -> LabelNameId {
181        LabelNameId(self.0 >> 3)
182    }
183}
184
185#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
186pub struct DirEntry(pub(crate) u64);
187
188impl From<u64> for DirEntry {
189    #[inline(always)]
190    fn from(n: u64) -> DirEntry {
191        DirEntry(n)
192    }
193}
194
195impl DirEntry {
196    /// Returns a new [`DirEntry`]
197    ///
198    /// or `None` if `label_name_id` is 2^61 or greater
199    pub fn new(permission: Permission, label_name_id: LabelNameId) -> Option<DirEntry> {
200        label_name_id
201            .0
202            .checked_shl(3)
203            .map(|shifted_label_name_id| DirEntry(shifted_label_name_id | (permission as u64)))
204    }
205
206    #[deprecated(since = "7.0.0", note = "filename_id was renamed label_name_id")]
207    /// Deprecated alias for [`label_name_id`](Self::label_name_id)
208    #[inline(always)]
209    pub fn filename_id(self) -> LabelNameId {
210        self.label_name_id()
211    }
212
213    /// Returns an id of the filename of the entry.
214    ///
215    /// The id can be resolved to the label name through graph properties.
216    #[inline(always)]
217    pub fn label_name_id(self) -> LabelNameId {
218        LabelNameId(self.0 >> 3)
219    }
220
221    /// Returns the file permission of the given directory entry
222    ///
223    /// Returns `None` when the labeled graph is corrupt or generated by a newer swh-graph
224    /// version with more [`Permission`] variants
225    pub fn permission(self) -> Option<Permission> {
226        use Permission::*;
227        match self.0 & 0b111 {
228            0 => Some(None),
229            1 => Some(Content),
230            2 => Some(ExecutableContent),
231            3 => Some(Symlink),
232            4 => Some(Directory),
233            5 => Some(Revision),
234            _ => Option::None,
235        }
236    }
237
238    /// Returns the file permission of the given directory entry
239    ///
240    /// # Safety
241    ///
242    /// May return an invalid [`Permission`] variant if the labeled graph is corrupt
243    /// or generated by a newer swh-graph version with more variants
244    pub unsafe fn permission_unchecked(self) -> Permission {
245        use Permission::*;
246        match self.0 & 0b111 {
247            0 => None,
248            1 => Content,
249            2 => ExecutableContent,
250            3 => Symlink,
251            4 => Directory,
252            5 => Revision,
253            n => unreachable!("{} mode", n),
254        }
255    }
256}
257
258#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
259pub struct LabelNameId(pub u64);
260
261#[deprecated(since = "7.0.0", note = "FilenameId was renamed to LabelNameId")]
262pub type FilenameId = LabelNameId;
263
264#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
265#[repr(u8)]
266pub enum Permission {
267    None = 0,
268    Content = 1,
269    ExecutableContent = 2,
270    Symlink = 3,
271    Directory = 4,
272    Revision = 5,
273}
274
275impl Permission {
276    /// Returns a UNIX-like mode matching the permission
277    ///
278    /// `0100644` for contents, `0100755` for executable contents, `0120000` for symbolic
279    /// links, `0040000` for directories, and `0160000` for revisions (git submodules);
280    /// or `0` if the [`DirEntry`] has no associated permission.
281    #[inline(always)]
282    pub fn to_git(self) -> u16 {
283        use Permission::*;
284        match self {
285            None => 0,
286            Content => 0o100644,
287            ExecutableContent => 0o100755,
288            Symlink => 0o120000,
289            Directory => 0o040000,
290            Revision => 0o160000,
291        }
292    }
293
294    /// Returns a permission from a subset of UNIX-like modes.
295    ///
296    /// This is the inverse of [`Permission::to_git`].
297    pub fn from_git(mode: u16) -> Option<Permission> {
298        use Permission::*;
299        match mode {
300            0 => Some(None),
301            0o100644 => Some(Content),
302            0o100755 => Some(ExecutableContent),
303            0o120000 => Some(Symlink),
304            0o040000 => Some(Directory),
305            0o160000 => Some(Revision),
306            _ => Option::None,
307        }
308    }
309
310    /// Returns a permission from a subset of UNIX-like modes.
311    ///
312    /// This is the inverse of [`Permission::to_git`].
313    ///
314    /// # Safety
315    ///
316    /// Undefined behavior if the given mode is not one of the values returned by [`Permission::to_git`]
317    pub unsafe fn from_git_unchecked(mode: u16) -> Permission {
318        use Permission::*;
319        match mode {
320            0 => None,
321            0o100644 => Content,
322            0o100755 => ExecutableContent,
323            0o120000 => Symlink,
324            0o040000 => Directory,
325            0o160000 => Revision,
326            _ => unreachable!("{} mode", mode),
327        }
328    }
329}