Skip to content

Commit d6d24f5

Browse files
authored
Merge pull request #512 from yeroo/issue-450-yppx-a-package-with-one-unreadab
yppx: open packages with unreadable extra parts; refuse lossy .yppx saves (#450)
2 parents 5977021 + a1e75a0 commit d6d24f5

14 files changed

Lines changed: 294 additions & 70 deletions

File tree

‎mppread/tests/real_mpp.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,7 @@ fn resource_calendars_survive_mpp_saves_when_present() {
379379
|c: &projcore::Calendar| (c.uid, c.name.clone(), c.base_calendar_uid, c.week.clone());
380380
for saved in [
381381
projcore::mspdi::read_mspdi(&projcore::mspdi::write_mspdi(&imported)).unwrap(),
382-
projcore::yppx::read_yppx(&projcore::yppx::write_yppx(&imported)).unwrap(),
382+
projcore::yppx::read_yppx(&projcore::yppx::write_yppx(&imported).unwrap()).unwrap(),
383383
] {
384384
assert_eq!(
385385
imported.calendars.iter().map(fields).collect::<Vec<_>>(),

‎projcore/Cargo.toml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,7 @@ workspace = true
1818
[dependencies]
1919
# Intentionally std-only — the one dependency is our own shared OPC/XML layer.
2020
opccore = { version = "0.1.0", path = "../opccore" }
21+
22+
[[example]]
23+
name = "convert"
24+
test = true

‎projcore/examples/convert.rs‎

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,7 @@ fn main() {
2929

3030
// Save in the format named by the output extension; refuse any other name.
3131
let target = yppx::save_target(Path::new(output)).unwrap_or_else(|e| fail(output, &e));
32-
let bytes = if has_ext(&target, "yppx") {
33-
yppx::write_yppx(&proj)
34-
} else {
35-
mspdi::write_mspdi(&proj).into_bytes()
36-
};
32+
let bytes = output_bytes(&proj, &target).unwrap_or_else(|e| fail(output, &e));
3733
opccore::fsio::write_atomic(&target, &bytes).unwrap_or_else(|e| fail(output, &e.to_string()));
3834
eprintln!(
3935
"{} task(s): {input} -> {}",
@@ -42,6 +38,14 @@ fn main() {
4238
);
4339
}
4440

41+
fn output_bytes(proj: &projcore::Project, target: &Path) -> Result<Vec<u8>, String> {
42+
if has_ext(target, "yppx") {
43+
yppx::write_yppx(proj)
44+
} else {
45+
Ok(mspdi::write_mspdi(proj).into_bytes())
46+
}
47+
}
48+
4549
fn has_ext(path: &Path, ext: &str) -> bool {
4650
path.extension()
4751
.and_then(|e| e.to_str())
@@ -52,3 +56,17 @@ fn fail(what: &str, msg: &str) -> ! {
5256
eprintln!("{what}: {msg}");
5357
std::process::exit(1);
5458
}
59+
60+
#[cfg(test)]
61+
mod tests {
62+
use super::*;
63+
64+
#[test]
65+
fn converter_refuses_unreadable_yppx_parts_and_allows_xml() {
66+
let mut project = projcore::Project::default();
67+
project.package.unreadable.push("media/unknown.bin".into());
68+
let error = output_bytes(&project, Path::new("copy.yppx")).unwrap_err();
69+
assert!(error.contains("media/unknown.bin") && error.contains("save as .xml"));
70+
assert!(output_bytes(&project, Path::new("copy.xml")).is_ok());
71+
}
72+
}

‎projcore/src/editor.rs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1766,7 +1766,7 @@ mod tests {
17661766
let mut ed = Editor::new(read_yppx(&write_zip(&entries)).unwrap());
17671767
ed.set_duration_min(1, 960, false).unwrap();
17681768
for stage in 0..3 {
1769-
let output = write_yppx(ed.project());
1769+
let output = write_yppx(ed.project()).unwrap();
17701770
let zip = ZipArchive::open(&output).unwrap();
17711771
assert_eq!(zip.read("views.xml").unwrap(), views, "stage {stage}");
17721772
let types = String::from_utf8(zip.read("[Content_Types].xml").unwrap()).unwrap();
@@ -2284,7 +2284,7 @@ mod tests {
22842284
ed.project().tasks
22852285
);
22862286
assert_eq!(
2287-
crate::yppx::read_yppx(&crate::yppx::write_yppx(ed.project()))
2287+
crate::yppx::read_yppx(&crate::yppx::write_yppx(ed.project()).unwrap())
22882288
.unwrap()
22892289
.tasks,
22902290
ed.project().tasks
@@ -2829,7 +2829,7 @@ mod tests {
28292829
.active,
28302830
Some(false)
28312831
);
2832-
let package = crate::yppx::write_yppx(ed.project());
2832+
let package = crate::yppx::write_yppx(ed.project()).unwrap();
28332833
assert_eq!(
28342834
crate::yppx::read_yppx(&package)
28352835
.unwrap()

‎projcore/src/model.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ use std::sync::Arc;
1616
pub struct PackageParts {
1717
/// ZIP name and uncompressed bytes, in package order.
1818
pub parts: Vec<(String, Arc<[u8]>)>,
19+
/// ZIP names of parts that could not be extracted, in package order.
20+
pub unreadable: Vec<String>,
1921
/// Extension and content type for non-XML Defaults.
2022
pub defaults: Vec<(String, String)>,
2123
/// Part name and content type for retained Overrides.

‎projcore/src/mspdi.rs‎

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2609,7 +2609,7 @@ mod tests {
26092609
}
26102610
assert_eq!(read_mspdi(&written).unwrap(), project);
26112611
assert_eq!(
2612-
crate::yppx::read_yppx(&crate::yppx::write_yppx(&project)).unwrap(),
2612+
crate::yppx::read_yppx(&crate::yppx::write_yppx(&project).unwrap()).unwrap(),
26132613
project
26142614
);
26152615
}
@@ -2742,7 +2742,7 @@ mod tests {
27422742
read_mspdi(&xml).unwrap().honor_constraints,
27432743
honor_constraints
27442744
);
2745-
let package = crate::yppx::write_yppx(&proj);
2745+
let package = crate::yppx::write_yppx(&proj).unwrap();
27462746
assert_eq!(
27472747
crate::yppx::read_yppx(&package).unwrap().honor_constraints,
27482748
honor_constraints
@@ -2790,7 +2790,7 @@ mod tests {
27902790
assert!(xml.contains("<ManualStart>2026-03-02T08:00:00</ManualStart>"));
27912791
assert!(xml.contains("<ManualDuration>PT8H0M0S</ManualDuration>"));
27922792
assert_eq!(read_mspdi(&xml).unwrap().tasks, proj.tasks);
2793-
let package = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj)).unwrap();
2793+
let package = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj).unwrap()).unwrap();
27942794
assert_eq!(package.tasks, proj.tasks);
27952795
assert!(package.new_tasks_are_manual);
27962796
}
@@ -3373,7 +3373,7 @@ mod tests {
33733373
));
33743374
let back = read_mspdi(&xml).unwrap();
33753375
assert_eq!(back.calendars, proj.calendars);
3376-
let package = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj)).unwrap();
3376+
let package = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj).unwrap()).unwrap();
33773377
assert_eq!(package.calendars, proj.calendars);
33783378
}
33793379

@@ -3611,7 +3611,9 @@ mod tests {
36113611
let loaded = read_mspdi(&write_mspdi(proj)).unwrap();
36123612
let editor = crate::editor::Editor::new(loaded);
36133613
assert!(read_mspdi(&write_mspdi(editor.project())).is_ok());
3614-
assert!(crate::yppx::read_yppx(&crate::yppx::write_yppx(editor.project())).is_ok());
3614+
assert!(
3615+
crate::yppx::read_yppx(&crate::yppx::write_yppx(editor.project()).unwrap()).is_ok()
3616+
);
36153617
}
36163618
let mut proj = empty_calendar_project();
36173619
assert_editor_reopens(&proj);
@@ -3702,7 +3704,7 @@ mod tests {
37023704
assert!(xml.contains("<ToTime>00:00:00</ToTime>"));
37033705
assert_eq!(read_mspdi(&xml).unwrap().calendars, proj.calendars);
37043706
assert_eq!(
3705-
crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj))
3707+
crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj).unwrap())
37063708
.unwrap()
37073709
.calendars,
37083710
proj.calendars
@@ -3862,7 +3864,7 @@ mod tests {
38623864
read_mspdi(&xml).unwrap().tasks[0].baselines,
38633865
proj.tasks[0].baselines
38643866
);
3865-
let back = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj)).unwrap();
3867+
let back = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj).unwrap()).unwrap();
38663868
assert_eq!(back.tasks[0].baselines, proj.tasks[0].baselines);
38673869
}
38683870

