mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 06:34:54 +08:00
实现 Binary、JSON、Text patch 与通用 manifest 校验基础。 验证:cargo test -p bat-patch --locked;cargo clippy -p bat-patch --all-targets --locked -- -D warnings。
367 lines
12 KiB
Rust
367 lines
12 KiB
Rust
//! RFC 6902 JSON Patch support.
|
|
|
|
use crate::PatchError;
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::Value;
|
|
|
|
/// One RFC 6902 JSON Patch operation.
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
#[serde(tag = "op", rename_all = "lowercase")]
|
|
pub enum JsonPatchOperation {
|
|
/// Add a value at the target JSON Pointer.
|
|
Add {
|
|
/// Target JSON Pointer.
|
|
path: String,
|
|
/// Value to insert.
|
|
value: Value,
|
|
},
|
|
/// Remove the value at the target JSON Pointer.
|
|
Remove {
|
|
/// Target JSON Pointer.
|
|
path: String,
|
|
},
|
|
/// Replace the value at the target JSON Pointer.
|
|
Replace {
|
|
/// Target JSON Pointer.
|
|
path: String,
|
|
/// Replacement value.
|
|
value: Value,
|
|
},
|
|
/// Move a value from one JSON Pointer to another.
|
|
Move {
|
|
/// Source JSON Pointer.
|
|
from: String,
|
|
/// Target JSON Pointer.
|
|
path: String,
|
|
},
|
|
/// Copy a value from one JSON Pointer to another.
|
|
Copy {
|
|
/// Source JSON Pointer.
|
|
from: String,
|
|
/// Target JSON Pointer.
|
|
path: String,
|
|
},
|
|
/// Assert that a JSON Pointer currently contains a value.
|
|
Test {
|
|
/// Target JSON Pointer.
|
|
path: String,
|
|
/// Expected value.
|
|
value: Value,
|
|
},
|
|
}
|
|
|
|
/// Applies an RFC 6902 JSON Patch document to a JSON document string.
|
|
pub fn apply_json_patch(doc: &str, patch: &str) -> crate::Result<String> {
|
|
let mut document: Value = serde_json::from_str(doc)
|
|
.map_err(|error| PatchError::ApplyFailed(format!("invalid JSON document: {error}")))?;
|
|
let operations: Vec<JsonPatchOperation> = serde_json::from_str(patch)
|
|
.map_err(|error| PatchError::ApplyFailed(format!("invalid JSON patch: {error}")))?;
|
|
apply_json_patch_value(&mut document, &operations)?;
|
|
serde_json::to_string(&document)
|
|
.map_err(|error| PatchError::ApplyFailed(format!("failed to serialize JSON: {error}")))
|
|
}
|
|
|
|
/// Applies parsed JSON Patch operations to a JSON value.
|
|
///
|
|
/// Each operation is applied atomically: when one operation fails, the document
|
|
/// remains at the state produced by the previous successful operation.
|
|
pub fn apply_json_patch_value(
|
|
document: &mut Value,
|
|
operations: &[JsonPatchOperation],
|
|
) -> crate::Result<()> {
|
|
for operation in operations {
|
|
let mut next = document.clone();
|
|
apply_operation(&mut next, operation)?;
|
|
*document = next;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn apply_operation(document: &mut Value, operation: &JsonPatchOperation) -> crate::Result<()> {
|
|
match operation {
|
|
JsonPatchOperation::Add { path, value } => add_value(document, path, value.clone()),
|
|
JsonPatchOperation::Remove { path } => remove_value(document, path).map(drop),
|
|
JsonPatchOperation::Replace { path, value } => replace_value(document, path, value.clone()),
|
|
JsonPatchOperation::Move { from, path } => {
|
|
if from == path {
|
|
return Ok(());
|
|
}
|
|
let value = get_value(document, from)?.clone();
|
|
remove_value(document, from)?;
|
|
add_value(document, path, value)
|
|
}
|
|
JsonPatchOperation::Copy { from, path } => {
|
|
let value = get_value(document, from)?.clone();
|
|
add_value(document, path, value)
|
|
}
|
|
JsonPatchOperation::Test { path, value } => {
|
|
let actual = get_value(document, path)?;
|
|
if actual == value {
|
|
Ok(())
|
|
} else {
|
|
Err(failed(format!(
|
|
"JSON patch test failed at {path}: expected {value}, actual {actual}"
|
|
)))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn add_value(document: &mut Value, path: &str, value: Value) -> crate::Result<()> {
|
|
let tokens = parse_json_pointer(path)?;
|
|
if tokens.is_empty() {
|
|
*document = value;
|
|
return Ok(());
|
|
}
|
|
|
|
let key = tokens.last().expect("checked non-empty").clone();
|
|
let parent = get_mut_by_tokens(document, &tokens[..tokens.len() - 1])?;
|
|
match parent {
|
|
Value::Object(map) => {
|
|
map.insert(key, value);
|
|
Ok(())
|
|
}
|
|
Value::Array(items) => {
|
|
if key == "-" {
|
|
items.push(value);
|
|
return Ok(());
|
|
}
|
|
let index = parse_array_index(&key, items.len(), true)?;
|
|
items.insert(index, value);
|
|
Ok(())
|
|
}
|
|
other => Err(failed(format!(
|
|
"cannot add JSON patch value below non-container value {other}"
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn remove_value(document: &mut Value, path: &str) -> crate::Result<Value> {
|
|
let tokens = parse_json_pointer(path)?;
|
|
if tokens.is_empty() {
|
|
return Ok(std::mem::take(document));
|
|
}
|
|
|
|
let key = tokens.last().expect("checked non-empty").clone();
|
|
let parent = get_mut_by_tokens(document, &tokens[..tokens.len() - 1])?;
|
|
match parent {
|
|
Value::Object(map) => map
|
|
.remove(&key)
|
|
.ok_or_else(|| failed(format!("JSON patch path {path} does not exist"))),
|
|
Value::Array(items) => {
|
|
let index = parse_array_index(&key, items.len(), false)?;
|
|
Ok(items.remove(index))
|
|
}
|
|
other => Err(failed(format!(
|
|
"cannot remove JSON patch value below non-container value {other}"
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn replace_value(document: &mut Value, path: &str, value: Value) -> crate::Result<()> {
|
|
let tokens = parse_json_pointer(path)?;
|
|
if tokens.is_empty() {
|
|
*document = value;
|
|
return Ok(());
|
|
}
|
|
|
|
let key = tokens.last().expect("checked non-empty").clone();
|
|
let parent = get_mut_by_tokens(document, &tokens[..tokens.len() - 1])?;
|
|
match parent {
|
|
Value::Object(map) => {
|
|
let slot = map
|
|
.get_mut(&key)
|
|
.ok_or_else(|| failed(format!("JSON patch path {path} does not exist")))?;
|
|
*slot = value;
|
|
Ok(())
|
|
}
|
|
Value::Array(items) => {
|
|
let index = parse_array_index(&key, items.len(), false)?;
|
|
items[index] = value;
|
|
Ok(())
|
|
}
|
|
other => Err(failed(format!(
|
|
"cannot replace JSON patch value below non-container value {other}"
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn get_value<'a>(document: &'a Value, path: &str) -> crate::Result<&'a Value> {
|
|
let tokens = parse_json_pointer(path)?;
|
|
let mut current = document;
|
|
for token in &tokens {
|
|
current = match current {
|
|
Value::Object(map) => map
|
|
.get(token)
|
|
.ok_or_else(|| failed(format!("JSON patch path {path} does not exist")))?,
|
|
Value::Array(items) => {
|
|
let index = parse_array_index(token, items.len(), false)?;
|
|
items
|
|
.get(index)
|
|
.ok_or_else(|| failed(format!("JSON patch path {path} does not exist")))?
|
|
}
|
|
other => {
|
|
return Err(failed(format!(
|
|
"cannot traverse JSON patch path {path} through non-container value {other}"
|
|
)))
|
|
}
|
|
};
|
|
}
|
|
Ok(current)
|
|
}
|
|
|
|
fn get_mut_by_tokens<'a>(
|
|
document: &'a mut Value,
|
|
tokens: &[String],
|
|
) -> crate::Result<&'a mut Value> {
|
|
let mut current = document;
|
|
for token in tokens {
|
|
current = match current {
|
|
Value::Object(map) => map
|
|
.get_mut(token)
|
|
.ok_or_else(|| failed(format!("JSON patch path segment {token} does not exist")))?,
|
|
Value::Array(items) => {
|
|
let index = parse_array_index(token, items.len(), false)?;
|
|
items.get_mut(index).ok_or_else(|| {
|
|
failed(format!("JSON patch path segment {token} does not exist"))
|
|
})?
|
|
}
|
|
other => {
|
|
return Err(failed(format!(
|
|
"cannot traverse JSON patch path through non-container value {other}"
|
|
)))
|
|
}
|
|
};
|
|
}
|
|
Ok(current)
|
|
}
|
|
|
|
fn parse_array_index(token: &str, len: usize, allow_end: bool) -> crate::Result<usize> {
|
|
if token.is_empty() || token == "-" {
|
|
return Err(failed(format!("invalid JSON patch array index {token}")));
|
|
}
|
|
let index = token
|
|
.parse::<usize>()
|
|
.map_err(|_| failed(format!("invalid JSON patch array index {token}")))?;
|
|
let max = if allow_end {
|
|
len
|
|
} else {
|
|
len.checked_sub(1)
|
|
.ok_or_else(|| failed("JSON patch array index exceeds empty array".to_string()))?
|
|
};
|
|
if index > max {
|
|
return Err(failed(format!(
|
|
"JSON patch array index {index} exceeds length {len}"
|
|
)));
|
|
}
|
|
Ok(index)
|
|
}
|
|
|
|
fn parse_json_pointer(pointer: &str) -> crate::Result<Vec<String>> {
|
|
if pointer.is_empty() {
|
|
return Ok(Vec::new());
|
|
}
|
|
if !pointer.starts_with('/') {
|
|
return Err(failed(format!(
|
|
"JSON patch pointer must be empty or start with '/': {pointer}"
|
|
)));
|
|
}
|
|
pointer[1..]
|
|
.split('/')
|
|
.map(decode_json_pointer_token)
|
|
.collect()
|
|
}
|
|
|
|
fn decode_json_pointer_token(token: &str) -> crate::Result<String> {
|
|
let mut decoded = String::with_capacity(token.len());
|
|
let mut chars = token.chars();
|
|
while let Some(character) = chars.next() {
|
|
if character != '~' {
|
|
decoded.push(character);
|
|
continue;
|
|
}
|
|
match chars.next() {
|
|
Some('0') => decoded.push('~'),
|
|
Some('1') => decoded.push('/'),
|
|
Some(other) => {
|
|
return Err(failed(format!(
|
|
"invalid JSON patch pointer escape ~{other}"
|
|
)))
|
|
}
|
|
None => return Err(failed("invalid JSON patch pointer escape ~".to_string())),
|
|
}
|
|
}
|
|
Ok(decoded)
|
|
}
|
|
|
|
fn failed(message: String) -> PatchError {
|
|
PatchError::ApplyFailed(message)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use serde_json::json;
|
|
|
|
#[test]
|
|
fn apply_json_patch_handles_all_core_operations() {
|
|
let document = r#"{"name":"alice","items":["a","b"],"meta":{"keep":true}}"#;
|
|
let patch = r#"[
|
|
{"op":"test","path":"/meta/keep","value":true},
|
|
{"op":"add","path":"/items/-","value":"c"},
|
|
{"op":"replace","path":"/name","value":"bob"},
|
|
{"op":"copy","from":"/meta","path":"/copied"},
|
|
{"op":"move","from":"/items/0","path":"/first"},
|
|
{"op":"remove","path":"/meta/keep"}
|
|
]"#;
|
|
|
|
let output = apply_json_patch(document, patch).unwrap();
|
|
let value: Value = serde_json::from_str(&output).unwrap();
|
|
|
|
assert_eq!(value["name"], json!("bob"));
|
|
assert_eq!(value["items"], json!(["b", "c"]));
|
|
assert_eq!(value["first"], json!("a"));
|
|
assert_eq!(value["copied"], json!({"keep": true}));
|
|
assert_eq!(value["meta"], json!({}));
|
|
}
|
|
|
|
#[test]
|
|
fn apply_json_patch_supports_pointer_escapes() {
|
|
let document = r#"{"a/b":{"tilde~key":1}}"#;
|
|
let patch = r#"[{"op":"replace","path":"/a~1b/tilde~0key","value":2}]"#;
|
|
|
|
let output = apply_json_patch(document, patch).unwrap();
|
|
let value: Value = serde_json::from_str(&output).unwrap();
|
|
|
|
assert_eq!(value["a/b"]["tilde~key"], json!(2));
|
|
}
|
|
|
|
#[test]
|
|
fn apply_json_patch_rejects_failed_test_without_mutating_value() {
|
|
let mut value = json!({"enabled": true});
|
|
let operations = vec![
|
|
JsonPatchOperation::Add {
|
|
path: "/count".to_string(),
|
|
value: json!(1),
|
|
},
|
|
JsonPatchOperation::Test {
|
|
path: "/enabled".to_string(),
|
|
value: json!(false),
|
|
},
|
|
];
|
|
|
|
let error = apply_json_patch_value(&mut value, &operations).unwrap_err();
|
|
|
|
assert!(matches!(error, crate::PatchError::ApplyFailed(_)));
|
|
assert_eq!(value, json!({"enabled": true, "count": 1}));
|
|
}
|
|
|
|
#[test]
|
|
fn apply_json_patch_rejects_missing_remove_path() {
|
|
let error = apply_json_patch(r#"{"items":[]}"#, r#"[{"op":"remove","path":"/missing"}]"#)
|
|
.unwrap_err();
|
|
|
|
assert!(matches!(error, crate::PatchError::ApplyFailed(_)));
|
|
}
|
|
}
|