1//! Changes one stored property value: a text object's text (`6000e 1c001c22`, UTF-16LE,
2//! a trailing NUL dropped) as a text op, the notebook's colour on the table of contents root
3//! (`20001 14001cbe`) as a TOC edit, each appending a revision; any other value of the same
4//! length only in a copy, patched where it is stored, as a fixture that is no edit.
5
6#[path = "../src/flush.rs"]
7mod flush;
8#[path = "support/typing.rs"]
9mod typing;
10
11use onestore::{
12 ExGuid, ObjectData, PropertySets, RevisionIndex, Store, TocEdit, Transaction, Value,
13};
14use std::{env, fs, io::Write};
15
16fn hex(text: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
17 if !text.is_ascii() || !text.len().is_multiple_of(2) {
18 return Err("Expected an even number of hexadecimal digits".into());
19 }
20 text.as_bytes()
21 .as_chunks::<2>()
22 .0
23 .iter()
24 .map(|pair| Ok(u8::from_str_radix(std::str::from_utf8(pair)?, 16)?))
25 .collect()
26}
27
28fn main() -> Result<(), Box<dyn std::error::Error>> {
29 let args: Vec<_> = env::args().skip(1).collect();
30 if args.len() != 6 && args.len() != 8 {
31 return Err(
32 "Usage: edit_property INPUT OUTPUT|--in-place JCID PROPERTY EXPECTED_HEX VALUE_HEX [SPACE OBJECT]"
33 .into(),
34 );
35 }
36 let jcid = u32::from_str_radix(&args[2], 16)?;
37 let id = u32::from_str_radix(&args[3], 16)?;
38 let expected = hex(&args[4])?;
39 let value = hex(&args[5])?;
40 let bytes = onestore::read_file(&args[0])?;
41 let store = Store::parse(&bytes)?;
42 let index = RevisionIndex::parse(&store)?;
43 index.validate_current()?;
44 let mut selected = None;
45 for (osid, space) in &index.spaces {
46 let Some(&rid) = space.labels.get(&(ExGuid::default(), 1)) else {
47 continue;
48 };
49 let revision = index.resolve(*osid, rid)?;
50 for oid in revision.reachable()? {
51 if args.len() == 8 && (args[6] != osid.to_string() || args[7] != oid.to_string()) {
52 continue;
53 }
54 let object = &revision.objects[&oid];
55 if object.jcid == jcid
56 && let ObjectData::Properties(data) = object.data
57 {
58 for property in &PropertySets::parse(data)?.sets[0] {
59 if property.id == id
60 && property.value == Value::Bytes(&expected)
61 && selected.replace((*osid, oid)).is_some()
62 {
63 return Err("Multiple matching objects".into());
64 }
65 }
66 }
67 }
68 }
69 let (space, object) = selected.ok_or("No matching object")?;
70 let transaction: Option<Transaction> = match (jcid, id) {
71 (0x6000e, 0x1c001c22) => {
72 let units = |bytes: &[u8]| -> Vec<u16> {
73 let mut units: Vec<u16> = bytes
74 .as_chunks::<2>().0.iter()
75 .map(|pair| u16::from_le_bytes([pair[0], pair[1]]))
76 .collect();
77 if units.last() == Some(&0) {
78 units.pop();
79 }
80 units
81 };
82 let length = u32::try_from(units(&expected).len())?;
83 let text = String::from_utf16(&units(&value))?;
84 let edit = typing::text(space, object, 0..length, &text);
85 typing::sealed(&bytes, "Property editor", &edit)?
86 }
87 (0x20001, 0x14001cbe) => {
88 if index.resolve_active(space)?.roots.get(&1) != Some(&object) {
89 return Err("Only the notebook's color, on the TOC root, changes".into());
90 }
91 let color = u32::from_le_bytes(value.as_slice().try_into()?);
92 onestore::edit_table_of_contents(&bytes, &[TocEdit::Color(color)])?
93 }
94 _ if args[1] != "--in-place" && value.len() == expected.len() => {
95 let revision = index.resolve_active(space)?;
96 let ObjectData::Properties(data) = revision.objects[&object].data else {
97 return Err("The object stores no properties".into());
98 };
99 let properties = PropertySets::parse(data)?;
100 let Some(Value::Bytes(stored)) = properties.sets[0]
101 .iter()
102 .find(|property| property.id == id)
103 .map(|property| &property.value)
104 else {
105 return Err("The property stores no bytes".into());
106 };
107 let at = stored.as_ptr().addr() - bytes.as_ptr().addr();
108 let mut patched = bytes.clone();
109 patched[at..at + value.len()].copy_from_slice(&value);
110 write(&args[1], &patched)?;
111 return Ok(());
112 }
113 _ => return Err("Only text and TOC colors change as edits; other values are patched into a copy of equal length".into()),
114 };
115 let transaction = transaction.ok_or("The value is already stored")?;
116 if args[1] == "--in-place" {
117 transaction.commit_file(&args[0])?;
118 return Ok(());
119 }
120 let mut written = bytes.clone();
121 transaction.apply(&mut written)?;
122 write(&args[1], &written)
123}
124
125fn write(path: &str, written: &[u8]) -> Result<(), Box<dyn std::error::Error>> {
126 let mut output = fs::OpenOptions::new()
127 .write(true)
128 .create_new(true)
129 .open(path)?;
130 output.write_all(written)?;
131 flush::flush(&output)?;
132 Ok(())
133}