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/// Lossy representation of visit types in the
106/// [SWH data model](https://docs.softwareheritage.org/devel/swh-model/data-model.html)
107///
108/// While the SWH data model precisely identifies the type of loader used to load content of an
109/// origin, swh-graph has to group them in order to fit in the compressed graph representation.
110///
111/// To get the exact visit type, you need to cross-reference with the [columnar
112/// export](https://docs.softwareheritage.org/devel/swh-export/graph/dataset.html)
113#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
114pub enum VisitType {
115    /// tar, zip, ...
116    Archive,
117    /// single content, patch, ...
118    Misc,
119    /// npm, pypi, nixguix, ...
120    Package,
121    /// deposit, possibly coar-notify in the future
122    Push,
123    /// git, hg, cvs, ...
124    Vcs,
125    /// partial archiving of a repository (git-checkout, hg-checkout, svn-export)
126    VcsCheckout,
127    /// Either a type not categorized above, or graph is too old to support visit types
128    Unknown,
129}
130
131impl VisitType {
132    #[rustfmt::skip]
133    pub fn from_swh_type(swh_type: &str) -> Option<Self> {
134        Some(match swh_type {
135            "ftp" |
136            "tar" |
137            "tarball-directory"
138            => VisitType::Archive,
139
140            "content"
141            => VisitType::Misc,
142
143            "bioconductor" |
144            "cpan" |
145            "cran" |
146            "crates" |
147            "deb" |
148            "golang" |
149            "hackage" |
150            "hex" |
151            "opam" |
152            "nixguix" |
153            "npm" |
154            "maven" |
155            "pubdev" |
156            "puppet" |
157            "pypi" |
158            "rubygems"
159            => VisitType::Package,
160
161            "deposit"
162            => VisitType::Push,
163
164            "bzr" |
165            "cvs" |
166            "git" |
167            "hg" |
168            "svn"
169            => VisitType::Vcs,
170
171            "hg-checkout" |
172            "git-checkout" |
173            "svn-export"
174            => VisitType::VcsCheckout,
175
176            _ => return None,
177        })
178    }
179
180    pub fn to_bits(self) -> u8 {
181        use VisitType::*;
182
183        match self {
184            Unknown => 0b000,
185            Archive => 0b001,
186            Misc => 0b010,
187            Package => 0b011,
188            Push => 0b100,
189            Vcs => 0b101,
190            VcsCheckout => 0b110,
191        }
192    }
193
194    pub fn from_bits(bits: u8) -> Self {
195        use VisitType::*;
196
197        match bits {
198            0b000 => Unknown, // used by graphs 2024-08-23 to 2026-03-02
199            0b001 => Archive,
200            0b010 => Misc,
201            0b011 => Package,
202            0b100 => Push,
203            0b101 => Vcs,
204            0b110 => VcsCheckout,
205            0b111 => Unknown, // used by graph 2024-05-16
206            0b1000..=u8::MAX => {
207                panic!("VisitType::from_bits got value greater than 0b111");
208            }
209        }
210    }
211}
212
213#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
214pub enum VisitStatus {
215    Full,
216    Partial,
217}
218
219#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
220pub struct Visit(pub(crate) u64);
221
222impl From<u64> for Visit {
223    #[inline(always)]
224    fn from(n: u64) -> Visit {
225        Visit(n)
226    }
227}
228
229impl Visit {
230    /// Returns a new [`Visit`]
231    ///
232    /// or `None` if `timestamp` is 2^59 or greater
233    pub fn new(status: VisitStatus, timestamp: u64, visit_type: VisitType) -> Option<Visit> {
234        let is_full = match status {
235            VisitStatus::Full => 1u64,
236            VisitStatus::Partial => 0,
237        };
238        let reserved_bits = 0b1000u64;
239        let visit_type = u64::from(visit_type.to_bits());
240        assert!(visit_type <= 0b111);
241        timestamp.checked_shl(5).map(|shifted_timestamp| {
242            Visit(shifted_timestamp | (is_full << 4) | reserved_bits | visit_type)
243        })
244    }
245
246    #[inline(always)]
247    pub fn timestamp(&self) -> u64 {
248        self.0 >> 5
249    }
250
251    #[inline(always)]
252    pub fn status(&self) -> VisitStatus {
253        if self.0 & 0b10000 != 0 {
254            VisitStatus::Full
255        } else {
256            VisitStatus::Partial
257        }
258    }
259
260    pub fn visit_type(&self) -> VisitType {
261        VisitType::from_bits((self.0 & 0b111) as u8)
262    }
263}
264
265#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
266pub struct Branch(pub(crate) u64);
267
268impl From<u64> for Branch {
269    #[inline(always)]
270    fn from(n: u64) -> Branch {
271        Branch(n)
272    }
273}
274
275impl Branch {
276    /// Returns a new [`Branch`]
277    ///
278    /// or `None` if `label_name_id` is 2^61 or greater
279    pub fn new(label_name_id: LabelNameId) -> Option<Branch> {
280        label_name_id.0.checked_shl(3).map(Branch)
281    }
282
283    #[deprecated(since = "7.0.0", note = "filename_id was renamed label_name_id")]
284    /// Deprecated alias for [`label_name_id`](Self::label_name_id)
285    #[inline(always)]
286    pub fn filename_id(self) -> LabelNameId {
287        self.label_name_id()
288    }
289
290    /// Returns an id of the label name of the entry.
291    ///
292    /// The id can be resolved to the label name through graph properties.
293    #[inline(always)]
294    pub fn label_name_id(self) -> LabelNameId {
295        LabelNameId(self.0 >> 3)
296    }
297}
298
299#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
300pub struct DirEntry(pub(crate) u64);
301
302impl From<u64> for DirEntry {
303    #[inline(always)]
304    fn from(n: u64) -> DirEntry {
305        DirEntry(n)
306    }
307}
308
309impl DirEntry {
310    /// Returns a new [`DirEntry`]
311    ///
312    /// or `None` if `label_name_id` is 2^61 or greater
313    pub fn new(permission: Permission, label_name_id: LabelNameId) -> Option<DirEntry> {
314        label_name_id
315            .0
316            .checked_shl(3)
317            .map(|shifted_label_name_id| DirEntry(shifted_label_name_id | (permission as u64)))
318    }
319
320    #[deprecated(since = "7.0.0", note = "filename_id was renamed label_name_id")]
321    /// Deprecated alias for [`label_name_id`](Self::label_name_id)
322    #[inline(always)]
323    pub fn filename_id(self) -> LabelNameId {
324        self.label_name_id()
325    }
326
327    /// Returns an id of the filename of the entry.
328    ///
329    /// The id can be resolved to the label name through graph properties.
330    #[inline(always)]
331    pub fn label_name_id(self) -> LabelNameId {
332        LabelNameId(self.0 >> 3)
333    }
334
335    /// Returns the file permission of the given directory entry
336    ///
337    /// Returns `None` when the labeled graph is corrupt or generated by a newer swh-graph
338    /// version with more [`Permission`] variants
339    pub fn permission(self) -> Option<Permission> {
340        use Permission::*;
341        match self.0 & 0b111 {
342            0 => Some(None),
343            1 => Some(Content),
344            2 => Some(ExecutableContent),
345            3 => Some(Symlink),
346            4 => Some(Directory),
347            5 => Some(Revision),
348            _ => Option::None,
349        }
350    }
351
352    /// Returns the file permission of the given directory entry
353    ///
354    /// # Safety
355    ///
356    /// May return an invalid [`Permission`] variant if the labeled graph is corrupt
357    /// or generated by a newer swh-graph version with more variants
358    pub unsafe fn permission_unchecked(self) -> Permission {
359        use Permission::*;
360        match self.0 & 0b111 {
361            0 => None,
362            1 => Content,
363            2 => ExecutableContent,
364            3 => Symlink,
365            4 => Directory,
366            5 => Revision,
367            n => unreachable!("{} mode", n),
368        }
369    }
370}
371
372#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
373pub struct LabelNameId(pub u64);
374
375#[deprecated(since = "7.0.0", note = "FilenameId was renamed to LabelNameId")]
376pub type FilenameId = LabelNameId;
377
378#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
379#[repr(u8)]
380pub enum Permission {
381    None = 0,
382    Content = 1,
383    ExecutableContent = 2,
384    Symlink = 3,
385    Directory = 4,
386    Revision = 5,
387}
388
389impl Permission {
390    /// Returns a UNIX-like mode matching the permission
391    ///
392    /// `0100644` for contents, `0100755` for executable contents, `0120000` for symbolic
393    /// links, `0040000` for directories, and `0160000` for revisions (git submodules);
394    /// or `0` if the [`DirEntry`] has no associated permission.
395    #[inline(always)]
396    pub fn to_git(self) -> u16 {
397        use Permission::*;
398        match self {
399            None => 0,
400            Content => 0o100644,
401            ExecutableContent => 0o100755,
402            Symlink => 0o120000,
403            Directory => 0o040000,
404            Revision => 0o160000,
405        }
406    }
407
408    /// Returns a permission from a subset of UNIX-like modes.
409    ///
410    /// This is the inverse of [`Permission::to_git`].
411    pub fn from_git(mode: u16) -> Option<Permission> {
412        use Permission::*;
413        match mode {
414            0 => Some(None),
415            0o100644 => Some(Content),
416            0o100755 => Some(ExecutableContent),
417            0o120000 => Some(Symlink),
418            0o040000 => Some(Directory),
419            0o160000 => Some(Revision),
420            _ => Option::None,
421        }
422    }
423
424    /// Returns a permission from a subset of UNIX-like modes.
425    ///
426    /// This is the inverse of [`Permission::to_git`].
427    ///
428    /// # Safety
429    ///
430    /// Undefined behavior if the given mode is not one of the values returned by [`Permission::to_git`]
431    pub unsafe fn from_git_unchecked(mode: u16) -> Permission {
432        use Permission::*;
433        match mode {
434            0 => None,
435            0o100644 => Content,
436            0o100755 => ExecutableContent,
437            0o120000 => Symlink,
438            0o040000 => Directory,
439            0o160000 => Revision,
440            _ => unreachable!("{} mode", mode),
441        }
442    }
443}