Skip to main content

acorde_io/
lib.rs

1//! MusicXML, MIDI, ABC notation, and MuseScore (.mscz/.mscx) parsers and serializer for
2//! [`acorde-core`](https://docs.rs/acorde-core) [`Score`](https://docs.rs/acorde-core/latest/acorde_core/struct.Score.html)
3//! values. Feature-gated per format; accepts `&str`/`&[u8]`, never touches the filesystem.
4
5#[cfg(feature = "abc")]
6pub mod abc;
7mod error;
8#[cfg(feature = "mei")]
9pub mod mei;
10#[cfg(feature = "midi")]
11pub mod midi;
12#[cfg(feature = "mscz")]
13pub mod mscz;
14#[cfg(feature = "musicxml")]
15pub mod musicxml;
16mod report;
17
18/// Baseline limit for uncompressed non-archive parser inputs.
19pub(crate) const MAX_INPUT_BYTES: usize = 64 * 1024 * 1024;
20/// Baseline limit for one logical ABC line, preventing pathological token scans.
21#[cfg(feature = "abc")]
22pub(crate) const MAX_ABC_LINE_BYTES: usize = 1024 * 1024;
23/// Baseline limit for decoded MIDI events before score construction.
24pub(crate) const MAX_MIDI_EVENTS: usize = 500_000;
25
26pub use error::Error;
27pub use report::{
28    Diagnostic, DiagnosticSeverity, ExportReport, ImportReport, REPORT_SCHEMA_VERSION,
29};
30
31#[cfg(test)]
32mod security_tests {
33    use super::Error;
34
35    #[cfg(feature = "midi")]
36    #[test]
37    fn midi_rejects_input_over_baseline_limit() {
38        let data = vec![0_u8; super::MAX_INPUT_BYTES + 1];
39        assert!(matches!(
40            super::midi::parse_midi(&data),
41            Err(Error::TooLarge(size)) if size == super::MAX_INPUT_BYTES + 1
42        ));
43    }
44
45    #[cfg(feature = "abc")]
46    #[test]
47    fn abc_rejects_pathological_line_length() {
48        let text = format!("X:1\n{}", "x".repeat(super::MAX_ABC_LINE_BYTES + 1));
49        assert!(matches!(
50            super::abc::parse_abc(&text),
51            Err(Error::Abc(message)) if message.contains("line exceeds")
52        ));
53    }
54
55    #[cfg(feature = "abc")]
56    #[test]
57    fn abc_rejects_input_over_baseline_limit() {
58        let text = "x".repeat(super::MAX_INPUT_BYTES + 1);
59        assert!(matches!(
60            super::abc::parse_abc(&text),
61            Err(Error::TooLarge(size)) if size == super::MAX_INPUT_BYTES + 1
62        ));
63    }
64}
65
66#[cfg(feature = "musicxml")]
67pub use musicxml::{parse_musicxml, parse_mxl, serialize_musicxml};
68
69#[cfg(feature = "musicxml")]
70pub fn parse_musicxml_with_report(xml: &str) -> Result<ImportReport, Error> {
71    let score = parse_musicxml(xml)?;
72    Ok(ImportReport {
73        schema_version: REPORT_SCHEMA_VERSION,
74        format: "musicxml".to_string(),
75        score,
76        diagnostics: musicxml::loss_diagnostics(xml),
77    })
78}
79
80#[cfg(feature = "musicxml")]
81pub fn parse_mxl_with_report(data: &[u8]) -> Result<ImportReport, Error> {
82    Ok(ImportReport::for_format(parse_mxl(data)?, "mxl"))
83}
84
85#[cfg(feature = "mei")]
86pub use mei::{parse_mei, serialize_mei};
87
88#[cfg(feature = "mei")]
89pub fn parse_mei_with_report(text: &str) -> Result<ImportReport, Error> {
90    mei::parse_mei_with_report(text)
91}
92
93#[cfg(feature = "mei")]
94pub fn serialize_mei_with_report(
95    score: &acorde_core::Score,
96) -> Result<ExportReport<String>, Error> {
97    Ok(ExportReport {
98        schema_version: REPORT_SCHEMA_VERSION,
99        format: "mei".to_string(),
100        output: serialize_mei(score)?,
101        diagnostics: mei::export_loss_diagnostics(score),
102    })
103}
104
105#[cfg(feature = "musicxml")]
106pub fn serialize_musicxml_with_report(
107    score: &acorde_core::Score,
108) -> Result<ExportReport<String>, Error> {
109    Ok(ExportReport {
110        schema_version: REPORT_SCHEMA_VERSION,
111        format: "musicxml".to_string(),
112        output: serialize_musicxml(score)?,
113        diagnostics: musicxml::export_loss_diagnostics(score),
114    })
115}
116
117#[cfg(feature = "midi")]
118pub use midi::{parse_midi, serialize_midi, serialize_midi_region};
119
120#[cfg(feature = "midi")]
121pub fn parse_midi_with_report(data: &[u8]) -> Result<ImportReport, Error> {
122    let score = parse_midi(data)?;
123    let diagnostics = midi::loss_diagnostics(data)?;
124    Ok(ImportReport {
125        schema_version: REPORT_SCHEMA_VERSION,
126        format: "midi".to_string(),
127        score,
128        diagnostics,
129    })
130}
131
132#[cfg(feature = "midi")]
133pub fn serialize_midi_with_report(
134    score: &acorde_core::Score,
135) -> Result<ExportReport<Vec<u8>>, Error> {
136    Ok(ExportReport {
137        schema_version: REPORT_SCHEMA_VERSION,
138        format: "midi".to_string(),
139        output: serialize_midi(score)?,
140        diagnostics: midi::export_loss_diagnostics(score),
141    })
142}
143
144#[cfg(feature = "abc")]
145pub use abc::{parse_abc, serialize_abc};
146
147#[cfg(feature = "abc")]
148pub fn parse_abc_with_report(text: &str) -> Result<ImportReport, Error> {
149    let score = parse_abc(text)?;
150    Ok(ImportReport {
151        schema_version: REPORT_SCHEMA_VERSION,
152        format: "abc".to_string(),
153        score,
154        diagnostics: abc::loss_diagnostics(text),
155    })
156}
157
158#[cfg(feature = "abc")]
159pub fn serialize_abc_with_report(
160    score: &acorde_core::Score,
161) -> Result<ExportReport<String>, Error> {
162    Ok(ExportReport {
163        schema_version: REPORT_SCHEMA_VERSION,
164        format: "abc".to_string(),
165        output: serialize_abc(score)?,
166        diagnostics: abc::export_loss_diagnostics(score),
167    })
168}
169
170#[cfg(feature = "mscz")]
171pub use mscz::{parse_mscx, parse_mscz};
172
173#[cfg(feature = "mscz")]
174pub fn parse_mscx_with_report(text: &str) -> Result<ImportReport, Error> {
175    let score = parse_mscx(text)?;
176    let mut diagnostics = mscz::loss_diagnostics(text);
177    diagnostics.extend(mscz::tab_position_diagnostics(&score));
178    Ok(ImportReport {
179        schema_version: REPORT_SCHEMA_VERSION,
180        format: "mscx".to_string(),
181        score,
182        diagnostics,
183    })
184}
185
186#[cfg(feature = "mscz")]
187pub fn parse_mscz_with_report(data: &[u8]) -> Result<ImportReport, Error> {
188    let score = parse_mscz(data)?;
189    let mut diagnostics = mscz::loss_diagnostics_mscz(data)?;
190    diagnostics.extend(mscz::tab_position_diagnostics(&score));
191    Ok(ImportReport {
192        schema_version: REPORT_SCHEMA_VERSION,
193        format: "mscz".to_string(),
194        score,
195        diagnostics,
196    })
197}