diff options
| author | Jesper Jensen <jesper@jnsn.dev> | 2026-02-06 12:45:45 +0100 |
|---|---|---|
| committer | Jesper Jensen <jesper@jnsn.dev> | 2026-02-06 12:45:45 +0100 |
| commit | 6ef619e5ec2349a1598896b40f2e9ea5d0163a4f (patch) | |
| tree | ee227b58a50afa72358d0bb8c3749d6833e944ec /src/main.rs | |
| parent | 822de848fc1803dbc6032f90ef449b5b37717667 (diff) | |
Add tests for the parsing logic
Diffstat (limited to 'src/main.rs')
| -rw-r--r-- | src/main.rs | 231 |
1 files changed, 44 insertions, 187 deletions
diff --git a/src/main.rs b/src/main.rs index 49ffcfb..afb6e77 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,19 @@ mod parser; +mod version; +mod docker; +mod manifest; + use crate::parser::*; +use crate::version::{VersionPattern, CompareOutcome}; +use crate::docker::DockerRef; +use crate::manifest::ManifestFile; use base64::prelude::*; use rand::distr::{Alphanumeric, SampleString}; use rusqlite::Connection; -use yaml_rust2::parser::Parser; -use yaml_rust2::Event; +use rusqlite::OptionalExtension; use std::collections::HashMap; use std::ops::Range; -use std::sync; use std::io::Read; use std::io::Seek; use std::io::Write; @@ -194,123 +199,7 @@ struct Repository { dest: std::path::PathBuf, } -enum YContext { - InDocument, - InObject, - InSequence, - InValue(bool), -} - -#[derive(Debug)] -struct ManifestFile { - image_tags: Vec<Range<usize>>, -} - -impl ManifestFile { - fn parse(content: &str) -> Self { - let mut yaml = Parser::new_from_str(content); - - 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 { - 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), - } - } - - return Self{ - image_tags: images, - }; - } -} - -#[derive(Debug, Clone)] -struct DockerRef { - registry: Option<Range<usize>>, - image: Range<usize>, - tag: Option<Range<usize>>, - digest: Option<Range<usize>>, -} - -impl DockerRef{ - fn parse(content: &str, chunk: &Range<usize>) -> DockerRef { - let mut string_range = chunk.clone(); - - let mut digest = None; - 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) = 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) = 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; - } else { - registry = None; - image = string_range; - } - } else { - image = string_range; - } - return DockerRef { - registry, - image, - tag, - digest, - }; - } -} #[derive(Debug)] struct FilePatch { @@ -318,84 +207,34 @@ struct FilePatch { content: String, } -#[derive(Debug)] -enum VersionPart { - String(String), - Number(u64), - Hash, -} - -#[derive(Debug)] -struct VersionPattern { - parts: Vec<VersionPart>, -} - -#[derive(Debug)] -enum CompareOutcome { - Higher, - Lower, - - Incompatible, - Identical, -} - -impl VersionPattern { - fn parse(tag: &str) -> Self { - 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) { - 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 VersionPattern { - parts, - } - } - - fn compare(&self, other: &Self) -> CompareOutcome { - if self.parts.len() != other.parts.len() { - return CompareOutcome::Incompatible; - } - - let mut state = CompareOutcome::Identical; - for (self_part, other_part) in self.parts.iter().zip(other.parts.iter()) { - match (&state, self_part, other_part) { - (_, VersionPart::String(x1), VersionPart::String(x2)) => if x1 != x2 { return CompareOutcome::Incompatible }, - (_, VersionPart::String(_), _) => return CompareOutcome::Incompatible, - (CompareOutcome::Identical, VersionPart::Number(x1), VersionPart::Number(x2)) => { - if x1 > x2 { - state = CompareOutcome::Lower; - } else if x1 < x2 { - state = CompareOutcome::Higher; - } - }, - (_, VersionPart::Number(_), VersionPart::Number(_)) => {}, - (_, VersionPart::Number(_), _) => return CompareOutcome::Incompatible, - (_, VersionPart::Hash, VersionPart::Hash) => {}, - (_, VersionPart::Hash, _) => return CompareOutcome::Incompatible, - } - } - - return state; - } -} fn update_images(db: &Connection, 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()); + let image = &file[img.image.clone()]; + + let _last_checked = match db.query_one(" + SELECT last_checked FROM images + WHERE registry = ?1 AND image = ?2 + ", (registry, image), |row| row.get::<_, chrono::DateTime<chrono::Utc>>(0)).optional() { + Ok(Some(x)) => x, + Ok(None) => { + let time = chrono::offset::Utc::now(); + db.execute(" + INSERT INTO images(registry, image, last_checked) VALUES (?1, ?2, ?3) + ", (registry, &image, time)).unwrap(); + time + }, + Err(_x) => panic!("Database Failure"), + }; + if let Some(ref tag_str) = tag { let mut current = VersionPattern::parse(&tag_str); let mut new_tag = None; - let mut url = format!("/v2/{}/tags/list", &file[img.image.clone()]); + let mut url = format!("/v2/{}/tags/list", &image); loop { let mut response = perform_registry_request(registry, &url, "application/vnd.oci.image.index.v1+json", auth).unwrap(); @@ -847,6 +686,24 @@ fn main() { db.execute("INSERT INTO migrations(id) VALUES (?1)", (4, )).unwrap(); } + + if newest_migration < 5 { + db.execute(" + CREATE TABLE images ( + id INTEGER PRIMARY KEY NOT NULL, + registry TEXT NOT NULL, + image TEXT NOT NULL, + last_checked DATETIME NOT NULL + ) + ", ()).unwrap(); + + db.execute(" + CREATE UNIQUE INDEX images__registry_image + ON images(registry, image) + ", ()).unwrap(); + + db.execute("INSERT INTO migrations(id) VALUES (?1)", (5, )).unwrap(); + } } let mut repos = vec![]; |
