summaryrefslogtreecommitdiff
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
INITIAL COMMIT
-rw-r--r--.gitignore1
-rw-r--r--Cargo.lock397
-rw-r--r--Cargo.toml10
-rw-r--r--src/main.rs279
4 files changed, 687 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..cfa1796
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,397 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "arraydeque"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bytes"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
+
+[[package]]
+name = "cc"
+version = "1.2.49"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
+
+[[package]]
+name = "flate2"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "getrandom"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "foldhash",
+]
+
+[[package]]
+name = "hashlink"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
+dependencies = [
+ "hashbrown",
+]
+
+[[package]]
+name = "http"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "libc"
+version = "0.2.178"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
+
+[[package]]
+name = "log"
+version = "0.4.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom",
+ "libc",
+ "untrusted",
+ "windows-sys",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f"
+dependencies = [
+ "log",
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c"
+dependencies = [
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "tinyjson"
+version = "2.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ab95735ea2c8fd51154d01e39cf13912a78071c2d89abc49a7ef102a7dd725a"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "ureq"
+version = "3.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d39cb1dbab692d82a977c0392ffac19e188bd9186a9f32806f0aaa859d75585a"
+dependencies = [
+ "base64",
+ "flate2",
+ "log",
+ "percent-encoding",
+ "rustls",
+ "rustls-pki-types",
+ "ureq-proto",
+ "utf-8",
+ "webpki-roots",
+]
+
+[[package]]
+name = "ureq-proto"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f"
+dependencies = [
+ "base64",
+ "http",
+ "httparse",
+ "log",
+]
+
+[[package]]
+name = "utf-8"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
+[[package]]
+name = "version-bumper"
+version = "0.1.0"
+dependencies = [
+ "base64",
+ "tinyjson",
+ "ureq",
+ "yaml-rust2",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "webpki-roots"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "yaml-rust2"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9"
+dependencies = [
+ "arraydeque",
+ "encoding_rs",
+ "hashlink",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..e8ba899
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "version-bumper"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
+base64 = "0.22.1"
+tinyjson = "2.5.1"
+ureq = "3.1.4"
+yaml-rust2 = "0.10.4"
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);
+}