1use std::path::Path;
7
8use toml_edit::{DocumentMut, Item, Table, value};
9
10use crate::error::{Error, Result};
11use crate::meta::{Author, Entry, Kind, Meta, RefItem, extract};
12use crate::util;
13
14pub fn toml_str(s: &str) -> String {
16 let mut out = String::with_capacity(s.len() + 2);
17 out.push('"');
18 for c in s.chars() {
19 match c {
20 '"' => out.push_str("\\\""),
21 '\\' => out.push_str("\\\\"),
22 '\n' => out.push_str("\\n"),
23 '\r' => out.push_str("\\r"),
24 '\t' => out.push_str("\\t"),
25 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04X}", c as u32)),
26 c => out.push(c),
27 }
28 }
29 out.push('"');
30 out
31}
32
33pub fn meta_from_text(text: &str) -> Result<Meta> {
35 let doc: DocumentMut = text
36 .parse()
37 .map_err(|e| Error::Other(format!("内部模板 TOML 解析失败:{e}")))?;
38 let (meta, _issues) = extract(doc, "<template>");
39 Ok(meta)
40}
41
42pub fn render_root_meta(
44 name: &str,
45 title: Option<&str>,
46 summary: Option<&str>,
47 root_id: &str,
48 created: &str,
49 schema_count: usize,
50 id_version: usize,
51) -> String {
52 let mut s = String::new();
53 s.push_str("# ── STR bundle 根元数据 ──────────────────────────────────────────\n");
54 s.push_str("# ROOT 的 [[entries]] 中 role = \"node\" 的条目即一级分支结构。\n");
55 s.push_str(&format!("str = {}\n", crate::STR_MAJOR));
56 s.push_str(&format!("spec = {}\n", toml_str(crate::SPEC_VERSION)));
57 s.push_str("kind = \"root\"\n");
58 s.push_str(&format!("id = {}\n", toml_str(root_id)));
59 s.push_str(&format!("name = {}\n", toml_str(name)));
60 if let Some(t) = title {
61 s.push_str(&format!("title = {}\n", toml_str(t)));
62 }
63 if let Some(t) = summary {
64 s.push_str(&format!("summary = {}\n", toml_str(t)));
65 }
66 s.push_str("tags = []\n");
67 s.push_str("revision = 1\n");
68 s.push_str(&format!("created_at = {created}\n"));
69 s.push_str(&format!("updated_at = {created}\n"));
70 s.push('\n');
71 s.push_str("[policies]\n");
72 s.push_str(&format!("id_version = {id_version}\n"));
73 s.push_str("max_depth = 32\n");
74 s.push_str("manifest = \"strict\"\n");
75 s.push_str("sha256 = \"required\"\n");
76 s.push_str("large_asset_bytes = 10485760\n");
77 s.push_str("deep_tree_warn = 16\n");
78 s.push('\n');
79 if schema_count > 0 {
80 s.push_str("[[entries]]\n");
81 s.push_str("path = \"._schema\"\n");
82 s.push_str("role = \"schema\"\n");
83 s.push_str(&format!("count = {schema_count}\n"));
84 s.push_str("note = \"bundle 级校验 Schema 存放处\"\n");
85 s.push('\n');
86 }
87 s.push_str("[ext]\n");
88 s
89}
90
91pub fn render_branch_meta(
93 kind: Kind,
94 id: &str,
95 type_: Option<&str>,
96 title: Option<&str>,
97 summary: Option<&str>,
98 created: &str,
99) -> String {
100 let mut s = String::new();
101 s.push_str(&format!("str = {}\n", crate::STR_MAJOR));
102 s.push_str(&format!("spec = {}\n", toml_str(crate::SPEC_VERSION)));
103 s.push_str(&format!("kind = {}\n", toml_str(kind.as_str())));
104 s.push_str(&format!("id = {}\n", toml_str(id)));
105 if let Some(t) = type_ {
106 s.push_str(&format!("type = {}\n", toml_str(t)));
107 }
108 if let Some(t) = title {
109 s.push_str(&format!("title = {}\n", toml_str(t)));
110 }
111 if let Some(t) = summary {
112 s.push_str(&format!("summary = {}\n", toml_str(t)));
113 }
114 s.push_str("tags = []\n");
115 s.push_str("revision = 1\n");
116 s.push_str(&format!("created_at = {created}\n"));
117 s.push_str(&format!("updated_at = {created}\n"));
118 s.push('\n');
119 s.push_str("[ext]\n");
120 s
121}
122
123pub fn entry_to_table(e: &Entry) -> Table {
125 let mut t = Table::new();
126 t.insert("path", value(e.path.clone()));
127 t.insert("role", value(e.role.clone()));
128 if let Some(v) = &e.id {
129 t.insert("id", value(v.clone()));
130 }
131 if let Some(v) = &e.r#type {
132 t.insert("type", value(v.clone()));
133 }
134 if let Some(v) = &e.title {
135 t.insert("title", value(v.clone()));
136 }
137 if let Some(v) = &e.summary {
138 t.insert("summary", value(v.clone()));
139 }
140 if let Some(v) = e.order {
141 t.insert("order", value(v));
142 }
143 if let Some(v) = &e.media_type {
144 t.insert("media_type", value(v.clone()));
145 }
146 if let Some(v) = e.size {
147 t.insert("size", value(v));
148 }
149 if let Some(v) = &e.sha256 {
150 t.insert("sha256", value(v.clone()));
151 }
152 if let Some(v) = e.count {
153 t.insert("count", value(v));
154 }
155 if let Some(v) = &e.schema {
156 t.insert("schema", value(v.clone()));
157 }
158 if e.optional {
159 t.insert("optional", value(true));
160 }
161 if let Some(v) = &e.note {
162 t.insert("note", value(v.clone()));
163 }
164 t
165}
166
167pub fn ref_to_table(r: &RefItem) -> Table {
169 let mut t = Table::new();
170 t.insert("id", value(r.id.clone()));
171 t.insert("target", value(r.target.clone()));
172 t.insert("rel", value(r.rel.clone()));
173 if let Some(v) = &r.title {
174 t.insert("title", value(v.clone()));
175 }
176 if let Some(v) = r.order {
177 t.insert("order", value(v));
178 }
179 if let Some(v) = &r.note {
180 t.insert("note", value(v.clone()));
181 }
182 t
183}
184
185pub fn author_to_table(a: &Author) -> Table {
187 let mut t = Table::new();
188 t.insert("id", value(a.id.clone()));
189 if let Some(v) = &a.name {
190 t.insert("name", value(v.clone()));
191 }
192 t.insert("role", value(a.role.clone()));
193 if let Some(at) = &a.at
194 && let Ok(dt) = at.parse::<toml_edit::Datetime>()
195 {
196 t.insert("at", value(dt));
197 }
198 t
199}
200
201pub fn strip_line_comments(text: &str) -> String {
203 let mut out = String::with_capacity(text.len());
204 for line in text.lines() {
205 if line.trim_start().starts_with('#') {
206 continue;
207 }
208 out.push_str(line);
209 out.push('\n');
210 }
211 out
212}
213
214impl Meta {
215 pub fn resync(&mut self) {
217 let doc = std::mem::replace(&mut self.doc, DocumentMut::new());
218 let (meta, _issues) = extract(doc, "<resync>");
219 *self = meta;
220 }
221
222 fn place_table(&mut self, key: &'static str, item: Item) {
224 let order = crate::meta::TABLE_ORDER;
225 let pos = order.iter().position(|k| *k == key).unwrap_or(order.len());
226 let table = self.doc.as_table_mut();
227 let mut moved: Vec<(String, Item)> = Vec::new();
228 for k in order.iter().skip(pos + 1) {
229 if let Some(it) = table.remove(k) {
230 moved.push(((*k).to_string(), it));
231 }
232 }
233 table.insert(key, item);
234 for (k, it) in moved {
235 table.insert(&k, it);
236 }
237 }
238
239 pub fn touch(&mut self) {
243 let next = self.revision.unwrap_or(0) + 1;
244 let now = util::now_rfc3339();
245 let table = self.doc.as_table_mut();
246 table.insert("revision", value(next));
247 if let Ok(dt) = now.parse::<toml_edit::Datetime>() {
248 table.insert("updated_at", value(dt));
249 }
250 self.resync();
251 }
252
253 pub fn set_str(&mut self, key: &str, v: &str) {
255 self.doc.as_table_mut().insert(key, value(v.to_string()));
256 self.resync();
257 }
258
259 pub fn set_str_or_remove(&mut self, key: &str, v: &str) {
261 if v.is_empty() {
262 self.doc.as_table_mut().remove(key);
263 } else {
264 self.doc.as_table_mut().insert(key, value(v.to_string()));
265 }
266 self.resync();
267 }
268
269 pub fn set_str_array(&mut self, key: &str, values: &[String]) {
271 let arr: toml_edit::Array = values
272 .iter()
273 .map(|s| toml_edit::Value::from(s.clone()))
274 .collect();
275 self.doc.as_table_mut().insert(key, value(arr));
276 self.resync();
277 }
278
279 pub fn upsert_author(&mut self, a: &Author) -> bool {
281 let new_table = author_to_table(a);
282 let mut aot = self
283 .doc
284 .as_table()
285 .get("authors")
286 .and_then(|i| i.as_array_of_tables())
287 .cloned()
288 .unwrap_or_default();
289 let mut replaced = false;
290 for t in aot.iter_mut() {
291 if t.get("id").and_then(|v| v.as_str()) == Some(a.id.as_str()) {
292 *t = new_table.clone();
293 replaced = true;
294 break;
295 }
296 }
297 if !replaced {
298 aot.push(new_table);
299 }
300 self.place_table("authors", Item::ArrayOfTables(aot));
301 self.resync();
302 !replaced
303 }
304
305 pub fn remove_author(&mut self, id: &str) -> bool {
307 let Some(aot) = self
308 .doc
309 .as_table_mut()
310 .get_mut("authors")
311 .and_then(|i| i.as_array_of_tables_mut())
312 else {
313 return false;
314 };
315 let before = aot.len();
316 aot.retain(|t| t.get("id").and_then(|v| v.as_str()) != Some(id));
317 let changed = aot.len() != before;
318 if changed {
319 self.resync();
320 }
321 changed
322 }
323
324 pub fn set_entry_str(&mut self, path: &str, key: &str, v: &str) -> bool {
329 self.with_entry(path, |t| {
330 if v.is_empty() {
331 t.remove(key);
332 } else {
333 t.insert(key, value(v.to_string()));
334 }
335 })
336 }
337
338 pub fn set_entry_int(&mut self, path: &str, key: &str, v: Option<i64>) -> bool {
340 self.with_entry(path, |t| match v {
341 Some(n) => {
342 t.insert(key, value(n));
343 }
344 None => {
345 t.remove(key);
346 }
347 })
348 }
349
350 fn with_entry(&mut self, path: &str, edit: impl FnOnce(&mut Table)) -> bool {
352 let Some(aot) = self
353 .doc
354 .as_table_mut()
355 .get_mut("entries")
356 .and_then(|i| i.as_array_of_tables_mut())
357 else {
358 return false;
359 };
360 let mut hit = false;
361 for t in aot.iter_mut() {
362 if t.get("path").and_then(|v| v.as_str()) == Some(path) {
363 edit(t);
364 hit = true;
365 break;
366 }
367 }
368 if hit {
369 self.resync();
370 }
371 hit
372 }
373
374 pub fn upsert_entry(&mut self, e: &Entry) -> bool {
376 let new_table = entry_to_table(e);
377 let mut aot = self
378 .doc
379 .as_table()
380 .get("entries")
381 .and_then(|i| i.as_array_of_tables())
382 .cloned()
383 .unwrap_or_default();
384
385 let mut replaced = false;
386 for t in aot.iter_mut() {
387 if t.get("path").and_then(|v| v.as_str()) == Some(e.path.as_str()) {
388 *t = new_table.clone();
389 replaced = true;
390 break;
391 }
392 }
393 if !replaced {
394 aot.push(new_table);
395 }
396 self.place_table("entries", Item::ArrayOfTables(aot));
397 self.resync();
398 !replaced
399 }
400
401 pub fn remove_entry_path(&mut self, path: &str) -> bool {
403 let Some(aot) = self
404 .doc
405 .as_table_mut()
406 .get_mut("entries")
407 .and_then(|i| i.as_array_of_tables_mut())
408 else {
409 return false;
410 };
411 let before = aot.len();
412 aot.retain(|t| t.get("path").and_then(|v| v.as_str()) != Some(path));
413 let changed = aot.len() != before;
414 if changed {
415 self.resync();
416 }
417 changed
418 }
419
420 pub fn push_ref(&mut self, r: &RefItem) {
422 let mut aot = self
423 .doc
424 .as_table()
425 .get("refs")
426 .and_then(|i| i.as_array_of_tables())
427 .cloned()
428 .unwrap_or_default();
429 aot.push(ref_to_table(r));
430 self.place_table("refs", Item::ArrayOfTables(aot));
431 self.resync();
432 }
433
434 pub fn remove_ref(&mut self, id: &str) -> bool {
436 let Some(aot) = self
437 .doc
438 .as_table_mut()
439 .get_mut("refs")
440 .and_then(|i| i.as_array_of_tables_mut())
441 else {
442 return false;
443 };
444 let before = aot.len();
445 aot.retain(|t| t.get("id").and_then(|v| v.as_str()) != Some(id));
446 let changed = aot.len() != before;
447 if changed {
448 self.resync();
449 }
450 changed
451 }
452
453 pub fn canonical_text(&self, strip_comments: bool) -> String {
457 let mut doc = self.doc.clone();
458 crate::meta::canonicalize_doc(&mut doc);
459 let mut text = doc.to_string();
460 if !text.ends_with('\n') {
461 text.push('\n');
462 }
463 if strip_comments {
464 text = strip_line_comments(&text);
465 }
466 text
467 }
468
469 pub fn save(&self, path: &Path) -> Result<()> {
471 let text = self.canonical_text(false);
472 std::fs::write(path, text).map_err(|e| Error::io(path, e))
473 }
474}