max / synckit
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
1 file changed,
+58 insertions,
-0 deletions
| @@ -1883,6 +1883,64 @@ | |||
| 1883 | 1883 | assert!(decrypt_blob_chunked(&buf, &key, "hash").is_err()); | |
| 1884 | 1884 | } | |
| 1885 | 1885 | ||
| 1886 | + | /// The header-length guard is an exact bound: one byte short of the header | |
| 1887 | + | /// must fail, and exactly the header (or more) must parse. | |
| 1888 | + | #[test] | |
| 1889 | + | fn v3_header_length_bound_is_exact() { | |
| 1890 | + | fn body_of(len: usize) -> Vec<u8> { | |
| 1891 | + | let mut buf = Vec::from(WIRE_V3_TAG_BYTES); | |
| 1892 | + | let mut body = vec![0u8; len]; | |
| 1893 | + | if !body.is_empty() { | |
| 1894 | + | body[0] = 3; | |
| 1895 | + | } | |
| 1896 | + | // chunk_size must be nonzero for the header to be accepted. | |
| 1897 | + | if len >= 5 { | |
| 1898 | + | body[1..5].copy_from_slice(&(BLOB_CHUNK_SIZE as u32).to_le_bytes()); | |
| 1899 | + | } | |
| 1900 | + | buf.extend_from_slice(&body); | |
| 1901 | + | buf | |
| 1902 | + | } | |
| 1903 | + | ||
| 1904 | + | let short = body_of(BLOB_V3_HEADER_LEN - 1); | |
| 1905 | + | assert!( | |
| 1906 | + | parse_blob_header(&short).is_err(), | |
| 1907 | + | "a body one byte short of the header must be rejected" | |
| 1908 | + | ); | |
| 1909 | + | ||
| 1910 | + | let exact = body_of(BLOB_V3_HEADER_LEN); | |
| 1911 | + | let (header, consumed) = parse_blob_header(&exact).expect("exact header parses"); | |
| 1912 | + | assert_eq!(consumed, WIRE_V3_TAG_BYTES.len() + BLOB_V3_HEADER_LEN); | |
| 1913 | + | assert_eq!(header.chunk_size, BLOB_CHUNK_SIZE); | |
| 1914 | + | assert_eq!(header.total_len, 0); | |
| 1915 | + | ||
| 1916 | + | let long = body_of(BLOB_V3_HEADER_LEN + 1); | |
| 1917 | + | let (header, consumed) = | |
| 1918 | + | parse_blob_header(&long).expect("header plus a stream byte parses"); | |
| 1919 | + | assert_eq!( | |
| 1920 | + | consumed, | |
| 1921 | + | WIRE_V3_TAG_BYTES.len() + BLOB_V3_HEADER_LEN, | |
| 1922 | + | "the trailing byte belongs to the chunk stream, not the header" | |
| 1923 | + | ); | |
| 1924 | + | assert_eq!(header.chunk_size, BLOB_CHUNK_SIZE); | |
| 1925 | + | } | |
| 1926 | + | ||
| 1927 | + | /// Losing the last byte of the final sealed chunk is a clean error, not a | |
| 1928 | + | /// slice panic. | |
| 1929 | + | #[test] | |
| 1930 | + | fn v3_blob_truncated_one_byte_into_final_chunk_errs() { | |
| 1931 | + | let key = generate_master_key(); | |
| 1932 | + | let plaintext = vec![7u8; 4096]; | |
| 1933 | + | let sealed = encrypt_blob_chunked(&plaintext, &key, "hash").unwrap(); | |
| 1934 | + | let truncated = &sealed[..sealed.len() - 1]; | |
| 1935 | + | assert!( | |
| 1936 | + | matches!( | |
| 1937 | + | decrypt_blob_chunked(truncated, &key, "hash"), | |
| 1938 | + | Err(SyncKitError::Crypto(_)) | |
| 1939 | + | ), | |
| 1940 | + | "a one-byte truncation must report a truncated blob" | |
| 1941 | + | ); | |
| 1942 | + | } | |
| 1943 | + | ||
| 1886 | 1944 | #[test] | |
| 1887 | 1945 | fn v2_data_is_tagged_and_roundtrips() { | |
| 1888 | 1946 | let key = generate_master_key(); |