Skip to main content

oku_fs/fs/
util.rs

1use bytes::Bytes;
2use iroh_docs::DocTicket;
3use log::error;
4use miette::IntoDiagnostic;
5use path_clean::PathClean;
6use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator};
7use std::ffi::CString;
8use std::path::PathBuf;
9
10/// Cleans a path and ensures it begins with the root.
11///
12/// # Arguments
13///
14/// * `path` - The path to normalise.
15///
16/// # Returns
17///
18/// The given path, prefixed with `/` if missing, and `.` & `..` components processed.
19pub fn normalise_path(path: &PathBuf) -> PathBuf {
20    PathBuf::from("/").join(path).clean()
21}
22
23/// Converts a path to a key for an entry in a file system replica.
24///
25/// # Arguments
26///
27/// * `path` - The path to convert to a key.
28///
29/// # Returns
30///
31/// A null-terminated byte string representing the path.
32pub fn path_to_entry_key(path: &PathBuf) -> Bytes {
33    let path = normalise_path(path);
34    let mut path_bytes = path.into_os_string().into_encoded_bytes();
35    path_bytes.push(b'\0');
36    path_bytes.into()
37}
38
39/// Converts a key of a replica entry into a path within a replica.
40///
41/// # Arguments
42///
43/// * `key` - The replica entry key, being a null-terminated byte string.
44///
45/// # Returns
46///
47/// A path pointing to the file with the key.
48pub fn entry_key_to_path(key: &[u8]) -> miette::Result<PathBuf> {
49    Ok(PathBuf::from(
50        CString::from_vec_with_nul(key.to_vec())
51            .into_diagnostic()?
52            .into_string()
53            .into_diagnostic()?,
54    ))
55}
56
57/// Converts a path to a key prefix for entries in a file system replica.
58///
59/// # Arguments
60///
61/// * `path` - The path to convert to a key prefix.
62///
63/// # Returns
64///
65/// A byte string representing the path, without a null byte at the end.
66pub fn path_to_entry_prefix(path: &PathBuf) -> Bytes {
67    let path = normalise_path(path);
68    let path_bytes = path.into_os_string().into_encoded_bytes();
69    path_bytes.into()
70}
71
72/// Format bytes as a base32-encoded lowercase string.
73///
74/// # Arguments
75///
76/// * `bytes` - The bytes to encode.
77///
78/// # Return
79///
80/// The bytes encoded as a lowercase string, represented in base32.
81pub fn fmt(bytes: impl AsRef<[u8]>) -> String {
82    let mut text = data_encoding::BASE32_NOPAD.encode(bytes.as_ref());
83    text.make_ascii_lowercase();
84    text
85}
86
87/// Format first ten bytes of a byte list as a base32-encoded lowercase string.
88///
89/// # Arguments
90///
91/// * `bytes` - The byte list to encode.
92///
93/// # Return
94///
95/// The first ten bytes encoded as a lowercase string, represented in base32.
96pub fn fmt_short(bytes: impl AsRef<[u8]>) -> String {
97    let len = bytes.as_ref().len().min(10);
98    let mut text = data_encoding::BASE32_NOPAD.encode(&bytes.as_ref()[..len]);
99    text.make_ascii_lowercase();
100    text
101}
102
103/// Parse a string as a base32-encoded byte array of length `N`.
104///
105/// # Arguments
106///
107/// * `input` - The string to parse.
108///
109/// # Returns
110///
111/// An array of bytes of length `N`.
112pub fn parse_array<const N: usize>(input: &str) -> miette::Result<[u8; N]> {
113    data_encoding::BASE32_NOPAD
114        .decode(input.to_ascii_uppercase().as_bytes())
115        .into_diagnostic()?
116        .try_into()
117        .map_err(|_| {
118            miette::miette!(
119                "Unable to parse {input} as a base32-encoded byte array of length {N} … "
120            )
121        })
122}
123
124/// Parse a string either as a hex-encoded or base32-encoded byte array of length `LEN`.
125///
126/// # Arguments
127///
128/// * `input` - The string to parse.
129///
130/// # Returns
131///
132/// An array of bytes of length `LEN`.
133pub fn parse_array_hex_or_base32<const LEN: usize>(input: &str) -> miette::Result<[u8; LEN]> {
134    let mut bytes = [0u8; LEN];
135    if input.len() == LEN * 2 {
136        hex::decode_to_slice(input, &mut bytes).into_diagnostic()?;
137        Ok(bytes)
138    } else {
139        Ok(parse_array(input)?)
140    }
141}
142
143/// Merge multiple tickets into one, returning `None` if no tickets were given.
144///
145/// # Arguments
146///
147/// * `tickets` - A vector of tickets to merge.
148///
149/// # Returns
150///
151/// `None` if no tickets were given, or a ticket with a merged capability and merged list of nodes.
152pub fn merge_tickets(tickets: &Vec<DocTicket>) -> Option<DocTicket> {
153    let ticket_parts: Vec<_> = tickets
154        .par_iter()
155        .map(|ticket| ticket.capability.clone())
156        .zip(tickets.par_iter().map(|ticket| ticket.nodes.clone()))
157        .collect();
158    ticket_parts
159        .into_iter()
160        .reduce(|mut merged_tickets, next_ticket| {
161            if let Err(e) = merged_tickets.0.merge(next_ticket.0) {
162                error!("{e}");
163            }
164            merged_tickets.1.extend_from_slice(&next_ticket.1);
165            merged_tickets
166        })
167        .map(|mut merged_tickets| {
168            merged_tickets.1.sort_unstable();
169            merged_tickets.1.dedup();
170            DocTicket {
171                capability: merged_tickets.0,
172                nodes: merged_tickets.1,
173            }
174        })
175}