max / quasi-type
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
- Claude-Session
- https://claude.ai/code/session_01MptwXZ8k65v19rFmdGAyki
1 file changed,
+257 insertions,
-14 deletions
| @@ -553,23 +553,19 @@ | |||
| 553 | 553 | /// for `https://example.invalid/bases/<sha256>`. | |
| 554 | 554 | pub const MIRROR_ENV: &str = "QUASI_TYPE_MIRROR"; | |
| 555 | 555 | ||
| 556 | - | /// Where a mirror would hold the file whose pinned digest is `sha256`. | |
| 556 | + | /// Where a mirror holds the file whose pinned digest is `sha256`. | |
| 557 | 557 | /// | |
| 558 | 558 | /// Content-addressed on purpose, and it is what keeps this from being a new | |
| 559 | 559 | /// trust assumption: the digest is the one the pin already carries and the | |
| 560 | 560 | /// bytes are verified against it either way, so a mirror can serve the pinned | |
| 561 | 561 | /// file or nothing. It cannot serve a different one. That is also why no | |
| 562 | - | /// signature or TLS pinning is wanted here — the integrity guarantee was never | |
| 562 | + | /// signature or TLS pinning is wanted here: the integrity guarantee was never | |
| 563 | 563 | /// the transport. | |
| 564 | - | fn mirrored(sha256: &str) -> Option<String> { | |
| 565 | - | mirror_url(&std::env::var(MIRROR_ENV).ok()?, sha256) | |
| 566 | - | } | |
| 567 | - | ||
| 568 | - | /// [`mirrored`] against an explicit base, so the shaping is testable. | |
| 569 | 564 | /// | |
| 570 | - | /// Split out because `set_var` is unsafe in a threaded test binary and a test | |
| 571 | - | /// that sets the mirror under the other tests is a flake waiting for a slow | |
| 572 | - | /// machine. | |
| 565 | + | /// Takes the base rather than reading [`MIRROR_ENV`], so the shaping and the | |
| 566 | + | /// order [`cached_from`] tries its two sources in are both testable without | |
| 567 | + | /// `set_var`, which is unsafe in a threaded test binary and would set the | |
| 568 | + | /// mirror under every other test in the run. | |
| 573 | 569 | fn mirror_url(base: &str, sha256: &str) -> Option<String> { | |
| 574 | 570 | let base = base.trim().trim_end_matches('/'); | |
| 575 | 571 | (!base.is_empty()).then(|| format!("{base}/{sha256}")) | |
| @@ -599,6 +595,19 @@ | |||
| 599 | 595 | /// the way out, which is unchanged — these fail differently, the same way the | |
| 600 | 596 | /// archive's two checks do. | |
| 601 | 597 | fn cached(url: &str, path: &Path, offline: bool, expect: &str) -> Result<Vec<u8>, Error> { | |
| 598 | + | let mirror = std::env::var(MIRROR_ENV).ok(); | |
| 599 | + | cached_from(mirror.as_deref(), url, path, offline, expect) | |
| 600 | + | } | |
| 601 | + | ||
| 602 | + | /// [`cached`] against an explicit mirror base, which is where the two sources | |
| 603 | + | /// are actually ordered. | |
| 604 | + | fn cached_from( | |
| 605 | + | mirror: Option<&str>, | |
| 606 | + | url: &str, | |
| 607 | + | path: &Path, | |
| 608 | + | offline: bool, | |
| 609 | + | expect: &str, | |
| 610 | + | ) -> Result<Vec<u8>, Error> { | |
| 602 | 611 | if !path.exists() { | |
| 603 | 612 | if offline { | |
| 604 | 613 | return Err(Error::Offline { | |
| @@ -606,10 +615,12 @@ | |||
| 606 | 615 | url: url.to_owned(), | |
| 607 | 616 | }); | |
| 608 | 617 | } | |
| 609 | - | let mirror = mirrored(expect).filter(|mirror| { | |
| 610 | - | fetch_with(mirror, path, Attempt::Mirror).is_ok() | |
| 611 | - | && std::fs::read(path).is_ok_and(|bytes| verify(&bytes, expect).is_ok()) | |
| 612 | - | }); | |
| 618 | + | let mirror = mirror | |
| 619 | + | .and_then(|base| mirror_url(base, expect)) | |
| 620 | + | .filter(|mirror| { | |
| 621 | + | fetch_with(mirror, path, Attempt::Mirror).is_ok() | |
| 622 | + | && std::fs::read(path).is_ok_and(|bytes| verify(&bytes, expect).is_ok()) | |
| 623 | + | }); | |
| 613 | 624 | if mirror.is_none() { | |
| 614 | 625 | let _ = std::fs::remove_file(path); | |
| 615 | 626 | fetch(url, path)?; | |
| @@ -766,6 +777,115 @@ | |||
| 766 | 777 | mod tests { | |
| 767 | 778 | use super::*; | |
| 768 | 779 | ||
| 780 | + | use std::collections::HashMap; | |
| 781 | + | use std::io::{BufRead, BufReader, Write}; | |
| 782 | + | use std::net::{TcpListener, TcpStream}; | |
| 783 | + | use std::sync::{Arc, Mutex}; | |
| 784 | + | ||
| 785 | + | /// An HTTP server that answers a fixed table of paths and records every | |
| 786 | + | /// path it was asked for. | |
| 787 | + | /// | |
| 788 | + | /// Real sockets and the real `curl` invocation, because what is under test | |
| 789 | + | /// is which host the bytes came from, and a stubbed fetch would be a test | |
| 790 | + | /// of the stub. The table is exact: a path the test did not name gets a | |
| 791 | + | /// 404, so a mirror URL shaped wrongly reaches the same fallback a mirror | |
| 792 | + | /// that lacks the file does. | |
| 793 | + | struct Stub { | |
| 794 | + | base: String, | |
| 795 | + | asked: Arc<Mutex<Vec<String>>>, | |
| 796 | + | } | |
| 797 | + | ||
| 798 | + | impl Stub { | |
| 799 | + | fn new(routes: &[(&str, &[u8])]) -> Self { | |
| 800 | + | let listener = TcpListener::bind("127.0.0.1:0").expect("binding a stub server"); | |
| 801 | + | let port = listener.local_addr().expect("stub address").port(); | |
| 802 | + | let asked: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new())); | |
| 803 | + | let table: HashMap<String, Vec<u8>> = routes | |
| 804 | + | .iter() | |
| 805 | + | .map(|(path, body)| ((*path).to_owned(), body.to_vec())) | |
| 806 | + | .collect(); | |
| 807 | + | let log = Arc::clone(&asked); | |
| 808 | + | std::thread::spawn(move || { | |
| 809 | + | for stream in listener.incoming() { | |
| 810 | + | let Ok(stream) = stream else { continue }; | |
| 811 | + | Self::answer(stream, &table, &log); | |
| 812 | + | } | |
| 813 | + | }); | |
| 814 | + | Self { | |
| 815 | + | base: format!("http://127.0.0.1:{port}/bases"), | |
| 816 | + | asked, | |
| 817 | + | } | |
| 818 | + | } | |
| 819 | + | ||
| 820 | + | fn answer( | |
| 821 | + | mut stream: TcpStream, | |
| 822 | + | table: &HashMap<String, Vec<u8>>, | |
| 823 | + | log: &Mutex<Vec<String>>, | |
| 824 | + | ) { | |
| 825 | + | let mut request = String::new(); | |
| 826 | + | let mut reader = BufReader::new(stream.try_clone().expect("cloning the socket")); | |
| 827 | + | loop { | |
| 828 | + | let mut line = String::new(); | |
| 829 | + | if reader.read_line(&mut line).unwrap_or(0) == 0 || line.trim().is_empty() { | |
| 830 | + | break; | |
| 831 | + | } | |
| 832 | + | if request.is_empty() { | |
| 833 | + | request = line; | |
| 834 | + | } | |
| 835 | + | } | |
| 836 | + | let path = request.split_whitespace().nth(1).unwrap_or("").to_owned(); | |
| 837 | + | log.lock().expect("the request log").push(path.clone()); | |
| 838 | + | let response = match table.get(&path) { | |
| 839 | + | Some(body) => { | |
| 840 | + | let mut head = format!( | |
| 841 | + | "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", | |
| 842 | + | body.len(), | |
| 843 | + | ) | |
| 844 | + | .into_bytes(); | |
| 845 | + | head.extend_from_slice(body); | |
| 846 | + | head | |
| 847 | + | } | |
| 848 | + | None => b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" | |
| 849 | + | .to_vec(), | |
| 850 | + | }; | |
| 851 | + | let _ = stream.write_all(&response); | |
| 852 | + | let _ = stream.flush(); | |
| 853 | + | } | |
| 854 | + | ||
| 855 | + | /// The URL a pin would carry, for the server standing in for upstream. | |
| 856 | + | fn url(&self, path: &str) -> String { | |
| 857 | + | format!("{}{path}", self.base.trim_end_matches("/bases")) | |
| 858 | + | } | |
| 859 | + | ||
| 860 | + | fn asked(&self) -> Vec<String> { | |
| 861 | + | self.asked.lock().expect("the request log").clone() | |
| 862 | + | } | |
| 863 | + | } | |
| 864 | + | ||
| 865 | + | /// A cache directory that removes itself, so a failing assertion leaves no | |
| 866 | + | /// tree behind. | |
| 867 | + | struct Cache(PathBuf); | |
| 868 | + | ||
| 869 | + | impl Cache { | |
| 870 | + | fn new(label: &str) -> Self { | |
| 871 | + | let dir = std::env::temp_dir() | |
| 872 | + | .join(format!("quasi-type-mirror-{label}-{}", std::process::id())); | |
| 873 | + | let _ = std::fs::remove_dir_all(&dir); | |
| 874 | + | std::fs::create_dir_all(&dir).expect("cache dir"); | |
| 875 | + | Self(dir) | |
| 876 | + | } | |
| 877 | + | ||
| 878 | + | fn file(&self) -> PathBuf { | |
| 879 | + | self.0.join("atkinson-mono-2.001-LICENSE.txt") | |
| 880 | + | } | |
| 881 | + | } | |
| 882 | + | ||
| 883 | + | impl Drop for Cache { | |
| 884 | + | fn drop(&mut self) { | |
| 885 | + | let _ = std::fs::remove_dir_all(&self.0); | |
| 886 | + | } | |
| 887 | + | } | |
| 888 | + | ||
| 769 | 889 | // The mirror is addressed by the digest the pin already carries, so a | |
| 770 | 890 | // mirror can serve the pinned file or nothing at all. | |
| 771 | 891 | #[test] | |
| @@ -794,4 +914,127 @@ | |||
| 794 | 914 | assert!(mirror_url("", "abc123").is_none()); | |
| 795 | 915 | assert!(mirror_url(" ", "abc123").is_none()); | |
| 796 | 916 | } | |
| 917 | + | ||
| 918 | + | // The mirror is the first source and upstream is not asked at all when it | |
| 919 | + | // answers. Ordering is the whole point of the mirror: upstream rate-limits | |
| 920 | + | // by IP, and a mirror consulted only after a failure would still pay for | |
| 921 | + | // the upstream round trip on every build that works. Reverse the two and | |
| 922 | + | // the upstream server here records a request. | |
| 923 | + | #[test] | |
| 924 | + | fn a_mirrored_file_is_taken_from_the_mirror_and_upstream_is_not_asked() { | |
| 925 | + | let bytes = b"the pinned licence text"; | |
| 926 | + | let digest = hex(&Sha256::digest(bytes)); | |
| 927 | + | let mirror = Stub::new(&[(&format!("/bases/{digest}"), bytes)]); | |
| 928 | + | let upstream = Stub::new(&[("/mono/OFL.txt", bytes)]); | |
| 929 | + | let cache = Cache::new("hit"); | |
| 930 | + | ||
| 931 | + | let got = cached_from( | |
| 932 | + | Some(&format!("{}/", mirror.base)), | |
| 933 | + | &upstream.url("/mono/OFL.txt"), | |
| 934 | + | &cache.file(), | |
| 935 | + | false, | |
| 936 | + | &digest, | |
| 937 | + | ) | |
| 938 | + | .expect("the mirrored file"); | |
| 939 | + | ||
| 940 | + | assert_eq!(got, bytes, "the bytes are not the ones the mirror served"); | |
| 941 | + | assert_eq!( | |
| 942 | + | mirror.asked(), | |
| 943 | + | vec![format!("/bases/{digest}")], | |
| 944 | + | "the mirror was asked for something other than the pinned digest", | |
| 945 | + | ); | |
| 946 | + | assert!( | |
| 947 | + | upstream.asked().is_empty(), | |
| 948 | + | "upstream was asked for a file the mirror had: {:?}", | |
| 949 | + | upstream.asked(), | |
| 950 | + | ); | |
| 951 | + | assert_eq!( | |
| 952 | + | std::fs::read(cache.file()).expect("the cached file"), | |
| 953 | + | bytes, | |
| 954 | + | "the mirrored bytes did not land in the cache", | |
| 955 | + | ); | |
| 956 | + | } | |
| 957 | + | ||
| 958 | + | // A mirror serving the wrong bytes falls through to upstream rather than | |
| 959 | + | // failing the build, or an out-of-date mirror would be worse than no | |
| 960 | + | // mirror. The digest is checked here and again by the caller. | |
| 961 | + | #[test] | |
| 962 | + | fn a_mirror_serving_the_wrong_bytes_falls_through_to_upstream() { | |
| 963 | + | let bytes = b"the pinned licence text"; | |
| 964 | + | let digest = hex(&Sha256::digest(bytes)); | |
| 965 | + | let mirror = Stub::new(&[(&format!("/bases/{digest}"), b"an older licence")]); | |
| 966 | + | let upstream = Stub::new(&[("/mono/OFL.txt", bytes)]); | |
| 967 | + | let cache = Cache::new("wrong-bytes"); | |
| 968 | + | ||
| 969 | + | let got = cached_from( | |
| 970 | + | Some(&mirror.base), | |
| 971 | + | &upstream.url("/mono/OFL.txt"), | |
| 972 | + | &cache.file(), | |
| 973 | + | false, | |
| 974 | + | &digest, | |
| 975 | + | ) | |
| 976 | + | .expect("the upstream file"); | |
| 977 | + | ||
| 978 | + | assert_eq!(got, bytes, "the wrong bytes were kept"); | |
| 979 | + | assert_eq!( | |
| 980 | + | upstream.asked(), | |
| 981 | + | vec!["/mono/OFL.txt".to_owned()], | |
| 982 | + | "the fallback did not reach upstream", | |
| 983 | + | ); | |
| 984 | + | assert_eq!( | |
| 985 | + | std::fs::read(cache.file()).expect("the cached file"), | |
| 986 | + | bytes, | |
| 987 | + | "the mirror's bytes were left in the cache for the caller to verify", | |
| 988 | + | ); | |
| 989 | + | } | |
| 990 | + | ||
| 991 | + | // A mirror that does not hold the file is a slower build and not a broken | |
| 992 | + | // one, which is what lets the variable default to a host that may be down. | |
| 993 | + | #[test] | |
| 994 | + | fn a_mirror_without_the_file_falls_through_to_upstream() { | |
| 995 | + | let bytes = b"the pinned licence text"; | |
| 996 | + | let digest = hex(&Sha256::digest(bytes)); | |
| 997 | + | let mirror = Stub::new(&[]); | |
| 998 | + | let upstream = Stub::new(&[("/mono/OFL.txt", bytes)]); | |
| 999 | + | let cache = Cache::new("miss"); | |
| 1000 | + | ||
| 1001 | + | let got = cached_from( | |
| 1002 | + | Some(&mirror.base), | |
| 1003 | + | &upstream.url("/mono/OFL.txt"), | |
| 1004 | + | &cache.file(), | |
| 1005 | + | false, | |
| 1006 | + | &digest, | |
| 1007 | + | ) | |
| 1008 | + | .expect("the upstream file"); | |
| 1009 | + | ||
| 1010 | + | assert_eq!(got, bytes); | |
| 1011 | + | assert_eq!( | |
| 1012 | + | mirror.asked(), | |
| 1013 | + | vec![format!("/bases/{digest}")], | |
| 1014 | + | "the mirror was not asked first", | |
| 1015 | + | ); | |
| 1016 | + | assert_eq!(upstream.asked(), vec!["/mono/OFL.txt".to_owned()]); | |
| 1017 | + | } | |
| 1018 | + | ||
| 1019 | + | // No mirror named is no request, which is what `QUASI_TYPE_MIRROR=` in a | |
| 1020 | + | // build is asking for. | |
| 1021 | + | #[test] | |
| 1022 | + | fn no_mirror_named_asks_only_upstream() { | |
| 1023 | + | let bytes = b"the pinned licence text"; | |
| 1024 | + | let digest = hex(&Sha256::digest(bytes)); | |
| 1025 | + | let upstream = Stub::new(&[("/mono/OFL.txt", bytes)]); | |
| 1026 | + | let cache = Cache::new("no-mirror"); | |
| 1027 | + | ||
| 1028 | + | let got = cached_from( | |
| 1029 | + | None, | |
| 1030 | + | &upstream.url("/mono/OFL.txt"), | |
| 1031 | + | &cache.file(), | |
| 1032 | + | false, | |
| 1033 | + | &digest, | |
| 1034 | + | ) | |
| 1035 | + | .expect("the upstream file"); | |
| 1036 | + | ||
| 1037 | + | assert_eq!(got, bytes); | |
| 1038 | + | assert_eq!(upstream.asked(), vec!["/mono/OFL.txt".to_owned()]); | |
| 1039 | + | } | |
| 797 | 1040 | } |