@@ -3886,7 +3888,7 @@ mod tests {
38863888
let xml = write_mspdi(&proj);
38873889
assert!(xml.find("<Number>0").unwrap() < xml.find("<Number>1").unwrap());
38883890
assert_eq!(read_mspdi(&xml).unwrap().tasks[0].baselines, expected);
3889-
let back = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj)).unwrap();
3891+
let back = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj).unwrap()).unwrap();
38903892
assert_eq!(back.tasks[0].baselines, expected);
38913893
}
38923894

@@ -4243,7 +4245,7 @@ mod tests {
42434245
assert!(xml.contains("<IsNull>0</IsNull>"));
42444246
assert!(xml.contains("<OutlineNumber>1</OutlineNumber>"));
42454247
assert_eq!(read_mspdi(&xml).unwrap().tasks, proj.tasks);
4246-
let package = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj)).unwrap();
4248+
let package = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj).unwrap()).unwrap();
42474249
assert_eq!(package.tasks, proj.tasks);
42484250
}
42494251

@@ -4308,7 +4310,7 @@ mod tests {
43084310
let xml = write_mspdi(&proj);
43094311
assert!(xml.contains("<Notes>Line 1&#13;\nLine 2 &amp; &lt;x&gt;</Notes>"));
43104312
assert_eq!(read_mspdi(&xml).unwrap().tasks, proj.tasks);
4311-
let package = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj)).unwrap();
4313+
let package = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj).unwrap()).unwrap();
43124314
assert_eq!(package.tasks, proj.tasks);
43134315
}
43144316

