use crate::dependent_version_content::{PolicyIncomplete, display_path};
use monochromatic_jsonc_edit::{JsoncEntry, JsoncValue, parse_jsonc};
const BYTE_ORDER_MARK: char = '\u{feff}';
const RUNTIME_DEPENDENCY_FIELDS: &[&str] =
&["dependencies", "peerDependencies", "optionalDependencies"];
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ManifestFacts {
pub name: String,
pub version: Option<Vec<u16>>,
pub runtime_dependency_names: Vec<String>,
pub dev_dependency_names: Vec<String>,
}
pub fn strict_text<'bytes>(
path: &[u8],
bytes: &'bytes [u8],
) -> Result<&'bytes str, PolicyIncomplete> {
let text: &str = match std::str::from_utf8(bytes) {
Ok(decoded) => decoded,
Err(_) => {
return Err(PolicyIncomplete::NotUtf8 {
path: path.to_vec(),
});
}
};
return Ok(text.strip_prefix(BYTE_ORDER_MARK).unwrap_or(text));
}
fn jsonc_extension(text: &str) -> Option<&'static str> {
let mut in_string: bool = false;
let mut escaped: bool = false;
let mut comma_pending: bool = false;
for byte in text.bytes() {
if in_string {
if escaped {
escaped = false;
} else if byte == b'\\' {
escaped = true;
} else if byte == b'"' {
in_string = false;
}
continue;
}
match byte {
b'/' => return Some("comments"),
b',' => comma_pending = true,
b'}' | b']' if comma_pending => return Some("trailing commas"),
b' ' | b'\t' | b'\r' | b'\n' => {}
b'"' => {
in_string = true;
comma_pending = false;
}
_ => comma_pending = false,
}
}
if comma_pending {
return Some("trailing commas");
}
return None;
}
fn parse_json(path: &[u8], text: &str) -> Result<JsoncValue, PolicyIncomplete> {
let syntax = |detail: String| {
return PolicyIncomplete::ManifestSyntax {
path: path.to_vec(),
detail,
};
};
if let Some(extension) = jsonc_extension(text) {
return Err(syntax(format!("JSON allows no {extension}")));
}
let wrapped: JsoncValue = match parse_jsonc(&format!("[{text}]")) {
Ok(value) => value,
Err(error) => return Err(syntax(error.message)),
};
if let Some([single]) = wrapped.elements() {
return Ok(single.clone());
}
return Err(syntax(String::from(
"the text is not exactly one JSON value",
)));
}
fn member<'value>(entries: &'value [JsoncEntry], key: &str) -> Option<&'value JsoncValue> {
return entries
.iter()
.rev()
.find(|entry: &&JsoncEntry| return entry.key.units.iter().copied().eq(key.encode_utf16()))
.map(|entry: &JsoncEntry| return &entry.value);
}
fn dependency_names(entries: &[JsoncEntry], field: &str, names: &mut Vec<String>) {
let start: usize = names.len();
let fields: &[JsoncEntry] = member(entries, field)
.and_then(JsoncValue::entries)
.unwrap_or(&[]);
for entry in fields {
if let Ok(name) = String::from_utf16(&entry.key.units)
&& !names[start..].contains(&name)
{
names.push(name);
}
}
}
pub fn shape(path: &[u8], problem: &str) -> PolicyIncomplete {
return PolicyIncomplete::ManifestShape {
path: path.to_vec(),
problem: format!("{} {problem}", display_path(path)),
};
}
pub fn read_manifest_facts(path: &[u8], text: &str) -> Result<ManifestFacts, PolicyIncomplete> {
let value: JsoncValue = parse_json(path, text)?;
let Some(entries) = value.entries() else {
return Err(shape(path, "is not a JSON object"));
};
let Some(name_units) = member(entries, "name").and_then(JsoncValue::text_units) else {
return Err(shape(path, "has no string \"name\""));
};
let Ok(name) = String::from_utf16(name_units) else {
return Err(shape(
path,
"has a \"name\" holding an unpaired UTF-16 surrogate",
));
};
let version: Option<Vec<u16>> = match member(entries, "version") {
None => None,
Some(found) => match found.text_units() {
Some(units) => Some(units.to_vec()),
None => return Err(shape(path, "has a non-string \"version\"")),
},
};
let mut runtime_dependency_names: Vec<String> = Vec::new();
for field in RUNTIME_DEPENDENCY_FIELDS {
dependency_names(entries, field, &mut runtime_dependency_names);
}
let mut dev_dependency_names: Vec<String> = Vec::new();
dependency_names(entries, "devDependencies", &mut dev_dependency_names);
return Ok(ManifestFacts {
name,
version,
runtime_dependency_names,
dev_dependency_names,
});
}
#[cfg(test)]
#[path = "dependent_version_manifest_tests.rs"]
mod tests;