1use std::error::Error;
23use std::fmt;
24use std::fs;
25use std::io;
26use std::io::{Read, Write as _};
27use std::path::{Path, PathBuf};
28use std::sync::atomic::{AtomicUsize, Ordering};
29
30use crate::preset::edits::{EditError, EditErrors};
31use crate::preset::format::{DescribeOptions, FormatError, PresetFormat, format_for};
32use crate::preset::registry::registry_for;
33use crate::preset::template::{Severity, TemplateError, locate_template};
34use crate::synth::{SynthArg, SynthError};
35
36pub mod apply;
37pub mod catalogue;
38pub mod edits;
39pub mod format;
40pub mod mapper;
41pub mod registry;
42pub mod template;
43pub mod vital;
44
45#[derive(Debug)]
47pub enum PresetError {
48 Synth(SynthError),
50 Read {
52 path: PathBuf,
54 cause: io::Error,
56 },
57 Format {
59 path: PathBuf,
61 error: FormatError,
63 },
64 Edits {
66 path: PathBuf,
68 errors: EditErrors,
70 },
71 Template(TemplateError),
73 Output {
75 path: PathBuf,
77 cause: String,
79 fix: String,
81 },
82}
83
84impl fmt::Display for PresetError {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 match self {
87 Self::Synth(error) => error.fmt(f),
88 Self::Read { path, cause } => write!(
89 f,
90 "cannot read {}: {cause}. Check the path and the permissions",
91 path.display()
92 ),
93 Self::Format { path, error } => write!(f, "{}: {error}", path.display()),
94 Self::Edits { path, errors } => write!(f, "{}: {errors}", path.display()),
95 Self::Template(error) => error.fmt(f),
96 Self::Output { path, cause, fix } => {
97 write!(f, "cannot write {}: {cause}. {fix}", path.display())
98 }
99 }
100 }
101}
102
103impl Error for PresetError {
104 fn source(&self) -> Option<&(dyn Error + 'static)> {
105 match self {
106 Self::Synth(error) => Some(error),
107 Self::Read { cause, .. } => Some(cause),
108 Self::Format { error, .. } => Some(error),
109 Self::Edits { errors, .. } => Some(errors),
110 Self::Template(error) => Some(error),
111 Self::Output { .. } => None,
112 }
113 }
114}
115
116impl From<SynthError> for PresetError {
117 fn from(error: SynthError) -> Self {
118 Self::Synth(error)
119 }
120}
121
122pub fn inspect(
130 path: &Path,
131 synth: &SynthArg,
132 opts: &DescribeOptions,
133) -> Result<String, PresetError> {
134 let format = format_for(synth.resolve(Some(path))?)?;
135 let bytes = read_capped(path, format.max_bytes())?;
136 let model = format.read(&bytes).map_err(|error| PresetError::Format {
137 path: path.to_owned(),
138 error,
139 })?;
140 Ok(format!("{}\n{}", model.header(), model.describe(opts)))
141}
142
143pub(crate) fn read_prefix(path: &Path, max: usize) -> io::Result<Vec<u8>> {
146 let limit = u64::try_from(max).map_or(u64::MAX, |max| max.saturating_add(1));
147 let mut bytes = Vec::new();
148 fs::File::open(path).and_then(|file| file.take(limit).read_to_end(&mut bytes))?;
149 Ok(bytes)
150}
151
152const EDIT_LIST_MAX_BYTES: usize = 1 << 20;
155
156fn read_edit_list(path: &Path) -> Result<String, PresetError> {
160 let refuse = |cause: String, fix: &str| PresetError::Edits {
161 path: path.to_owned(),
162 errors: EditErrors(vec![EditError {
163 place: "the edit list".to_owned(),
164 cause,
165 fix: fix.to_owned(),
166 }]),
167 };
168 let bytes = read_prefix(path, EDIT_LIST_MAX_BYTES).map_err(|cause| PresetError::Read {
169 path: path.to_owned(),
170 cause,
171 })?;
172 if bytes.len() > EDIT_LIST_MAX_BYTES {
173 return Err(refuse(
174 format!("the file is larger than the limit of {EDIT_LIST_MAX_BYTES} bytes"),
175 "Pass the edit list itself; a list holds one short record per edit",
176 ));
177 }
178 String::from_utf8(bytes).map_err(|error| {
179 refuse(
180 format!(
181 "byte {} is not UTF-8 text",
182 error.utf8_error().valid_up_to()
183 ),
184 "Save the file as UTF-8",
185 )
186 })
187}
188
189fn read_capped(path: &Path, max: usize) -> Result<Vec<u8>, PresetError> {
192 let bytes = read_prefix(path, max).map_err(|cause| PresetError::Read {
193 path: path.to_owned(),
194 cause,
195 })?;
196 if bytes.len() > max {
197 return Err(PresetError::Format {
198 path: path.to_owned(),
199 error: FormatError::new(
200 "cannot read the preset",
201 format!("the file is larger than the limit of {max} bytes"),
202 "Check that the file is a preset of the target synth",
203 ),
204 });
205 }
206 Ok(bytes)
207}
208
209#[derive(Clone, Debug)]
211pub struct ApplyRequest<'a> {
212 pub edits: &'a Path,
214 pub out: &'a Path,
216 pub template: Option<&'a Path>,
218 pub synth: &'a SynthArg,
220 pub name: Option<&'a str>,
222 pub force: bool,
224}
225
226fn same_file(a: &Path, b: &Path) -> bool {
229 match (fs::canonicalize(a), fs::canonicalize(b)) {
230 (Ok(a), Ok(b)) => a == b,
231 _ => a == b,
232 }
233}
234
235fn check_output(request: &ApplyRequest<'_>, format: &dyn PresetFormat) -> Result<(), PresetError> {
239 let out = request.out;
240 let synth = format.synth();
241 let refuse = |cause: String, fix: String| PresetError::Output {
242 path: out.to_owned(),
243 cause,
244 fix,
245 };
246 let extension = out.extension().and_then(|ext| ext.to_str()).unwrap_or("");
247 if !extension.eq_ignore_ascii_case(format.extension()) {
248 return Err(refuse(
249 format!(
250 "a {} preset needs the .{} extension",
251 synth.name(),
252 format.extension()
253 ),
254 format!("Name the output like bass.{}", format.extension()),
255 ));
256 }
257 if same_file(out, request.edits)
258 || request
259 .template
260 .is_some_and(|template| same_file(out, template))
261 {
262 return Err(refuse(
263 "it is the edit list or the template".to_owned(),
264 "Write to a new file; the inputs stay as they are".to_owned(),
265 ));
266 }
267 if !request.force && fs::symlink_metadata(out).is_ok() {
268 return Err(exists(out));
269 }
270 let parent = out
274 .parent()
275 .filter(|dir| !dir.as_os_str().is_empty())
276 .unwrap_or_else(|| Path::new("."));
277 let parent = fs::canonicalize(parent).unwrap_or_else(|_| parent.to_owned());
278 let factory = format.factory_folder();
279 let inside = !factory.is_empty()
280 && parent.ancestors().any(|dir| {
281 let tail: Vec<_> = dir.iter().rev().take(factory.len()).collect();
282 tail.len() == factory.len()
283 && tail.iter().zip(factory.iter().rev()).all(|(have, want)| {
284 have.to_str()
285 .is_some_and(|have| have.eq_ignore_ascii_case(want))
286 })
287 });
288 if inside {
289 return Err(refuse(
290 format!(
291 "it is inside {}, the factory folder of {}",
292 factory.join("/"),
293 synth.name()
294 ),
295 "Write into your own preset folder; factory presets stay untouched".to_owned(),
296 ));
297 }
298 Ok(())
299}
300
301pub fn apply_file(request: &ApplyRequest<'_>) -> Result<String, PresetError> {
320 let out = request.out;
321 let synth = request.synth.resolve(request.template.or(Some(out)))?;
322 let format = format_for(synth)?;
323 let registry = registry_for(synth)?;
324 check_output(request, format)?;
325 let edit_errors = |errors: EditErrors| PresetError::Edits {
326 path: request.edits.to_owned(),
327 errors,
328 };
329 let text = read_edit_list(request.edits)?;
330 let list = edits::parse(&text, edits::Path::Agent).map_err(edit_errors)?;
331 let template = locate_template(format.template(), request.template, format.max_bytes())
332 .map_err(PresetError::Template)?;
333 let template_path = match &template.origin {
334 template::Origin::CommandLine(path) | template::Origin::Candidate(path) => path.clone(),
335 template::Origin::Builtin => PathBuf::from("built-in template"),
336 };
337 let mut model = format
338 .read(&template.bytes)
339 .map_err(|error| PresetError::Format {
340 path: template_path.clone(),
341 error,
342 })?;
343 let checks = format.template().check(model.as_ref());
344 if let Some(error) = checks
345 .iter()
346 .find(|check| check.severity == Severity::Error)
347 {
348 return Err(PresetError::Format {
349 path: template_path,
350 error: FormatError::new(
351 "cannot use the template",
352 error.text.clone(),
353 error.fix.clone(),
354 ),
355 });
356 }
357 let resolved = edits::resolve(&list, registry).map_err(edit_errors)?;
358 let report = apply::apply(format, registry, model.as_mut(), &resolved).map_err(edit_errors)?;
359 let name = request
360 .name
361 .map(str::to_owned)
362 .or_else(|| {
363 out.file_stem()
364 .and_then(|stem| stem.to_str())
365 .map(str::to_owned)
366 })
367 .unwrap_or_default();
368 let write_error = |error: FormatError| PresetError::Format {
369 path: out.to_owned(),
370 error,
371 };
372 model.set_name(&name).map_err(write_error)?;
373 let bytes = format.write(model.as_ref()).map_err(write_error)?;
374 format.read(&bytes).map_err(write_error)?;
375 write_atomically(out, format.extension(), &bytes, request.force)?;
376 let stamp = model
377 .header()
378 .fields
379 .into_iter()
380 .find(|field| field.label == "build")
381 .map(|field| field.value);
382 Ok(apply_summary(
383 out,
384 &template.origin,
385 &name,
386 stamp.as_deref(),
387 &report,
388 &checks,
389 ))
390}
391
392fn exists(out: &Path) -> PresetError {
394 PresetError::Output {
395 path: out.to_owned(),
396 cause: "the file exists".to_owned(),
397 fix: "Pass --force to replace it, or write to a new file".to_owned(),
398 }
399}
400
401fn write_atomically(
406 out: &Path,
407 extension: &str,
408 bytes: &[u8],
409 force: bool,
410) -> Result<(), PresetError> {
411 publish(out, extension, bytes, force, |from, to| {
412 fs::hard_link(from, to)
413 })
414}
415
416fn publish(
419 out: &Path,
420 extension: &str,
421 bytes: &[u8],
422 force: bool,
423 link: impl Fn(&Path, &Path) -> io::Result<()>,
424) -> Result<(), PresetError> {
425 static RUN: AtomicUsize = AtomicUsize::new(0);
426 let temporary = out.with_extension(format!(
427 "{extension}.{}-{}.partial",
428 std::process::id(),
429 RUN.fetch_add(1, Ordering::Relaxed)
430 ));
431 let output_error = |cause: io::Error| PresetError::Output {
432 path: out.to_owned(),
433 cause: cause.to_string(),
434 fix: "Check that the folder exists, is writable and has free space".to_owned(),
435 };
436 let mut file = fs::OpenOptions::new()
438 .write(true)
439 .create_new(true)
440 .open(&temporary)
441 .map_err(output_error)?;
442 let failed = |cause: io::Error| {
443 let _ = fs::remove_file(&temporary);
444 output_error(cause)
445 };
446 file.write_all(bytes)
447 .and_then(|()| file.sync_all())
448 .map_err(failed)?;
449 drop(file);
450 if force {
451 return fs::rename(&temporary, out).map_err(failed);
452 }
453 let linked = link(&temporary, out);
454 let _ = fs::remove_file(&temporary);
455 match linked {
456 Ok(()) => Ok(()),
457 Err(cause) if cause.kind() == io::ErrorKind::AlreadyExists => Err(exists(out)),
458 Err(_) => {
463 let mut file = fs::OpenOptions::new()
464 .write(true)
465 .create_new(true)
466 .open(out)
467 .map_err(|cause| match cause.kind() {
468 io::ErrorKind::AlreadyExists => exists(out),
469 _ => output_error(cause),
470 })?;
471 file.write_all(bytes)
472 .and_then(|()| file.sync_all())
473 .map_err(|cause| {
474 let _ = fs::remove_file(out);
475 output_error(cause)
476 })
477 }
478 }
479}
480
481fn apply_summary(
482 out: &Path,
483 origin: &template::Origin,
484 name: &str,
485 stamp: Option<&str>,
486 report: &apply::ApplyReport,
487 checks: &[template::TemplateCheck],
488) -> String {
489 use std::fmt::Write as _;
490 let mut text = format!("wrote {} ({name:?}) from {origin}\n", out.display());
491 if let Some(stamp) = stamp {
492 let _ = writeln!(text, " build {stamp}, kept from the template");
493 }
494 for change in &report.changes {
495 let mut notes = Vec::new();
496 if change.uncalibrated {
497 notes.push("uncalibrated");
498 }
499 if change.raw {
500 notes.push("raw");
501 }
502 let notes = if notes.is_empty() {
503 String::new()
504 } else {
505 format!(" [{}]", notes.join(", "))
506 };
507 let _ = writeln!(text, " set {} = {}{notes}", change.target, change.value);
508 }
509 for skipped in &report.skipped {
510 let _ = writeln!(text, " skipped {}: {}", skipped.target, skipped.why);
511 }
512 for check in checks {
513 let _ = writeln!(text, " {check}");
514 }
515 for warning in &report.warnings {
516 let _ = writeln!(text, " warning: {warning}");
517 }
518 text
519}
520
521#[cfg(test)]
522mod tests {
523 use super::*;
524
525 #[test]
526 fn a_file_that_appears_after_the_check_is_not_replaced() {
527 let dir = tempfile::tempdir().unwrap();
528 let out = dir.path().join("Out.vital");
529 fs::write(&out, "theirs").unwrap();
530 let error = write_atomically(&out, "vital", b"ours", false)
531 .unwrap_err()
532 .to_string();
533 assert!(error.contains("the file exists. Pass --force"), "{error}");
534 assert_eq!(fs::read_to_string(&out).unwrap(), "theirs");
535 assert_eq!(fs::read_dir(dir.path()).unwrap().count(), 1);
536 write_atomically(&out, "vital", b"ours", true).unwrap();
537 assert_eq!(fs::read_to_string(&out).unwrap(), "ours");
538 let new = dir.path().join("New.vital");
539 write_atomically(&new, "vital", b"new", false).unwrap();
540 assert_eq!(fs::read_to_string(&new).unwrap(), "new");
541 assert_eq!(fs::read_dir(dir.path()).unwrap().count(), 2);
542 }
543
544 #[test]
545 fn a_folder_without_hard_links_still_gets_a_new_file() {
546 let dir = tempfile::tempdir().unwrap();
547 let no_links = |_: &Path, _: &Path| Err(io::Error::from(io::ErrorKind::Unsupported));
548 let new = dir.path().join("New.vital");
549 publish(&new, "vital", b"new", false, no_links).unwrap();
550 assert_eq!(fs::read_to_string(&new).unwrap(), "new");
551 let error = publish(&new, "vital", b"again", false, no_links)
552 .unwrap_err()
553 .to_string();
554 assert!(error.contains("the file exists. Pass --force"), "{error}");
555 assert_eq!(fs::read_to_string(&new).unwrap(), "new");
556 assert_eq!(fs::read_dir(dir.path()).unwrap().count(), 1);
557 }
558
559 #[test]
560 fn an_edit_list_is_refused_as_an_edit_list() {
561 let dir = tempfile::tempdir().unwrap();
562 let big = dir.path().join("big.json");
563 fs::write(&big, vec![b' '; EDIT_LIST_MAX_BYTES + 1]).unwrap();
564 let error = read_edit_list(&big).unwrap_err().to_string();
565 assert_eq!(
566 error,
567 format!(
568 "{}: the edit list has 1 problem(s):\n the edit list: the file is larger than the limit of 1048576 bytes. Pass the edit list itself; a list holds one short record per edit",
569 big.display()
570 )
571 );
572 fs::write(&big, vec![b' '; EDIT_LIST_MAX_BYTES]).unwrap();
573 assert_eq!(read_edit_list(&big).unwrap().len(), EDIT_LIST_MAX_BYTES);
574 let latin = dir.path().join("latin.json");
575 fs::write(&latin, b"{\"x\": \"caf\xe9\"}").unwrap();
576 let error = read_edit_list(&latin).unwrap_err().to_string();
577 assert!(
578 error.ends_with("the edit list: byte 10 is not UTF-8 text. Save the file as UTF-8"),
579 "{error}"
580 );
581 }
582
583 #[test]
584 fn an_oversized_file_is_refused_unread() {
585 let dir = tempfile::tempdir().unwrap();
586 let path = dir.path().join("big.vital");
587 fs::write(&path, vec![b' '; 11]).unwrap();
588 let error = read_capped(&path, 10).unwrap_err().to_string();
589 assert!(
590 error.contains("larger than the limit of 10 bytes"),
591 "{error}"
592 );
593 assert_eq!(read_capped(&path, 11).unwrap().len(), 11);
594 }
595}