Files
BlueArchiveToolkit/crates/bat-patch/src/json.rs
T
nyaKazuha f4880a71bd feat(patch): 补齐通用补丁引擎基础
实现 Binary、JSON、Text patch 与通用 manifest 校验基础。

验证:cargo test -p bat-patch --locked;cargo clippy -p bat-patch --all-targets --locked -- -D warnings。
2026-07-31 00:21:50 +08:00

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(_)));
}
}