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