oxedyne/fe2o3/fe2o3_net/tests/gzip_oracle.rs
6.5 KiB, 10 runs
created by r1870400018:19670, 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 | #![cfg(feature = "async")] |
| 2 | //! The gzip encoder, checked against `gzip(1)` rather than against itself. |
| 3 | //! |
| 4 | //! A codec tested only against its own decoder agrees with itself, which is |
| 5 | //! something a consistently wrong codec does just as well. The authority on |
| 6 | //! whether a stream really is [RFC 1952] gzip is a program that did not come |
| 7 | //! from this repository and that every client in the world already trusts, so |
| 8 | //! both directions are put to it: what this crate writes, `gunzip` must read, |
| 9 | //! and what `gzip` writes, this crate must read. |
| 10 | //! |
| 11 | //! [RFC 1952]: https://www.rfc-editor.org/rfc/rfc1952 |
| 12 | //! |
| 13 | //! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\ |
| 14 | //! Anthropic Claude |
| 15 | |
| 16 | use oxedyne_fe2o3_core::prelude::*; |
| 17 | use oxedyne_fe2o3_net::http::encoding; |
| 18 | |
| 19 | use std::{ |
| 20 | fs, |
| 21 | path::PathBuf, |
| 22 | process::{ |
| 23 | Command, |
| 24 | Stdio, |
| 25 | }, |
| 26 | }; |
| 27 | |
| 28 | |
| 29 | /// A working directory under the target tree, which Cargo hands to an |
| 30 | /// integration test for exactly this. Never `/tmp`, which on these machines is |
| 31 | /// memory. |
| 32 | fn work_dir(name: &str) -> Outcome<PathBuf> { |
| 33 | let dir = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join(name); |
| 34 | res!(fs::create_dir_all(&dir)); |
| 35 | Ok(dir) |
| 36 | } |
| 37 | |
| 38 | /// Prose with the redundancy real markup has, so a ratio means something. |
| 39 | fn sample() -> Vec<u8> { |
| 40 | let mut out = String::new(); |
| 41 | for i in 0..400 { |
| 42 | out.push_str(&fmt!( |
| 43 | "<p class=\"line\" data-index=\"{}\">the quick brown fox jumps over \ |
| 44 | the lazy dog</p>\n", i)); |
| 45 | } |
| 46 | out.into_bytes() |
| 47 | } |
| 48 | |
| 49 | /// Is the external tool available at all? A missing oracle must be loud, not a |
| 50 | /// quietly passing test. |
| 51 | fn require(tool: &str) -> Outcome<()> { |
| 52 | let found = match Command::new("which").arg(tool).stdout(Stdio::null()).status() { |
| 53 | Ok(st) => st.success(), |
| 54 | Err(_) => false, |
| 55 | }; |
| 56 | if !found { |
| 57 | return Err(err!( |
| 58 | "The external oracle '{}' is not on PATH, so this test cannot check \ |
| 59 | anything it claims to check.", tool; |
| 60 | Missing, Init)); |
| 61 | } |
| 62 | Ok(()) |
| 63 | } |
| 64 | |
| 65 | /// What this crate writes, `gunzip` reads back byte for byte. |
| 66 | #[test] |
| 67 | fn gunzip_reads_what_this_crate_wrote() -> Outcome<()> { |
| 68 | res!(require("gunzip")); |
| 69 | let plain = sample(); |
| 70 | let encoded = res!(encoding::gzip(&plain)); |
| 71 | |
| 72 | let dir = res!(work_dir("gzip_oracle_out")); |
| 73 | let path = dir.join("body.gz"); |
| 74 | let _ = fs::remove_file(&path); |
| 75 | let _ = fs::remove_file(dir.join("body")); |
| 76 | res!(fs::write(&path, &encoded)); |
| 77 | |
| 78 | let out = res!(Command::new("gunzip").arg("-c").arg(&path).output()); |
| 79 | if !out.status.success() { |
| 80 | return Err(err!( |
| 81 | "gunzip refused the stream this crate produced: {}", |
| 82 | String::from_utf8_lossy(&out.stderr); |
| 83 | Invalid, Data)); |
| 84 | } |
| 85 | assert_eq!(out.stdout, plain, |
| 86 | "gunzip read back something other than what was encoded"); |
| 87 | // And the saving is the point of the exercise. |
| 88 | assert!(encoded.len() * 4 < plain.len(), |
| 89 | "{} bytes of markup encoded to {}, which is not worth doing", |
| 90 | plain.len(), encoded.len()); |
| 91 | Ok(()) |
| 92 | } |
| 93 | |
| 94 | /// What `gzip` writes, this crate reads back byte for byte. |
| 95 | #[test] |
| 96 | fn this_crate_reads_what_gzip_wrote() -> Outcome<()> { |
| 97 | res!(require("gzip")); |
| 98 | let plain = sample(); |
| 99 | |
| 100 | let dir = res!(work_dir("gzip_oracle_in")); |
| 101 | let path = dir.join("body"); |
| 102 | let gz_path = dir.join("body.gz"); |
| 103 | let _ = fs::remove_file(&path); |
| 104 | let _ = fs::remove_file(&gz_path); |
| 105 | res!(fs::write(&path, &plain)); |
| 106 | |
| 107 | let out = res!(Command::new("gzip").arg("-9").arg("-k").arg("-f").arg(&path).output()); |
| 108 | if !out.status.success() { |
| 109 | return Err(err!("gzip failed: {}", String::from_utf8_lossy(&out.stderr); |
| 110 | IO, Data)); |
| 111 | } |
| 112 | let encoded = res!(fs::read(&gz_path)); |
| 113 | assert_eq!(res!(encoding::gunzip(&encoded)), plain, |
| 114 | "a member written by gzip(1) did not read back as what went into it"); |
| 115 | Ok(()) |
| 116 | } |
| 117 | |
| 118 | /// The case that matters: the framing fields must describe the encoded body, |
| 119 | /// and the encoded body must be a stream a client can actually read. |
| 120 | #[test] |
| 121 | fn an_encoded_response_carries_a_stream_gunzip_accepts() -> Outcome<()> { |
| 122 | use oxedyne_fe2o3_net::http::{ |
| 123 | fields::{ |
| 124 | HeaderFields, |
| 125 | HeaderFieldValue, |
| 126 | HeaderName, |
| 127 | }, |
| 128 | msg::HttpMessage, |
| 129 | status::HttpStatus, |
| 130 | }; |
| 131 | |
| 132 | res!(require("gunzip")); |
| 133 | let plain = sample(); |
| 134 | |
| 135 | let mut req = HeaderFields::default(); |
| 136 | req.insert( |
| 137 | HeaderName::AcceptEncoding, |
| 138 | res!(HeaderFieldValue::new(&HeaderName::AcceptEncoding, "gzip, deflate, br")), |
| 139 | None, |
| 140 | ); |
| 141 | let coding = encoding::choose( |
| 142 | &req, |
| 143 | "text/html; charset=utf-8", |
| 144 | plain.len(), |
| 145 | encoding::MIN_BYTES_DEFAULT, |
| 146 | ); |
| 147 | assert_eq!(coding, encoding::ContentCoding::Gzip); |
| 148 | |
| 149 | let resp = HttpMessage::new_response(HttpStatus::OK) |
| 150 | .with_field( |
| 151 | HeaderName::ContentType, |
| 152 | res!(HeaderFieldValue::new(&HeaderName::ContentType, "text/html; charset=utf-8"))) |
| 153 | .with_field( |
| 154 | HeaderName::ETag, |
| 155 | res!(HeaderFieldValue::new(&HeaderName::ETag, "\"68a1-3b\""))) |
| 156 | .with_body(plain.clone()); |
| 157 | |
| 158 | let rt = res!(tokio::runtime::Runtime::new()); |
| 159 | let resp = res!(rt.block_on(encoding::encode(resp, coding))); |
| 160 | |
| 161 | let held = res!(resp.header.fields.get_one(&HeaderName::ContentEncoding).ok_or_else(|| |
| 162 | err!("The encoded response did not say so."; Missing))); |
| 163 | assert_eq!(fmt!("{}", held), "gzip"); |
| 164 | let vary = res!(resp.header.fields.get_one(&HeaderName::Vary).ok_or_else(|| |
| 165 | err!("The encoded response did not vary by the coding."; Missing))); |
| 166 | assert_eq!(fmt!("{}", vary).to_ascii_lowercase(), "accept-encoding"); |
| 167 | let etag = res!(resp.header.fields.get_one(&HeaderName::ETag).ok_or_else(|| |
| 168 | err!("The encoded response lost its validator."; Missing))); |
| 169 | assert_eq!(fmt!("{}", etag), "\"68a1-3b-gzip\""); |
| 170 | // `Content-Length` is taken from the body when the message is written, so |
| 171 | // this is the number that will go on the wire. |
| 172 | assert_eq!(resp.body_len(), resp.body.len()); |
| 173 | |
| 174 | let dir = res!(work_dir("gzip_oracle_resp")); |
| 175 | let path = dir.join("resp.gz"); |
| 176 | let _ = fs::remove_file(&path); |
| 177 | res!(fs::write(&path, &resp.body)); |
| 178 | let out = res!(Command::new("gunzip").arg("-c").arg(&path).output()); |
| 179 | if !out.status.success() { |
| 180 | return Err(err!( |
| 181 | "gunzip refused the body of an encoded response: {}", |
| 182 | String::from_utf8_lossy(&out.stderr); |
| 183 | Invalid, Data)); |
| 184 | } |
| 185 | assert_eq!(out.stdout, plain); |
| 186 | Ok(()) |
| 187 | } |