@@ -4854,7 +4856,7 @@ mod tests {
48544856
);
48554857
let saved = write_mspdi(&proj);
48564858
assert_eq!(read_mspdi(&saved).unwrap().tasks[0], *task);
4857-
let package = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj)).unwrap();
4859+
let package = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj).unwrap()).unwrap();
48584860
assert_eq!(package.tasks[0], *task);
48594861

48604862
let absent = task_project(
@@ -5102,7 +5104,7 @@ mod tests {
51025104
Some("2026-03-04T08:00:00")
51035105
);
51045106
assert_eq!(read_mspdi(&xml).unwrap().tasks, proj.tasks);
5105-
let package = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj)).unwrap();
5107+
let package = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj).unwrap()).unwrap();
51065108
assert_eq!(package.tasks, proj.tasks);
51075109
}
51085110

@@ -5231,7 +5233,7 @@ mod tests {
52315233
assert!(xml.contains("<ScheduleFromStart>0</ScheduleFromStart>"));
52325234
// A save stores the options in schema order; the saved file reads back
52335235
// to the same header.
5234-
let package = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj)).unwrap();
5236+
let package = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj).unwrap()).unwrap();
52355237
assert_eq!(header_of(&write_mspdi(&package)), expected);
52365238
}
52375239

@@ -5274,7 +5276,7 @@ mod tests {
52745276
assert!(proj.options.is_empty(), "{:?}", proj.options);
52755277
let xml = write_mspdi(&proj);
52765278
assert_eq!(modeled_options(&read_mspdi(&xml).unwrap()), expected);
5277-
let package = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj)).unwrap();
5279+
let package = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj).unwrap()).unwrap();
52785280
assert_eq!(modeled_options(&package), expected);
52795281
// The effective values follow the file.
52805282
assert!(proj.new_tasks_effort_driven() && !proj.new_tasks_estimated());
@@ -5479,7 +5481,7 @@ mod tests {
54795481
read_mspdi(&xml).unwrap().tasks[0].extended_attributes,
54805482
expected
54815483
);
5482-
let back = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj)).unwrap();
5484+
let back = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj).unwrap()).unwrap();
54835485
assert_eq!(back.tasks[0].extended_attributes, expected);
54845486
}
54855487

