about summary refs log tree commit diff
path: root/tvix/nix-compat
diff options
context:
space:
mode:
authorFlorian Klink <flokli@flokli.de>2024-08-21T08·06+0300
committerclbot <clbot@tvl.fyi>2024-08-21T09·40+0000
commite03ea11badbf40971d6bf87eede33fe3b046c98b (patch)
treed520dedadeb8193b2da323adb2623a269217951c /tvix/nix-compat
parent2357079891819c679821ea58a7715de7be431aaa (diff)
feat(nix-compat/nix_http): init parse_nar[info]_str r/8547
This moves the URL component parsing code we had in nar-bridge to
nix-compat.

We change the function signature to return an Option, not a
Result<_, StatusCode>.

This allows returning more appropriate error codes, as we can
ok_or(…) at the callsite, which we now do: on an upload to an
invalid path, we now return "unauthorized", while on a GET/HEAD, we
return "not found".

This also adds support to parse compression suffixes. While not
supported in nar-bridge, other users of nix-compat might very well want
to parse these paths.

Also fix the error message when parsing NAR urls, it mentioned 32, not
52, which is a copypasta error from the narinfo URL parsing code.

Change-Id: Id1be9a8044814b54ce68b125c52dfe933c9c4f74
Reviewed-on: https://cl.tvl.fyi/c/depot/+/12260
Reviewed-by: raitobezarius <tvl@lahfa.xyz>
Autosubmit: flokli <flokli@flokli.de>
Tested-by: BuildkiteCI
Diffstat (limited to 'tvix/nix-compat')
-rw-r--r--tvix/nix-compat/Cargo.toml1
-rw-r--r--tvix/nix-compat/src/lib.rs1
-rw-r--r--tvix/nix-compat/src/nix_http/mod.rs108
3 files changed, 110 insertions, 0 deletions
diff --git a/tvix/nix-compat/Cargo.toml b/tvix/nix-compat/Cargo.toml
index e399d5a2254a..22325ad12bfe 100644
--- a/tvix/nix-compat/Cargo.toml
+++ b/tvix/nix-compat/Cargo.toml
@@ -27,6 +27,7 @@ serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
 sha2 = "0.10.6"
 thiserror = "1.0.38"
+tracing = "0.1.37"
 
 [dependencies.tokio]
 optional = true
diff --git a/tvix/nix-compat/src/lib.rs b/tvix/nix-compat/src/lib.rs
index cc1ee082be38..1410a8264240 100644
--- a/tvix/nix-compat/src/lib.rs
+++ b/tvix/nix-compat/src/lib.rs
@@ -2,6 +2,7 @@ pub(crate) mod aterm;
 pub mod derivation;
 pub mod nar;
 pub mod narinfo;
+pub mod nix_http;
 pub mod nixbase32;
 pub mod nixcpp;
 pub mod nixhash;
diff --git a/tvix/nix-compat/src/nix_http/mod.rs b/tvix/nix-compat/src/nix_http/mod.rs
new file mode 100644
index 000000000000..cbb629784612
--- /dev/null
+++ b/tvix/nix-compat/src/nix_http/mod.rs
@@ -0,0 +1,108 @@
+use tracing::trace;
+
+use crate::nixbase32;
+
+/// Parses a `14cx20k6z4hq508kqi2lm79qfld5f9mf7kiafpqsjs3zlmycza0k.nar`
+/// string and returns the nixbase32-decoded digest, as well as the compression
+/// suffix (which might be empty).
+pub fn parse_nar_str(s: &str) -> Option<([u8; 32], &str)> {
+    if !s.is_char_boundary(52) {
+        trace!("invalid string, no char boundary at 52");
+        return None;
+    }
+
+    let (hash_str, suffix) = s.split_at(52);
+
+    // we know hash_str is 52 bytes, so it's ok to unwrap here.
+    let hash_str_fixed: [u8; 52] = hash_str.as_bytes().try_into().unwrap();
+
+    match suffix.strip_prefix(".nar") {
+        Some(compression_suffix) => match nixbase32::decode_fixed(hash_str_fixed) {
+            Err(e) => {
+                trace!(err=%e, "invalid nixbase32 encoding");
+                None
+            }
+            Ok(digest) => Some((digest, compression_suffix)),
+        },
+        None => {
+            trace!("no .nar suffix");
+            None
+        }
+    }
+}
+
+/// Parses a `3mzh8lvgbynm9daj7c82k2sfsfhrsfsy.narinfo` string and returns the
+/// nixbase32-decoded digest.
+pub fn parse_narinfo_str(s: &str) -> Option<[u8; 20]> {
+    if !s.is_char_boundary(32) {
+        trace!("invalid string, no char boundary at 32");
+        return None;
+    }
+
+    match s.split_at(32) {
+        (hash_str, ".narinfo") => {
+            // we know this is 32 bytes, so it's ok to unwrap here.
+            let hash_str_fixed: [u8; 32] = hash_str.as_bytes().try_into().unwrap();
+
+            match nixbase32::decode_fixed(hash_str_fixed) {
+                Err(e) => {
+                    trace!(err=%e, "invalid nixbase32 encoding");
+                    None
+                }
+                Ok(digest) => Some(digest),
+            }
+        }
+        _ => {
+            trace!("invalid string, no .narinfo suffix");
+            None
+        }
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::{parse_nar_str, parse_narinfo_str};
+    use hex_literal::hex;
+
+    #[test]
+    fn parse_nar_str_success() {
+        assert_eq!(
+            (
+                hex!("13a8cf7ca57f68a9f1752acee36a72a55187d3a954443c112818926f26109d91"),
+                ""
+            ),
+            parse_nar_str("14cx20k6z4hq508kqi2lm79qfld5f9mf7kiafpqsjs3zlmycza0k.nar").unwrap()
+        );
+
+        assert_eq!(
+            (
+                hex!("13a8cf7ca57f68a9f1752acee36a72a55187d3a954443c112818926f26109d91"),
+                ".xz"
+            ),
+            parse_nar_str("14cx20k6z4hq508kqi2lm79qfld5f9mf7kiafpqsjs3zlmycza0k.nar.xz").unwrap()
+        )
+    }
+
+    #[test]
+    fn parse_nar_str_failure() {
+        assert!(parse_nar_str("14cx20k6z4hq508kqi2lm79qfld5f9mf7kiafpqsjs3zlmycza0").is_none());
+        assert!(
+            parse_nar_str("14cx20k6z4hq508kqi2lm79qfld5f9mf7kiafpqsjs3zlmycza0🦊.nar").is_none()
+        )
+    }
+    #[test]
+    fn parse_narinfo_str_success() {
+        assert_eq!(
+            hex!("8a12321522fd91efbd60ebb2481af88580f61600"),
+            parse_narinfo_str("00bgd045z0d4icpbc2yyz4gx48ak44la.narinfo").unwrap()
+        );
+    }
+
+    #[test]
+    fn parse_narinfo_str_failure() {
+        assert!(parse_narinfo_str("00bgd045z0d4icpbc2yyz4gx48ak44la").is_none());
+        assert!(parse_narinfo_str("/00bgd045z0d4icpbc2yyz4gx48ak44la").is_none());
+        assert!(parse_narinfo_str("000000").is_none());
+        assert!(parse_narinfo_str("00bgd045z0d4icpbc2yyz4gx48ak44l🦊.narinfo").is_none());
+    }
+}