Skip to main content

crate_names/
read.rs

1//! Sans-io readers for the published artifacts.
2
3use crate::format::{folded_cmp, folded_cmp_key, folded_starts_with, normalize};
4use std::fmt;
5use std::ops::Range;
6
7/// Error returned when parsing an artifact.
8#[derive(Debug)]
9pub enum Error {
10    /// zstd decompression failed
11    Zstd(std::io::Error),
12    /// decompressed artifact was not valid utf8
13    Utf8(std::string::FromUtf8Error),
14    /// artifact exceeds the 4GiB this reader supports
15    TooLarge,
16    /// lines were not sorted (or not unique) by crate name
17    Unsorted {
18        /// 0-indexed line number
19        line: usize,
20    },
21    /// a line did not have the expected fields
22    Malformed {
23        /// 0-indexed line number
24        line: usize,
25    },
26}
27
28impl fmt::Display for Error {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        match self {
31            Error::Zstd(e) => write!(f, "zstd decompression failed: {e}"),
32            Error::Utf8(e) => write!(f, "artifact was not valid utf8: {e}"),
33            Error::TooLarge => write!(f, "artifact exceeds supported size"),
34            Error::Unsorted { line } => write!(f, "artifact not sorted by name at line {line}"),
35            Error::Malformed { line } => write!(f, "malformed artifact line {line}"),
36        }
37    }
38}
39
40impl std::error::Error for Error {}
41
42/// A single crate in the names artifact.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub struct Entry<'a> {
45    /// the crate's name
46    pub name: &'a str,
47    /// the crates.io "default version": the version the crates.io ui
48    /// itself presents, typically the highest stable non-yanked release
49    pub version: &'a str,
50    /// log-quantized all-time downloads; see [`crate::rank_from_downloads`]
51    pub rank: u8,
52}
53
54/// The decompressed text of an artifact plus line offsets. TSV sorted by
55/// folded name needs no further indexing: prefix queries are two binary
56/// searches.
57struct SortedTsv {
58    text: String,
59    /// byte offset of each line start, plus a sentinel at `text.len()`
60    offsets: Vec<u32>,
61}
62
63impl SortedTsv {
64    fn from_zstd(bytes: &[u8]) -> Result<Self, Error> {
65        let decompressed = zstd::decode_all(bytes).map_err(Error::Zstd)?;
66        Self::from_text(String::from_utf8(decompressed).map_err(Error::Utf8)?)
67    }
68
69    fn from_text(text: String) -> Result<Self, Error> {
70        if text.len() > u32::MAX as usize {
71            return Err(Error::TooLarge);
72        }
73        let mut offsets = vec![0];
74        for (index, byte) in text.bytes().enumerate() {
75            if byte == b'\n' {
76                offsets.push(index as u32 + 1);
77            }
78        }
79        if *offsets.last().unwrap() as usize != text.len() {
80            offsets.push(text.len() as u32);
81        } else {
82            // trailing newline: the sentinel is already in place
83        }
84        let tsv = Self { text, offsets };
85        for line in 1..tsv.len() {
86            if folded_cmp(tsv.name(line - 1), tsv.name(line)).is_ge() {
87                return Err(Error::Unsorted { line });
88            }
89        }
90        Ok(tsv)
91    }
92
93    fn len(&self) -> usize {
94        self.offsets.len() - 1
95    }
96
97    fn line(&self, index: usize) -> &str {
98        let start = self.offsets[index] as usize;
99        let end = self.offsets[index + 1] as usize;
100        self.text[start..end].trim_end_matches('\n')
101    }
102
103    fn name(&self, index: usize) -> &str {
104        let line = self.line(index);
105        line.split('\t').next().unwrap_or(line)
106    }
107
108    /// index of the first line for which `pred(name)` is false. `pred`
109    /// must be monotonic (true for a prefix of lines) over the sorted names.
110    fn partition_point(&self, pred: impl Fn(&str) -> bool) -> usize {
111        let (mut low, mut high) = (0, self.len());
112        while low < high {
113            let mid = low + (high - low) / 2;
114            if pred(self.name(mid)) {
115                low = mid + 1;
116            } else {
117                high = mid;
118            }
119        }
120        low
121    }
122
123    /// The lines whose folded name starts with the folded `prefix`. Folding
124    /// the needle once here keeps every comparison in the binary search
125    /// allocation-free.
126    fn prefix_range(&self, prefix: &str) -> Range<usize> {
127        let key = normalize(prefix);
128        let start = self.partition_point(|name| folded_cmp_key(name, &key).is_lt());
129        let end = self.partition_point(|name| {
130            folded_cmp_key(name, &key).is_lt() || folded_starts_with(name, &key)
131        });
132        start..end
133    }
134
135    fn find(&self, name: &str) -> Option<usize> {
136        let key = normalize(name);
137        let index = self.partition_point(|candidate| folded_cmp_key(candidate, &key).is_lt());
138        (index < self.len() && folded_cmp_key(self.name(index), &key).is_eq()).then_some(index)
139    }
140}
141
142/// Reader for the names artifact (`names-v1.tsv.zst`): every crate name
143/// with its default version and download rank.
144pub struct CrateNames(SortedTsv);
145
146impl fmt::Debug for CrateNames {
147    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148        f.debug_struct("CrateNames")
149            .field("len", &self.len())
150            .finish_non_exhaustive()
151    }
152}
153
154impl CrateNames {
155    /// Parse a zstd-compressed names artifact.
156    pub fn from_zstd(bytes: &[u8]) -> Result<Self, Error> {
157        Self::validate(SortedTsv::from_zstd(bytes)?)
158    }
159
160    /// Parse an already-decompressed names artifact.
161    pub fn from_tsv(text: String) -> Result<Self, Error> {
162        Self::validate(SortedTsv::from_text(text)?)
163    }
164
165    fn validate(tsv: SortedTsv) -> Result<Self, Error> {
166        let names = Self(tsv);
167        for line in 0..names.len() {
168            names.entry(line).ok_or(Error::Malformed { line })?;
169        }
170        Ok(names)
171    }
172
173    fn entry(&self, index: usize) -> Option<Entry<'_>> {
174        let mut fields = self.0.line(index).split('\t');
175        let name = fields.next()?;
176        let version = fields.next()?;
177        let rank = fields.next()?.parse().ok()?;
178        fields.next().is_none().then_some(Entry {
179            name,
180            version,
181            rank,
182        })
183    }
184
185    /// Number of crates in the artifact.
186    pub fn len(&self) -> usize {
187        self.0.len()
188    }
189
190    /// Whether the artifact contains no crates.
191    pub fn is_empty(&self) -> bool {
192        self.len() == 0
193    }
194
195    /// Name lookup, folded: case-insensitive, and `-` and `_` are the same
196    /// character. `get("Tokio_Util")` finds `tokio-util`, because crates.io
197    /// would not have let a second crate claim that name.
198    pub fn get(&self, name: &str) -> Option<Entry<'_>> {
199        self.entry(self.0.find(name)?)
200    }
201
202    /// The entry at line `index` (0-based, in the artifact's folded-name sort
203    /// order), or `None` if out of range. Paired with [`len`](Self::len) this
204    /// is the stable handle a caller needs to build a side index that refers
205    /// back into the artifact by position rather than copying names.
206    pub fn entry_at(&self, index: usize) -> Option<Entry<'_>> {
207        (index < self.len()).then(|| self.entry(index)).flatten()
208    }
209
210    /// The half-open range of line indices whose folded name starts with
211    /// `prefix` (folded as in [`get`](Self::get)) — the same range
212    /// [`count`](Self::count) measures, exposed as positions so a caller can
213    /// pair whole-name-prefix hits with [`entry_at`](Self::entry_at).
214    pub fn prefix_indices(&self, prefix: &str) -> Range<usize> {
215        self.0.prefix_range(prefix)
216    }
217
218    /// How many crate names start with `prefix`, folded as in [`get`](Self::get).
219    /// Two binary searches — no enumeration — so this is as cheap for `"s"`
220    /// as for `"trillium-"`.
221    pub fn count(&self, prefix: &str) -> usize {
222        self.0.prefix_range(prefix).len()
223    }
224
225    /// All crates whose name starts with `prefix`, folded as in
226    /// [`get`](Self::get), in artifact order.
227    pub fn prefix(&self, prefix: &str) -> impl Iterator<Item = Entry<'_>> {
228        self.0
229            .prefix_range(prefix)
230            .map(|index| self.entry(index).expect("validated at construction"))
231    }
232
233    /// The `limit` highest-ranked crates whose name starts with `prefix`
234    /// (folded as in [`get`](Self::get)), ties broken by name.
235    pub fn typeahead(&self, prefix: &str, limit: usize) -> Vec<Entry<'_>> {
236        let mut matches: Vec<Entry<'_>> = self.prefix(prefix).collect();
237        matches.sort_unstable_by(|a, b| b.rank.cmp(&a.rank).then(a.name.cmp(b.name)));
238        matches.truncate(limit);
239        matches
240    }
241}
242
243/// Reader for the descriptions artifact (`descriptions-v1.tsv.zst`).
244pub struct Descriptions(SortedTsv);
245
246impl fmt::Debug for Descriptions {
247    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248        f.debug_struct("Descriptions")
249            .field("len", &self.len())
250            .finish_non_exhaustive()
251    }
252}
253
254impl Descriptions {
255    /// Parse a zstd-compressed descriptions artifact.
256    pub fn from_zstd(bytes: &[u8]) -> Result<Self, Error> {
257        Ok(Self(SortedTsv::from_zstd(bytes)?))
258    }
259
260    /// Parse an already-decompressed descriptions artifact.
261    pub fn from_tsv(text: String) -> Result<Self, Error> {
262        Ok(Self(SortedTsv::from_text(text)?))
263    }
264
265    /// Number of crates in the artifact.
266    pub fn len(&self) -> usize {
267        self.0.len()
268    }
269
270    /// Whether the artifact contains no crates.
271    pub fn is_empty(&self) -> bool {
272        self.len() == 0
273    }
274
275    /// The description for `name`, if that crate has one. Folded, as in
276    /// [`CrateNames::get`].
277    pub fn get(&self, name: &str) -> Option<&str> {
278        let index = self.0.find(name)?;
279        self.0.line(index).split_once('\t').map(|(_, desc)| desc)
280    }
281
282    /// All `(name, description)` pairs, in artifact order.
283    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
284        (0..self.len()).filter_map(|index| self.0.line(index).split_once('\t'))
285    }
286}
287
288/// One crate's declared keywords and categories from the facets artifact.
289#[derive(Debug, Clone, Copy, PartialEq, Eq)]
290pub struct FacetsEntry<'a> {
291    /// the crate's name as spelled
292    pub name: &'a str,
293    keywords: &'a str,
294    categories: &'a str,
295}
296
297impl<'a> FacetsEntry<'a> {
298    /// The crate's Cargo.toml keywords, sorted, as spelled by the author
299    /// (crates.io allows at most five).
300    pub fn keywords(&self) -> impl Iterator<Item = &'a str> {
301        self.keywords.split_ascii_whitespace()
302    }
303
304    /// The crate's category slugs, sorted — crates.io's controlled
305    /// vocabulary, nested slugs joined with `::`
306    /// (e.g. `web-programming::http-server`).
307    pub fn categories(&self) -> impl Iterator<Item = &'a str> {
308        self.categories.split_ascii_whitespace()
309    }
310}
311
312/// Reader for the facets artifact (`facets-v1.tsv.zst`): the declared
313/// Cargo.toml keywords and category slugs for every crate that has any.
314pub struct Facets(SortedTsv);
315
316impl fmt::Debug for Facets {
317    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
318        f.debug_struct("Facets")
319            .field("len", &self.len())
320            .finish_non_exhaustive()
321    }
322}
323
324impl Facets {
325    /// Parse a zstd-compressed facets artifact.
326    pub fn from_zstd(bytes: &[u8]) -> Result<Self, Error> {
327        Self::validate(SortedTsv::from_zstd(bytes)?)
328    }
329
330    /// Parse an already-decompressed facets artifact.
331    pub fn from_tsv(text: String) -> Result<Self, Error> {
332        Self::validate(SortedTsv::from_text(text)?)
333    }
334
335    fn validate(tsv: SortedTsv) -> Result<Self, Error> {
336        let facets = Self(tsv);
337        for line in 0..facets.len() {
338            facets.entry(line).ok_or(Error::Malformed { line })?;
339        }
340        Ok(facets)
341    }
342
343    fn entry(&self, index: usize) -> Option<FacetsEntry<'_>> {
344        let mut fields = self.0.line(index).split('\t');
345        let name = fields.next()?;
346        let keywords = fields.next()?;
347        let categories = fields.next()?;
348        fields.next().is_none().then_some(FacetsEntry {
349            name,
350            keywords,
351            categories,
352        })
353    }
354
355    /// Number of crates in the artifact — only crates with at least one
356    /// keyword or category have a line.
357    pub fn len(&self) -> usize {
358        self.0.len()
359    }
360
361    /// Whether the artifact contains no crates.
362    pub fn is_empty(&self) -> bool {
363        self.len() == 0
364    }
365
366    /// The facets for `name`, if that crate declared any. Folded, as in
367    /// [`CrateNames::get`].
368    pub fn get(&self, name: &str) -> Option<FacetsEntry<'_>> {
369        self.entry(self.0.find(name)?)
370    }
371
372    /// All entries, in artifact order.
373    pub fn iter(&self) -> impl Iterator<Item = FacetsEntry<'_>> {
374        (0..self.len()).map(|index| self.entry(index).expect("validated at construction"))
375    }
376}