1use alloc::collections::{BTreeMap, BTreeSet};
15use core::str;
16use std::io;
17
18use camino::{Utf8Path, Utf8PathBuf};
19use schemars::JsonSchema;
20use serde::de::{self, DeserializeOwned};
21use serde::{Deserialize, Deserializer, Serialize, Serializer};
22use toml_edit::{ArrayOfTables, DocumentMut, Item, TomlError, Value, ser};
23
24use crate::digest::{Digest, Fingerprint};
25use crate::errors::{ParseError, Result, TargetError};
26use crate::part::{Scope, Slot};
27use crate::path::RelPath;
28use crate::profile::{Format, MergeSpec, Policy};
29use crate::source::{Oid, Source, SourceSpec};
30use crate::vars::VarName;
31
32pub(crate) const DIR: &str = ".devset";
34
35pub(crate) const CONFIG: &str = "config.toml";
37
38pub(crate) const LOCK: &str = "lock.toml";
40
41pub(crate) const STATE: &str = "state.toml";
43
44pub(crate) const CONFLICTS: &str = "conflicts";
46
47pub(crate) const PENDING: &str = "pending.toml";
49
50pub(crate) const UNDO: &str = "undo.toml";
52
53pub(crate) const UNDO_BLOBS: &str = "undo";
55
56pub(crate) const BASE: &str = "base";
58
59pub(crate) const ANSWERS: &str = "answers.toml";
61
62#[derive(Debug)]
64pub struct Target {
65 root: Utf8PathBuf,
67 config: Config,
69 config_text: String,
71 lock: Lock,
73 pending: Option<Lock>,
75 state: State,
77 answers: BTreeMap<VarName, String>,
79 given: BTreeSet<VarName>,
81 state_digest: Option<Digest>,
83}
84
85#[derive(Debug, Default, Deserialize, JsonSchema)]
87#[serde(deny_unknown_fields)]
88pub struct Config {
89 #[serde(default)]
91 pub layers: Vec<Source>,
92 #[serde(default)]
94 pub merge: MergeSpec,
95 #[serde(default)]
97 pub files: BTreeMap<RelPath, Override>,
98}
99
100#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
102#[serde(deny_unknown_fields)]
103pub struct Override {
104 #[serde(default)]
106 pub from: Option<String>,
107 #[serde(default)]
109 pub policy: Option<Policy>,
110 #[serde(default)]
112 pub validate: Option<Format>,
113}
114
115impl Target {
116 pub fn find(dir: &Utf8Path) -> Result<Self> {
122 let root = enclosing(dir).ok_or_else(|| TargetError::NotFound { dir: dir.to_owned() })?;
123 Self::load(root)
124 }
125
126 pub fn open_or_new(root: &Utf8Path) -> Result<Self> {
132 match enclosing(root) {
133 Some(found) if found == root => Self::load(root),
134 Some(outer) => {
135 Err(TargetError::Nested { dir: root.to_owned(), root: outer.to_owned() }.into())
136 },
137 None => Ok(Self {
138 root: root.to_owned(),
139 config: Config::default(),
140 config_text: String::new(),
141 lock: Lock::default(),
142 pending: None,
143 state: State::default(),
144 answers: BTreeMap::new(),
145 given: BTreeSet::new(),
146 state_digest: None,
147 }),
148 }
149 }
150
151 #[must_use]
153 pub fn root(&self) -> &Utf8Path {
154 &self.root
155 }
156
157 #[must_use]
159 pub const fn config(&self) -> &Config {
160 &self.config
161 }
162
163 pub fn add_layer(&mut self, source: Source) -> Result<()> {
170 if self.config.layers.contains(&source) {
171 return Err(TargetError::DuplicateLayer { layer: source.to_string() }.into());
172 }
173 let table =
174 ser::to_document(&SourceSpec::from(source)).map_err(io::Error::other)?.into_table();
175 self.edit(|doc| {
176 match doc.entry("layers").or_insert_with(|| Item::ArrayOfTables(ArrayOfTables::new())) {
177 Item::ArrayOfTables(layers) => layers.push(table),
178 Item::Value(Value::Array(layers)) => layers.push(table.into_inline_table()),
179 Item::None | Item::Value(_) | Item::Table(_) => return false,
180 }
181 true
182 })
183 }
184
185 #[must_use]
187 pub fn layer(&self, name: &str) -> Option<&Source> {
188 let lock = self.pending.as_ref().unwrap_or(&self.lock);
189 let locked = lock.layers.iter().find(|locked| locked.name.as_deref() == Some(name))?;
190 self.config.layers.iter().find(|source| **source == locked.source)
191 }
192
193 pub fn remove_layer(&mut self, source: &Source, name: &str) -> Result<()> {
201 let Some(index) = self.config.layers.iter().position(|layer| layer == source) else {
202 return Ok(());
203 };
204 self.edit(|doc| {
205 match doc.get_mut("layers") {
206 Some(Item::ArrayOfTables(layers)) if index < layers.len() => {
207 layers.remove(index);
208 },
209 Some(Item::Value(Value::Array(layers))) if index < layers.len() => {
210 layers.remove(index);
211 },
212 Some(_) | None => return false,
213 }
214 if let Some(files) = doc.get_mut("files").and_then(Item::as_table_like_mut) {
215 let picks = |item: &Item| item.get("from").and_then(Item::as_str) == Some(name);
216 let picked: Vec<String> = files
217 .iter()
218 .filter(|(_, item)| picks(item))
219 .map(|(key, _)| key.to_owned())
220 .collect();
221 for key in picked {
222 files.remove(&key);
223 }
224 }
225 true
226 })
227 }
228
229 pub fn remove_override(&mut self, path: &RelPath) -> Result<()> {
234 self.edit(|doc| {
235 if let Some(files) = doc.get_mut("files").and_then(Item::as_table_like_mut) {
236 files.remove(path.as_str());
237 }
238 true
239 })
240 }
241
242 fn edit(&mut self, edit: impl FnOnce(&mut DocumentMut) -> bool) -> Result<()> {
246 let file = label(CONFIG);
247 let failed = |text: &str, message: String| ParseError {
248 file: file.clone(),
249 text: text.to_owned(),
250 span: None,
251 message,
252 };
253 let mut doc: DocumentMut = self
254 .config_text
255 .parse()
256 .map_err(|e: TomlError| failed(&self.config_text, e.to_string()))?;
257 if !edit(&mut doc) {
258 let message = "`layers` must be a list of tables".to_owned();
259 return Err(failed(&self.config_text, message).into());
260 }
261 let text = doc.to_string();
262 self.config = from_toml(text.as_bytes(), &file)?;
263 self.config_text = text;
264 Ok(())
265 }
266
267 pub fn answer(&mut self, name: VarName, value: String) {
269 self.given.insert(name.clone());
270 self.answers.insert(name, value);
271 }
272
273 #[must_use]
275 pub const fn answers(&self) -> &BTreeMap<VarName, String> {
276 &self.answers
277 }
278
279 pub(crate) fn given(&self) -> impl Iterator<Item = &VarName> {
281 self.given.iter()
282 }
283
284 pub(crate) fn dir(&self) -> Utf8PathBuf {
286 self.root.join(DIR)
287 }
288
289 pub(crate) fn config_text(&self) -> &str {
291 &self.config_text
292 }
293
294 pub(crate) fn locked(&self, source: &Source) -> Option<&Locked> {
299 let lock = self.pending.as_ref().unwrap_or(&self.lock);
300 lock.layers.iter().find(|locked| locked.source == *source)
301 }
302
303 pub(crate) fn sidecar(&self, path: &RelPath) -> Utf8PathBuf {
305 path.under(&self.dir().join(CONFLICTS))
306 }
307
308 pub(crate) fn unfinished(&self) -> Utf8PathBuf {
313 self.dir().join(CONFLICTS).join(DIR)
314 }
315
316 pub(crate) fn base(&self, record: &Fingerprint) -> Result<Option<Vec<u8>>> {
318 let blob = read_optional(&self.dir().join(BASE).join(record.exact.to_string()))?;
319 Ok(blob.filter(|bytes| Digest::of(bytes) == record.exact))
320 }
321
322 pub(crate) fn records(&self) -> impl Iterator<Item = (Slot, Record)> {
324 self.state.records()
325 }
326
327 pub(crate) const fn state_digest(&self) -> Option<Digest> {
329 self.state_digest
330 }
331
332 fn load(root: &Utf8Path) -> Result<Self> {
334 let dir = root.join(DIR);
335 let config_raw = fs_err::read(dir.join(CONFIG))?;
336 let config = from_toml(&config_raw, &label(CONFIG))?;
337 let config_text = String::from_utf8(config_raw).map_err(io::Error::other)?;
338 let state_raw = read_optional(&dir.join(STATE))?;
339 let state_digest = state_raw.as_deref().map(Digest::of);
340 let state = state_raw.map(|raw| from_toml(&raw, &label(STATE))).transpose()?;
341 Ok(Self {
342 root: root.to_owned(),
343 config,
344 config_text,
345 lock: read_toml(&dir, LOCK)?.unwrap_or_default(),
346 pending: read_toml(&dir, &format!("{CONFLICTS}/{DIR}/{PENDING}"))?,
347 state: state.unwrap_or_default(),
348 state_digest,
349 answers: read_toml(&dir, ANSWERS)?.unwrap_or_default(),
350 given: BTreeSet::new(),
351 })
352 }
353}
354
355pub(crate) fn label(name: &str) -> String {
357 format!("{DIR}/{name}")
358}
359
360fn enclosing(dir: &Utf8Path) -> Option<&Utf8Path> {
362 dir.ancestors().find(|d| d.join(DIR).join(CONFIG).is_file())
363}
364
365#[derive(Debug, Default, Serialize, Deserialize)]
367#[serde(deny_unknown_fields)]
368pub(crate) struct Lock {
369 version: V1,
371 #[serde(default)]
373 layers: Vec<Locked>,
374}
375
376#[derive(Debug, Serialize, Deserialize)]
378#[serde(deny_unknown_fields)]
379pub(crate) struct Locked {
380 #[serde(default, skip_serializing_if = "Option::is_none")]
382 pub name: Option<String>,
383 #[serde(default, skip_serializing_if = "Option::is_none")]
385 pub rev: Option<Oid>,
386 pub digest: Digest,
388 pub source: Source,
390}
391
392impl Lock {
393 pub(crate) const fn new(layers: Vec<Locked>) -> Self {
395 Self { version: V1, layers }
396 }
397}
398
399#[derive(Debug, Default, Serialize, Deserialize)]
401#[serde(deny_unknown_fields)]
402pub(crate) struct State {
403 version: V1,
405 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
407 files: BTreeMap<RelPath, FileRecord>,
408 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
410 parts: BTreeMap<RelPath, BTreeMap<String, PartRecord>>,
411}
412
413#[derive(Clone, Copy, Debug)]
415pub(crate) struct Record {
416 pub scope: Scope,
418 pub fingerprint: Fingerprint,
420 pub policy: Policy,
422}
423
424#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
426#[serde(deny_unknown_fields)]
427struct FileRecord {
428 exact: Digest,
430 canonical: Digest,
432 #[serde(default, skip_serializing_if = "Policy::is_owned")]
434 policy: Policy,
435}
436
437#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
439#[serde(deny_unknown_fields)]
440struct PartRecord {
441 scope: Scope,
443 exact: Digest,
445 canonical: Digest,
447 #[serde(default, skip_serializing_if = "Policy::is_owned")]
449 policy: Policy,
450}
451
452impl State {
453 pub(crate) fn new(records: BTreeMap<Slot, Record>) -> Self {
455 let mut state = Self::default();
456 for (Slot { path, part }, Record { scope, fingerprint, policy }) in records {
457 let Fingerprint { exact, canonical } = fingerprint;
458 match part {
459 None => {
460 state.files.insert(path, FileRecord { exact, canonical, policy });
461 },
462 Some(owner) => {
463 let record = PartRecord { scope, exact, canonical, policy };
464 state.parts.entry(path).or_default().insert(owner, record);
465 },
466 }
467 }
468 state
469 }
470
471 fn records(&self) -> impl Iterator<Item = (Slot, Record)> {
473 let files = self.files.iter().map(|(path, record)| {
474 let FileRecord { exact, canonical, policy } = *record;
475 let fingerprint = Fingerprint { exact, canonical };
476 (Slot::whole(path.clone()), Record { scope: Scope::File, fingerprint, policy })
477 });
478 let parts = self.parts.iter().flat_map(|(path, parts)| {
479 parts.iter().map(|(owner, record)| {
480 let slot = Slot { path: path.clone(), part: Some(owner.clone()) };
481 let PartRecord { scope, exact, canonical, policy } = *record;
482 (slot, Record { scope, fingerprint: Fingerprint { exact, canonical }, policy })
483 })
484 });
485 files.chain(parts)
486 }
487
488 pub(crate) fn bases(&self) -> impl Iterator<Item = Digest> {
490 self.records().map(|(_, record)| record.fingerprint.exact)
491 }
492}
493
494#[derive(Clone, Copy, Debug, Default)]
496pub(crate) struct V1;
497
498impl Serialize for V1 {
499 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
500 serializer.serialize_u8(1)
501 }
502}
503
504impl<'de> Deserialize<'de> for V1 {
505 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
506 match u8::deserialize(deserializer)? {
507 1 => Ok(Self),
508 other => Err(de::Error::custom(format_args!("unsupported format version {other}"))),
509 }
510 }
511}
512
513pub(crate) fn from_toml<T: DeserializeOwned>(bytes: &[u8], file: &str) -> Result<T> {
515 let failed = |text: &str, span, message| ParseError {
516 file: file.into(),
517 text: text.into(),
518 span,
519 message,
520 };
521 let text = str::from_utf8(bytes).map_err(|e| failed("", None, e.to_string()))?;
522 Ok(toml::from_str(text).map_err(|e| failed(text, e.span(), e.message().to_owned()))?)
523}
524
525pub(crate) fn read_toml<T: DeserializeOwned>(dir: &Utf8Path, name: &str) -> Result<Option<T>> {
527 read_optional(&dir.join(name))?.map(|raw| from_toml(&raw, &label(name))).transpose()
528}
529
530pub(crate) fn read_optional(path: &Utf8Path) -> Result<Option<Vec<u8>>> {
532 match fs_err::read(path) {
533 Ok(bytes) => Ok(Some(bytes)),
534 Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
535 Err(e) => Err(e.into()),
536 }
537}