use crate::dependent_version_content::PolicyIncomplete;
use crate::dependent_version_manifest::shape;
use crate::dependent_version_release::json_quote_units;
const VERSION_KEY: &[u8] = b"\"version\"";
fn literal_end(bytes: &[u8], start: usize) -> Option<usize> {
let mut escaped: bool = false;
for (index, byte) in bytes.iter().enumerate().skip(start + 1) {
if escaped {
escaped = false;
} else if *byte == b'\\' {
escaped = true;
} else if *byte == b'"' {
return Some(index);
}
}
return None;
}
fn skip_whitespace(bytes: &[u8], start: usize) -> usize {
return bytes
.iter()
.skip(start)
.position(|byte: &u8| return !matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
.map_or(bytes.len(), |offset: usize| return start + offset);
}
fn unterminated(path: &[u8]) -> PolicyIncomplete {
return PolicyIncomplete::ManifestShape {
path: path.to_vec(),
problem: String::from("manifest ends inside a string literal"),
};
}
pub fn replace_manifest_version(
path: &[u8],
text: &str,
from: &str,
to: &str,
) -> Result<String, PolicyIncomplete> {
let bytes: &[u8] = text.as_bytes();
let mut depth: i64 = 0;
let mut resume: usize = 0;
for (index, byte) in bytes.iter().enumerate() {
if index < resume {
continue;
}
match byte {
b'{' | b'[' => depth += 1,
b'}' | b']' => depth -= 1,
b'"' => {
let end: usize =
literal_end(bytes, index).ok_or_else(|| return unterminated(path))?;
let after: usize = skip_whitespace(bytes, end + 1);
if depth == 1
&& &bytes[index..=end] == VERSION_KEY
&& bytes.get(after) == Some(&b':')
{
return replace_value(path, text, after + 1, from, to);
}
resume = end + 1;
}
_ => {}
}
}
return Err(shape(path, "has no top-level \"version\""));
}
fn replace_value(
path: &[u8],
text: &str,
start: usize,
from: &str,
to: &str,
) -> Result<String, PolicyIncomplete> {
let bytes: &[u8] = text.as_bytes();
let value_start: usize = skip_whitespace(bytes, start);
if bytes.get(value_start) != Some(&b'"') {
return Err(shape(path, "has a non-string top-level \"version\""));
}
let value_end: usize =
literal_end(bytes, value_start).ok_or_else(|| return unterminated(path))?;
let literal: &str = &text[value_start..=value_end];
let expected: String = json_quote_units(&from.encode_utf16().collect::<Vec<u16>>());
if literal != expected {
return Err(shape(
path,
&format!("declares version {literal}, expected {expected}"),
));
}
let replacement: String = json_quote_units(&to.encode_utf16().collect::<Vec<u16>>());
return Ok(format!(
"{}{replacement}{}",
&text[..value_start],
&text[value_end + 1..]
));
}
#[cfg(test)]
#[path = "dependent_version_text_tests.rs"]
mod tests;