oxedyne/fe2o3/fe2o3_net/tests/range_source.rs
5.0 KiB, 1 run
created by r1870400018:60844, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! A PMTiles archive read over HTTP by range, from a local server holding only the byte ranges |
| 2 | //! the reference `pmtiles` reader read from the Protomaps planet (see |
| 3 | //! `fe2o3_geom/tests/data/protomaps/NOTICE.md`). |
| 4 | |
| 5 | use oxedyne_fe2o3_geom::tile::pmtiles::{ |
| 6 | Archive, |
| 7 | RangeSource, |
| 8 | }; |
| 9 | use oxedyne_fe2o3_net::http::range_source::HttpRangeSource; |
| 10 | |
| 11 | use oxedyne_fe2o3_core::prelude::*; |
| 12 | |
| 13 | use std::{ |
| 14 | io::{ |
| 15 | Read, |
| 16 | Write, |
| 17 | }, |
| 18 | net::TcpListener, |
| 19 | path::PathBuf, |
| 20 | thread, |
| 21 | }; |
| 22 | |
| 23 | fn ranges() -> Outcome<Vec<(u64, Vec<u8>)>> { |
| 24 | let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) |
| 25 | .join("../fe2o3_geom/tests/data/protomaps/ocean_ranges.bin"); |
| 26 | let b = res!(std::fs::read(&path), File, Read); |
| 27 | let mut out = Vec::new(); |
| 28 | let mut i = 0usize; |
| 29 | while i + 12 <= b.len() { |
| 30 | let mut o = [0u8; 8]; |
| 31 | o.copy_from_slice(&b[i..i + 8]); |
| 32 | let n = u32::from_le_bytes([b[i + 8], b[i + 9], b[i + 10], b[i + 11]]) as usize; |
| 33 | out.push((u64::from_le_bytes(o), b[i + 12..i + 12 + n].to_vec())); |
| 34 | i += 12 + n; |
| 35 | } |
| 36 | Ok(out) |
| 37 | } |
| 38 | |
| 39 | /// Serves `n` requests: a `Range` within the known ranges is answered `206` on `/planet`, and |
| 40 | /// `/whole` answers `200` with a body, as a server ignoring `Range` does. |
| 41 | fn serve(n: usize) -> Outcome<u16> { |
| 42 | let data = res!(ranges()); |
| 43 | let listener = res!(TcpListener::bind("127.0.0.1:0"), Network, Init); |
| 44 | let port = res!(listener.local_addr(), Network, Init).port(); |
| 45 | thread::spawn(move || { |
| 46 | for _ in 0..n { |
| 47 | let (mut s, _) = match listener.accept() { |
| 48 | Ok(c) => c, |
| 49 | Err(_) => return, |
| 50 | }; |
| 51 | let mut req = Vec::new(); |
| 52 | let mut buf = [0u8; 1024]; |
| 53 | while !req.windows(4).any(|w| w == b"\r\n\r\n") { |
| 54 | match s.read(&mut buf) { |
| 55 | Ok(0) | Err(_) => break, |
| 56 | Ok(k) => req.extend_from_slice(&buf[..k]), |
| 57 | } |
| 58 | } |
| 59 | let text = String::from_utf8_lossy(&req).to_string(); |
| 60 | let whole = text.starts_with("GET /whole "); |
| 61 | let range = text.lines() |
| 62 | .find_map(|l| l.strip_prefix("Range: bytes=")) |
| 63 | .and_then(|r| r.split_once('-')) |
| 64 | .and_then(|(a, b)| Some((a.trim().parse::<u64>().ok()?, b.trim().parse::<u64>().ok()?))); |
| 65 | let reply = match (whole, range) { |
| 66 | (true, _) => { |
| 67 | let body = vec![7u8; 64]; |
| 68 | let mut r = fmt!("HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n", body.len()).into_bytes(); |
| 69 | r.extend_from_slice(&body); |
| 70 | r |
| 71 | }, |
| 72 | (false, Some((a, b))) => { |
| 73 | let hit = data.iter().find(|(o, d)| a >= *o && b < *o + d.len() as u64); |
| 74 | match hit { |
| 75 | Some((o, d)) => { |
| 76 | let body = &d[(a - o) as usize..=(b - o) as usize]; |
| 77 | let mut r = fmt!("HTTP/1.1 206 Partial Content\r\nContent-Length: {}\r\n\ |
| 78 | Content-Range: bytes {}-{}/*\r\n\r\n", body.len(), a, b).into_bytes(); |
| 79 | r.extend_from_slice(body); |
| 80 | r |
| 81 | }, |
| 82 | None => b"HTTP/1.1 416 Range Not Satisfiable\r\nContent-Length: 0\r\n\r\n".to_vec(), |
| 83 | } |
| 84 | }, |
| 85 | (false, None) => b"HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n".to_vec(), |
| 86 | }; |
| 87 | let _ = s.write_all(&reply); |
| 88 | } |
| 89 | }); |
| 90 | Ok(port) |
| 91 | } |
| 92 | |
| 93 | fn fnv1a64(b: &[u8]) -> String { |
| 94 | let mut h: u64 = 0xcbf29ce484222325; |
| 95 | for x in b { |
| 96 | h ^= *x as u64; |
| 97 | h = h.wrapping_mul(0x100000001b3); |
| 98 | } |
| 99 | fmt!("{:016x}", h) |
| 100 | } |
| 101 | |
| 102 | #[test] |
| 103 | fn test_an_archive_reads_over_http_ranges_00() -> Outcome<()> { |
| 104 | // Header, root, one leaf and one tile for the first; the second's leaf and the shared tile. |
| 105 | let port = res!(serve(8)); |
| 106 | let src = res!(HttpRangeSource::new(&fmt!("http://127.0.0.1:{}/planet", port), None)); |
| 107 | let a = res!(Archive::open(src)); |
| 108 | req!(a.header().max_zoom, 15); |
| 109 | // The two open-ocean tiles, and the reference reader's reading of them. |
| 110 | for (z, x, y) in [(12u8, 2958u32, 2545u32), (15, 5152, 21497)] { |
| 111 | let t = res!(res!(a.tile_decoded(z, x, y)).ok_or_else(|| err!("{}/{}/{} is missing.", z, x, y; Test))); |
| 112 | req!(t.len(), 75); |
| 113 | req!(fnv1a64(&t), "b36d34914f4c291c".to_string(), "{}/{}/{} read other bytes.", z, x, y); |
| 114 | } |
| 115 | Ok(()) |
| 116 | } |
| 117 | |
| 118 | #[test] |
| 119 | fn test_a_server_ignoring_range_is_refused_01() -> Outcome<()> { |
| 120 | let port = res!(serve(1)); |
| 121 | let src = res!(HttpRangeSource::new(&fmt!("http://127.0.0.1:{}/whole", port), None)); |
| 122 | let refused = src.read(0, 16).is_err(); |
| 123 | req!(refused, true, "A 200 with the whole file was read as a range."); |
| 124 | req!(HttpRangeSource::new("ftp://example.com/x", None).is_err(), true, "An ftp URL was taken."); |
| 125 | req!(HttpRangeSource::new("https://example.com/x", None).is_err(), true, |
| 126 | "An https URL was taken with no TLS configuration."); |
| 127 | Ok(()) |
| 128 | } |