diff options
| author | Jesper Jensen <jesper@jnsn.dev> | 2026-01-19 21:05:38 +0100 |
|---|---|---|
| committer | Jesper Jensen <jesper@jnsn.dev> | 2026-01-19 21:05:38 +0100 |
| commit | ef21d01f08dc94ba355f596ddb23e1e32c43c8d4 (patch) | |
| tree | 6044255f32f894ea52c28c63e7a56d347386b618 /src | |
| parent | 42e48a4704242b1f63092902b6b40e254c64cad7 (diff) | |
INITIAL COMMIT (again)
Diffstat (limited to 'src')
| -rw-r--r-- | src/main.rs | 556 | ||||
| -rw-r--r-- | src/parser.rs | 1474 |
2 files changed, 801 insertions, 1229 deletions
diff --git a/src/main.rs b/src/main.rs index dd0041c..8933c96 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,11 +2,15 @@ mod parser; use crate::parser::*; use base64::prelude::*; +use rand::distr::{Alphanumeric, SampleString}; use yaml_rust2::parser::Parser; use yaml_rust2::Event; use std::collections::HashMap; use std::ops::Range; use std::sync; +use std::io::Read; +use std::io::Seek; +use std::io::Write; fn help(cmd: &str) { println!( @@ -28,29 +32,17 @@ enum AuthMethod { impl AuthMethod { fn from_header(header: &ureq::http::HeaderValue) -> Option<Self> { let header_str = header.to_str().unwrap(); - let (parse, _) = parse_challenge(&header_str.chars().collect::<Vec<char>>(), 0).unwrap(); + let parse = parse_authenticate_header(header_str).unwrap(); if &header_str[parse.scheme.clone()] == "Basic" { return Some(AuthMethod::Basic); } if &header_str[parse.scheme.clone()] == "Bearer" { - let mut realm_param = None; - let mut scope_param = None; - let mut service_param = None; - for param in parse.params { - match &header_str[param.key.clone()] { - "realm" => realm_param = Some(param), - "scope" => scope_param = Some(param), - "service" => service_param = Some(param), - _ => {}, - } - } - return Some(AuthMethod::Bearer{ - realm: header_str[realm_param.unwrap().value].to_string(), - scope: header_str[scope_param.unwrap().value].to_string(), - service: header_str[service_param.unwrap().value].to_string(), + realm: header_str[parse.realm.unwrap()].to_string(), + scope: header_str[parse.scope.unwrap()].to_string(), + service: header_str[parse.service.unwrap()].to_string(), }); } @@ -66,7 +58,6 @@ struct AuthInfo { enum AuthStage { Idle, - Unauthorized, Authorized(String), } @@ -104,60 +95,56 @@ impl AuthState{ return req } - fn attempt_authorization(&mut self, response: &ureq::http::Response<ureq::Body>) -> Result<(), ()> { - if let AuthStage::Idle = self.stage { - if let Some(auth_header) = response.headers().get("www-authenticate") { - let auth_header = AuthMethod::from_header(auth_header); - match auth_header { - Some(AuthMethod::Basic) => { - let basic_auth = format!("Basic {}", BASE64_STANDARD.encode(format!("{}:{}", self.info.username, self.info.password))); - self.stage = AuthStage::Authorized(basic_auth); - - return Ok(()); - }, - Some(AuthMethod::Bearer{realm, scope, service}) => { - let basic_auth = format!("Basic {}", BASE64_STANDARD.encode(format!("{}:{}", self.info.username, self.info.password))); + fn authenticate(&mut self, response: &ureq::http::Response<ureq::Body>) -> Result<(), String> { + match self.stage { + AuthStage::Authorized(_) => + // The token must have expired + self.stage = AuthStage::Idle, + AuthStage::Idle => {}, + } - let url = format!("{}?service={}&scope={}", realm, service, scope); + let auth_header = response.headers().get("www-authenticate").unwrap(); + match AuthMethod::from_header(auth_header) { + Some(AuthMethod::Basic) => { + let basic_auth = format!("Basic {}", BASE64_STANDARD.encode(format!("{}:{}", self.info.username, self.info.password))); + self.stage = AuthStage::Authorized(basic_auth); - let body = ureq::get(url) - .config().http_status_as_error(false).build() - .header("Authorization", basic_auth) - .call(); + return Ok(()); + }, + Some(AuthMethod::Bearer{realm, scope, service}) => { + let basic_auth = format!("Basic {}", BASE64_STANDARD.encode(format!("{}:{}", self.info.username, self.info.password))); - dbg!(&body); + let url = format!("{}?service={}&scope={}", realm, service, scope); - let body = body.unwrap() - .body_mut().read_to_string().unwrap(); + let body = ureq::get(url) + .config().http_status_as_error(false).build() + .header("Authorization", basic_auth) + .call(); - dbg!(&body); + let body = body.map_err(|x| format!("Server {} authentication request failed: {}", self.info.host, x.to_string()))? + .body_mut().read_to_string().expect(format!("Server {} responded with something non-string like", self.info.host).as_str()); - let body: tinyjson::JsonValue = body - .parse().unwrap(); + let body = body.parse::<tinyjson::JsonValue>() + .unwrap(); - let token: &String = body["token"].get().unwrap(); + let token = body["token"].get::<String>().unwrap(); + self.stage = AuthStage::Authorized(format!("Bearer {}", token)); + return Ok(()); - self.stage = AuthStage::Authorized(format!("Bearer {}", token)); - return Ok(()); - }, - None => todo!("Failed parsing the challenge header"), - } - } else { - todo!("Server didn't ask us to authenticate"); - } - } else { - todo!("Authorized request somehow failed (expired token?)"); + }, + None => Err(format!("Server {} provided us with a challenge, but we didn't understand it", self.info.host)), } } } -fn perform_registry_request(registry: &str, url: &str, auth: &mut Auth) -> Result<ureq::http::Response<ureq::Body>, ()> { +fn perform_registry_request(registry: &str, url: &str, accept: &'static str, auth: &mut Auth) -> Result<ureq::http::Response<ureq::Body>, String> { let mut state = auth.states.get_mut(registry); let url = format!("https://{}{}", registry, &url); for _ in 0..2 { let mut request = ureq::get(&url) + .header("Accept", accept) .config().http_status_as_error(false).build(); if let Some(ref state) = state { @@ -168,17 +155,19 @@ fn perform_registry_request(registry: &str, url: &str, auth: &mut Auth) -> Resul if response.status() == 401 { if let Some(ref mut state) = state { - state.attempt_authorization(&response)?; + state.authenticate(&response)?; continue; } else { - todo!("Server returned 401 but we have no credentials"); + return Err(format!("Server {} returned 401 but we have no credentials", registry)); } + } else if response.status() != 200 { + return Err(format!("Unexpected status code: {}", response.status())) } return Ok(response); } - return Err(()); + return Err("Authorization failed".to_string()); } enum YContext { @@ -189,94 +178,70 @@ enum YContext { } #[derive(Debug)] -struct Chunk { - position: Range<usize>, -} - -fn scan_yaml_for_images<T: Iterator<Item = char>>(mut yaml: Parser<T>) -> Vec<Chunk> { - let mut images = vec!(); - - let mut scope = vec!(); - - loop { - let (ev, mark) = yaml.next_token().unwrap(); - match ev { - Event::StreamStart => {} - Event::StreamEnd => { break; } - Event::DocumentStart => { scope.push(YContext::InDocument); } - Event::DocumentEnd => { - assert!(matches!(scope.pop().unwrap(), YContext::InDocument)); - scope.pop_if(|x| matches!(x, YContext::InValue(_))); - }, - Event::MappingStart(_, _) => { scope.push(YContext::InObject); }, - Event::MappingEnd => { - assert!(matches!(scope.pop().unwrap(), YContext::InObject)); - scope.pop_if(|x| matches!(x, YContext::InValue(_))); - }, - Event::SequenceStart(_, _) => { scope.push(YContext::InSequence); }, - Event::SequenceEnd => { - assert!(matches!(scope.pop().unwrap(), YContext::InSequence)); - scope.pop_if(|x| matches!(x, YContext::InValue(_))); - }, - - Event::Scalar(ref txt, _, _, _) => { - let parent = scope.last().unwrap(); - match parent { - YContext::InObject => { - // We are the key of a mapping, which means the next even is the value - scope.push(YContext::InValue(txt == "image")); - }, - YContext::InSequence => {}, - YContext::InValue(img) => { - if *img { - let next_idx = images.len(); - images.push(Chunk{ - position: mark.index()..mark.index() + txt.len(), - }); - } - scope.pop(); - }, - - _ => panic!(), - } - }, - x => todo!("{:?}", x), - } - } - - return images; -} - -pub trait SubsliceOffset<T> { - fn subslice_range(&self, inner: &Self) -> Option<std::ops::Range<usize>>; +struct ManifestFile { + image_tags: Vec<Range<usize>>, } -impl<T> SubsliceOffset<T> for [T] { - fn subslice_range(&self, subslice: &[T]) -> Option<std::ops::Range<usize>> { - if size_of::<T>() == 0 { - panic!("elements are zero-sized"); - } - - let self_start = self.as_ptr().addr(); - let subslice_start = subslice.as_ptr().addr(); +impl ManifestFile { + fn parse(content: &str) -> Self { + let mut yaml = Parser::new_from_str(content); - let byte_start = subslice_start.wrapping_sub(self_start); + let mut images = vec!(); + let mut scope = vec!(); - if !byte_start.is_multiple_of(size_of::<T>()) { - return None; + loop { + let (ev, mark) = yaml.next_token().unwrap(); + match ev { + Event::StreamStart => {} + Event::StreamEnd => { break; } + Event::DocumentStart => { scope.push(YContext::InDocument); } + Event::DocumentEnd => { + assert!(matches!(scope.pop().unwrap(), YContext::InDocument)); + scope.pop_if(|x| matches!(x, YContext::InValue(_))); + }, + Event::MappingStart(_, _) => { scope.push(YContext::InObject); }, + Event::MappingEnd => { + assert!(matches!(scope.pop().unwrap(), YContext::InObject)); + scope.pop_if(|x| matches!(x, YContext::InValue(_))); + }, + Event::SequenceStart(_, _) => { scope.push(YContext::InSequence); }, + Event::SequenceEnd => { + assert!(matches!(scope.pop().unwrap(), YContext::InSequence)); + scope.pop_if(|x| matches!(x, YContext::InValue(_))); + }, + + Event::Scalar(ref txt, _, _, _) => { + let parent = scope.last().unwrap(); + match parent { + YContext::InObject => { + // We are the key of a mapping, which means the next even is the value + scope.push(YContext::InValue(txt == "image")); + }, + YContext::InSequence => {}, + YContext::InValue(img) => { + if *img { + images.push(mark.index()..mark.index() + txt.len()); + } + scope.pop(); + }, + // This should only happen for entirely empty documents + YContext::InDocument => assert!(txt == ""), + + _ => panic!(), + } + }, + x => todo!("{:?}", x), + } } - let start = byte_start / size_of::<T>(); - let end = start.wrapping_add(subslice.len()); - - if start <= self.len() && end <= self.len() { Some(start..end) } else { None } + return Self{ + image_tags: images, + }; } } #[derive(Debug, Clone)] struct DockerRef { - full_range: Range<usize>, - registry: Option<Range<usize>>, image: Range<usize>, tag: Option<Range<usize>>, @@ -284,25 +249,25 @@ struct DockerRef { } impl DockerRef{ - fn parse(file: &str, chunk: &Chunk) -> DockerRef { - let mut string_range = chunk.position.clone(); + fn parse(content: &str, chunk: &Range<usize>) -> DockerRef { + let mut string_range = chunk.clone(); let mut digest = None; - if let Some(idx) = file[string_range.clone()].rfind("@") { + if let Some(idx) = content[string_range.clone()].rfind("@") { digest = Some(string_range.start+idx+1..string_range.end); string_range.end = string_range.start+idx; } let mut tag = None; - if let Some(idx) = file[string_range.clone()].rfind(":") { + if let Some(idx) = content[string_range.clone()].rfind(":") { tag = Some(string_range.start+idx+1..string_range.end); string_range.end = string_range.start+idx; } let mut registry = None; let image; - if let Some(idx) = file[string_range.clone()].find("/") { - let head = &file[string_range.clone()][..idx]; + if let Some(idx) = content[string_range.clone()].find("/") { + let head = &content[string_range.clone()][..idx]; if head.contains(":") || head.contains(".") { registry = Some(string_range.start..string_range.start+idx); image = string_range.start+idx+1..string_range.end; @@ -315,8 +280,6 @@ impl DockerRef{ } return DockerRef { - full_range: chunk.position.clone(), - registry, image, tag, @@ -326,16 +289,21 @@ impl DockerRef{ } #[derive(Debug)] -struct Update { +struct FilePatch { position: Range<usize>, content: String, } #[derive(Debug)] -struct Version { - // It always starts with a string part, so the first string can be empty - string_parts: Vec<String>, - number_parts: Vec<u64>, +enum VersionPart { + String(String), + Number(u64), + Hash, +} + +#[derive(Debug)] +struct VersionPattern { + parts: Vec<VersionPart>, } #[derive(Debug)] @@ -347,41 +315,44 @@ enum CompareOutcome { Identical, } -impl Version { +impl VersionPattern { fn parse(tag: &str) -> Self { - static RE: sync::LazyLock<regex::Regex> = sync::LazyLock::new(|| regex::Regex::new(r"(?<str>[^0-9]*)(?<num>[0-9]+)").unwrap()); - let mut string_parts = vec!(); - let mut number_parts = vec!(); + static RE: sync::LazyLock<regex::Regex> = sync::LazyLock::new(|| regex::Regex::new(r"(?<hash>[a-f0-9]{32})|(?<str>[^0-9]+)|(?<num>[0-9]+)").unwrap()); + let mut parts = vec![]; for it in RE.captures_iter(tag) { - string_parts.push(it.name("str").unwrap().as_str().to_string()); - number_parts.push(it.name("num").unwrap().as_str().to_string().parse().unwrap()); + if let Some(x) = it.name("str") { + parts.push(VersionPart::String(x.as_str().to_string())); + } else if let Some(x) = it.name("num") { + parts.push(VersionPart::Number(x.as_str().parse().unwrap())); + } else if let Some(_) = it.name("hash") { + parts.push(VersionPart::Hash); + } } - return Version { - string_parts, - number_parts, + return VersionPattern { + parts, } } fn compare(&self, other: &Self) -> CompareOutcome { - assert!(self.number_parts.len() == self.string_parts.len()); - assert!(other.number_parts.len() == other.string_parts.len()); - - if self.number_parts.len() != other.number_parts.len() { + if self.parts.len() != other.parts.len() { return CompareOutcome::Incompatible; } - for (self_string, other_string) in self.string_parts.iter().zip(other.string_parts.iter()) { - if self_string != other_string { - return CompareOutcome::Incompatible; - } - } - - for (self_number, other_number) in self.number_parts.iter().zip(other.number_parts.iter()) { - if self_number > other_number { - return CompareOutcome::Lower; - } else if self_number < other_number { - return CompareOutcome::Higher; + for (self_part, other_part) in self.parts.iter().zip(other.parts.iter()) { + match (self_part, other_part) { + (VersionPart::String(x1), VersionPart::String(x2)) => if x1 != x2 { return CompareOutcome::Incompatible }, + (VersionPart::String(_), _) => return CompareOutcome::Incompatible, + (VersionPart::Number(x1), VersionPart::Number(x2)) => { + if x1 > x2 { + return CompareOutcome::Lower + } else if x1 > x2 { + return CompareOutcome::Higher + } + }, + (VersionPart::Number(_), _) => return CompareOutcome::Incompatible, + (VersionPart::Hash, VersionPart::Hash) => {}, + (VersionPart::Hash, _) => return CompareOutcome::Incompatible, } } @@ -389,31 +360,33 @@ impl Version { } } -fn fetch_new_image(file: &str, auth: &mut Auth, img: DockerRef, edits: &mut Vec<Update>) { - let registry = img.registry.map(|x| &file[x]).unwrap_or("registry.jnsn.dev/"); - let mut tag = img.tag.map(|x| file[x].to_string()); - - let mut digest = None; - +fn update_images(file: &str, auth: &mut Auth, img: DockerRef, edits: &mut Vec<FilePatch>) -> Result<(), String> { + let registry = img.registry.map(|x| &file[x]).unwrap_or("registry.hub.docker.com"); + let mut tag = img.tag.as_ref().map(|x| file[x.clone()].to_string()); if let Some(ref tag_str) = tag { - let mut current = Version::parse(&tag_str); + let mut current = VersionPattern::parse(&tag_str); + + let mut new_tag = None; let mut url = format!("/v2/{}/tags/list", &file[img.image.clone()]); loop { - let mut response = perform_registry_request(registry, &url, auth).unwrap(); + let mut response = perform_registry_request(registry, &url, "application/vnd.oci.image.index.v1+json", auth).unwrap(); - let link_str = response.headers()["link"].to_str().unwrap(); - let (link, _) = parse_link(&link_str.chars().collect(), 0).unwrap(); - dbg!(&link_str[link.get(0).unwrap().params.get(0).unwrap().value().clone()]); + let content_type = response.headers()["Content-Type"].to_str().unwrap(); + if !content_type.starts_with("application/json") { + return Err(format!("Unexpected Content-Type: {}", content_type)); + } + + let body = response.body_mut().read_to_string().unwrap(); + let body = body.parse::<tinyjson::JsonValue>().unwrap(); - let body: tinyjson::JsonValue = response.body_mut().read_to_string().unwrap().parse().unwrap(); for it in body["tags"].get::<Vec<tinyjson::JsonValue>>().unwrap().iter() { let candidate_str = it.get::<String>().unwrap(); - let candidate = Version::parse(candidate_str); + let candidate = VersionPattern::parse(candidate_str); match current.compare(&candidate) { CompareOutcome::Higher => { - tag = Some(candidate_str.clone()); + new_tag = Some(candidate_str.clone()); current = candidate; }, CompareOutcome::Lower => {}, @@ -421,33 +394,113 @@ fn fetch_new_image(file: &str, auth: &mut Auth, img: DockerRef, edits: &mut Vec< CompareOutcome::Identical => {}, } } - dbg!(&tag); + + if let Some(link_header) = response.headers().get("link") { + let link_str = link_header.to_str().unwrap(); + let link = extract_next_page(&link_str).unwrap(); + url = link_str[link.next_uri.unwrap().clone()].to_string(); + } else { + break; + } + } + + if let Some(new_tag) = new_tag { + tag = Some(new_tag.clone()); + edits.push(FilePatch { + position: img.tag.unwrap(), + content: new_tag, + }); } } - if img.digest.is_some() { + if let Some(ref digest) = img.digest { // Find the digest for the selected tag - let url = format!("/v2/{}/manifests/{}", &file[img.image], tag.unwrap_or("latest".to_string())); - let mut response = perform_registry_request(registry, &url, auth).unwrap(); + let url = format!("/v2/{}/manifests/{}", &file[img.image.clone()], tag.as_deref().unwrap_or("latest")); + let response = perform_registry_request(registry, &url, "application/vnd.oci.image.manifest.v1+json,application/vnd.oci.image.index.v1+json", auth).unwrap(); - let body: tinyjson::JsonValue = response.body_mut().read_to_string().unwrap().parse().unwrap(); - dbg!(&body); + let digest_string = response.headers()["docker-content-digest"].to_str().unwrap().to_string(); - // let media_type : &String = body["mediaType"].get().unwrap(); - // assert!(media_type == "application/vnd.docker.distribution.manifest.v2+json"); + if file[digest.clone()] != digest_string { + edits.push(FilePatch{ + position: img.digest.unwrap(), + content: digest_string, + }); + } + } + + return Ok(()); +} + +enum Output { + Overlay(std::fs::File, std::path::PathBuf, std::path::PathBuf), + Stdout(std::io::Stdout), +} - digest = Some(response.headers()["docker-content-digest"].to_str().unwrap().to_string()); +impl Output { + fn overlay_file(infile_path: &std::path::Path) -> Self { + loop { + let mut filename = std::ffi::OsString::new(); + filename.push(infile_path.file_name().unwrap()); + filename.push(std::ffi::OsStr::new(".edit")); + filename.push(Alphanumeric.sample_string(&mut rand::rng(), 16)); + let outfile_path = Some(infile_path.parent().unwrap().join(filename)); + if let Ok(output_file) = std::fs::File::create_new(outfile_path.as_ref().unwrap()) { + return Output::Overlay(output_file, outfile_path.unwrap().into(), infile_path.into()); + } + } } - let image_ref = { - let prefix = &file[img.full_range.start..img.digest.unwrap().start]; - format!("{}{}", prefix, digest.unwrap_or("".to_string())) - }; + fn commit(&mut self) { + match self { + Output::Overlay(_, outfile_path, infile_path) => { + std::fs::remove_file(&infile_path).unwrap(); + std::fs::rename(&outfile_path, &infile_path).unwrap(); + } + Output::Stdout(_) => {} + } + } +} - edits.push(Update{ - position: img.full_range, - content: image_ref, - }); +impl std::ops::Deref for Output { + type Target = dyn Write; + + fn deref(&self) -> &Self::Target { + match self { + Output::Overlay(file, _, _) => file, + Output::Stdout(stdout) => stdout, + } + } +} + +impl std::ops::DerefMut for Output { + fn deref_mut(&mut self) -> &mut Self::Target { + match self { + Output::Overlay(file, _, _) => file, + Output::Stdout(stdout) => stdout, + } + } +} + +impl std::io::Write for Output { + fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { + return (**self).write(buf); + } + + fn flush(&mut self) -> std::io::Result<()> { + return (**self).flush(); + } + + fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result<usize> { + return (**self).write_vectored(bufs); + } + + fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> { + return (**self).write_all(buf); + } + + fn write_fmt(&mut self, args: std::fmt::Arguments<'_>) -> std::io::Result<()> { + return (**self).write_fmt(args); + } } fn main() { @@ -457,6 +510,7 @@ fn main() { let mut auths = vec![]; let mut positional: Vec<&str> = vec!(); + let mut overwrite = false; loop { match it.next().map(|x| x.as_str()) { @@ -474,6 +528,9 @@ fn main() { std::process::exit(1); } }, + Some("-i") | Some("--inplace") => { + overwrite = true; + }, Some("-h") | Some("--help") => { help(cmd); std::process::exit(0); @@ -486,28 +543,89 @@ fn main() { if positional.len() != 1 { panic!("Bad arguments"); } - let file = positional[0]; - + let infile_path = std::path::PathBuf::from(positional[0]); let mut auth = Auth::new(auths); - let file_content = &std::fs::read_to_string(file).unwrap(); - let yaml = Parser::new_from_str(&file_content); - let images : Vec<_> = scan_yaml_for_images(yaml); + let inpaths = if infile_path.is_dir() { + let mut unsearched = vec![infile_path.clone()]; + let mut paths = vec![]; - let mut edits = vec![]; + while let Some(next) = unsearched.pop() { + for child in next.read_dir().unwrap() { + let child = child.unwrap(); + let path = child.path(); - let images : Vec<_> = images.iter() - .map(|x| DockerRef::parse(&file_content, x)) - .map(|x| fetch_new_image(&file_content, &mut auth, x, &mut edits)) - .collect(); + let ft = child.file_type().unwrap(); + if ft.is_dir() { + unsearched.push(path); + continue; + } - dbg!(&images); - dbg!(&edits); - // dbg!(&file_content[images[0].digest.as_ref().unwrap().clone()]); + if let Some(ext) = path.extension() { + if ext == "yaml" { + paths.push(path); + } + } + } + } + + paths + } else { + vec![infile_path] + }; + + for infile_path in inpaths { + let mut file = std::fs::File::open(&infile_path).unwrap(); + let mut file_content = String::new(); + file.read_to_string(&mut file_content).unwrap(); - // let body: String = auth.apply(ureq::get("https://registry.jnsn.dev/v2/autobrr/tags/list")) - // .call().unwrap() - // .body_mut() - // .read_to_string().unwrap(); - // dbg!(body); + let images = ManifestFile::parse(&file_content); + + let mut failed = None; + + let mut edits = vec![]; + for ref image in images.image_tags { + let image_ref = DockerRef::parse(&file_content, image); + if let Err(msg) = update_images(&file_content, &mut auth, image_ref, &mut edits) { + failed = Some(msg); + break; + } + } + + if let Some(msg) = failed { + println!("{}: {} ", infile_path.display(), msg); + continue; + } + + let mut out = if overwrite { + Output::overlay_file(&infile_path) + } else { + Output::Stdout(std::io::stdout()) + }; + + let mut current_position = 0; + file.seek(std::io::SeekFrom::Start(0)).unwrap(); + let mut file_block = file.take(0); + for edit in edits { + if edit.position.start > current_position { + file_block.set_limit((edit.position.start - current_position) as u64); + std::io::copy(&mut file_block, &mut out).unwrap(); + } + + out.write_all(edit.content.as_bytes()).unwrap(); + + // We should have been able to just seek to the position.end here, but that doesn't work + // for whatever reason. What we can do is calculate the amount to skip ahead, and then do + // that. + let skip = (edit.position.end - edit.position.start) as i64; + file_block.set_limit(skip as u64); + file_block.seek(std::io::SeekFrom::Current(skip)).unwrap(); + current_position = edit.position.end; + } + + file_block.set_limit(u64::MAX); + std::io::copy(&mut file_block, &mut out).unwrap(); + + out.commit(); + } } diff --git a/src/parser.rs b/src/parser.rs index 1f028a0..38fa3fe 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3,11 +3,32 @@ use std::ops::Range; #[derive(Debug)] pub struct Challenge { pub scheme: Range<usize>, - pub params: Vec<AuthParam>, + pub realm: Option<Range<usize>>, + pub scope: Option<Range<usize>>, + pub service: Option<Range<usize>>, +} + +#[derive(Default)] +pub struct PartialChallenge { + pub scheme: Option<Range<usize>>, + pub realm: Option<Range<usize>>, + pub scope: Option<Range<usize>>, + pub service: Option<Range<usize>>, +} + +impl Into<Challenge> for PartialChallenge { + fn into(self) -> Challenge { + return Challenge { + scheme: self.scheme.unwrap(), + realm: self.realm, + scope: self.scope, + service: self.service, + } + } } // RFC 7230 -fn parse_token(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { +fn parse_token(str: &Vec<char>, start: usize) -> Option<usize> { let mut pos = start; while str.len() > pos { match str[pos] { @@ -26,40 +47,7 @@ fn parse_token(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { return None; } - return Some((start..pos, pos)); -} - -#[cfg(test)] -mod test_token { - use super::*; - - #[test] - fn rejects_nontchar() { - let data = vec!['>']; - - let res = parse_token(&data, 0); - assert!(res.is_none()); - } - - #[test] - fn matches_letter() { - let data = vec!['a']; - - let (range, pos) = parse_token(&data, 0).unwrap(); - assert_eq!(pos, 1); - assert_eq!(range.start, 0); - assert_eq!(range.end, 1); - } - - #[test] - fn matches_multiple_letters() { - let data = vec!['a', '1', '|']; - - let (range, pos) = parse_token(&data, 0).unwrap(); - assert_eq!(pos, 3); - assert_eq!(range.start, 0); - assert_eq!(range.end, 3); - } + return Some(pos); } fn parse_qdtext(str: &Vec<char>, start: usize) -> Option<usize> { @@ -77,13 +65,14 @@ fn parse_quotedpair(str: &Vec<char>, start: usize) -> Option<usize> { return Some(start + 2); } -fn parse_quotedstring(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { +fn parse_quotedstring(str: &Vec<char>, start: usize, range: &mut Option<&mut Range<usize>>) -> Option<usize> { let mut pos = start; if str[pos] != '"' { return None; } pos += 1; + range.as_mut().map(|x| x.start = pos); loop { if let Some(npos) = parse_qdtext(str, pos) { pos = npos; @@ -93,16 +82,17 @@ fn parse_quotedstring(str: &Vec<char>, start: usize) -> Option<(Range<usize>, us break; } } + range.as_mut().map(|x| x.end = pos); if str[pos] != '"' { return None; } pos += 1; - return Some((start+1..pos-1, pos)); + return Some(pos); } -fn parse_sp(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { +fn parse_sp(str: &Vec<char>, start: usize) -> Option<usize> { if str.len() <= start { return None; } @@ -117,7 +107,20 @@ fn parse_sp(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { return None; } - return Some((start..pos, pos)); + return Some(pos); +} + +fn parse_ows(str: &Vec<char>, start: usize) -> Option<usize> { + // @COMPL: This is not right + let mut pos = start; + loop { + match parse_sp(str, pos) { + Some(npos) => pos = npos, + None => break, + } + } + + return Some(pos); } fn parse_crlf(str: &Vec<char>, start: usize) -> Option<usize> { @@ -132,7 +135,7 @@ fn parse_crlf(str: &Vec<char>, start: usize) -> Option<usize> { return None; } -fn parse_lws(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { +fn parse_lws(str: &Vec<char>, start: usize) -> Option<usize> { let mut pos = start; match parse_crlf(str, pos) { @@ -141,28 +144,47 @@ fn parse_lws(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { } match parse_sp(str, pos) { - Some((_, npos)) => pos = npos, + Some(npos) => pos = npos, None => return None, } loop { match parse_sp(str, pos) { - Some((_, npos)) => pos = npos, + Some(npos) => pos = npos, None => break, } } - return Some((start..pos, pos)); + return Some(pos); } -#[derive(Debug)] -pub struct AuthParam { - pub key: Range<usize>, - pub value: Range<usize>, -} +fn parse_param(str: &Vec<char>, start: usize, partial: &mut PartialChallenge) -> Option<usize> { + let mut pos = start; -fn parse_param(str: &Vec<char>, start: usize) -> Option<(AuthParam, usize)> { - let pos = start; - let (param_range, pos) = parse_token(str, pos)?; + let mut value_range_slot = None; + + if let Some(npos) = parse_exact_string(str, pos, "realm") { + partial.realm = Some(Range { + ..Default::default() + }); + value_range_slot = partial.realm.as_mut(); + pos = npos; + } else if let Some(npos) = parse_exact_string(str, pos, "scope") { + partial.scope = Some(Range { + ..Default::default() + }); + value_range_slot = partial.scope.as_mut(); + pos = npos; + } else if let Some(npos) = parse_exact_string(str, pos, "service") { + partial.service = Some(Range { + ..Default::default() + }); + value_range_slot = partial.service.as_mut(); + pos = npos; + } else if let Some(npos) = parse_token(str, pos) { + pos = npos; + } else { + return None; + } let mut pos = pos; if str[pos] == '=' { @@ -171,49 +193,65 @@ fn parse_param(str: &Vec<char>, start: usize) -> Option<(AuthParam, usize)> { todo!(); } - let value_range; - if let Some((range, npos)) = parse_token(str, pos) { - value_range = range; + value_range_slot.as_mut().map(|x| x.start = pos); + if let Some(npos) = parse_token(str, pos) { + value_range_slot.map(|x| x.end = npos); pos = npos; - } else if let Some((range, npos)) = parse_quotedstring(str, pos) { - value_range = range; + } else if let Some(npos) = parse_quotedstring(str, pos, &mut value_range_slot) { pos = npos; } else { todo!("{:?}", &str[pos..]); } - return Some((AuthParam{key: param_range, value: value_range}, pos)); + return Some(pos); } -pub fn parse_challenge(str: &Vec<char>, start: usize) -> Option<(Challenge, usize)> { - let pos = start; - let (scheme_range, pos) = parse_token(str, pos).unwrap(); +pub fn parse_authenticate_header(header: &str) -> Option<Challenge> { + let mut partial: PartialChallenge = Default::default(); + let data = header.chars().collect(); + + parse_challenge(&data, 0, &mut partial)?; - let (_, pos) = parse_sp(str, pos).unwrap(); + return Some(partial.into()); +} + +fn parse_challenge(str: &Vec<char>, start: usize, partial: &mut PartialChallenge) -> Option<usize> { + let mut pos = start; + + { + let mut scheme_range = Range { + start: pos, + ..Default::default() + }; + + pos = parse_token(str, pos).unwrap(); + + scheme_range.end = pos; + partial.scheme = Some(scheme_range); + } + + let pos = parse_sp(str, pos).unwrap(); let mut pos = pos; loop { match parse_sp(str, pos) { - Some((_, npos)) => pos = npos, + Some(npos) => pos = npos, None => break, } } - let mut params = vec!(); - let mut pos = pos; loop { match parse_lws(str, pos) { - Some((_, npos)) => pos = npos, + Some(npos) => pos = npos, None => break, } } - let (param, pos) = parse_param(str, pos).unwrap(); - params.push(param); + let pos = parse_param(str, pos, partial).unwrap(); let mut pos = pos; loop { loop { match parse_lws(str, pos) { - Some((_, npos)) => pos = npos, + Some(npos) => pos = npos, None => break, } } @@ -225,15 +263,14 @@ pub fn parse_challenge(str: &Vec<char>, start: usize) -> Option<(Challenge, usiz loop { match parse_lws(str, pos) { - Some((_, npos)) => pos = npos, + Some(npos) => pos = npos, None => break, } } - match parse_param(str, pos) { - Some((param, npos)) => { + match parse_param(str, pos, partial) { + Some(npos) => { pos = npos; - params.push(param); }, None => return None, } @@ -242,58 +279,39 @@ pub fn parse_challenge(str: &Vec<char>, start: usize) -> Option<(Challenge, usiz if pos < str.len() { return None; } - return Some((Challenge {scheme: scheme_range, params}, pos)); -} - -#[derive(Debug)] -pub enum LinkParam { - Rel{relation: Range<usize>}, -} - -impl LinkParam { - pub fn value(&self) -> &Range<usize> { - match self { - LinkParam::Rel{relation} => &relation, - } - } + return Some(pos); } -#[derive(Debug)] -pub struct LinkValue { - uri: Range<usize>, - pub params: Vec<LinkParam>, -} - -fn parse_alpha(str: &Vec<char>, pos: usize) -> Option<(Range<usize>, usize)> { +fn parse_alpha(str: &Vec<char>, pos: usize) -> Option<usize> { if pos >= str.len() { return None; } return match str[pos] { - 'a'..='z' | 'A'..='Z' => Some((pos..pos+1, pos+1)), + 'a'..='z' | 'A'..='Z' => Some(pos+1), _ => None, } } -fn parse_digit(str: &Vec<char>, pos: usize) -> Option<(Range<usize>, usize)> { +fn parse_digit(str: &Vec<char>, pos: usize) -> Option<usize> { if pos >= str.len() { return None; } return match str[pos] { - '0'..='9' => Some((pos..pos+1, pos+1)), + '0'..='9' => Some(pos+1), _ => None, } } -fn parse_dec_octet(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { +fn parse_dec_octet(str: &Vec<char>, start: usize) -> Option<usize> { let mut pos = start; let mut len = 0; let mut value: u16 = 0; while len < 3 { - if let Some((_, npos)) = parse_digit(str, pos) { + if let Some(npos) = parse_digit(str, pos) { value = value * 10 + (str[pos] as u16 - '0' as u16); pos = npos; len += 1; @@ -314,13 +332,13 @@ fn parse_dec_octet(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize return None; } - return Some((start..pos, pos)); + return Some(pos); } -fn parse_ipv4address(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { +fn parse_ipv4address(str: &Vec<char>, start: usize) -> Option<usize> { let mut pos = start; - let (_, npos) = parse_dec_octet(str, pos)?; + let npos = parse_dec_octet(str, pos)?; pos = npos; if let Some(ch) = str.get(pos) && *ch == '.' { @@ -329,7 +347,7 @@ fn parse_ipv4address(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usi return None; } - let (_, npos) = parse_dec_octet(str, pos)?; + let npos = parse_dec_octet(str, pos)?; pos = npos; if let Some(ch) = str.get(pos) && *ch == '.' { @@ -338,7 +356,7 @@ fn parse_ipv4address(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usi return None; } - let (_, npos) = parse_dec_octet(str, pos)?; + let npos = parse_dec_octet(str, pos)?; pos = npos; if let Some(ch) = str.get(pos) && *ch == '.' { @@ -347,31 +365,31 @@ fn parse_ipv4address(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usi return None; } - let (_, npos) = parse_dec_octet(str, pos)?; + let npos = parse_dec_octet(str, pos)?; pos = npos; - return Some((start..pos, pos)); + return Some(pos); } -fn parse_hexdig(str: &Vec<char>, pos: usize) -> Option<(Range<usize>, usize)> { +fn parse_hexdig(str: &Vec<char>, pos: usize) -> Option<usize> { if pos >= str.len() { return None; } return match str[pos] { - '0'..='9' | 'A'..='F' | 'a'..='f' => Some((pos..pos+1, pos+1)), + '0'..='9' | 'A'..='F' | 'a'..='f' => Some(pos+1), _ => None, } } -fn parse_scheme(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { +fn parse_scheme(str: &Vec<char>, start: usize) -> Option<usize> { let mut pos = start; - let (_, npos) = parse_alpha(str, pos)?; + let npos = parse_alpha(str, pos)?; pos = npos; loop { - if let Some((_, npos)) = parse_alpha(str, pos) { + if let Some(npos) = parse_alpha(str, pos) { pos = npos; } else { break; @@ -381,685 +399,404 @@ fn parse_scheme(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> return None; } -fn parse_reg_name(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { +fn parse_reg_name(str: &Vec<char>, start: usize) -> Option<usize> { let mut pos = start; loop { - if let Some((_, npos)) = parse_unreserved(str, pos) { + if let Some(npos) = parse_unreserved(str, pos) { pos = npos; - } else if let Some((_, npos)) = parse_pct_encoded(str, pos) { + } else if let Some(npos) = parse_pct_encoded(str, pos) { pos = npos; - } else if let Some((_, npos)) = parse_sub_delims(str, pos) { + } else if let Some(npos) = parse_sub_delims(str, pos) { pos = npos; } else { break; } } - return Some((start..pos, pos)); + return Some(pos); } -fn parse_h16(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { +fn parse_h16(str: &Vec<char>, start: usize) -> Option<usize> { let mut pos = start; - let (_, npos) = parse_hexdig(str, pos)?; + let npos = parse_hexdig(str, pos)?; pos = npos; for _ in 0..3 { - if let Some((_, npos)) = parse_hexdig(str, pos) { + if let Some(npos) = parse_hexdig(str, pos) { pos = npos; } else { break; } } - return Some((start..pos, pos)); + return Some(pos); } -fn parse_ls32(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { +fn parse_ls32(str: &Vec<char>, start: usize) -> Option<usize> { let mut pos = start; - if let Some((_, npos)) = parse_h16(str, pos) { - if let Some((_, npos)) = parse_exact_string(str, npos, ":") { - if let Some((_, npos)) = parse_h16(str, npos) { - return Some((start..npos, npos)); + if let Some(npos) = parse_h16(str, pos) { + if let Some(npos) = parse_exact_string(str, npos, ":") { + if let Some(npos) = parse_h16(str, npos) { + return Some(npos); } } } - if let Some((_, npos)) = parse_ipv4address(str, pos) { - return Some((start..npos, npos)); + if let Some(npos) = parse_ipv4address(str, pos) { + return Some(npos); } return None; } -fn parse_ipv6address(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { +fn parse_first_part_ipv6(str: &Vec<char>, start: usize, max_reps: usize) -> Option<usize> { + let mut pos; + + if let Some(npos) = parse_h16(str, start) { + pos = Some(npos); + } else { + pos = Some(start); + } + + for _ in 0..max_reps { + if let Some(npos) = parse_exact_string(str, pos.unwrap(), ":") { + if let Some(npos) = parse_h16(str, npos) { + pos = Some(npos); + continue; + } + } + + break; + } + + if let Some(npos) = parse_exact_string(str, pos.unwrap(), "::") { + pos = Some(npos); + } else { + pos = None; + } + + return pos; +} + +fn parse_ipv6address(str: &Vec<char>, start: usize) -> Option<usize> { let mut pos = start; - { - let mut tpos = pos; + // 6( h16 ":" ) ls32 + if pos == start { // Just for symmetry, this is always true + let mut tpos = Some(pos); + for _ in 0..6 { - if let Some((_, npos)) = parse_h16(str, tpos) { - if let Some((_, npos)) = parse_exact_string(str, npos, ":") { - tpos = npos; + if let Some(npos) = parse_h16(str, tpos.unwrap()) { + if let Some(npos) = parse_exact_string(str, npos, ":") { + tpos = Some(npos); continue; } } // We fail the match, Rollback and stop - tpos = pos; + tpos = None; break; } - if tpos != pos { - // We successfully matched the last part - if let Some((_, npos)) = parse_ls32(str, tpos) { - pos = npos; + if let Some(ref mut tpos_val) = tpos { + if let Some(npos) = parse_ls32(str, *tpos_val) { + *tpos_val = npos; + } else { + tpos = None; } } + + if let Some(tpos_val) = tpos { + pos = tpos_val; + } } + // "::" 5( h16 ":" ) ls32 if pos == start { - let mut tpos = pos; - if let Some((_, npos)) = parse_exact_string(str, tpos, "::") { - tpos = npos; + let mut tpos; + + if let Some(npos) = parse_exact_string(str, pos, "::") { + tpos = Some(npos); } else { - tpos = pos; + tpos = None; } - if tpos != pos { + if let Some(ref mut tpos_val) = tpos { for _ in 0..5 { - if let Some((_, npos)) = parse_h16(str, tpos) { - if let Some((_, npos)) = parse_exact_string(str, npos, ":") { - tpos = npos; + if let Some(npos) = parse_h16(str, *tpos_val) { + if let Some(npos) = parse_exact_string(str, npos, ":") { + *tpos_val = npos; continue; } } // We fail the match, Rollback and stop - tpos = pos; + tpos = None; break; } } - if tpos != pos { - // We successfully matched the last part - if let Some((_, npos)) = parse_ls32(str, tpos) { - pos = npos; + if let Some(ref mut tpos_val) = tpos { + if let Some(npos) = parse_ls32(str, *tpos_val) { + *tpos_val = npos; + } else { + tpos = None; } } + + if let Some(tpos_val) = tpos { + pos = tpos_val; + } } + // [ h16 ] "::" 4( h16 ":" ) ls32 if pos == start { - let mut tpos = pos; + let mut tpos; - if let Some((_, npos)) = parse_h16(str, tpos) { - tpos = npos; + if let Some(npos) = parse_h16(str, pos) { + tpos = Some(npos); + } else { + tpos = Some(pos); } - if let Some((_, npos)) = parse_exact_string(str, tpos, "::") { - tpos = npos; + if let Some(npos) = parse_exact_string(str, tpos.unwrap(), "::") { + tpos = Some(npos); } else { - tpos = pos; + tpos = None; } - if tpos != pos { + if let Some(ref mut tpos_val) = tpos { for _ in 0..4 { - if let Some((_, npos)) = parse_h16(str, tpos) { - if let Some((_, npos)) = parse_exact_string(str, npos, ":") { - tpos = npos; + if let Some(npos) = parse_h16(str, *tpos_val) { + if let Some(npos) = parse_exact_string(str, npos, ":") { + *tpos_val = npos; continue; } } // We fail the match, Rollback and stop - tpos = pos; + tpos = None; break; } } - if tpos != pos { - // We successfully matched the last part - if let Some((_, npos)) = parse_ls32(str, tpos) { - pos = npos; - } - } - } - - if pos == start { - let mut tpos = pos; - - for _ in 0..1 { - if let Some((_, npos)) = parse_h16(str, tpos) { - if let Some((_, npos)) = parse_exact_string(str, npos, ":") { - tpos = npos; - continue; - } + if let Some(ref mut tpos_val) = tpos { + if let Some(npos) = parse_ls32(str, *tpos_val) { + *tpos_val = npos; + } else { + tpos = None; } - - // Match failed - break; } - if let Some((_, npos)) = parse_h16(str, tpos) { - tpos = npos; + if let Some(tpos_val) = tpos { + pos = tpos_val; } + } - if let Some((_, npos)) = parse_exact_string(str, tpos, "::") { - tpos = npos; - } else { - tpos = pos; - } + // [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32 + // Implementing this parsing rule sucks. When reading the first repeating block ( h16 ":" ) we + // have to speculatively read those two tokens, but we can't commit them since we don't know if + // they're valid until we then read ANOTHER h16 followed by another colon, which can then of + // course ALSO be part of the repetition if not followed by a second colon. I don't want to + // write that lookahead/rollback code. Instead we can reformulate that production into: + // [ h16 *1( ":" h16 ) ] "::" 3( h16 ":" ) ls32 + // This is much easier to implement, since we now just have to try and read a colon followed by + // an h16, and if we don't find that fall through to the double colon or fail. + // @COMPL We have to be a little careful, since this may interact differently with whitespace + // and such, but that's for later. + if pos == start { + let mut tpos = parse_first_part_ipv6(str, pos, 1); - if tpos != pos { + if let Some(ref mut tpos_val) = tpos { for _ in 0..3 { - if let Some((_, npos)) = parse_h16(str, tpos) { - if let Some((_, npos)) = parse_exact_string(str, npos, ":") { - tpos = npos; + if let Some(npos) = parse_h16(str, *tpos_val) { + if let Some(npos) = parse_exact_string(str, npos, ":") { + *tpos_val = npos; continue; } } // We fail the match, Rollback and stop - tpos = pos; + tpos = None; break; } } - if tpos != pos { - // We successfully matched the last part - if let Some((_, npos)) = parse_ls32(str, tpos) { - pos = npos; - } - } - } - - if pos == start { - let mut tpos = pos; - - for _ in 0..2 { - if let Some((_, npos)) = parse_h16(str, tpos) { - if let Some((_, npos)) = parse_exact_string(str, npos, ":") { - tpos = npos; - continue; - } + if let Some(ref mut tpos_val) = tpos { + if let Some(npos) = parse_ls32(str, *tpos_val) { + *tpos_val = npos; + } else { + tpos = None } - - // Match failed - break; } - if let Some((_, npos)) = parse_h16(str, tpos) { - tpos = npos; + if let Some(tpos_val) = tpos { + pos = tpos_val; } + } - if let Some((_, npos)) = parse_exact_string(str, tpos, "::") { - tpos = npos; - } else { - tpos = pos; - } + // [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32 + if pos == start { + let mut tpos = parse_first_part_ipv6(str, pos, 2); - if tpos != pos { + if let Some(ref mut tpos_val) = tpos { for _ in 0..2 { - if let Some((_, npos)) = parse_h16(str, tpos) { - if let Some((_, npos)) = parse_exact_string(str, npos, ":") { - tpos = npos; + if let Some(npos) = parse_h16(str, *tpos_val) { + if let Some(npos) = parse_exact_string(str, npos, ":") { + *tpos_val = npos; continue; } } // We fail the match, Rollback and stop - tpos = pos; + tpos = None; break; } } - if tpos != pos { - // We successfully matched the last part - if let Some((_, npos)) = parse_ls32(str, tpos) { - pos = npos; + if let Some(ref mut tpos_val) = tpos { + if let Some(npos) = parse_ls32(str, *tpos_val) { + *tpos_val = npos; + } else { + tpos = None } } - } - - if pos == start { - let mut tpos = pos; - for _ in 0..3 { - if let Some((_, npos)) = parse_h16(str, tpos) { - if let Some((_, npos)) = parse_exact_string(str, npos, ":") { - tpos = npos; - continue; - } - } - - // Match failed - break; - } - - if let Some((_, npos)) = parse_h16(str, tpos) { - tpos = npos; + if let Some(tpos_val) = tpos { + pos = tpos_val; } + } - if let Some((_, npos)) = parse_exact_string(str, tpos, "::") { - tpos = npos; - } else { - tpos = pos; - } + // [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32 + if pos == start { + let mut tpos = parse_first_part_ipv6(str, pos, 3); - if tpos != pos { + if let Some(ref mut tpos_val) = tpos { for _ in 0..1 { - if let Some((_, npos)) = parse_h16(str, tpos) { - if let Some((_, npos)) = parse_exact_string(str, npos, ":") { - tpos = npos; + if let Some(npos) = parse_h16(str, *tpos_val) { + if let Some(npos) = parse_exact_string(str, npos, ":") { + *tpos_val = npos; continue; } } // We fail the match, Rollback and stop - tpos = pos; + tpos = None; break; } } - if tpos != pos { - // We successfully matched the last part - if let Some((_, npos)) = parse_ls32(str, tpos) { - pos = npos; + if let Some(ref mut tpos_val) = tpos { + if let Some(npos) = parse_ls32(str, *tpos_val) { + *tpos_val = npos; + } else { + tpos = None } } + + if let Some(tpos_val) = tpos { + pos = tpos_val; + } } + // [ *4( h16 ":" ) h16 ] "::" ls32 if pos == start { - let mut tpos = pos; + let mut tpos = parse_first_part_ipv6(str, pos, 4); - for _ in 0..4 { - if let Some((_, npos)) = parse_h16(str, tpos) { - if let Some((_, npos)) = parse_exact_string(str, npos, ":") { - tpos = npos; - continue; - } + if let Some(ref mut tpos_val) = tpos { + if let Some(npos) = parse_ls32(str, *tpos_val) { + *tpos_val = npos; + } else { + tpos = None } - - // Match failed - break; - } - - if let Some((_, npos)) = parse_h16(str, tpos) { - tpos = npos; - } - - if let Some((_, npos)) = parse_exact_string(str, tpos, "::") { - tpos = npos; - } else { - tpos = pos; } - if tpos != pos { - // We successfully matched the last part - if let Some((_, npos)) = parse_ls32(str, tpos) { - pos = npos; - } + if let Some(tpos_val) = tpos { + pos = tpos_val; } } + // [ *5( h16 ":" ) h16 ] "::" h16 if pos == start { - let mut tpos = pos; + let mut tpos = parse_first_part_ipv6(str, pos, 5); - for _ in 0..5 { - if let Some((_, npos)) = parse_h16(str, tpos) { - if let Some((_, npos)) = parse_exact_string(str, npos, ":") { - tpos = npos; - continue; - } + if let Some(ref mut tpos_val) = tpos { + if let Some(npos) = parse_h16(str, *tpos_val) { + *tpos_val = npos; + } else { + tpos = None } - - // Match failed - break; - } - - if let Some((_, npos)) = parse_h16(str, tpos) { - tpos = npos; } - if let Some((_, npos)) = parse_exact_string(str, tpos, "::") { - tpos = npos; - } else { - tpos = pos; - } - - if tpos != pos { - // We successfully matched the last part - if let Some((_, npos)) = parse_h16(str, tpos) { - pos = npos; - } + if let Some(tpos_val) = tpos { + pos = tpos_val; } } if pos == start { - let mut tpos = pos; - - for _ in 0..6 { - if let Some((_, npos)) = parse_h16(str, tpos) { - if let Some((_, npos)) = parse_exact_string(str, npos, ":") { - tpos = npos; - continue; - } - } + let tpos = parse_first_part_ipv6(str, pos, 6); - // Match failed - break; - } - - if let Some((_, npos)) = parse_h16(str, tpos) { - tpos = npos; + if let Some(tpos_val) = tpos { + pos = tpos_val; } - - if let Some((_, npos)) = parse_exact_string(str, tpos, "::") { - tpos = npos; - } else { - tpos = pos; - } - - pos = tpos; } if pos == start { return None; } - return Some((start..pos, pos)); -} - -#[cfg(test)] -mod test_ipv6address { - use super::*; - - #[test] - fn parses_full_ipv6address() { - let data = "2001:0db8:85a3:0000:0000:8a2e:0370:7334".chars().collect(); - - let (range, pos) = parse_ipv6address(&data, 0).unwrap(); - - assert_eq!(pos, 39); - assert_eq!(range.start, 0); - assert_eq!(range.end, 39); - } - - #[test] - fn parses_7part_ipv6address() { - let data = "::0db8:85a3:0000:0000:8a2e:0370:7334".chars().collect(); - - let (range, pos) = parse_ipv6address(&data, 0).unwrap(); - - assert_eq!(pos, 36); - assert_eq!(range.start, 0); - assert_eq!(range.end, 36); - } - - #[test] - fn parses_6part_ipv6address() { - let data = "::85a3:0000:0000:8a2e:0370:7334".chars().collect(); - - let (range, pos) = parse_ipv6address(&data, 0).unwrap(); - - assert_eq!(pos, 31); - assert_eq!(range.start, 0); - assert_eq!(range.end, 31); - } - - #[test] - fn parses_5part_ipv6address() { - let data = "::0000:0000:8a2e:0370:7334".chars().collect(); - - let (range, pos) = parse_ipv6address(&data, 0).unwrap(); - - assert_eq!(pos, 26); - assert_eq!(range.start, 0); - assert_eq!(range.end, 26); - } - - #[test] - fn parses_4part_ipv6address() { - let data = "::0000:8a2e:0370:7334".chars().collect(); - - let (range, pos) = parse_ipv6address(&data, 0).unwrap(); - - assert_eq!(pos, 21); - assert_eq!(range.start, 0); - assert_eq!(range.end, 21); - } - - #[test] - fn parses_3part_ipv6address() { - let data = "::8a2e:0370:7334".chars().collect(); - - let (range, pos) = parse_ipv6address(&data, 0).unwrap(); - - assert_eq!(pos, 16); - assert_eq!(range.start, 0); - assert_eq!(range.end, 16); - } - - #[test] - fn parses_2part_ipv6address() { - let data = "::0370:7334".chars().collect(); - - let (range, pos) = parse_ipv6address(&data, 0).unwrap(); - - assert_eq!(pos, 11); - assert_eq!(range.start, 0); - assert_eq!(range.end, 11); - } - - #[test] - fn parses_1part_ipv6address() { - let data = "::7334".chars().collect(); - - let (range, pos) = parse_ipv6address(&data, 0).unwrap(); - - assert_eq!(pos, 6); - assert_eq!(range.start, 0); - assert_eq!(range.end, 6); - } - - #[test] - fn parses_0part_ipv6address() { - let data = "::".chars().collect(); - - let (range, pos) = parse_ipv6address(&data, 0).unwrap(); - - assert_eq!(pos, 2); - assert_eq!(range.start, 0); - assert_eq!(range.end, 2); - } - - #[test] - fn parses_full_ipv6address_trailing_ipv4() { - let data = "2001:0db8:85a3:0000:0000:8a2e:127.0.0.1".chars().collect(); - - let (range, pos) = parse_ipv6address(&data, 0).unwrap(); - - assert_eq!(pos, 39); - assert_eq!(range.start, 0); - assert_eq!(range.end, 39); - } - - #[test] - fn parses_compressed_ipv6_loopback() { - let data = "::1".chars().collect(); - - let (range, pos) = parse_ipv6address(&data, 0).unwrap(); - - assert_eq!(pos, 3); - assert_eq!(range.start, 0); - assert_eq!(range.end, 3); - } - - #[test] - fn parses_ipv6_with_embedded_ipv4() { - let data = "::ffff:192.0.2.128".chars().collect(); - - let (range, pos) = parse_ipv6address(&data, 0).unwrap(); - - assert_eq!(pos, 18); - assert_eq!(range.start, 0); - assert_eq!(range.end, 18); - } - - #[test] - fn rejects_invalid_ipv6_hex() { - let data = "2001:db8::g1".chars().collect(); - - let res = parse_ipv6address(&data, 0); - - assert!(res.is_none()); - } - - #[test] - fn rejects_multiple_compressions() { - let data = "2001::85a3::7334".chars().collect(); - - let res = parse_ipv6address(&data, 0); - - assert!(res.is_none()); - } - - #[test] - fn rejects_too_many_segments() { - let data = "1:2:3:4:5:6:7:8:9".chars().collect(); - - let res = parse_ipv6address(&data, 0); - - assert!(res.is_none()); - } - - #[test] - fn rejects_missing_segments_without_compression() { - let data = "2001:db8:85a3:0:0:8a2e:370".chars().collect(); - - let res = parse_ipv6address(&data, 0); - - assert!(res.is_none()); - } - - #[test] - fn parses_trailing_compression() { - let data = "2001:db8:85a3::".chars().collect(); - - let (range, pos) = parse_ipv6address(&data, 0).unwrap(); - - assert_eq!(pos, 15); - assert_eq!(range.start, 0); - assert_eq!(range.end, 15); - } - - #[test] - fn parses_compressed_ipv6_with_ipv4_tail() { - let data = "2001:db8::192.0.2.33".chars().collect(); - - let (range, pos) = parse_ipv6address(&data, 0).unwrap(); - - assert_eq!(pos, 21); - assert_eq!(range.start, 0); - assert_eq!(range.end, 21); - } - - #[test] - fn rejects_ipv4_tail_out_of_range() { - let data = "2001::256.0.0.1".chars().collect(); - - let res = parse_ipv6address(&data, 0); - - assert!(res.is_none()); - } + return Some(pos); } - -fn parse_ipvfuture(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { +fn parse_ipvfuture(str: &Vec<char>, start: usize) -> Option<usize> { let mut pos = start; - let (_, npos) = parse_exact_string(str, pos, "v")?; + let npos = parse_exact_string(str, pos, "v")?; pos = npos; - let (_, npos) = parse_hexdig(str, pos)?; + let npos = parse_hexdig(str, pos)?; pos = npos; loop { - if let Some((_, npos)) = parse_hexdig(str, pos) { + if let Some(npos) = parse_hexdig(str, pos) { pos = npos; } else { break; } } - let (_, npos) = parse_exact_string(str, pos, ".")?; + let npos = parse_exact_string(str, pos, ".")?; pos = npos; - if let Some((_, npos)) = parse_unreserved(str, pos) { + if let Some(npos) = parse_unreserved(str, pos) { pos = npos; - } else if let Some((_, npos)) = parse_sub_delims(str, pos) { + } else if let Some(npos) = parse_sub_delims(str, pos) { pos = npos; - } else if let Some((_, npos)) = parse_exact_string(str, pos, ":") { + } else if let Some(npos) = parse_exact_string(str, pos, ":") { pos = npos; } else { return None; } loop { - if let Some((_, npos)) = parse_unreserved(str, pos) { + if let Some(npos) = parse_unreserved(str, pos) { pos = npos; - } else if let Some((_, npos)) = parse_sub_delims(str, pos) { + } else if let Some(npos) = parse_sub_delims(str, pos) { pos = npos; - } else if let Some((_, npos)) = parse_exact_string(str, pos, ":") { + } else if let Some(npos) = parse_exact_string(str, pos, ":") { pos = npos; } else { break; } } - return Some((start..pos, pos)); -} - -#[cfg(test)] -mod test_ipvfuture { - use super::*; - - #[test] - fn parses_ipvfuture_with_colons_and_subdelims() { - let data = "vF.fe80::1-._~+!".chars().collect(); - - let (range, pos) = parse_ipvfuture(&data, 0).unwrap(); - - assert_eq!(pos, 16); - assert_eq!(range.start, 0); - assert_eq!(range.end, 16); - } - - #[test] - fn rejects_ipvfuture_missing_dot() { - let data = "v1fe80::1".chars().collect(); - - let res = parse_ipvfuture(&data, 0); - - assert!(res.is_none()); - } - - #[test] - fn rejects_ipvfuture_with_percent_encoded() { - let data = "v1.fe80::1%25".chars().collect(); - - let (range, pos) = parse_ipvfuture(&data, 0).unwrap(); - - assert_eq!(pos, 10); - assert_eq!(range.start, 0); - assert_eq!(range.end, 10); - } + return Some(pos); } - -fn parse_ip_literal(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { +fn parse_ip_literal(str: &Vec<char>, start: usize) -> Option<usize> { let mut pos = start; if let Some(ch) = str.get(pos) && *ch == '[' { @@ -1068,9 +805,9 @@ fn parse_ip_literal(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usiz return None; } - if let Some((_, npos)) = parse_ipv6address(str, pos) { + if let Some(npos) = parse_ipv6address(str, pos) { pos = npos; - } else if let Some((_, npos)) = parse_ipvfuture(str, pos) { + } else if let Some(npos) = parse_ipvfuture(str, pos) { pos = npos; } else { return None; @@ -1078,13 +815,13 @@ fn parse_ip_literal(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usiz if let Some(ch) = str.get(pos) && *ch == ']' { pos += 1; - return Some((start..pos, pos)); + return Some(pos); } return None; } -fn parse_port(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { +fn parse_port(str: &Vec<char>, start: usize) -> Option<usize> { let mut pos = start; while let Some(ch) = str.get(pos) { @@ -1094,39 +831,39 @@ fn parse_port(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { } } - return Some((start..pos, pos)); + return Some(pos); } -fn parse_host(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { +fn parse_host(str: &Vec<char>, start: usize) -> Option<usize> { let mut pos = start; - if let Some((_, npos)) = parse_ip_literal(str, pos) { + if let Some(npos) = parse_ip_literal(str, pos) { pos = npos; - return Some((start..pos, pos)); + return Some(pos); } - if let Some((_, npos)) = parse_ipv4address(str, pos) { + if let Some(npos) = parse_ipv4address(str, pos) { pos = npos; - return Some((start..pos, pos)); + return Some(pos); } - if let Some((_, npos)) = parse_reg_name(str, pos) { + if let Some(npos) = parse_reg_name(str, pos) { pos = npos; - return Some((start..pos, pos)); + return Some(pos); } return None; } -fn parse_userinfo(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { +fn parse_userinfo(str: &Vec<char>, start: usize) -> Option<usize> { let mut pos = start; loop { - if let Some((_, npos)) = parse_unreserved(str, pos) { + if let Some(npos) = parse_unreserved(str, pos) { pos = npos; - } else if let Some((_, npos)) = parse_pct_encoded(str, pos) { + } else if let Some(npos) = parse_pct_encoded(str, pos) { pos = npos; - } else if let Some((_, npos)) = parse_sub_delims(str, pos) { + } else if let Some(npos) = parse_sub_delims(str, pos) { pos = npos; } else if let Some(ch) = str.get(pos) && *ch == ':' { pos += 1; @@ -1135,37 +872,37 @@ fn parse_userinfo(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize) } } - return Some((start..pos, pos)); + return Some(pos); } -fn parse_authority(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { +fn parse_authority(str: &Vec<char>, start: usize) -> Option<usize> { let mut pos = start; - if let Some((_, npos)) = parse_userinfo(str, pos) { + if let Some(npos) = parse_userinfo(str, pos) { if let Some(ch) = str.get(npos) && *ch == '@' { pos = npos; } } // If the character after the userinfo isn't a @ we roll back that parse and try it as a host. - if let Some((_, npos)) = parse_host(str, pos) { + if let Some(npos) = parse_host(str, pos) { pos = npos; } else { return None; } if let Some(ch) = str.get(pos) && *ch == ':' { - if let Some((_, npos)) = parse_port(str, pos+1) { + if let Some(npos) = parse_port(str, pos+1) { pos = npos; } else { return None; } } - return Some((start..pos, pos)); + return Some(pos); } -fn parse_relative_part_1(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { +fn parse_relative_part_1(str: &Vec<char>, start: usize) -> Option<usize> { let mut pos = start; if str[pos] != '/' || str[pos+1] != '/' { @@ -1173,16 +910,16 @@ fn parse_relative_part_1(str: &Vec<char>, start: usize) -> Option<(Range<usize>, } pos += 2; - let (_, npos) = parse_authority(str, pos)?; + let npos = parse_authority(str, pos)?; pos = npos; - let (_, npos) = parse_path_abempty(str, pos)?; + let npos = parse_path_abempty(str, pos)?; pos = npos; - return Some((start..pos, pos)); + return Some(pos); } -fn parse_unreserved(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { +fn parse_unreserved(str: &Vec<char>, start: usize) -> Option<usize> { if let Some(x) = parse_alpha(str, start) { return Some(x); } @@ -1195,12 +932,12 @@ fn parse_unreserved(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usiz return None; } match str[start] { - '-' | '.' | '_' | '~' => Some((start..start+1, start+1)), + '-' | '.' | '_' | '~' => Some(start+1), _ => None } } -fn parse_pct_encoded(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { +fn parse_pct_encoded(str: &Vec<char>, start: usize) -> Option<usize> { if start + 2 >= str.len() { return None; } @@ -1212,25 +949,25 @@ fn parse_pct_encoded(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usi let is_hexdig = |ch: char| matches!(ch, '0'..='9' | 'A'..='F' | 'a'..='f'); if is_hexdig(str[start + 1]) && is_hexdig(str[start + 2]) { - return Some((start..start + 3, start + 3)); + return Some(start + 3); } return None; } -fn parse_sub_delims(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { +fn parse_sub_delims(str: &Vec<char>, start: usize) -> Option<usize> { if start >= str.len() { return None; } match str[start] { - '!' | '$' | '&' | '\'' | '(' | ')' | '*' | '+' | ',' | ';' | '=' => Some((start..start+1, start+1)), + '!' | '$' | '&' | '\'' | '(' | ')' | '*' | '+' | ',' | ';' | '=' => Some(start+1), _ => None } } -fn parse_pchar(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { +fn parse_pchar(str: &Vec<char>, start: usize) -> Option<usize> { if let Some(x) = parse_unreserved(str, start) { return Some(x); } @@ -1245,7 +982,7 @@ fn parse_pchar(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { if let Some(ch) = str.get(start) { match ch { - ':' | '@' => return Some((start..start + 1, start + 1)), + ':' | '@' => return Some(start + 1), _ => {} } } @@ -1253,38 +990,38 @@ fn parse_pchar(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { return None; } -fn parse_segment_nz(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { +fn parse_segment_nz(str: &Vec<char>, start: usize) -> Option<usize> { let mut pos = start; - let (_, npos) = parse_pchar(str, pos)?; + let npos = parse_pchar(str, pos)?; pos = npos; loop { - if let Some((_, npos)) = parse_pchar(str, pos) { + if let Some(npos) = parse_pchar(str, pos) { pos = npos; } else { break; } } - return Some((start..pos, pos)); + return Some(pos); } -fn parse_segment(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { +fn parse_segment(str: &Vec<char>, start: usize) -> Option<usize> { let mut pos = start; loop { - if let Some((_, npos)) = parse_pchar(str, pos) { + if let Some(npos) = parse_pchar(str, pos) { pos = npos; } else { break; } } - return Some((start..pos, pos)); + return Some(pos); } -fn parse_path_absolute(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { +fn parse_path_absolute(str: &Vec<char>, start: usize) -> Option<usize> { let mut pos = start; if str[pos] != '/' { @@ -1292,7 +1029,7 @@ fn parse_path_absolute(str: &Vec<char>, start: usize) -> Option<(Range<usize>, u } pos += 1; - if let Some((_, npos)) = parse_segment_nz(str, pos) { + if let Some(npos) = parse_segment_nz(str, pos) { pos = npos; loop { @@ -1300,7 +1037,7 @@ fn parse_path_absolute(str: &Vec<char>, start: usize) -> Option<(Range<usize>, u break; } - if let Some((_, npos)) = parse_segment(str, pos+1) { + if let Some(npos) = parse_segment(str, pos+1) { pos = npos; } else { break; @@ -1308,15 +1045,15 @@ fn parse_path_absolute(str: &Vec<char>, start: usize) -> Option<(Range<usize>, u } } - return Some((start..pos, pos)); + return Some(pos); } -fn parse_path_abempty(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { +fn parse_path_abempty(str: &Vec<char>, start: usize) -> Option<usize> { let mut pos = start; loop { if let Some(ch) = str.get(pos) && *ch == '/' { - if let Some((_, npos)) = parse_segment(str, pos+1) { + if let Some(npos) = parse_segment(str, pos+1) { pos = npos; } else { break; @@ -1326,41 +1063,26 @@ fn parse_path_abempty(str: &Vec<char>, start: usize) -> Option<(Range<usize>, us } } - return Some((start..pos, pos)); + return Some(pos); } -fn parse_relative_part(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { +fn parse_relative_part(str: &Vec<char>, start: usize) -> Option<usize> { let mut pos = start; - if let Some((_, npos)) = parse_relative_part_1(str, pos) { + if let Some(npos) = parse_relative_part_1(str, pos) { pos = npos; - return Some((start..pos, pos)); + return Some(pos); } - if let Some((_, npos)) = parse_path_absolute(str, pos) { + if let Some(npos) = parse_path_absolute(str, pos) { pos = npos; - return Some((start..pos, pos)); + return Some(pos); } return None; } -#[cfg(test)] -mod test_relative_part { - use super::*; - - #[test] - fn absolute_path() { - let data = "/foo?".chars().collect(); - - let (range, pos) = parse_relative_part(&data, 0).unwrap(); - assert_eq!(pos, 4); - assert_eq!(range.start, 0); - assert_eq!(range.end, 4); - } -} - -fn parse_query(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { +fn parse_query(str: &Vec<char>, start: usize) -> Option<usize> { let mut pos = start; if start >= str.len() { @@ -1368,7 +1090,7 @@ fn parse_query(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { } loop { - if let Some((_, npos)) = parse_pchar(str, pos) { + if let Some(npos) = parse_pchar(str, pos) { pos = npos; } else if let Some(ch) = str.get(pos) && *ch == '/' { pos += 1; @@ -1379,10 +1101,10 @@ fn parse_query(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { } } - return Some((start..pos, pos)); + return Some(pos); } -fn parse_fragment(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { +fn parse_fragment(str: &Vec<char>, start: usize) -> Option<usize> { let mut pos = start; if start >= str.len() { @@ -1390,7 +1112,7 @@ fn parse_fragment(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize) } loop { - if let Some((_, npos)) = parse_pchar(str, pos) { + if let Some(npos) = parse_pchar(str, pos) { pos = npos; } else if let Some(ch) = str.get(pos) && (*ch == '/' || *ch == '?') { pos += 1; @@ -1399,90 +1121,37 @@ fn parse_fragment(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize) } } - return Some((start..pos, pos)); + return Some(pos); } // RFC 3986 -fn parse_relative_ref(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { +fn parse_relative_ref(str: &Vec<char>, start: usize) -> Option<usize> { let mut pos = start; - let (_, npos) = parse_relative_part(str, pos)?; + let npos = parse_relative_part(str, pos)?; pos = npos; - if let Some(ch) = str.get(pos) && *ch == '?' && let Some((_, npos)) = parse_query(str, pos+1) { + if let Some(ch) = str.get(pos) && *ch == '?' && let Some(npos) = parse_query(str, pos+1) { pos = npos; } - if let Some(ch) = str.get(pos) && *ch == '#' && let Some((_, npos)) = parse_fragment(str, pos+1) { + if let Some(ch) = str.get(pos) && *ch == '#' && let Some(npos) = parse_fragment(str, pos+1) { pos = npos; } - return Some((start..pos, pos)); + return Some(pos); } -#[cfg(test)] -mod test_relative_ref { - use super::*; - - #[test] - fn reject_not_starting_slash() { - let data = vec!['?']; - - let res = parse_relative_ref(&data, 0); - assert!(res.is_none()); - } - - #[test] - fn simple() { - let data = vec!['/', '/', 'h', 'e', 'l', 'l', 'o', '/']; - - let (range, pos) = parse_relative_ref(&data, 0).unwrap(); - assert_eq!(pos, 8); - assert_eq!(range.start, 0); - assert_eq!(range.end, 8); - } - - #[test] - fn with_query_parameter() { - let data = "/foo?bar=baz".chars().collect(); - - let (range, pos) = parse_relative_ref(&data, 0).unwrap(); - assert_eq!(pos, 12); - assert_eq!(range.start, 0); - assert_eq!(range.end, 12); - } - - #[test] - fn with_fragment() { - let data = "/foo#frag".chars().collect(); - - let (range, pos) = parse_relative_ref(&data, 0).unwrap(); - assert_eq!(pos, data.len()); - assert_eq!(range.start, 0); - assert_eq!(range.end, data.len()); - } - - #[test] - fn with_percent_encoded_fragment() { - let data = "/foo#fr%20ag".chars().collect(); - - let (range, pos) = parse_relative_ref(&data, 0).unwrap(); - assert_eq!(pos, data.len()); - assert_eq!(range.start, 0); - assert_eq!(range.end, data.len()); - } -} - -fn parse_uri(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { +fn parse_uri(str: &Vec<char>, start: usize) -> Option<usize> { let mut pos = start; - let(_, npos) = parse_scheme(str, pos)?; + let npos = parse_scheme(str, pos)?; pos = npos; - return Some((start..pos, pos)); + return Some(pos); } -fn parse_exact_string(str: &Vec<char>, start: usize, to_match: &str) -> Option<(Range<usize>, usize)> { +fn parse_exact_string(str: &Vec<char>, start: usize, to_match: &str) -> Option<usize> { let mut pos = start; let end = pos + to_match.len(); @@ -1495,171 +1164,117 @@ fn parse_exact_string(str: &Vec<char>, start: usize, to_match: &str) -> Option<( } pos += to_match.len(); - return Some((start..pos, pos)); -} - -fn parse_loalpha(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { - match str.get(start) { - Some('a'..='z') => Some((start..start+1, start+1)), - _ => None, - } + return Some(pos); } -fn parse_relation_type(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { +fn parse_link_param(str: &Vec<char>, start: usize, res: &mut PartialNextPage) -> Option<usize> { let mut pos = start; - let (_, npos) = parse_loalpha(str, pos)?; - pos = npos; + let mut is_rel = false; - loop { - if let Some((_, npos)) = parse_loalpha(str, pos) { - pos = npos; - continue; - } else if let Some((_, npos)) = parse_digit(str, pos) { - pos = npos; - continue; - } else if let Some((_, npos)) = parse_exact_string(str, pos, ".") { - pos = npos; - continue; - } else if let Some((_, npos)) = parse_exact_string(str, pos, "-") { - pos = npos; - continue; - } else { - break; - } + if let Some(npos) = parse_exact_string(str, pos, "rel") { + is_rel = true; + pos = npos; + } else if let Some(npos) = parse_token(str, pos) { + pos = npos; + } else { + return None; } - return Some((start..pos, pos)); -} - -fn parse_relation_types(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> { - let mut pos = start; - - if let Some((_, npos)) = parse_relation_type(str, pos) { + // @CORRECT This part is actually supposed to be optional + if let Some(npos) = parse_ows(str, pos) { pos = npos; + } - return Some((start..pos, pos)); - } else if let Some((_, npos)) = parse_exact_string(str, pos, "\"") { + if let Some(npos) = parse_exact_string(str, pos, "=") { pos = npos; - - if let Some((_, npos)) = parse_relation_type(str, pos) { - pos = npos; - - loop { - if let Some((_, npos)) = parse_sp(str, pos) { - - loop { - if let Some((_, npos)) = parse_sp(str, pos) { - pos = npos; - } else { - break; - } - } - - pos = npos; - if let Some((_, npos)) = parse_relation_type(str, pos) { - pos = npos; - continue; - } - } - break; - } - - if let Some((_, npos)) = parse_exact_string(str, pos, "\"") { - pos = npos; - return Some((start+1..pos-1, pos)); - } - } + } else { + return None; } - return None; -} - -fn parse_link_param(str: &Vec<char>, start: usize) -> Option<(LinkParam, usize)> { - let mut pos = start; - - // Non-standard but seen in the wild - if let Some((_, npos)) = parse_sp(str, pos) { + if let Some(npos) = parse_ows(str, pos) { pos = npos; } - if let Some((_, npos)) = parse_exact_string(str, pos, "rel=") { - if let Some((relation, npos)) = parse_relation_types(str, npos) { - pos = npos; - return Some((LinkParam::Rel{relation}, pos)); - } + // @INCOMPLETE This is not exhaustive + if is_rel && let Some(npos) = parse_exact_string(str, pos, "\"next\"") { + res.found_next_uri = Some(res.current_uri.clone()); + pos = npos; + } else if let Some(npos) = parse_token(str, pos) { + pos = npos; + } else if let Some(npos) = parse_quotedstring(str, pos, &mut None) { + pos = npos; + } else { + return None; } - return None; + return Some(pos); } -fn parse_link_value(str: &Vec<char>, start: usize) -> Option<(LinkValue, usize)> { +fn parse_link_value(str: &Vec<char>, start: usize, res: &mut PartialNextPage) -> Option<usize> { let mut pos = start; - if let Some((_, npos)) = parse_exact_string(str, pos, "<") { + if let Some(npos) = parse_exact_string(str, pos, "<") { pos = npos; } else { return None; } - let uri_range; - let mut params = vec!(); - - if let Some((range, npos)) = parse_uri(str, pos) { - uri_range = range; + res.current_uri.start = pos; + if let Some(npos) = parse_uri(str, pos) { pos = npos; - } else if let Some((range, npos)) = parse_relative_ref(str, pos) { - uri_range = range; + } else if let Some(npos) = parse_relative_ref(str, pos) { pos = npos; } else { return None; } + res.current_uri.end = pos; - if let Some((_, npos)) = parse_exact_string(str, pos, ">") { + if let Some(npos) = parse_exact_string(str, pos, ">") { pos = npos; } else { return None; } loop { - if let Some((_, npos)) = parse_exact_string(str, pos, ";") { + if let Some(npos) = parse_exact_string(str, pos, ";") { pos = npos; } else { break; } - if let Some((param, npos)) = parse_link_param(str, pos) { + if let Some(npos) = parse_ows(str, pos) { + pos = npos; + } + + if let Some(npos) = parse_link_param(str, pos, res) { pos = npos; - params.push(param); } else { break; } } - return Some((LinkValue{ uri: uri_range, params: params }, pos)); + return Some(pos); } -pub fn parse_link(str: &Vec<char>, start: usize) -> Option<(Vec<LinkValue>, usize)> { +fn parse_link_into(str: &Vec<char>, start: usize, res: &mut PartialNextPage) -> Option<usize> { let mut pos = start; - let mut values = vec!(); - loop { match parse_lws(str, pos) { - Some((_, npos)) => pos = npos, + Some(npos) => pos = npos, None => break, } } - let (param, npos) = parse_link_value(str, pos)?; - values.push(param); + let npos = parse_link_value(str, pos, res)?; pos = npos; loop { let mut ipos = pos; loop { match parse_lws(str, ipos) { - Some((_, npos)) => ipos = npos, + Some(npos) => ipos = npos, None => break, } } @@ -1671,15 +1286,14 @@ pub fn parse_link(str: &Vec<char>, start: usize) -> Option<(Vec<LinkValue>, usiz loop { match parse_lws(str, ipos) { - Some((_, npos)) => ipos = npos, + Some(npos) => ipos = npos, None => break, } } - match parse_link_value(str, ipos) { - Some((param, npos)) => { + match parse_link_value(str, ipos, res) { + Some(npos) => { ipos = npos; - values.push(param); }, None => { break; @@ -1690,194 +1304,34 @@ pub fn parse_link(str: &Vec<char>, start: usize) -> Option<(Vec<LinkValue>, usiz } - return Some((values, pos)); + return Some(pos); } -#[cfg(test)] -mod test_link { - use super::*; - - #[test] - fn parses_relative_authority_only() { - let data: Vec<char> = "<//example.com>".chars().collect(); - - let (links, pos) = parse_link(&data, 0).unwrap(); - - assert_eq!(pos, 15); - assert_eq!(links.len(), 1); - assert_eq!(links[0].uri, 1..14); - assert_eq!(links[0].params.len(), 0); - } - - #[test] - fn parses_relative_absolute_path() { - let data: Vec<char> = "</path/to/resource>".chars().collect(); - - let (links, pos) = parse_link(&data, 0).unwrap(); - - assert_eq!(pos, 19); - assert_eq!(links.len(), 1); - assert_eq!(links[0].uri, 1..18); - } - - #[test] - fn parses_relative_with_query() { - let data: Vec<char> = "</foo?bar=baz>".chars().collect(); - - let (links, pos) = parse_link(&data, 0).unwrap(); - - assert_eq!(pos, 14); - assert_eq!(links.len(), 1); - assert_eq!(links[0].uri, 1..13); - } - - #[test] - fn parses_rel_param_unquoted() { - let data: Vec<char> = "<//example.com>; rel=preconnect".chars().collect(); - - let (links, pos) = parse_link(&data, 0).unwrap(); - - assert_eq!(pos, 31); - - match &links[0].params[0] { - LinkParam::Rel { relation } => { - assert_eq!(relation.clone(), 21..31); - } - _ => assert!(false), - } - } - - #[test] - fn parses_multiple_links_with_lws() { - let data: Vec<char> = "<//a>; rel=preconnect, <//b>".chars().collect(); - - let (links, pos) = parse_link(&data, 0).unwrap(); - - assert_eq!(pos, 28); - assert_eq!(links.len(), 2); - - match &links[0].params[0] { - LinkParam::Rel { relation } => { - assert_eq!(relation.clone(), 11..21); - } - } - - assert_eq!(links[1].params.len(), 0); - assert_eq!(links[0].uri, 1..4); - assert_eq!(links[1].uri, 24..27); - } - - #[test] - fn parses_lws_between_link_values() { - let data: Vec<char> = "<//a>,\r\n <//b>".chars().collect(); - - let (links, pos) = parse_link(&data, 0).unwrap(); - - assert_eq!(pos, 14); - assert_eq!(links.len(), 2); - assert_eq!(links[0].uri, 1..4); - assert_eq!(links[1].uri, 10..13); - } - - #[test] - // Is this correct? - fn rejects_absolute_uri() { - let data: Vec<char> = "<http://example.com>".chars().collect(); - - let res = parse_link(&data, 0); - assert!(res.is_none()); - } - - #[test] - fn rejects_missing_closing_delimiter() { - let data: Vec<char> = "<//example.com".chars().collect(); - - let res = parse_link(&data, 0); - assert!(res.is_none()); - } +#[derive(Default)] +struct PartialNextPage { + current_uri: Range<usize>, - #[test] - fn parses_quoted_rel_multiple_values() { - let data: Vec<char> = "<//example.com>; rel=\"preconnect dns-prefetch\"".chars().collect(); - - let (links, pos) = parse_link(&data, 0).unwrap(); - - assert_eq!(pos, data.len()); - assert_eq!(links.len(), 1); - assert_eq!(links[0].params.len(), 1); - - match &links[0].params[0] { - LinkParam::Rel { relation } => { - assert_eq!(relation.clone(), 22..45); - } - } - } - - #[test] - #[ignore = "absolute URI targets are not parsed yet"] - fn parses_absolute_uri_with_rel() { - let data: Vec<char> = "<http://example.com>; rel=preconnect".chars().collect(); - - let (links, pos) = parse_link(&data, 0).unwrap(); + found_next_uri: Option<Range<usize>>, +} - assert_eq!(pos, 35); - assert_eq!(links.len(), 1); - assert_eq!(links[0].uri, 1..19); - match &links[0].params[0] { - LinkParam::Rel { relation } => assert_eq!(relation.clone(), 22..32), +impl Into<NextPage> for PartialNextPage { + fn into(self) -> NextPage { + return NextPage { + next_uri: self.found_next_uri, } } +} - #[test] - #[ignore = "additional parameters beyond rel= are not parsed"] - fn parses_title_parameter() { - let data: Vec<char> = "<//example.com>; rel=preconnect; title=\"Example\"".chars().collect(); - - let (links, pos) = parse_link(&data, 0).unwrap(); - - assert_eq!(pos, 57); - assert_eq!(links.len(), 1); - assert_eq!(links[0].params.len(), 2); - } - - #[test] - #[ignore = "extended title* parameter not supported"] - fn parses_title_star_parameter() { - let data: Vec<char> = "<//example.com>; rel=preconnect; title*=UTF-8''Ex%C3%A4mple".chars().collect(); - - let (links, pos) = parse_link(&data, 0).unwrap(); - - assert_eq!(pos, 65); - assert_eq!(links.len(), 1); - } - - #[test] - #[ignore = "anchor parameter not parsed"] - fn parses_anchor_parameter() { - let data: Vec<char> = "<//example.com>; rel=preconnect; anchor=\"/base\"".chars().collect(); - - let (links, pos) = parse_link(&data, 0).unwrap(); - - assert_eq!(pos, 56); - assert_eq!(links.len(), 1); - } - - #[test] - #[ignore = "hreflang parameter not parsed"] - fn parses_hreflang_parameter() { - let data: Vec<char> = "<//example.com>; rel=preconnect; hreflang=en".chars().collect(); +pub struct NextPage { + pub next_uri: Option<Range<usize>> +} - let (links, pos) = parse_link(&data, 0).unwrap(); +pub fn extract_next_page(header: &str) -> Option<NextPage> { + let mut partial = Default::default(); - assert_eq!(pos, 53); - assert_eq!(links.len(), 1); - } + let data = header.chars().collect(); - #[test] - fn rejects_percent_encoded_path() { - let data: Vec<char> = "</pa%20th>".chars().collect(); + parse_link_into(&data, 0, &mut partial)?; - let res = parse_link(&data, 0); - assert!(res.is_some()); - } + return Some(partial.into()); } |
