This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-lfs / tests / properties.rs
3.7 kB 117 lines
1mod common; 2 3use common::{oid_of, repo}; 4use knot_lfs::{ 5 ClaimedSize, DiskStore, LfsError, LfsOid, LfsSize, LfsStore, LfsStorePath, MemoryStore, 6}; 7use proptest::prelude::*; 8 9fn round_trip(store: &dyn LfsStore, body: &[u8]) -> Result<(), TestCaseError> { 10 let repo = repo(); 11 let oid = oid_of(body); 12 let bytes = body.len() as u64; 13 store 14 .put(&repo, &oid, ClaimedSize::new(bytes), &mut &body[..]) 15 .expect("put with matching size and oid succeeds"); 16 prop_assert_eq!( 17 store.probe(&repo, &oid).expect("probe"), 18 Some(LfsSize::new(bytes)) 19 ); 20 let mut out = Vec::new(); 21 store 22 .read(&repo, &oid) 23 .expect("stored object opens") 24 .read_to_end(&mut out) 25 .expect("stored object reads"); 26 prop_assert_eq!(out, body); 27 Ok(()) 28} 29 30#[derive(Debug, Clone)] 31enum Tamper { 32 Flip { at: usize, xor: u8 }, 33 Truncate { keep: usize }, 34 Extend { extra: Vec<u8> }, 35} 36 37fn tampered(body: &[u8], tamper: &Tamper) -> Vec<u8> { 38 match tamper { 39 Tamper::Flip { at, xor } => { 40 let mut bytes = body.to_vec(); 41 bytes[at % body.len()] ^= xor; 42 bytes 43 } 44 Tamper::Truncate { keep } => body[..keep % body.len()].to_vec(), 45 Tamper::Extend { extra } => [body, extra].concat(), 46 } 47} 48 49fn tamper_strategy() -> impl Strategy<Value = Tamper> { 50 prop_oneof![ 51 (any::<usize>(), 1u8..).prop_map(|(at, xor)| Tamper::Flip { at, xor }), 52 any::<usize>().prop_map(|keep| Tamper::Truncate { keep }), 53 proptest::collection::vec(any::<u8>(), 1..64).prop_map(|extra| Tamper::Extend { extra }), 54 ] 55} 56 57fn rejects_tampering( 58 store: &dyn LfsStore, 59 body: &[u8], 60 tamper: &Tamper, 61) -> Result<(), TestCaseError> { 62 let repo = repo(); 63 let oid = oid_of(body); 64 let size = ClaimedSize::new(body.len() as u64); 65 let forged = tampered(body, tamper); 66 let verdict = store.put(&repo, &oid, size, &mut &forged[..]); 67 prop_assert!( 68 matches!( 69 verdict, 70 Err(LfsError::HashMismatch { .. } | LfsError::SizeMismatch { .. }) 71 ), 72 "a tampered body must fail the verifier, got {verdict:?}" 73 ); 74 prop_assert_eq!(store.probe(&repo, &oid).expect("probe"), None); 75 Ok(()) 76} 77 78fn disk_store() -> (DiskStore, tempfile::TempDir) { 79 let dir = tempfile::tempdir().expect("tempdir"); 80 let store = DiskStore::open(LfsStorePath::new(dir.path())).expect("store opens"); 81 (store, dir) 82} 83 84proptest! { 85 #![proptest_config(ProptestConfig::with_cases(64))] 86 87 #[test] 88 fn any_body_round_trips_through_both_stores( 89 body in proptest::collection::vec(any::<u8>(), 0..4096) 90 ) { 91 round_trip(&MemoryStore::new(), &body)?; 92 let (store, _dir) = disk_store(); 93 round_trip(&store, &body)?; 94 } 95 96 #[test] 97 fn the_verifier_rejects_any_tampered_or_truncated_body( 98 body in proptest::collection::vec(any::<u8>(), 1..2048), 99 tamper in tamper_strategy(), 100 ) { 101 prop_assume!(!matches!(&tamper, Tamper::Flip { xor: 0, .. })); 102 rejects_tampering(&MemoryStore::new(), &body, &tamper)?; 103 let (store, _dir) = disk_store(); 104 rejects_tampering(&store, &body, &tamper)?; 105 } 106 107 #[test] 108 fn a_pointer_file_round_trips(digest in any::<[u8; 32]>(), size in any::<u64>()) { 109 let oid = LfsOid::from_digest(digest); 110 let text = format!( 111 "version https://git-lfs.github.com/spec/v1\noid sha256:{oid}\nsize {size}\n" 112 ); 113 let parsed = knot_lfs::parse_pointer(text.as_bytes()).expect("a spec pointer parses"); 114 prop_assert_eq!(parsed.oid, oid); 115 prop_assert_eq!(parsed.size, ClaimedSize::new(size)); 116 } 117}