summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJesper Jensen <jesper@jnsn.dev>2025-12-15 07:50:43 +0100
committerJesper Jensen <jesper@jnsn.dev>2025-12-15 07:50:43 +0100
commit813916f1729af7b9f1f2b66126e9a23f3bb49113 (patch)
treefef4ed00356e0a30e810b0f35394e9e23c3e0fa7 /src
INITIAL COMMIT
Diffstat (limited to 'src')
-rw-r--r--src/main.rs279
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);
+}