diff options
Diffstat (limited to 'src/main.rs')
| -rw-r--r-- | src/main.rs | 279 |
1 files changed, 279 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..fefa4cf --- /dev/null +++ b/src/main.rs @@ -0,0 +1,279 @@ +use base64::prelude::*; +use yaml_rust2::parser::Parser; +use yaml_rust2::Event; +use std::ops::Range; + +fn help(cmd: &str) { + println!( + "{} [options] [--] <FILE> +Search FILE for docker images and suggest updates + +Options: +--auth <USER> <PASS> Authenticate against registry", + cmd + ); +} + +struct Auth { + header: Option<String>, +} + +impl Auth { + fn new() -> Self { + return Auth{ + header: None, + }; + } + + fn parse(&mut self, username: &str, password: &str) { + self.header = Some(format!("Basic {}", BASE64_STANDARD.encode(format!("{}:{}", username, password)))); + } + + fn apply<B>(&self, req: ureq::RequestBuilder<B>) -> ureq::RequestBuilder<B> { + if let Some(header) = &self.header { + return req.header("Authorization", header.clone()); + } else { + return req; + } + } +} + +enum YContext { + InDocument, + InObject, + InSequence, + InValue(bool), +} + +#[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>>; +} + +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(); + + let byte_start = subslice_start.wrapping_sub(self_start); + + if !byte_start.is_multiple_of(size_of::<T>()) { + return None; + } + + 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 } + } +} + +#[derive(Debug, Clone)] +struct DockerRef { + full_range: Range<usize>, + + registry: Option<Range<usize>>, + image: Range<usize>, + tag: Option<Range<usize>>, + digest: Option<Range<usize>>, +} + +impl DockerRef{ + fn parse(file: &str, chunk: &Chunk) -> DockerRef { + let mut string_range = chunk.position.clone(); + + let mut digest = None; + if let Some(idx) = file[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(":") { + tag = Some(string_range.start+idx+1..string_range.end); + string_range.end = idx; + } + + let mut registry = None; + let image; + if let Some(idx) = file[string_range.clone()].find("/") { + let head = &file[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; + } else { + registry = None; + image = string_range; + } + } else { + image = string_range; + } + + return DockerRef { + full_range: chunk.position.clone(), + + registry, + image, + tag, + digest, + }; + } +} + +#[derive(Debug)] +struct Update { + position: Range<usize>, + content: String, +} + +fn fetch_new_image(file: &str, auth: &Auth, img: DockerRef, edits: &mut Vec<Update>) { + let mut resulting_ref = img.clone(); + + dbg!(&img); + let registry = img.registry.map(|x| &file[x]).unwrap_or("https://registry.jnsn.dev/"); + let tag = img.tag.map(|x| &file[x]).unwrap_or("latest"); + + if tag == "latest" || img.digest.is_some() { + let url = format!("https://{}/v2/{}/manifests/{}", registry, &file[img.image], tag); + dbg!(&url); + let mut response = auth.apply(ureq::get(url)) + .call().unwrap(); + + + let body: tinyjson::JsonValue = response + .body_mut() + .read_to_string().unwrap().parse().unwrap(); + + + let media_type : &String = body["mediaType"].get().unwrap(); + assert!(media_type == "application/vnd.docker.distribution.manifest.v2+json"); + + let digest = &response.headers()["docker-content-digest"]; + // resulting_ref.digest = Some(digest.to_str().unwrap().to_string()); + + // dbg!(&resulting_ref.raw[resulting_ref.digest.as_ref().unwrap().clone()]); + dbg!(&resulting_ref); + + edits.push(Update{ + position: img.full_range, + content: "AHH".to_string(), + }); + } +} + +fn main() { + let argv: Vec<String> = std::env::args().collect(); + let mut it = argv.iter(); + let cmd = &it.next().unwrap(); + + let mut auth = Auth::new(); + let mut positional: Vec<&str> = vec!(); + + loop { + match it.next().map(|x| x.as_str()) { + None => break, + Some("--auth") => { + if let Some(username) = it.next() && let Some(password) = it.next() { + auth.parse(username, password); + } else { + println!("Error: --auth requires two parameters"); + help(cmd); + std::process::exit(1); + } + }, + Some("-h") | Some("--help") => { + help(cmd); + std::process::exit(0); + }, + Some(arg) => positional.push(arg), + }; + } + + if positional.len() != 1 { + panic!("Bad arguments"); + } + let file = positional[0]; + + 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 mut edits = vec![]; + + let images : Vec<_> = images.iter() + .map(|x| DockerRef::parse(&file_content, x)) + .map(|x| fetch_new_image(&file_content, &auth, x, &mut edits)) + .collect(); + + dbg!(&images); + dbg!(&edits); + // dbg!(&file_content[images[0].digest.as_ref().unwrap().clone()]); + + 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); +} |
