summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJesper Jensen <jesper@jnsn.dev>2026-01-19 21:05:38 +0100
committerJesper Jensen <jesper@jnsn.dev>2026-01-19 21:05:38 +0100
commitef21d01f08dc94ba355f596ddb23e1e32c43c8d4 (patch)
tree6044255f32f894ea52c28c63e7a56d347386b618
parent42e48a4704242b1f63092902b6b40e254c64cad7 (diff)
INITIAL COMMIT (again)
-rw-r--r--.dockerignore2
-rw-r--r--Cargo.lock129
-rw-r--r--Cargo.toml1
-rw-r--r--Dockerfile16
-rw-r--r--src/main.rs556
-rw-r--r--src/parser.rs1474
6 files changed, 948 insertions, 1230 deletions
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..71b413c
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,2 @@
+target/
+docs/
diff --git a/Cargo.lock b/Cargo.lock
index 9c044fa..be1b6d8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -103,6 +103,18 @@ dependencies = [
]
[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasip2",
+]
+
+[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -183,6 +195,68 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "rand"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
+dependencies = [
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
+dependencies = [
+ "getrandom 0.3.4",
+]
+
+[[package]]
name = "regex"
version = "1.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -219,7 +293,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
- "getrandom",
+ "getrandom 0.2.16",
"libc",
"untrusted",
"windows-sys",
@@ -279,12 +353,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
+name = "syn"
+version = "2.0.112"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21f182278bf2d2bcb3c88b1b08a37df029d71ce3d3ae26168e3c653b213b99d4"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
name = "tinyjson"
version = "2.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ab95735ea2c8fd51154d01e39cf13912a78071c2d89abc49a7ef102a7dd725a"
[[package]]
+name = "unicode-ident"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
+
+[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -330,6 +421,7 @@ name = "version-bumper"
version = "0.1.0"
dependencies = [
"base64",
+ "rand",
"regex",
"tinyjson",
"ureq",
@@ -343,6 +435,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
+name = "wasip2"
+version = "1.0.1+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
name = "webpki-roots"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -425,6 +526,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
+name = "wit-bindgen"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
+
+[[package]]
name = "yaml-rust2"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -436,6 +543,26 @@ dependencies = [
]
[[package]]
+name = "zerocopy"
+version = "0.8.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
name = "zeroize"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 37cc872..4ec2ac7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,6 +5,7 @@ edition = "2024"
[dependencies]
base64 = "0.22.1"
+rand = "0.9.2"
regex = "1.12.2"
tinyjson = "2.5.1"
ureq = "3.1.4"
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..6758b33
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,16 @@
+FROM alpine:3.23.2@sha256:865b95f46d98cf867a156fe4a135ad3fe50d2056aa3f25ed31662dff6da4eb62 AS build
+
+RUN apk add --no-cache rust cargo
+
+COPY . /app
+
+WORKDIR /app
+RUN cargo build --release
+
+
+FROM alpine:3.23.2@sha256:865b95f46d98cf867a156fe4a135ad3fe50d2056aa3f25ed31662dff6da4eb62
+
+RUN apk add --no-cache libgcc
+
+COPY --from=build /app/target/release/version-bumper /app/version-bump
+CMD [ "/app/version-bump" ]
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());
}