Skip to content

Commit 20a9052

Browse files
committed
fix: repair OpenList copy reconciliation
1 parent 49daca4 commit 20a9052

2 files changed

Lines changed: 95 additions & 10 deletions

File tree

src/openlist.rs

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ impl ManifestInspectError {
192192

193193
#[derive(Debug, Deserialize)]
194194
struct CopyResult {
195-
#[serde(default)]
195+
#[serde(default, deserialize_with = "deserialize_null_default")]
196196
tasks: Vec<OpenListTask>,
197197
}
198198

@@ -228,12 +228,20 @@ struct ListRequest<'a> {
228228

229229
#[derive(Deserialize)]
230230
struct ListResult {
231-
#[serde(default)]
231+
#[serde(default, deserialize_with = "deserialize_null_default")]
232232
content: Vec<OpenListObject>,
233233
#[serde(default)]
234234
total: usize,
235235
}
236236

237+
fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
238+
where
239+
D: serde::Deserializer<'de>,
240+
T: Deserialize<'de> + Default,
241+
{
242+
Option::<T>::deserialize(deserializer).map(Option::unwrap_or_default)
243+
}
244+
237245
#[derive(Serialize)]
238246
struct RemoveRequest<'a> {
239247
dir: &'a str,
@@ -1067,9 +1075,9 @@ mod tests {
10671075
use serde_json::{Value, json};
10681076

10691077
use super::{
1070-
ManifestFileState, ManifestInspectError, OpenListClient, OpenListObject,
1071-
OpenListRequestError, OpenListTask, openlist_identity_key, valid_child_name,
1072-
validate_manifest_file,
1078+
CopyResult, ListResult, ManifestFileState, ManifestInspectError, OpenListClient,
1079+
OpenListObject, OpenListRequestError, OpenListTask, openlist_identity_key,
1080+
valid_child_name, validate_manifest_file,
10731081
};
10741082

10751083
#[test]
@@ -1084,6 +1092,19 @@ mod tests {
10841092
);
10851093
}
10861094

1095+
#[test]
1096+
fn openlist_collection_results_accept_missing_or_null_fields() {
1097+
for raw in [r#"{"total":0}"#, r#"{"content":null,"total":0}"#] {
1098+
let result: ListResult = serde_json::from_str(raw).unwrap();
1099+
assert!(result.content.is_empty(), "response: {raw}");
1100+
}
1101+
1102+
for raw in [r#"{}"#, r#"{"tasks":null}"#] {
1103+
let result: CopyResult = serde_json::from_str(raw).unwrap();
1104+
assert!(result.tasks.is_empty(), "response: {raw}");
1105+
}
1106+
}
1107+
10871108
#[derive(Default)]
10881109
struct FakeOpenListState {
10891110
copies: Vec<(String, String, String)>,

src/relocation.rs

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1797,6 +1797,74 @@ mod tests {
17971797
assert_eq!(file.path, "Season/E01.mkv");
17981798
}
17991799

1800+
#[tokio::test]
1801+
async fn directory_checkpoint_accepts_missing_ancestor_of_derived_target_root() {
1802+
let harness = RelocationTestHarness::new(BTreeMap::new()).await;
1803+
let checkpoint = copy_checkpoint(
1804+
"/cmcc/Download/media/云母/动漫",
1805+
0,
1806+
CopyCheckpointOperation::CreateDirectory,
1807+
CopyCheckpointPhase::Prepared,
1808+
);
1809+
let mut job = harness
1810+
.seed_job(
1811+
"copy_submitting",
1812+
Some(checkpoint.clone()),
1813+
None,
1814+
"Show/E01.mkv",
1815+
0,
1816+
)
1817+
.await;
1818+
job.target_openlist_path = "/cmcc/Download/media/云母/动漫/动画/2024".to_string();
1819+
1820+
let (index, file) = validate_copy_checkpoint(&checkpoint, &job).unwrap();
1821+
1822+
assert_eq!(index, 0);
1823+
assert_eq!(file.path, "Show/E01.mkv");
1824+
}
1825+
1826+
#[tokio::test]
1827+
async fn directory_checkpoint_rejects_paths_outside_current_target_parent_chain() {
1828+
let harness = RelocationTestHarness::new(BTreeMap::new()).await;
1829+
let initial_checkpoint = copy_checkpoint(
1830+
"/cmcc/Download/media/云母/动漫",
1831+
0,
1832+
CopyCheckpointOperation::CreateDirectory,
1833+
CopyCheckpointPhase::Prepared,
1834+
);
1835+
let mut job = harness
1836+
.seed_job(
1837+
"copy_submitting",
1838+
Some(initial_checkpoint),
1839+
None,
1840+
"Show/E01.mkv",
1841+
0,
1842+
)
1843+
.await;
1844+
job.target_openlist_path = "/cmcc/Download/media/云母/动漫/动画/2024".to_string();
1845+
1846+
for path in [
1847+
"/cmcc/Download/media/云母/电影",
1848+
"/cmcc/Download/media/云母/动漫2",
1849+
"/cmcc/Download/media/云母/动漫/动画/2024/Other",
1850+
"/cmcc/Download/media/云母/动漫/动画/2024/Show/E01.mkv",
1851+
] {
1852+
let checkpoint = copy_checkpoint(
1853+
path,
1854+
0,
1855+
CopyCheckpointOperation::CreateDirectory,
1856+
CopyCheckpointPhase::Prepared,
1857+
);
1858+
1859+
let error = validate_copy_checkpoint(&checkpoint, &job).unwrap_err();
1860+
1861+
assert!(
1862+
error.contains("不在当前目标文件的父目录链中"),
1863+
"unexpected validation result for {path}: {error}"
1864+
);
1865+
}
1866+
}
1867+
18001868
#[test]
18011869
fn qb_transfer_completion_rejects_stale_or_unstable_states() {
18021870
assert!(!torrent_is_complete(1, 99, 100, 0.99, "downloading"));
@@ -5648,14 +5716,10 @@ fn validate_copy_checkpoint(
56485716
.rsplit_once('/')
56495717
.map(|(parent, _)| if parent.is_empty() { "/" } else { parent })
56505718
.ok_or("目录 checkpoint 对应的目标文件路径无效")?;
5651-
let target_root = normalize_path(&job.target_openlist_path)?;
56525719
let directory = normalize_path(&checkpoint.path)?;
5653-
let target_root_identity = openlist_identity_key(&target_root);
56545720
let directory_identity = openlist_identity_key(&directory);
56555721
let target_parent_identity = openlist_identity_key(target_parent);
5656-
if !is_path_prefix(&target_root_identity, &directory_identity)
5657-
|| !is_path_prefix(&directory_identity, &target_parent_identity)
5658-
{
5722+
if !is_path_prefix(&directory_identity, &target_parent_identity) {
56595723
return Err(format!(
56605724
"目录 checkpoint 不在当前目标文件的父目录链中: {directory}"
56615725
));

0 commit comments

Comments
 (0)