Skip to main content

sui_tool/db_shell/
vfs.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Virtual filesystem path types for the db-shell.
5
6use anyhow::anyhow;
7use consensus_core::CommitIndex;
8use std::fmt;
9use sui_types::{
10    base_types::EpochId,
11    digests::{
12        CheckpointContentsDigest, CheckpointDigest, TransactionDigest, TransactionEffectsDigest,
13    },
14    messages_checkpoint::CheckpointSequenceNumber,
15};
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum VfsPath {
19    Root,
20    Epochs,
21    Epoch(EpochId),
22    EpochFirstCheckpoint(EpochId),
23    EpochLastCheckpoint(EpochId),
24    EpochCommittee(EpochId),
25    EpochCheckpoints(EpochId),
26    EpochCheckpointBySeq(EpochId, CheckpointSequenceNumber),
27    EpochCheckpointByDigest(EpochId, CheckpointDigest),
28    CheckpointsRoot,
29    CheckpointsSeqRoot,
30    /// `/checkpoints/seq/<seq>`: directory containing `summary`, `contents`, `contents-short`.
31    /// When given as an explicit `ls` argument, acts as a start cursor.
32    CheckpointsBySeq(CheckpointSequenceNumber),
33    CheckpointSeqSummary(CheckpointSequenceNumber),
34    CheckpointSeqContents(CheckpointSequenceNumber),
35    CheckpointSeqContentsShort(CheckpointSequenceNumber),
36    CheckpointsDigestRoot,
37    CheckpointsByDigest(CheckpointDigest),
38    CheckpointDigestSummary(CheckpointDigest),
39    CheckpointDigestContents(CheckpointDigest),
40    CheckpointDigestContentsShort(CheckpointDigest),
41    CheckpointContentsRoot,
42    CheckpointContentsEntry(CheckpointContentsDigest),
43    TransactionsRoot,
44    /// `/transactions/<txdigest>`: the transaction. Acts as a start cursor when used as ls arg.
45    TransactionEntry(TransactionDigest),
46    /// `/transactions/<txdigest>.fx-<fxdigest>`: the effects for the transaction.
47    TransactionEffectsEntry(TransactionDigest, TransactionEffectsDigest),
48    ConsensusRoot,
49    /// `/consensus/latest`: alias to the latest known commit index.
50    ConsensusLatest,
51    ConsensusCommitsRoot,
52    /// `/consensus/commits/<index>`: directory for a commit. Acts as a start cursor when used as ls arg.
53    ConsensusCommitDir(CommitIndex),
54    ConsensusCommitSummary(CommitIndex),
55}
56
57impl VfsPath {
58    pub fn is_dir(&self) -> bool {
59        matches!(
60            self,
61            VfsPath::Root
62                | VfsPath::Epochs
63                | VfsPath::Epoch(_)
64                | VfsPath::EpochCheckpoints(_)
65                | VfsPath::CheckpointsRoot
66                | VfsPath::CheckpointsSeqRoot
67                | VfsPath::CheckpointsBySeq(_)
68                | VfsPath::CheckpointsDigestRoot
69                | VfsPath::CheckpointsByDigest(_)
70                | VfsPath::CheckpointContentsRoot
71                | VfsPath::TransactionsRoot
72                | VfsPath::ConsensusRoot
73                | VfsPath::ConsensusCommitsRoot
74                | VfsPath::ConsensusCommitDir(_)
75        )
76    }
77
78    /// True when this path, used as an explicit `ls` argument, should be treated as a
79    /// start cursor in the parent namespace rather than listing its own children.
80    pub fn is_ls_cursor(&self) -> bool {
81        matches!(
82            self,
83            VfsPath::CheckpointsBySeq(_)
84                | VfsPath::CheckpointsByDigest(_)
85                | VfsPath::EpochCheckpointBySeq(_, _)
86                | VfsPath::CheckpointContentsEntry(_)
87                | VfsPath::TransactionEntry(_)
88                | VfsPath::ConsensusCommitDir(_)
89        )
90    }
91
92    /// Return the parent path, or `None` if already at root.
93    pub fn parent(&self) -> Option<VfsPath> {
94        match self {
95            VfsPath::Root => None,
96            VfsPath::Epochs
97            | VfsPath::CheckpointsRoot
98            | VfsPath::CheckpointContentsRoot
99            | VfsPath::TransactionsRoot
100            | VfsPath::ConsensusRoot => Some(VfsPath::Root),
101            VfsPath::Epoch(_) => Some(VfsPath::Epochs),
102            VfsPath::EpochFirstCheckpoint(e)
103            | VfsPath::EpochLastCheckpoint(e)
104            | VfsPath::EpochCommittee(e)
105            | VfsPath::EpochCheckpoints(e) => Some(VfsPath::Epoch(*e)),
106            VfsPath::EpochCheckpointBySeq(e, _) | VfsPath::EpochCheckpointByDigest(e, _) => {
107                Some(VfsPath::EpochCheckpoints(*e))
108            }
109            VfsPath::CheckpointsSeqRoot | VfsPath::CheckpointsDigestRoot => {
110                Some(VfsPath::CheckpointsRoot)
111            }
112            VfsPath::CheckpointsBySeq(_) => Some(VfsPath::CheckpointsSeqRoot),
113            VfsPath::CheckpointSeqSummary(s)
114            | VfsPath::CheckpointSeqContents(s)
115            | VfsPath::CheckpointSeqContentsShort(s) => Some(VfsPath::CheckpointsBySeq(*s)),
116            VfsPath::CheckpointsByDigest(_) => Some(VfsPath::CheckpointsDigestRoot),
117            VfsPath::CheckpointDigestSummary(d)
118            | VfsPath::CheckpointDigestContents(d)
119            | VfsPath::CheckpointDigestContentsShort(d) => Some(VfsPath::CheckpointsByDigest(*d)),
120            VfsPath::CheckpointContentsEntry(_) => Some(VfsPath::CheckpointContentsRoot),
121            VfsPath::TransactionEntry(_) | VfsPath::TransactionEffectsEntry(_, _) => {
122                Some(VfsPath::TransactionsRoot)
123            }
124            VfsPath::ConsensusLatest => Some(VfsPath::ConsensusRoot),
125            VfsPath::ConsensusCommitsRoot => Some(VfsPath::ConsensusRoot),
126            VfsPath::ConsensusCommitDir(_) => Some(VfsPath::ConsensusCommitsRoot),
127            VfsPath::ConsensusCommitSummary(i) => Some(VfsPath::ConsensusCommitDir(*i)),
128        }
129    }
130}
131
132impl fmt::Display for VfsPath {
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        match self {
135            VfsPath::Root => write!(f, "/"),
136            VfsPath::Epochs => write!(f, "/epochs"),
137            VfsPath::Epoch(e) => write!(f, "/epochs/{e}"),
138            VfsPath::EpochFirstCheckpoint(e) => write!(f, "/epochs/{e}/first-checkpoint"),
139            VfsPath::EpochLastCheckpoint(e) => write!(f, "/epochs/{e}/last-checkpoint"),
140            VfsPath::EpochCommittee(e) => write!(f, "/epochs/{e}/committee"),
141            VfsPath::EpochCheckpoints(e) => write!(f, "/epochs/{e}/checkpoints"),
142            VfsPath::EpochCheckpointBySeq(e, s) => write!(f, "/epochs/{e}/checkpoints/{s}"),
143            VfsPath::EpochCheckpointByDigest(e, d) => write!(f, "/epochs/{e}/checkpoints/{d}"),
144            VfsPath::CheckpointsRoot => write!(f, "/checkpoints"),
145            VfsPath::CheckpointsSeqRoot => write!(f, "/checkpoints/seq"),
146            VfsPath::CheckpointsBySeq(s) => write!(f, "/checkpoints/seq/{s}"),
147            VfsPath::CheckpointSeqSummary(s) => write!(f, "/checkpoints/seq/{s}/summary"),
148            VfsPath::CheckpointSeqContents(s) => write!(f, "/checkpoints/seq/{s}/contents"),
149            VfsPath::CheckpointSeqContentsShort(s) => {
150                write!(f, "/checkpoints/seq/{s}/contents-short")
151            }
152            VfsPath::CheckpointsDigestRoot => write!(f, "/checkpoints/digest"),
153            VfsPath::CheckpointsByDigest(d) => write!(f, "/checkpoints/digest/{d}"),
154            VfsPath::CheckpointDigestSummary(d) => write!(f, "/checkpoints/digest/{d}/summary"),
155            VfsPath::CheckpointDigestContents(d) => write!(f, "/checkpoints/digest/{d}/contents"),
156            VfsPath::CheckpointDigestContentsShort(d) => {
157                write!(f, "/checkpoints/digest/{d}/contents-short")
158            }
159            VfsPath::CheckpointContentsRoot => write!(f, "/checkpoint-contents"),
160            VfsPath::CheckpointContentsEntry(d) => write!(f, "/checkpoint-contents/{d}"),
161            VfsPath::TransactionsRoot => write!(f, "/transactions"),
162            VfsPath::TransactionEntry(d) => write!(f, "/transactions/{d}"),
163            VfsPath::TransactionEffectsEntry(tx, fx) => {
164                write!(f, "/transactions/{tx}.fx-{fx}")
165            }
166            VfsPath::ConsensusRoot => write!(f, "/consensus"),
167            VfsPath::ConsensusLatest => write!(f, "/consensus/latest"),
168            VfsPath::ConsensusCommitsRoot => write!(f, "/consensus/commits"),
169            VfsPath::ConsensusCommitDir(i) => write!(f, "/consensus/commits/{i}"),
170            VfsPath::ConsensusCommitSummary(i) => write!(f, "/consensus/commits/{i}/summary"),
171        }
172    }
173}
174
175pub fn parse_path(s: &str) -> anyhow::Result<VfsPath> {
176    let parts: Vec<&str> = s
177        .trim_start_matches('/')
178        .split('/')
179        .filter(|p| !p.is_empty())
180        .collect();
181
182    let v = match parts.as_slice() {
183        [] => VfsPath::Root,
184        ["epochs"] => VfsPath::Epochs,
185        ["epochs", e] => VfsPath::Epoch(e.parse().map_err(|_| anyhow!("invalid epoch: '{e}'"))?),
186        ["epochs", e, "first-checkpoint"] => {
187            VfsPath::EpochFirstCheckpoint(e.parse().map_err(|_| anyhow!("invalid epoch: '{e}'"))?)
188        }
189        ["epochs", e, "last-checkpoint"] => {
190            VfsPath::EpochLastCheckpoint(e.parse().map_err(|_| anyhow!("invalid epoch: '{e}'"))?)
191        }
192        ["epochs", e, "committee"] => {
193            VfsPath::EpochCommittee(e.parse().map_err(|_| anyhow!("invalid epoch: '{e}'"))?)
194        }
195        ["epochs", e, "checkpoints"] => {
196            VfsPath::EpochCheckpoints(e.parse().map_err(|_| anyhow!("invalid epoch: '{e}'"))?)
197        }
198        ["epochs", e, "checkpoints", ref_str] => {
199            let epoch = e.parse().map_err(|_| anyhow!("invalid epoch: '{e}'"))?;
200            if let Ok(seq) = ref_str.parse::<CheckpointSequenceNumber>() {
201                VfsPath::EpochCheckpointBySeq(epoch, seq)
202            } else {
203                let digest: CheckpointDigest = ref_str
204                    .parse()
205                    .map_err(|_| anyhow!("invalid checkpoint ref: '{ref_str}'"))?;
206                VfsPath::EpochCheckpointByDigest(epoch, digest)
207            }
208        }
209        ["checkpoints"] => VfsPath::CheckpointsRoot,
210        ["checkpoints", "seq"] => VfsPath::CheckpointsSeqRoot,
211        ["checkpoints", "seq", s] => VfsPath::CheckpointsBySeq(
212            s.parse()
213                .map_err(|_| anyhow!("invalid sequence number: '{s}'"))?,
214        ),
215        ["checkpoints", "seq", s, "summary"] => VfsPath::CheckpointSeqSummary(
216            s.parse()
217                .map_err(|_| anyhow!("invalid sequence number: '{s}'"))?,
218        ),
219        ["checkpoints", "seq", s, "contents"] => VfsPath::CheckpointSeqContents(
220            s.parse()
221                .map_err(|_| anyhow!("invalid sequence number: '{s}'"))?,
222        ),
223        ["checkpoints", "seq", s, "contents-short"] => VfsPath::CheckpointSeqContentsShort(
224            s.parse()
225                .map_err(|_| anyhow!("invalid sequence number: '{s}'"))?,
226        ),
227        ["checkpoints", "digest"] => VfsPath::CheckpointsDigestRoot,
228        ["checkpoints", "digest", d] => VfsPath::CheckpointsByDigest(
229            d.parse()
230                .map_err(|_| anyhow!("invalid checkpoint digest: '{d}'"))?,
231        ),
232        ["checkpoints", "digest", d, "summary"] => VfsPath::CheckpointDigestSummary(
233            d.parse()
234                .map_err(|_| anyhow!("invalid checkpoint digest: '{d}'"))?,
235        ),
236        ["checkpoints", "digest", d, "contents"] => VfsPath::CheckpointDigestContents(
237            d.parse()
238                .map_err(|_| anyhow!("invalid checkpoint digest: '{d}'"))?,
239        ),
240        ["checkpoints", "digest", d, "contents-short"] => VfsPath::CheckpointDigestContentsShort(
241            d.parse()
242                .map_err(|_| anyhow!("invalid checkpoint digest: '{d}'"))?,
243        ),
244        ["checkpoint-contents"] => VfsPath::CheckpointContentsRoot,
245        ["checkpoint-contents", d] => VfsPath::CheckpointContentsEntry(
246            d.parse()
247                .map_err(|_| anyhow!("invalid contents digest: '{d}'"))?,
248        ),
249        ["transactions"] => VfsPath::TransactionsRoot,
250        ["transactions", seg] => parse_transaction_seg(seg)?,
251        ["consensus"] => VfsPath::ConsensusRoot,
252        ["consensus", "latest"] => VfsPath::ConsensusLatest,
253        ["consensus", "commits"] => VfsPath::ConsensusCommitsRoot,
254        ["consensus", "commits", i] => VfsPath::ConsensusCommitDir(
255            i.parse()
256                .map_err(|_| anyhow!("invalid commit index: '{i}'"))?,
257        ),
258        ["consensus", "commits", i, "summary"] => VfsPath::ConsensusCommitSummary(
259            i.parse()
260                .map_err(|_| anyhow!("invalid commit index: '{i}'"))?,
261        ),
262        _ => return Err(anyhow!("unknown path: '{s}'")),
263    };
264    Ok(v)
265}
266
267fn parse_transaction_seg(seg: &str) -> anyhow::Result<VfsPath> {
268    if let Some((tx_str, fx_str)) = seg.split_once(".fx-") {
269        let tx: TransactionDigest = tx_str
270            .parse()
271            .map_err(|_| anyhow!("invalid transaction digest: '{tx_str}'"))?;
272        let fx: TransactionEffectsDigest = fx_str
273            .parse()
274            .map_err(|_| anyhow!("invalid effects digest: '{fx_str}'"))?;
275        Ok(VfsPath::TransactionEffectsEntry(tx, fx))
276    } else {
277        let tx: TransactionDigest = seg
278            .parse()
279            .map_err(|_| anyhow!("invalid transaction digest: '{seg}'"))?;
280        Ok(VfsPath::TransactionEntry(tx))
281    }
282}
283
284/// Resolve a path string (absolute or relative) against a CWD.
285pub fn resolve_path(cwd: &VfsPath, path: &str) -> anyhow::Result<VfsPath> {
286    if path.starts_with('/') {
287        return parse_path(path);
288    }
289    let cwd_str = cwd.to_string();
290    let mut parts: Vec<&str> = cwd_str
291        .trim_start_matches('/')
292        .split('/')
293        .filter(|p| !p.is_empty())
294        .collect();
295    for component in path.split('/') {
296        match component {
297            "" | "." => {}
298            ".." => {
299                parts.pop();
300            }
301            c => parts.push(c),
302        }
303    }
304    let absolute = format!("/{}", parts.join("/"));
305    parse_path(&absolute)
306}