diff options
| author | Jesper Jensen <jesper@jnsn.dev> | 2026-01-19 21:05:38 +0100 |
|---|---|---|
| committer | Jesper Jensen <jesper@jnsn.dev> | 2026-01-19 21:05:38 +0100 |
| commit | ef21d01f08dc94ba355f596ddb23e1e32c43c8d4 (patch) | |
| tree | 6044255f32f894ea52c28c63e7a56d347386b618 /src/main.rs | |
| parent | 42e48a4704242b1f63092902b6b40e254c64cad7 (diff) | |
INITIAL COMMIT (again)
Diffstat (limited to 'src/main.rs')
| -rw-r--r-- | src/main.rs | 556 |
1 files changed, 337 insertions, 219 deletions
diff --git a/src/main.rs b/src/main.rs index dd0041c..8933c96 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,11 +2,15 @@ mod parser; use crate::parser::*; use base64::prelude::*; +use rand::distr::{Alphanumeric, SampleString}; use yaml_rust2::parser::Parser; use yaml_rust2::Event; use std::collections::HashMap; use std::ops::Range; use std::sync; +use std::io::Read; +use std::io::Seek; +use std::io::Write; fn help(cmd: &str) { println!( @@ -28,29 +32,17 @@ enum AuthMethod { impl AuthMethod { fn from_header(header: &ureq::http::HeaderValue) -> Option<Self> { let header_str = header.to_str().unwrap(); - let (parse, _) = parse_challenge(&header_str.chars().collect::<Vec<char>>(), 0).unwrap(); + let parse = parse_authenticate_header(header_str).unwrap(); if &header_str[parse.scheme.clone()] == "Basic" { return Some(AuthMethod::Basic); } if &header_str[parse.scheme.clone()] == "Bearer" { - let mut realm_param = None; - let mut scope_param = None; - let mut service_param = None; - for param in parse.params { - match &header_str[param.key.clone()] { - "realm" => realm_param = Some(param), - "scope" => scope_param = Some(param), - "service" => service_param = Some(param), - _ => {}, - } - } - return Some(AuthMethod::Bearer{ - realm: header_str[realm_param.unwrap().value].to_string(), - scope: header_str[scope_param.unwrap().value].to_string(), - service: header_str[service_param.unwrap().value].to_string(), + realm: header_str[parse.realm.unwrap()].to_string(), + scope: header_str[parse.scope.unwrap()].to_string(), + service: header_str[parse.service.unwrap()].to_string(), }); } @@ -66,7 +58,6 @@ struct AuthInfo { enum AuthStage { Idle, - Unauthorized, Authorized(String), } @@ -104,60 +95,56 @@ impl AuthState{ return req } - fn attempt_authorization(&mut self, response: &ureq::http::Response<ureq::Body>) -> Result<(), ()> { - if let AuthStage::Idle = self.stage { - if let Some(auth_header) = response.headers().get("www-authenticate") { - let auth_header = AuthMethod::from_header(auth_header); - match auth_header { - Some(AuthMethod::Basic) => { - let basic_auth = format!("Basic {}", BASE64_STANDARD.encode(format!("{}:{}", self.info.username, self.info.password))); - self.stage = AuthStage::Authorized(basic_auth); - - return Ok(()); - }, - Some(AuthMethod::Bearer{realm, scope, service}) => { - let basic_auth = format!("Basic {}", BASE64_STANDARD.encode(format!("{}:{}", self.info.username, self.info.password))); + fn authenticate(&mut self, response: &ureq::http::Response<ureq::Body>) -> Result<(), String> { + match self.stage { + AuthStage::Authorized(_) => + // The token must have expired + self.stage = AuthStage::Idle, + AuthStage::Idle => {}, + } - let url = format!("{}?service={}&scope={}", realm, service, scope); + let auth_header = response.headers().get("www-authenticate").unwrap(); + match AuthMethod::from_header(auth_header) { + Some(AuthMethod::Basic) => { + let basic_auth = format!("Basic {}", BASE64_STANDARD.encode(format!("{}:{}", self.info.username, self.info.password))); + self.stage = AuthStage::Authorized(basic_auth); - let body = ureq::get(url) - .config().http_status_as_error(false).build() - .header("Authorization", basic_auth) - .call(); + return Ok(()); + }, + Some(AuthMethod::Bearer{realm, scope, service}) => { + let basic_auth = format!("Basic {}", BASE64_STANDARD.encode(format!("{}:{}", self.info.username, self.info.password))); - dbg!(&body); + let url = format!("{}?service={}&scope={}", realm, service, scope); - let body = body.unwrap() - .body_mut().read_to_string().unwrap(); + let body = ureq::get(url) + .config().http_status_as_error(false).build() + .header("Authorization", basic_auth) + .call(); - dbg!(&body); + let body = body.map_err(|x| format!("Server {} authentication request failed: {}", self.info.host, x.to_string()))? + .body_mut().read_to_string().expect(format!("Server {} responded with something non-string like", self.info.host).as_str()); - let body: tinyjson::JsonValue = body - .parse().unwrap(); + let body = body.parse::<tinyjson::JsonValue>() + .unwrap(); - let token: &String = body["token"].get().unwrap(); + let token = body["token"].get::<String>().unwrap(); + self.stage = AuthStage::Authorized(format!("Bearer {}", token)); + return Ok(()); - self.stage = AuthStage::Authorized(format!("Bearer {}", token)); - return Ok(()); - }, - None => todo!("Failed parsing the challenge header"), - } - } else { - todo!("Server didn't ask us to authenticate"); - } - } else { - todo!("Authorized request somehow failed (expired token?)"); + }, + None => Err(format!("Server {} provided us with a challenge, but we didn't understand it", self.info.host)), } } } -fn perform_registry_request(registry: &str, url: &str, auth: &mut Auth) -> Result<ureq::http::Response<ureq::Body>, ()> { +fn perform_registry_request(registry: &str, url: &str, accept: &'static str, auth: &mut Auth) -> Result<ureq::http::Response<ureq::Body>, String> { let mut state = auth.states.get_mut(registry); let url = format!("https://{}{}", registry, &url); for _ in 0..2 { let mut request = ureq::get(&url) + .header("Accept", accept) .config().http_status_as_error(false).build(); if let Some(ref state) = state { @@ -168,17 +155,19 @@ fn perform_registry_request(registry: &str, url: &str, auth: &mut Auth) -> Resul if response.status() == 401 { if let Some(ref mut state) = state { - state.attempt_authorization(&response)?; + state.authenticate(&response)?; continue; } else { - todo!("Server returned 401 but we have no credentials"); + return Err(format!("Server {} returned 401 but we have no credentials", registry)); } + } else if response.status() != 200 { + return Err(format!("Unexpected status code: {}", response.status())) } return Ok(response); } - return Err(()); + return Err("Authorization failed".to_string()); } enum YContext { @@ -189,94 +178,70 @@ enum YContext { } #[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>>; +struct ManifestFile { + image_tags: Vec<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(); +impl ManifestFile { + fn parse(content: &str) -> Self { + let mut yaml = Parser::new_from_str(content); - let byte_start = subslice_start.wrapping_sub(self_start); + let mut images = vec!(); + let mut scope = vec!(); - if !byte_start.is_multiple_of(size_of::<T>()) { - return None; + 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), + } } - 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 } + return Self{ + image_tags: images, + }; } } #[derive(Debug, Clone)] struct DockerRef { - full_range: Range<usize>, - registry: Option<Range<usize>>, image: Range<usize>, tag: Option<Range<usize>>, @@ -284,25 +249,25 @@ struct DockerRef { } impl DockerRef{ - fn parse(file: &str, chunk: &Chunk) -> DockerRef { - let mut string_range = chunk.position.clone(); + fn parse(content: &str, chunk: &Range<usize>) -> DockerRef { + let mut string_range = chunk.clone(); let mut digest = None; - if let Some(idx) = file[string_range.clone()].rfind("@") { + 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) = file[string_range.clone()].rfind(":") { + 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) = file[string_range.clone()].find("/") { - let head = &file[string_range.clone()][..idx]; + 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; @@ -315,8 +280,6 @@ impl DockerRef{ } return DockerRef { - full_range: chunk.position.clone(), - registry, image, tag, @@ -326,16 +289,21 @@ impl DockerRef{ } #[derive(Debug)] -struct Update { +struct FilePatch { position: Range<usize>, content: String, } #[derive(Debug)] -struct Version { - // It always starts with a string part, so the first string can be empty - string_parts: Vec<String>, - number_parts: Vec<u64>, +enum VersionPart { + String(String), + Number(u64), + Hash, +} + +#[derive(Debug)] +struct VersionPattern { + parts: Vec<VersionPart>, } #[derive(Debug)] @@ -347,41 +315,44 @@ enum CompareOutcome { Identical, } -impl Version { +impl VersionPattern { fn parse(tag: &str) -> Self { - static RE: sync::LazyLock<regex::Regex> = sync::LazyLock::new(|| regex::Regex::new(r"(?<str>[^0-9]*)(?<num>[0-9]+)").unwrap()); - let mut string_parts = vec!(); - let mut number_parts = vec!(); + 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) { - string_parts.push(it.name("str").unwrap().as_str().to_string()); - number_parts.push(it.name("num").unwrap().as_str().to_string().parse().unwrap()); + 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 Version { - string_parts, - number_parts, + return VersionPattern { + parts, } } fn compare(&self, other: &Self) -> CompareOutcome { - assert!(self.number_parts.len() == self.string_parts.len()); - assert!(other.number_parts.len() == other.string_parts.len()); - - if self.number_parts.len() != other.number_parts.len() { + if self.parts.len() != other.parts.len() { return CompareOutcome::Incompatible; } - for (self_string, other_string) in self.string_parts.iter().zip(other.string_parts.iter()) { - if self_string != other_string { - return CompareOutcome::Incompatible; - } - } - - for (self_number, other_number) in self.number_parts.iter().zip(other.number_parts.iter()) { - if self_number > other_number { - return CompareOutcome::Lower; - } else if self_number < other_number { - return CompareOutcome::Higher; + for (self_part, other_part) in self.parts.iter().zip(other.parts.iter()) { + match (self_part, other_part) { + (VersionPart::String(x1), VersionPart::String(x2)) => if x1 != x2 { return CompareOutcome::Incompatible }, + (VersionPart::String(_), _) => return CompareOutcome::Incompatible, + (VersionPart::Number(x1), VersionPart::Number(x2)) => { + if x1 > x2 { + return CompareOutcome::Lower + } else if x1 > x2 { + return CompareOutcome::Higher + } + }, + (VersionPart::Number(_), _) => return CompareOutcome::Incompatible, + (VersionPart::Hash, VersionPart::Hash) => {}, + (VersionPart::Hash, _) => return CompareOutcome::Incompatible, } } @@ -389,31 +360,33 @@ impl Version { } } -fn fetch_new_image(file: &str, auth: &mut Auth, img: DockerRef, edits: &mut Vec<Update>) { - let registry = img.registry.map(|x| &file[x]).unwrap_or("registry.jnsn.dev/"); - let mut tag = img.tag.map(|x| file[x].to_string()); - - let mut digest = None; - +fn update_images(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()); if let Some(ref tag_str) = tag { - let mut current = Version::parse(&tag_str); + let mut current = VersionPattern::parse(&tag_str); + + let mut new_tag = None; let mut url = format!("/v2/{}/tags/list", &file[img.image.clone()]); loop { - let mut response = perform_registry_request(registry, &url, auth).unwrap(); + let mut response = perform_registry_request(registry, &url, "application/vnd.oci.image.index.v1+json", auth).unwrap(); - let link_str = response.headers()["link"].to_str().unwrap(); - let (link, _) = parse_link(&link_str.chars().collect(), 0).unwrap(); - dbg!(&link_str[link.get(0).unwrap().params.get(0).unwrap().value().clone()]); + let content_type = response.headers()["Content-Type"].to_str().unwrap(); + if !content_type.starts_with("application/json") { + return Err(format!("Unexpected Content-Type: {}", content_type)); + } + + let body = response.body_mut().read_to_string().unwrap(); + let body = body.parse::<tinyjson::JsonValue>().unwrap(); - let body: tinyjson::JsonValue = response.body_mut().read_to_string().unwrap().parse().unwrap(); for it in body["tags"].get::<Vec<tinyjson::JsonValue>>().unwrap().iter() { let candidate_str = it.get::<String>().unwrap(); - let candidate = Version::parse(candidate_str); + let candidate = VersionPattern::parse(candidate_str); match current.compare(&candidate) { CompareOutcome::Higher => { - tag = Some(candidate_str.clone()); + new_tag = Some(candidate_str.clone()); current = candidate; }, CompareOutcome::Lower => {}, @@ -421,33 +394,113 @@ fn fetch_new_image(file: &str, auth: &mut Auth, img: DockerRef, edits: &mut Vec< CompareOutcome::Identical => {}, } } - dbg!(&tag); + + if let Some(link_header) = response.headers().get("link") { + let link_str = link_header.to_str().unwrap(); + let link = extract_next_page(&link_str).unwrap(); + url = link_str[link.next_uri.unwrap().clone()].to_string(); + } else { + break; + } + } + + if let Some(new_tag) = new_tag { + tag = Some(new_tag.clone()); + edits.push(FilePatch { + position: img.tag.unwrap(), + content: new_tag, + }); } } - if img.digest.is_some() { + if let Some(ref digest) = img.digest { // Find the digest for the selected tag - let url = format!("/v2/{}/manifests/{}", &file[img.image], tag.unwrap_or("latest".to_string())); - let mut response = perform_registry_request(registry, &url, auth).unwrap(); + let url = format!("/v2/{}/manifests/{}", &file[img.image.clone()], tag.as_deref().unwrap_or("latest")); + let response = perform_registry_request(registry, &url, "application/vnd.oci.image.manifest.v1+json,application/vnd.oci.image.index.v1+json", auth).unwrap(); - let body: tinyjson::JsonValue = response.body_mut().read_to_string().unwrap().parse().unwrap(); - dbg!(&body); + let digest_string = response.headers()["docker-content-digest"].to_str().unwrap().to_string(); - // let media_type : &String = body["mediaType"].get().unwrap(); - // assert!(media_type == "application/vnd.docker.distribution.manifest.v2+json"); + if file[digest.clone()] != digest_string { + edits.push(FilePatch{ + position: img.digest.unwrap(), + content: digest_string, + }); + } + } + + return Ok(()); +} + +enum Output { + Overlay(std::fs::File, std::path::PathBuf, std::path::PathBuf), + Stdout(std::io::Stdout), +} - digest = Some(response.headers()["docker-content-digest"].to_str().unwrap().to_string()); +impl Output { + fn overlay_file(infile_path: &std::path::Path) -> Self { + loop { + let mut filename = std::ffi::OsString::new(); + filename.push(infile_path.file_name().unwrap()); + filename.push(std::ffi::OsStr::new(".edit")); + filename.push(Alphanumeric.sample_string(&mut rand::rng(), 16)); + let outfile_path = Some(infile_path.parent().unwrap().join(filename)); + if let Ok(output_file) = std::fs::File::create_new(outfile_path.as_ref().unwrap()) { + return Output::Overlay(output_file, outfile_path.unwrap().into(), infile_path.into()); + } + } } - let image_ref = { - let prefix = &file[img.full_range.start..img.digest.unwrap().start]; - format!("{}{}", prefix, digest.unwrap_or("".to_string())) - }; + fn commit(&mut self) { + match self { + Output::Overlay(_, outfile_path, infile_path) => { + std::fs::remove_file(&infile_path).unwrap(); + std::fs::rename(&outfile_path, &infile_path).unwrap(); + } + Output::Stdout(_) => {} + } + } +} - edits.push(Update{ - position: img.full_range, - content: image_ref, - }); +impl std::ops::Deref for Output { + type Target = dyn Write; + + fn deref(&self) -> &Self::Target { + match self { + Output::Overlay(file, _, _) => file, + Output::Stdout(stdout) => stdout, + } + } +} + +impl std::ops::DerefMut for Output { + fn deref_mut(&mut self) -> &mut Self::Target { + match self { + Output::Overlay(file, _, _) => file, + Output::Stdout(stdout) => stdout, + } + } +} + +impl std::io::Write for Output { + fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { + return (**self).write(buf); + } + + fn flush(&mut self) -> std::io::Result<()> { + return (**self).flush(); + } + + fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result<usize> { + return (**self).write_vectored(bufs); + } + + fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> { + return (**self).write_all(buf); + } + + fn write_fmt(&mut self, args: std::fmt::Arguments<'_>) -> std::io::Result<()> { + return (**self).write_fmt(args); + } } fn main() { @@ -457,6 +510,7 @@ fn main() { let mut auths = vec![]; let mut positional: Vec<&str> = vec!(); + let mut overwrite = false; loop { match it.next().map(|x| x.as_str()) { @@ -474,6 +528,9 @@ fn main() { std::process::exit(1); } }, + Some("-i") | Some("--inplace") => { + overwrite = true; + }, Some("-h") | Some("--help") => { help(cmd); std::process::exit(0); @@ -486,28 +543,89 @@ fn main() { if positional.len() != 1 { panic!("Bad arguments"); } - let file = positional[0]; - + let infile_path = std::path::PathBuf::from(positional[0]); let mut auth = Auth::new(auths); - 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 inpaths = if infile_path.is_dir() { + let mut unsearched = vec![infile_path.clone()]; + let mut paths = vec![]; - let mut edits = vec![]; + while let Some(next) = unsearched.pop() { + for child in next.read_dir().unwrap() { + let child = child.unwrap(); + let path = child.path(); - let images : Vec<_> = images.iter() - .map(|x| DockerRef::parse(&file_content, x)) - .map(|x| fetch_new_image(&file_content, &mut auth, x, &mut edits)) - .collect(); + let ft = child.file_type().unwrap(); + if ft.is_dir() { + unsearched.push(path); + continue; + } - dbg!(&images); - dbg!(&edits); - // dbg!(&file_content[images[0].digest.as_ref().unwrap().clone()]); + if let Some(ext) = path.extension() { + if ext == "yaml" { + paths.push(path); + } + } + } + } + + paths + } else { + vec![infile_path] + }; + + for infile_path in inpaths { + let mut file = std::fs::File::open(&infile_path).unwrap(); + let mut file_content = String::new(); + file.read_to_string(&mut file_content).unwrap(); - // 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); + let images = ManifestFile::parse(&file_content); + + let mut failed = None; + + let mut edits = vec![]; + for ref image in images.image_tags { + let image_ref = DockerRef::parse(&file_content, image); + if let Err(msg) = update_images(&file_content, &mut auth, image_ref, &mut edits) { + failed = Some(msg); + break; + } + } + + if let Some(msg) = failed { + println!("{}: {} ", infile_path.display(), msg); + continue; + } + + let mut out = if overwrite { + Output::overlay_file(&infile_path) + } else { + Output::Stdout(std::io::stdout()) + }; + + let mut current_position = 0; + file.seek(std::io::SeekFrom::Start(0)).unwrap(); + let mut file_block = file.take(0); + for edit in edits { + if edit.position.start > current_position { + file_block.set_limit((edit.position.start - current_position) as u64); + std::io::copy(&mut file_block, &mut out).unwrap(); + } + + out.write_all(edit.content.as_bytes()).unwrap(); + + // We should have been able to just seek to the position.end here, but that doesn't work + // for whatever reason. What we can do is calculate the amount to skip ahead, and then do + // that. + let skip = (edit.position.end - edit.position.start) as i64; + file_block.set_limit(skip as u64); + file_block.seek(std::io::SeekFrom::Current(skip)).unwrap(); + current_position = edit.position.end; + } + + file_block.set_limit(u64::MAX); + std::io::copy(&mut file_block, &mut out).unwrap(); + + out.commit(); + } } |
