use base64::prelude::*; use yaml_rust2::parser::Parser; use yaml_rust2::Event; use std::ops::Range; fn help(cmd: &str) { println!( "{} [options] [--] Search FILE for docker images and suggest updates Options: --auth Authenticate against registry", cmd ); } struct Auth { header: Option, } 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(&self, req: ureq::RequestBuilder) -> ureq::RequestBuilder { 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, } fn scan_yaml_for_images>(mut yaml: Parser) -> Vec { 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 { fn subslice_range(&self, inner: &Self) -> Option>; } impl SubsliceOffset for [T] { fn subslice_range(&self, subslice: &[T]) -> Option> { if size_of::() == 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::()) { return None; } let start = byte_start / size_of::(); 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, registry: Option>, image: Range, tag: Option>, digest: Option>, } 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, content: String, } fn fetch_new_image(file: &str, auth: &Auth, img: DockerRef, edits: &mut Vec) { 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 = 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); }