oku_fs/fs/
util.rs

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