crate_names/format.rs
1//! Constants and pure functions that define wire format v2. Builder and
2//! reader both depend on this module, so the format has a single source
3//! of truth.
4
5use std::cmp::Ordering;
6
7/// Artifact file name for the names/versions/ranks table.
8pub const NAMES_FILE_V2: &str = "names-v2.tsv.zst";
9
10/// Artifact file name for the descriptions table.
11pub const DESCRIPTIONS_FILE_V2: &str = "descriptions-v2.tsv.zst";
12
13/// Canonical public URL of the names artifact, republished daily by this
14/// repository's scheduled workflow. Redirects (GitHub release asset), so
15/// fetch with redirect-following enabled.
16pub const NAMES_URL_V2: &str =
17 "https://github.com/jbr/crate-names/releases/download/artifacts/names-v2.tsv.zst";
18
19/// Canonical public URL of the descriptions artifact; see [`NAMES_URL_V2`].
20pub const DESCRIPTIONS_URL_V2: &str =
21 "https://github.com/jbr/crate-names/releases/download/artifacts/descriptions-v2.tsv.zst";
22
23/// Artifact file name for the facets (keywords and categories) table.
24pub const FACETS_FILE_V1: &str = "facets-v1.tsv.zst";
25
26/// Canonical public URL of the facets artifact; see [`NAMES_URL_V2`].
27pub const FACETS_URL_V1: &str =
28 "https://github.com/jbr/crate-names/releases/download/artifacts/facets-v1.tsv.zst";
29
30/// Fold one byte of a crate name the way crates.io folds names when it
31/// decides whether two of them collide: ASCII-case-insensitively, with `-`
32/// and `_` equivalent.
33const fn fold(byte: u8) -> u8 {
34 match byte {
35 b'_' => b'-',
36 other => other.to_ascii_lowercase(),
37 }
38}
39
40/// The key a crate name is stored and searched under: ASCII-lowercased,
41/// with `-` and `_` folded together — the same way crates.io decides
42/// whether two names collide.
43///
44/// crates.io rejects a new crate whose folded name matches an existing one,
45/// so this key is unique across the registry — which is what lets the
46/// artifacts be sorted by it and still be searched with a binary search.
47/// Names keep their original spelling in the artifact; only the *ordering*
48/// and the comparisons are folded.
49pub fn normalize(name: &str) -> String {
50 name.bytes().map(fold).map(char::from).collect()
51}
52
53/// Order two crate names by their folded keys, without allocating.
54pub(crate) fn folded_cmp(left: &str, right: &str) -> Ordering {
55 left.bytes().map(fold).cmp(right.bytes().map(fold))
56}
57
58/// Order a crate name against an already-folded needle, without allocating.
59pub(crate) fn folded_cmp_key(name: &str, key: &str) -> Ordering {
60 name.bytes().map(fold).cmp(key.bytes())
61}
62
63/// Whether `name`'s folded key begins with the already-folded `key`.
64pub(crate) fn folded_starts_with(name: &str, key: &str) -> bool {
65 name.len() >= key.len()
66 && name
67 .bytes()
68 .map(fold)
69 .zip(key.bytes())
70 .all(|(name_byte, key_byte)| name_byte == key_byte)
71}
72
73/// Compression level used when producing artifacts.
74#[cfg(feature = "build")]
75pub(crate) const ZSTD_LEVEL: i32 = 19;
76
77/// Quantize an all-time download count into a rank in `0..=255`.
78///
79/// Eight sub-buckets per doubling of downloads (`floor(8·log₂(n+1))`),
80/// which preserves ordering at every scale while compressing to almost
81/// nothing. Saturates around 4×10⁹ downloads, comfortably above the most
82/// downloaded crate.
83pub fn rank_from_downloads(downloads: u64) -> u8 {
84 let rank = (8.0 * (downloads.saturating_add(1) as f64).log2()).floor();
85 if rank >= 255.0 { 255 } else { rank as u8 }
86}
87
88/// Collapse all whitespace runs (including newlines and tabs) to single
89/// spaces so descriptions fit in one TSV field.
90#[cfg(feature = "build")]
91pub(crate) fn flatten_whitespace(s: &str) -> String {
92 s.split_whitespace().collect::<Vec<_>>().join(" ")
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn rank_is_monotonic_and_bounded() {
101 assert_eq!(rank_from_downloads(0), 0);
102 let mut prev = 0;
103 for downloads in [1, 5, 100, 1_000, 1_000_000, 500_000_000, u64::MAX] {
104 let rank = rank_from_downloads(downloads);
105 assert!(rank >= prev, "rank must not decrease");
106 prev = rank;
107 }
108 assert_eq!(rank_from_downloads(u64::MAX), 255);
109 // half a billion downloads (serde territory) is still well under saturation
110 assert!(rank_from_downloads(500_000_000) < 255);
111 }
112}