summaryrefslogtreecommitdiff
path: root/src/main.rs
diff options
context:
space:
mode:
authorJesper Jensen <jesper@jnsn.dev>2025-12-22 22:08:42 +0100
committerJesper Jensen <jesper@jnsn.dev>2025-12-22 22:08:42 +0100
commit9fc38dcb2774234d178fe76511cabfb22c768c7b (patch)
tree731bf952904b449c72abca115f680f19a55cd122 /src/main.rs
parent8c3313d02e3053a43cb0c4cfba5b5b78376dfa20 (diff)
Add the initial skeleton of the parser
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs654
1 files changed, 596 insertions, 58 deletions
diff --git a/src/main.rs b/src/main.rs
index 074b580..dd7eb46 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,8 +1,11 @@
+mod rfc8288;
+
use base64::prelude::*;
use yaml_rust2::parser::Parser;
use yaml_rust2::Event;
use std::collections::HashMap;
use std::ops::Range;
+use std::sync;
fn help(cmd: &str) {
println!(
@@ -52,6 +55,62 @@ impl Auth {
}
}
+impl AuthState{
+ fn add_to_request<T>(&self, req: ureq::RequestBuilder<T>) -> ureq::RequestBuilder<T> {
+ if let AuthStage::Authorized(x) = &self.stage {
+ return req.header("Authorization", x);
+ }
+
+ 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)));
+
+ let url = format!("{}?service={}&scope={}", realm, service, scope);
+
+ let body = ureq::get(url)
+ .config().http_status_as_error(false).build()
+ .header("Authorization", basic_auth)
+ .call();
+
+ dbg!(&body);
+
+ let body = body.unwrap()
+ .body_mut().read_to_string().unwrap();
+
+ dbg!(&body);
+
+ let body: tinyjson::JsonValue = body
+ .parse().unwrap();
+
+ let token: &String = body["token"].get().unwrap();
+
+ 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?)");
+ }
+ }
+}
+
#[derive(Debug)]
enum AuthMethod {
Basic,
@@ -82,7 +141,7 @@ fn parse_token(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> {
fn parse_qdtext(str: &Vec<char>, start: usize) -> Option<usize> {
match str[start] {
- '\u{0}'..'\u{25}' | '\u{127}' => return None,
+ '\x00'..'\x1F' | '"' | '\x7F' => return None,
_ => return Some(start + 1),
}
}
@@ -197,7 +256,7 @@ fn parse_param(str: &Vec<char>, start: usize) -> Option<(AuthParam, usize)> {
value_range = range;
pos = npos;
} else {
- todo!();
+ todo!("{:?}", &str[pos..]);
}
return Some((AuthParam{key: param_range, value: value_range}, pos));
@@ -277,7 +336,6 @@ impl AuthMethod {
let mut scope_param = None;
let mut service_param = None;
for param in parse.params {
- dbg!(&header_str[param.key.clone()]);
match &header_str[param.key.clone()] {
"realm" => realm_param = Some(param),
"scope" => scope_param = Some(param),
@@ -297,70 +355,462 @@ impl AuthMethod {
}
}
-fn perform_registry_request(registry: &str, url: String, auth: &mut Auth) -> Result<ureq::http::Response<ureq::Body>, ()> {
- let mut state = auth.states.get_mut(registry);
+#[derive(Debug)]
+enum LinkParam {
+ Rel{relation: Range<usize>},
+}
- let url = format!("https://{}{}", registry, &url);
+impl LinkParam {
+ fn value(&self) -> &Range<usize> {
+ match self {
+ LinkParam::Rel{relation} => &relation,
+ }
+ }
+}
+
+#[derive(Debug)]
+struct LinkValue {
+ uri: Range<usize>,
+ params: Vec<LinkParam>,
+}
+fn parse_alpha(str: &Vec<char>, pos: usize) -> Option<(Range<usize>, usize)> {
+ return match str[pos] {
+ 'a'..'z' | 'A'..'Z' => Some((pos..pos+1, pos+1)),
+ _ => None,
+ }
+}
- for _ in 0..2 {
- let mut request = ureq::get(&url)
- .config().http_status_as_error(false).build();
+fn parse_digit(str: &Vec<char>, pos: usize) -> Option<(Range<usize>, usize)> {
+ return match str[pos] {
+ '0'..'9' => Some((pos..pos+1, pos+1)),
+ _ => None,
+ }
+}
- if let Some(ref state) = state {
- if let AuthStage::Authorized(x) = &state.stage {
- request = request.header("Authorization", x);
+fn parse_scheme(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> {
+ let mut pos = start;
+
+ let (_, npos) = parse_alpha(str, pos)?;
+ pos = npos;
+
+ loop {
+ if let Some((_, npos)) = parse_alpha(str, pos) {
+ pos = npos;
+ } else {
+ break;
+ }
+ };
+
+ return None;
+}
+
+fn parse_authority(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> {
+ return None;
+}
+
+fn parse_relative_part_1(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> {
+ let mut pos = start;
+
+ if str[pos] != '/' && str[pos+1] != '/' {
+ return None;
+ }
+ pos += 2;
+
+ let (_, npos) = parse_authority(str, pos)?;
+ pos = npos;
+
+ return Some((start..pos, pos));
+}
+
+fn parse_unreserved(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> {
+ if let Some(x) = parse_alpha(str, start) {
+ return Some(x);
+ }
+
+ if let Some(x) = parse_digit(str, start) {
+ return Some(x);
+ }
+
+ match str[start] {
+ '-' | '.' | '_' | '~' => Some((start..start+1, start+1)),
+ _ => None
+ }
+}
+
+fn parse_sub_delims(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> {
+ match str[start] {
+ '!' | '$' | '&' | '\'' | '(' | ')' | '*' | '+' | ',' | ';' | '=' => Some((start..start+1, start+1)),
+ _ => None
+ }
+
+}
+
+fn parse_pchar(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> {
+ if let Some(x) = parse_unreserved(str, start) {
+ return Some(x);
+ }
+
+ if let Some(x) = parse_sub_delims(str, start) {
+ return Some(x);
+ }
+
+ return None;
+}
+
+fn parse_segment_nz(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> {
+ let mut pos = start;
+
+ let (_, npos) = parse_pchar(str, pos)?;
+ pos = npos;
+
+ loop {
+ if let Some((_, npos)) = parse_pchar(str, pos) {
+ pos = npos;
+ } else {
+ break;
+ }
+ }
+
+ return Some((start..pos, pos));
+}
+
+fn parse_segment(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> {
+ let mut pos = start;
+
+ loop {
+ if let Some((_, npos)) = parse_pchar(str, pos) {
+ pos = npos;
+ } else {
+ break;
+ }
+ }
+
+ return Some((start..pos, pos));
+}
+
+fn parse_path_absolute(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> {
+ let mut pos = start;
+
+ if str[pos] != '/' {
+ return None;
+ }
+ pos += 1;
+
+ if let Some((_, npos)) = parse_segment_nz(str, pos) {
+ pos = npos;
+
+ loop {
+ if str[pos] != '/' {
+ break;
+ }
+
+ if let Some((_, npos)) = parse_segment(str, pos+1) {
+ pos = npos;
+ } else {
+ break;
}
}
+ }
- let response = request.call().unwrap();
+ return Some((start..pos, pos));
+}
- if response.status() == 401 {
- if let Some(ref mut state) = state {
- if let AuthStage::Idle = state.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!("{}:{}", state.info.username, state.info.password)));
- state.stage = AuthStage::Authorized(basic_auth);
+fn parse_relative_part(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> {
+ let mut pos = start;
+
+ if let Some((_, npos)) = parse_relative_part_1(str, pos) {
+ pos = npos;
+ return Some((start..pos, pos));
+ }
+
+ if let Some((_, npos)) = parse_path_absolute(str, pos) {
+ pos = npos;
+ return Some((start..pos, pos));
+ }
+
+ return None;
+}
+
+fn parse_query(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> {
+ let mut pos = start;
+
+ loop {
+ if let Some((_, npos)) = parse_pchar(str, pos) {
+ pos = npos;
+ } else if str[pos] == '/' {
+ pos += 1;
+ } else if str[pos] == '?' {
+ pos += 1;
+ } else {
+ break;
+ }
+ }
+
+ return Some((start..pos, pos));
+}
- continue; // try again
- },
- Some(AuthMethod::Bearer{realm, scope, service}) => {
- let basic_auth = format!("Basic {}", BASE64_STANDARD.encode(format!("{}:{}", state.info.username, state.info.password)));
+fn parse_fragment(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> {
+ return None;
+}
+
+fn parse_relative_ref(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> {
+ let mut pos = start;
+
+ let (_, npos) = parse_relative_part(str, pos)?;
+ pos = npos;
+
+ if str[pos] == '?' && let Some((_, npos)) = parse_query(str, pos+1) {
+ pos = npos;
+ }
+
+ if str[pos] == '#' && let Some((_, npos)) = parse_fragment(str, pos+1) {
+ pos = npos;
+ }
- let url = format!("{}?service={}&scope={}", realm, service, scope);
- dbg!(&url);
+ return Some((start..pos, pos));
+}
- let body = ureq::get(url)
- .config().http_status_as_error(false).build()
- .header("Authorization", basic_auth)
- .call();
+fn parse_uri(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> {
+ let mut pos = start;
- dbg!(&body);
+ let(_, npos) = parse_scheme(str, pos)?;
+ pos = npos;
- let body = body.unwrap()
- .body_mut().read_to_string().unwrap();
+ return Some((start..pos, pos));
+}
- dbg!(&body);
+fn parse_exact_string(str: &Vec<char>, start: usize, to_match: &str) -> Option<(Range<usize>, usize)> {
+ let mut pos = start;
+ let end = pos + to_match.len();
- let body: tinyjson::JsonValue = body
- .parse().unwrap();
+ if end > str.len() {
+ return None;
+ }
- let token: &String = body["token"].get().unwrap();
+ if !to_match.chars().eq(str[start..start+to_match.len()].iter().map(|x| *x)) {
+ return None;
+ }
+ pos += to_match.len();
- state.stage = AuthStage::Authorized(format!("Bearer {}", token));
- continue;
- },
- None => todo!(),
+ return Some((start..pos, pos));
+}
+
+fn parse_loalpha(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> {
+ match str[start] {
+ 'a'..'z' => Some((start..start+1, start+1)),
+ _ => None,
+ }
+}
+
+fn parse_relation_type(str: &Vec<char>, start: usize) -> Option<(Range<usize>, usize)> {
+ let mut pos = start;
+
+ let (_, npos) = parse_loalpha(str, pos)?;
+ pos = npos;
+
+ loop {
+ dbg!(&str[pos..]);
+ 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;
+ }
+ }
+
+ 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) {
+ pos = npos;
+
+ return Some((start..pos, pos));
+ } else 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;
}
- } else {
- todo!("Server didn't ask us to authenticate");
}
- } else {
- todo!("Authorized request somehow failed (expired token?)");
+
+ 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));
+ }
+ }
+ }
+
+ 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) {
+ 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));
+ }
+ }
+
+ return None;
+}
+
+fn parse_link_value(str: &Vec<char>, start: usize) -> Option<(LinkValue, usize)> {
+ let mut pos = start;
+
+ if str[pos] == '<' {
+ pos += 1;
+ } else {
+ return None;
+ }
+
+ let uri_range;
+ let mut params = vec!();
+
+ if let Some((range, npos)) = parse_uri(str, pos) {
+ uri_range = range;
+ pos = npos;
+ } else if let Some((range, npos)) = parse_relative_ref(str, pos) {
+ uri_range = range;
+ pos = npos;
+ } else {
+ return None;
+ }
+
+ if str[pos] == '>' {
+ pos += 1;
+ } else {
+ return None;
+ }
+
+ loop {
+ if let Some((_, npos)) = parse_exact_string(str, pos, ";") {
+ pos = npos;
+ } else {
+ break;
+ }
+
+ if let Some((param, npos)) = parse_link_param(str, pos) {
+ pos = npos;
+ params.push(param);
+ } else {
+ break;
+ }
+ }
+
+ return Some((LinkValue{ uri: uri_range, params: params }, pos));
+}
+
+fn parse_link(str: &Vec<char>, start: usize) -> Option<(Vec<LinkValue>, usize)> {
+ let mut pos = start;
+
+ let mut values = vec!();
+
+ loop {
+ match parse_lws(str, pos) {
+ Some((_, npos)) => pos = npos,
+ None => break,
+ }
+ }
+
+ let (param, npos) = parse_link_value(str, pos)?;
+ values.push(param);
+ pos = npos;
+
+ loop {
+ let mut ipos = pos;
+ loop {
+ match parse_lws(str, ipos) {
+ Some((_, npos)) => ipos = npos,
+ None => break,
+ }
+ }
+
+ if ipos >= str.len() || str[ipos] != ',' {
+ break;
+ }
+ ipos += 1;
+
+ loop {
+ match parse_lws(str, ipos) {
+ Some((_, npos)) => ipos = npos,
+ None => break,
+ }
+ }
+
+ match parse_link_value(str, ipos) {
+ Some((param, npos)) => {
+ ipos = npos;
+ values.push(param);
+ },
+ None => {
+ break;
+ },
+ }
+
+ pos = ipos;
+ }
+
+
+ return Some((values, pos));
+}
+
+fn perform_registry_request(registry: &str, url: &str, auth: &mut Auth) -> Result<ureq::http::Response<ureq::Body>, ()> {
+ let mut state = auth.states.get_mut(registry);
+
+ let url = format!("https://{}{}", registry, &url);
+
+ for _ in 0..2 {
+ let mut request = ureq::get(&url)
+ .config().http_status_as_error(false).build();
+
+ if let Some(ref state) = state {
+ request = state.add_to_request(request);
+ }
+
+ let response = request.call().unwrap();
+
+ if response.status() == 401 {
+ if let Some(ref mut state) = state {
+ state.attempt_authorization(&response)?;
+ continue;
} else {
todo!("Server returned 401 but we have no credentials");
}
@@ -369,7 +819,6 @@ fn perform_registry_request(registry: &str, url: String, auth: &mut Auth) -> Res
return Ok(response);
}
-
return Err(());
}
@@ -523,28 +972,117 @@ struct Update {
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>,
+}
+
+#[derive(Debug)]
+enum CompareOutcome {
+ Higher,
+ Lower,
+
+ Incompatible,
+ Identical,
+}
+
+impl Version {
+ 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!();
+ 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());
+ }
+
+ return Version {
+ string_parts,
+ number_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() {
+ 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;
+ }
+ }
+
+ return CompareOutcome::Identical;
+ }
+}
+
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 tag = img.tag.map(|x| &file[x]).unwrap_or("latest");
+ let mut tag = img.tag.map(|x| file[x].to_string());
- let digest = None;
+ let mut digest = None;
- // Find the digest for the tag
- if let Some(digest) = img.digest {
- let url = format!("/v2/{}/manifests/{}", &file[img.image], tag);
- let mut response = perform_registry_request(registry, url, auth).unwrap();
+
+ if let Some(ref tag_str) = tag {
+ let mut current = Version::parse(&tag_str);
+
+ let mut url = format!("/v2/{}/tags/list", &file[img.image.clone()]);
+ loop {
+ let mut response = perform_registry_request(registry, &url, 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 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);
+ match current.compare(&candidate) {
+ CompareOutcome::Higher => {
+ tag = Some(candidate_str.clone());
+ current = candidate;
+ },
+ CompareOutcome::Lower => {},
+ CompareOutcome::Incompatible => {},
+ CompareOutcome::Identical => {},
+ }
+ }
+ dbg!(&tag);
+ }
+ }
+
+ if img.digest.is_some() {
+ // 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 body: tinyjson::JsonValue = response.body_mut().read_to_string().unwrap().parse().unwrap();
dbg!(&body);
- let media_type : &String = body["mediaType"].get().unwrap();
+ // let media_type : &String = body["mediaType"].get().unwrap();
// assert!(media_type == "application/vnd.docker.distribution.manifest.v2+json");
+
+ digest = Some(response.headers()["docker-content-digest"].to_str().unwrap().to_string());
}
let image_ref = {
- let prefix = &file[img.full_range.start..digest.start];
- let digest = &response.headers()["docker-content-digest"].to_str().unwrap();
- format!("{}{}", prefix, digest)
+ let prefix = &file[img.full_range.start..img.digest.unwrap().start];
+ format!("{}{}", prefix, digest.unwrap_or("".to_string()))
};
edits.push(Update{