swh_graph_stdlib/
fs.rs

1// Copyright (C) 2024  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//! Filesystem manipulation functions.
7
8use std::collections::HashMap;
9
10use anyhow::{ensure, Context, Result};
11use log::warn;
12
13use swh_graph::graph::*;
14use swh_graph::labels::{EdgeLabel, LabelNameId, Permission};
15use swh_graph::properties;
16use swh_graph::NodeType;
17
18fn msg_no_label_name_id(name: impl AsRef<[u8]>) -> String {
19    format!(
20        "no label_name id found for entry \"{}\"",
21        String::from_utf8_lossy(name.as_ref())
22    )
23}
24
25/// Given a graph and a directory node, return the node id of a named directory
26/// entry located (not recursively) in that directory, if it exists.
27///
28/// See [fs_resolve_path] for a version of this function that traverses
29/// sub-directories recursively.
30///
31/// ```ignore
32/// if let Ok(Some(node)) == fs_resolve_name(&graph, 42, "README.md") {
33///     // do something with node
34/// }
35/// ```
36pub fn fs_resolve_name<G>(graph: &G, dir: NodeId, name: impl AsRef<[u8]>) -> Result<Option<NodeId>>
37where
38    G: SwhLabeledForwardGraph + SwhGraphWithProperties,
39    <G as SwhGraphWithProperties>::LabelNames: properties::LabelNames,
40    <G as SwhGraphWithProperties>::Maps: properties::Maps,
41{
42    let props = graph.properties();
43    let name_id = props
44        .label_name_id(name.as_ref())
45        .with_context(|| msg_no_label_name_id(name))?;
46    fs_resolve_name_by_id(&graph, dir, name_id)
47}
48
49/// Same as [fs_resolve_name], but using a pre-resolved [LabelNameId] as entry
50/// name. Using this function is more efficient in case the same name (e.g.,
51/// "README.md") is to be looked up in many directories.
52pub fn fs_resolve_name_by_id<G>(graph: &G, dir: NodeId, name: LabelNameId) -> Result<Option<NodeId>>
53where
54    G: SwhLabeledForwardGraph + SwhGraphWithProperties,
55    <G as SwhGraphWithProperties>::LabelNames: properties::LabelNames,
56    <G as SwhGraphWithProperties>::Maps: properties::Maps,
57{
58    let node_type = graph.properties().node_type(dir);
59    ensure!(
60        node_type == NodeType::Directory,
61        "Type of {dir} should be dir, but is {node_type} instead"
62    );
63
64    for (succ, label) in graph.labeled_successors(dir).flatten_labels() {
65        if let EdgeLabel::DirEntry(dentry) = label {
66            if dentry.label_name_id() == name {
67                return Ok(Some(succ));
68            }
69        }
70    }
71    Ok(None)
72}
73
74/// Given a graph and a directory node, return the node id of a directory entry
75/// located at a given path within that directory, if it exists.
76///
77/// Slashes (`/`) contained in `path` are interpreted as path separators.
78///
79/// See [fs_resolve_name] for a non-recursive version of this function.
80///
81/// ```ignore
82/// if let Ok(Some(node)) == fs_resolve_path(&graph, 42, "src/main.c") {
83///     // do something with node
84/// }
85/// ```
86pub fn fs_resolve_path<G>(graph: &G, dir: NodeId, path: impl AsRef<[u8]>) -> Result<Option<NodeId>>
87where
88    G: SwhLabeledForwardGraph + SwhGraphWithProperties,
89    <G as SwhGraphWithProperties>::LabelNames: properties::LabelNames,
90    <G as SwhGraphWithProperties>::Maps: properties::Maps,
91{
92    let props = graph.properties();
93    let path = path
94        .as_ref()
95        .split(|byte| *byte == b'/')
96        .map(|name| {
97            props
98                .label_name_id(name)
99                .with_context(|| msg_no_label_name_id(name))
100        })
101        .collect::<Result<Vec<LabelNameId>, _>>()?;
102    fs_resolve_path_by_id(&graph, dir, &path)
103}
104
105/// Same as [fs_resolve_path], but using as path a sequence of pre-resolved
106/// [LabelNameId]-s. Using this function is more efficient in case the same path
107/// (e.g., "src/main.c") is to be looked up in many directories.
108pub fn fs_resolve_path_by_id<G>(
109    graph: &G,
110    dir: NodeId,
111    path: &[LabelNameId],
112) -> Result<Option<NodeId>>
113where
114    G: SwhLabeledForwardGraph + SwhGraphWithProperties,
115    <G as SwhGraphWithProperties>::LabelNames: properties::LabelNames,
116    <G as SwhGraphWithProperties>::Maps: properties::Maps,
117{
118    let mut cur_entry = dir;
119    for name in path {
120        match fs_resolve_name_by_id(graph, cur_entry, *name)? {
121            None => return Ok(None),
122            Some(entry) => cur_entry = entry,
123        }
124    }
125    Ok(Some(cur_entry))
126}
127
128/// Recursive representation of a directory tree, ignoring sharing.
129///
130/// Note that a `Revision` variant can in fact point to either revision or
131/// release nodes.
132#[derive(Debug, Default, PartialEq)]
133pub enum FsTree {
134    #[default]
135    Content,
136    Directory(HashMap<Vec<u8>, (FsTree, Option<Permission>)>),
137    Revision(NodeId),
138}
139
140/// Given a graph and a directory node in it (usually, but not necessarily, the
141/// *root* directory of a repository), return a recursive list of the contained
142/// files and directories.
143///
144/// Note that symlinks are not followed during listing and are reported as
145/// files in the returned tree. To recognize them as links, check the
146/// permissions of the associated directory entries.
147pub fn fs_ls_tree<G>(graph: &G, dir: NodeId) -> Result<FsTree>
148where
149    G: SwhLabeledForwardGraph + SwhGraphWithProperties,
150    <G as SwhGraphWithProperties>::LabelNames: properties::LabelNames,
151    <G as SwhGraphWithProperties>::Maps: properties::Maps,
152{
153    let props = graph.properties();
154    let node_type = props.node_type(dir);
155    ensure!(
156        node_type == NodeType::Directory,
157        "Type of {dir} should be dir, but is {node_type} instead"
158    );
159
160    let mut dir_entries = HashMap::new();
161    for (succ, labels) in graph.labeled_successors(dir) {
162        let node_type = props.node_type(succ);
163        for label in labels {
164            if let EdgeLabel::DirEntry(dentry) = label {
165                let file_name = props.label_name(dentry.label_name_id());
166                let perm = dentry.permission();
167                match node_type {
168                    NodeType::Content => {
169                        dir_entries.insert(file_name, (FsTree::Content, perm));
170                    }
171                    NodeType::Directory => {
172                        // recurse into subdir
173                        if let Ok(subdir) = fs_ls_tree(graph, succ) {
174                            dir_entries.insert(file_name, (subdir, perm));
175                        } else {
176                            warn!("Cannot list (sub-)directory {succ}, skipping it");
177                        }
178                    }
179                    NodeType::Revision | NodeType::Release => {
180                        dir_entries.insert(file_name, (FsTree::Revision(succ), perm));
181                    }
182                    NodeType::Origin | NodeType::Snapshot => {
183                        warn!("Ignoring dir entry with unexpected type {node_type}");
184                    }
185                }
186            }
187        }
188    }
189
190    Ok(FsTree::Directory(dir_entries))
191}