@@ -5551,7 +5553,7 @@ mod tests {
55515553
let back = read_mspdi(&xml).unwrap();
55525554
assert_eq!(back.extended_attribute_definitions, expected);
55535555
assert_eq!(back.options, proj.options);
5554-
let back = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj)).unwrap();
5556+
let back = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj).unwrap()).unwrap();
55555557
assert_eq!(back.extended_attribute_definitions, expected);
55565558
}
55575559

@@ -5647,7 +5649,7 @@ mod tests {
56475649
assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
56485650
for back in [
56495651
read_mspdi(&xml).unwrap(),
5650-
crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj)).unwrap(),
5652+
crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj).unwrap()).unwrap(),
56515653
] {
56525654
assert_eq!(back.outline_code_definitions, proj.outline_code_definitions);
56535655
assert_eq!(back.wbs_masks, proj.wbs_masks);
@@ -6128,7 +6130,7 @@ mod tests {
61286130
let back = read_mspdi(&xml).unwrap();
61296131
assert_eq!(back.tasks, proj.tasks);
61306132
assert_eq!(back.assignments, proj.assignments);
6131-
let package = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj)).unwrap();
6133+
let package = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj).unwrap()).unwrap();
61326134
assert_eq!(package.tasks, proj.tasks);
61336135
assert_eq!(package.assignments, proj.assignments);
61346136
}
@@ -6544,7 +6546,7 @@ mod tests {
65446546
assert!(xml.contains(element), "missing {element}");
65456547
}
65466548
assert_eq!(read_mspdi(&xml).unwrap().resources, proj.resources);
6547-
let package = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj)).unwrap();
6549+
let package = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj).unwrap()).unwrap();
65486550
assert_eq!(package.resources, proj.resources);
65496551
}
65506552

@@ -6588,7 +6590,7 @@ mod tests {
65886590
assert!(assignment_xml(&xml).contains(element), "missing {element}");
65896591
}
65906592
assert_eq!(read_mspdi(&xml).unwrap().assignments, proj.assignments);
6591-
let package = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj)).unwrap();
6593+
let package = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj).unwrap()).unwrap();
65926594
assert_eq!(package.assignments, proj.assignments);
65936595
}
65946596

@@ -6865,7 +6867,7 @@ mod tests {
68656867
assert_eq!(read, proj.resources);
68666868
assert_eq!(read[0].rates.len(), 2);
68676869
assert_eq!(read[0].availability_periods.len(), 2);
6868-
let package = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj)).unwrap();
6870+
let package = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj).unwrap()).unwrap();
68696871
assert_eq!(package.resources, proj.resources);
68706872
}
68716873

@@ -6943,7 +6945,7 @@ mod tests {
69436945
let read = read_mspdi(&xml).unwrap().assignments;
69446946
assert_eq!(read, proj.assignments);
69456947
assert_eq!(read[0].timephased_data.len(), 2);
6946-
let package = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj)).unwrap();
6948+
let package = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj).unwrap()).unwrap();
69476949
assert_eq!(package.assignments, proj.assignments);
69486950
}
69496951

@@ -7232,7 +7234,7 @@ mod tests {
72327234

72337235
let written = write_mspdi(&proj);
72347236
assert_eq!(read_mspdi(&written).unwrap().calendars, proj.calendars);
7235-
let package = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj)).unwrap();
7237+
let package = crate::yppx::read_yppx(&crate::yppx::write_yppx(&proj).unwrap()).unwrap();
72367238
assert_eq!(package.calendars, proj.calendars);
72377239
// Each scheduled exception is also written in the legacy form, the
72387240
// yearly one only in `Exceptions`.

0 commit comments

Comments
 (0)