rcue/
parser.rs

1use std::env;
2use std::fs::File;
3use std::io::{BufRead, BufReader};
4
5use cue::{Command, Cue, CueFile, Track};
6use errors::CueError;
7use util::{next_string, next_token, next_values, timestamp_to_duration};
8
9/// Parses a CUE file at `path` into a [`Cue`](struct.Cue.html) struct.
10///
11/// Strict mode (`strict: true`) will return a [`CueError`](../errors/enum.CueError.html) if invalid fields or extra lines are detected.
12/// When not in strict mode, bad lines and fields will be skipped, and unknown
13/// fields will be stored in [`Cue.unknown`](struct.Cue.html).
14///
15/// # Example
16///
17/// ```
18/// use rcue::parser::parse_from_file;
19///
20/// let cue = parse_from_file("test/fixtures/unicode.cue", true).unwrap();
21/// assert_eq!(cue.title, Some("マジコカタストロフィ".to_string()));
22/// ```
23///
24/// # Failures
25///
26/// Fails if the CUE file can not be parsed from the file.
27#[allow(dead_code)]
28pub fn parse_from_file(path: &str, strict: bool) -> Result<Cue, CueError> {
29    let file = File::open(path)?;
30    let mut buf_reader = BufReader::new(file);
31    parse(&mut buf_reader, strict)
32}
33
34/// Parses a [`BufRead`](https://doc.rust-lang.org/std/io/trait.BufRead.html) into a [`Cue`](struct.Cue.html) struct.
35///
36/// Strict mode will return [`CueError`](../errors/enum.CueError.html) if invalid fields or extra lines are detected.
37/// When not in strict mode, bad lines and fields will be skipped, and unknown
38/// fields will be stored in [`Cue.unknown`](struct.Cue.html).
39///
40/// # Example
41///
42/// ```
43/// use rcue::parser::parse;
44/// use std::fs::File;
45/// use std::io::BufReader;
46///
47/// let file = File::open("test/fixtures/unicode.cue").unwrap();
48/// let mut buf_reader = BufReader::new(file);
49/// let cue = parse(&mut buf_reader, true).unwrap();
50/// assert_eq!(cue.title, Some("マジコカタストロフィ".to_string()));
51/// ```
52///
53/// # Failures
54///
55/// Fails if the CUE file can not be parsed.
56#[allow(dead_code)]
57pub fn parse(buf_reader: &mut dyn BufRead, strict: bool) -> Result<Cue, CueError> {
58    let verbose = env::var_os("RCUE_LOG").map(|s| s == "1").unwrap_or(false);
59
60    macro_rules! fail_if_strict {
61        ($line_no:ident, $line:ident, $reason:expr) => {
62            if strict {
63                if verbose {
64                    println!(
65                        "Strict mode failure: did not parse line {}: {}\n\tReason: {:?}",
66                        $line_no + 1,
67                        $line,
68                        $reason
69                    );
70                }
71                return Err(CueError::Parse(format!("strict mode failure: {}", $reason)));
72            }
73        };
74    }
75
76    let mut cue = Cue::new();
77
78    fn last_file(cue: &mut Cue) -> Option<&mut CueFile> {
79        cue.files.last_mut()
80    }
81
82    fn last_track(cue: &mut Cue) -> Option<&mut Track> {
83        last_file(cue).and_then(|f| f.tracks.last_mut())
84    }
85
86    for (i, line) in buf_reader.lines().enumerate() {
87        if let Ok(ref l) = line {
88            let token = tokenize_line(l);
89
90            match token {
91                Ok(Command::CdTextFile(path)) => {
92                    cue.cd_text_file = Some(path);
93                }
94                Ok(Command::Flags(flags)) => {
95                    if last_track(&mut cue).is_some() {
96                        last_track(&mut cue).unwrap().flags = flags;
97                    } else {
98                        fail_if_strict!(i, l, "FLAG assigned to no TRACK");
99                    }
100                }
101                Ok(Command::Isrc(isrc)) => {
102                    if last_track(&mut cue).is_some() {
103                        last_track(&mut cue).unwrap().isrc = Some(isrc);
104                    } else {
105                        fail_if_strict!(i, l, "ISRC assigned to no TRACK");
106                    }
107                }
108                Ok(Command::Rem(field, value)) => {
109                    let comment = (field, value);
110
111                    if last_track(&mut cue).is_some() {
112                        last_track(&mut cue).unwrap().comments.push(comment);
113                    } else if last_file(&mut cue).is_some() {
114                        last_file(&mut cue).unwrap().comments.push(comment);
115                    } else {
116                        cue.comments.push(comment);
117                    }
118                }
119                Ok(Command::File(file, format)) => {
120                    cue.files.push(CueFile::new(&file, &format));
121                }
122                Ok(Command::Track(idx, mode)) => {
123                    if let Some(file) = last_file(&mut cue) {
124                        file.tracks.push(Track::new(&idx, &mode));
125                    } else {
126                        fail_if_strict!(i, l, "TRACK assigned to no FILE");
127                    }
128                }
129                Ok(Command::Title(title)) => {
130                    if last_track(&mut cue).is_some() {
131                        last_track(&mut cue).unwrap().title = Some(title);
132                    } else {
133                        cue.title = Some(title)
134                    }
135                }
136                Ok(Command::Performer(performer)) => {
137                    // this double check might be able to go away under non-lexical lifetimes
138                    if last_track(&mut cue).is_some() {
139                        last_track(&mut cue).unwrap().performer = Some(performer);
140                    } else {
141                        cue.performer = Some(performer);
142                    }
143                }
144                Ok(Command::Songwriter(songwriter)) => {
145                    if last_track(&mut cue).is_some() {
146                        last_track(&mut cue).unwrap().songwriter = Some(songwriter);
147                    } else {
148                        cue.songwriter = Some(songwriter);
149                    }
150                }
151                Ok(Command::Index(idx, time)) => {
152                    if let Some(track) = last_track(&mut cue) {
153                        if let Ok(duration) = timestamp_to_duration(&time) {
154                            track.indices.push((idx, duration));
155                        } else {
156                            fail_if_strict!(i, l, "bad INDEX timestamp");
157                        }
158                    } else {
159                        fail_if_strict!(i, l, "INDEX assigned to no track");
160                    }
161                }
162                Ok(Command::Pregap(time)) => {
163                    if last_track(&mut cue).is_some() {
164                        if let Ok(duration) = timestamp_to_duration(&time) {
165                            last_track(&mut cue).unwrap().pregap = Some(duration);
166                        } else {
167                            fail_if_strict!(i, l, "bad PREGAP timestamp");
168                        }
169                    } else {
170                        fail_if_strict!(i, l, "PREGAP assigned to no track");
171                    }
172                }
173                Ok(Command::Postgap(time)) => {
174                    if last_track(&mut cue).is_some() {
175                        if let Ok(duration) = timestamp_to_duration(&time) {
176                            last_track(&mut cue).unwrap().postgap = Some(duration);
177                        } else {
178                            fail_if_strict!(i, l, "bad PREGAP timestamp");
179                        }
180                    } else {
181                        fail_if_strict!(i, l, "POSTGAP assigned to no track");
182                    }
183                }
184                Ok(Command::Catalog(id)) => {
185                    cue.catalog = Some(id);
186                }
187                Ok(Command::Unknown(line)) => {
188                    fail_if_strict!(i, l, &format!("unknown token -- {}", &line));
189
190                    if last_track(&mut cue).is_some() {
191                        last_track(&mut cue).unwrap().unknown.push(line);
192                    } else {
193                        cue.unknown.push(line)
194                    }
195                }
196                _ => {
197                    fail_if_strict!(i, l, &format!("bad line -- {:?}", &line));
198                    if verbose {
199                        println!("Bad line - did not parse line {}: {:?}", i + 1, l);
200                    }
201                }
202            }
203        }
204    }
205
206    Ok(cue)
207}
208
209#[allow(dead_code)]
210fn tokenize_line(line: &str) -> Result<Command, CueError> {
211    let mut chars = line.trim().chars();
212
213    let command = next_token(&mut chars);
214    let command = if command.is_empty() {
215        None
216    } else {
217        Some(command)
218    };
219
220    match command {
221        Some(c) => match c.to_uppercase().as_ref() {
222            "REM" => {
223                let key = next_token(&mut chars);
224                let val = next_string(&mut chars, "missing REM value")?;
225                Ok(Command::Rem(key, val))
226            }
227            "CATALOG" => {
228                let val = next_string(&mut chars, "missing CATALOG value")?;
229                Ok(Command::Catalog(val))
230            }
231            "CDTEXTFILE" => {
232                let val = next_string(&mut chars, "missing CDTEXTFILE value")?;
233                Ok(Command::CdTextFile(val))
234            }
235            "TITLE" => {
236                let val = next_string(&mut chars, "missing TITLE value")?;
237                Ok(Command::Title(val))
238            }
239            "FILE" => {
240                let path = next_string(&mut chars, "missing path for FILE")?;
241                let format = next_token(&mut chars);
242                Ok(Command::File(path, format))
243            }
244            "FLAGS" => {
245                let flags = next_values(&mut chars);
246                Ok(Command::Flags(flags))
247            }
248            "ISRC" => {
249                let val = next_token(&mut chars);
250                Ok(Command::Isrc(val))
251            }
252            "PERFORMER" => {
253                let val = next_string(&mut chars, "missing PERFORMER value")?;
254                Ok(Command::Performer(val))
255            }
256            "SONGWRITER" => {
257                let val = next_string(&mut chars, "missing SONGWRITER value")?;
258                Ok(Command::Songwriter(val))
259            }
260            "TRACK" => {
261                let val = next_token(&mut chars);
262                let mode = next_token(&mut chars);
263                Ok(Command::Track(val, mode))
264            }
265            "PREGAP" => {
266                let val = next_token(&mut chars);
267                Ok(Command::Pregap(val))
268            }
269            "POSTGAP" => {
270                let val = next_token(&mut chars);
271                Ok(Command::Postgap(val))
272            }
273            "INDEX" => {
274                let val = next_token(&mut chars);
275                let time = next_token(&mut chars);
276                Ok(Command::Index(val, time))
277            }
278            _ => {
279                let rest: String = chars.collect();
280                if rest.is_empty() {
281                    Ok(Command::None)
282                } else {
283                    Ok(Command::Unknown(line.to_string()))
284                }
285            }
286        },
287        _ => Ok(Command::None),
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294    use std::time::Duration;
295
296    #[test]
297    fn test_parsing_good_cue() {
298        let cue = parse_from_file("test/fixtures/good.cue", true).unwrap();
299        assert_eq!(cue.comments.len(), 4);
300        assert_eq!(
301            cue.comments[0],
302            ("GENRE".to_string(), "Alternative".to_string(),)
303        );
304        assert_eq!(cue.comments[1], ("DATE".to_string(), "1991".to_string()));
305        assert_eq!(
306            cue.comments[2],
307            ("DISCID".to_string(), "860B640B".to_string(),)
308        );
309        assert_eq!(
310            cue.comments[3],
311            ("COMMENT".to_string(), "ExactAudioCopy v0.95b4".to_string(),)
312        );
313        assert_eq!(cue.performer, Some("My Bloody Valentine".to_string()));
314        assert_eq!(cue.songwriter, Some("foobar".to_string()));
315        assert_eq!(cue.title, Some("Loveless".to_string()));
316        assert_eq!(cue.cd_text_file, Some("./cdtextfile".to_string()));
317
318        assert_eq!(cue.files.len(), 1);
319        let file = &cue.files[0];
320        assert_eq!(file.file, "My Bloody Valentine - Loveless.wav");
321        assert_eq!(file.format, "WAVE");
322
323        assert_eq!(file.tracks.len(), 2);
324        let track = &file.tracks[0];
325        assert_eq!(track.no, "01".to_string());
326        assert_eq!(track.format, "AUDIO".to_string());
327        assert_eq!(track.songwriter, Some("barbaz bax".to_string()));
328        assert_eq!(track.title, Some("Only Shallow".to_string()));
329        assert_eq!(track.performer, Some("My Bloody Valentine".to_string()));
330        assert_eq!(track.indices.len(), 1);
331        assert_eq!(track.indices[0], ("01".to_string(), Duration::new(0, 0)));
332        assert_eq!(track.isrc, Some("USRC17609839".to_string()));
333        assert_eq!(track.flags, vec!["DCP", "4CH", "PRE", "SCMS"]);
334    }
335
336    #[test]
337    fn test_parsing_unicode() {
338        let cue = parse_from_file("test/fixtures/unicode.cue", true).unwrap();
339        assert_eq!(cue.title, Some("マジコカタストロフィ".to_string()));
340    }
341
342    #[test]
343    fn test_case_sensitivity() {
344        let cue = parse_from_file("test/fixtures/case_sensitivity.cue", true).unwrap();
345        assert_eq!(cue.title, Some("Loveless".to_string()));
346        assert_eq!(cue.performer, Some("My Bloody Valentine".to_string()));
347    }
348
349    #[test]
350    fn test_bad_intentation() {
351        let cue = parse_from_file("test/fixtures/bad_indentation.cue", true).unwrap();
352        assert_eq!(cue.title, Some("Loveless".to_string()));
353        assert_eq!(cue.files.len(), 1);
354        assert_eq!(cue.files[0].tracks.len(), 2);
355        assert_eq!(
356            cue.files[0].tracks[0].title,
357            Some("Only Shallow".to_string())
358        );
359    }
360
361    #[test]
362    fn test_unknown_field_lenient() {
363        let cue = parse_from_file("test/fixtures/unknown_field.cue", false).unwrap();
364        assert_eq!(cue.unknown[0], "FOO WHAT 12345");
365    }
366
367    #[test]
368    fn test_unknown_field_strict() {
369        let cue = parse_from_file("test/fixtures/unknown_field.cue", true);
370        assert!(cue.is_err());
371    }
372
373    #[test]
374    fn test_empty_lines_lenient() {
375        let cue = parse_from_file("test/fixtures/empty_lines.cue", false).unwrap();
376        assert_eq!(cue.comments.len(), 4);
377        assert_eq!(cue.files.len(), 1);
378        assert_eq!(cue.files[0].tracks.len(), 2);
379    }
380
381    #[test]
382    fn test_empty_lines_strict() {
383        let cue = parse_from_file("test/fixtures/empty_lines.cue", true);
384        assert!(cue.is_err());
385    }
386
387    #[test]
388    fn test_duplicate_comment() {
389        let cue = parse_from_file("test/fixtures/duplicate_comment.cue", true).unwrap();
390        assert_eq!(cue.comments.len(), 5);
391        assert_eq!(cue.comments[1], ("DATE".to_string(), "1991".to_string()));
392        assert_eq!(cue.comments[2], ("DATE".to_string(), "1992".to_string()));
393    }
394
395    #[test]
396    fn test_duplicate_title() {
397        let cue = parse_from_file("test/fixtures/duplicate_title.cue", true).unwrap();
398        assert_eq!(cue.title, Some("Loveless 2".to_string()));
399    }
400
401    #[test]
402    fn test_duplicate_track() {
403        let cue = parse_from_file("test/fixtures/duplicate_track.cue", true).unwrap();
404        assert_eq!(cue.files[0].tracks[0], cue.files[0].tracks[1]);
405    }
406
407    #[test]
408    fn test_duplicate_file() {
409        let cue = parse_from_file("test/fixtures/duplicate_file.cue", true).unwrap();
410        assert_eq!(cue.files.len(), 2);
411        assert_eq!(cue.files[0], cue.files[1]);
412    }
413
414    #[test]
415    fn test_bad_index_lenient() {
416        let cue = parse_from_file("test/fixtures/bad_index.cue", false).unwrap();
417        assert_eq!(cue.files[0].tracks[0].indices.len(), 0);
418    }
419
420    #[test]
421    fn test_bad_index_strict() {
422        let cue = parse_from_file("test/fixtures/bad_index.cue", true);
423        assert!(cue.is_err());
424    }
425
426    #[test]
427    fn test_bad_index_timestamp_lenient() {
428        let cue = parse_from_file("test/fixtures/bad_index_timestamp.cue", false).unwrap();
429        assert_eq!(cue.files[0].tracks[0].indices.len(), 0);
430    }
431
432    #[test]
433    fn test_bad_index_timestamp_strict() {
434        let cue = parse_from_file("test/fixtures/bad_index_timestamp.cue", true);
435        assert!(cue.is_err());
436    }
437
438    #[test]
439    fn test_pregap_postgap() {
440        let cue = parse_from_file("test/fixtures/pregap.cue", true).unwrap();
441        assert_eq!(cue.files[0].tracks[0].pregap, Some(Duration::new(1, 0)));
442        assert_eq!(cue.files[0].tracks[0].postgap, Some(Duration::new(2, 0)));
443    }
444
445    #[test]
446    fn test_bad_pregap_timestamp_strict() {
447        let cue = parse_from_file("test/fixtures/bad_pregap_timestamp.cue", true);
448        assert!(cue.is_err());
449    }
450
451    #[test]
452    fn test_bad_pregap_timestamp_lenient() {
453        let cue = parse_from_file("test/fixtures/bad_pregap_timestamp.cue", false).unwrap();
454        assert!(cue.files[0].tracks[0].pregap.is_none());
455    }
456
457    #[test]
458    fn test_bad_postgap_timestamp_strict() {
459        let cue = parse_from_file("test/fixtures/bad_postgap_timestamp.cue", true);
460        assert!(cue.is_err());
461    }
462
463    #[test]
464    fn test_bad_postgap_timestamp_lenient() {
465        let cue = parse_from_file("test/fixtures/bad_postgap_timestamp.cue", false).unwrap();
466        assert!(cue.files[0].tracks[0].postgap.is_none());
467    }
468
469    #[test]
470    fn test_catalog() {
471        let cue = parse_from_file("test/fixtures/catalog.cue", true).unwrap();
472        assert_eq!(cue.catalog, Some("TESTCATALOG-ID 64".to_string()));
473    }
474
475    #[test]
476    fn test_comments() {
477        let cue = parse_from_file("test/fixtures/comments.cue", true).unwrap();
478        assert_eq!(cue.comments.len(), 4);
479        assert_eq!(cue.files[0].comments.len(), 1);
480        assert_eq!(cue.files[0].tracks[0].comments.len(), 1);
481        assert_eq!(cue.files[0].tracks[1].comments.len(), 2);
482        assert_eq!(
483            cue.files[0].tracks[1].comments[0],
484            ("TRACK".to_string(), "2".to_string(),)
485        );
486        assert_eq!(
487            cue.files[0].tracks[1].comments[1],
488            ("TRACK".to_string(), "2.1".to_string(),)
489        );
490    }
491
492    #[test]
493    fn test_orphan_track_strict() {
494        let cue = parse_from_file("test/fixtures/orphan_track.cue", true);
495        assert!(cue.is_err());
496    }
497
498    #[test]
499    fn test_orphan_track_lenient() {
500        let cue = parse_from_file("test/fixtures/orphan_track.cue", false).unwrap();
501        assert_eq!(cue.files.len(), 0);
502    }
503
504    #[test]
505    fn test_orphan_index_strict() {
506        let cue = parse_from_file("test/fixtures/orphan_index.cue", true);
507        assert!(cue.is_err());
508    }
509
510    #[test]
511    fn test_orphan_index_lenient() {
512        let cue = parse_from_file("test/fixtures/orphan_index.cue", false).unwrap();
513        assert_eq!(cue.files[0].tracks.len(), 1);
514        assert_eq!(cue.files[0].tracks[0].indices.len(), 1);
515        assert_eq!(
516            cue.files[0].tracks[0].indices[0],
517            ("01".to_string(), Duration::new(257, 693333333,),)
518        );
519    }
520
521    #[test]
522    fn test_orphan_pregap_strict() {
523        let cue = parse_from_file("test/fixtures/orphan_pregap.cue", true);
524        assert!(cue.is_err());
525    }
526
527    #[test]
528    fn test_orphan_pregap_lenient() {
529        let cue = parse_from_file("test/fixtures/orphan_pregap.cue", false).unwrap();
530        assert_eq!(cue.files[0].tracks.len(), 1);
531        assert!(cue.files[0].tracks[0].pregap.is_none());
532    }
533
534    #[test]
535    fn test_orphan_postgap_strict() {
536        let cue = parse_from_file("test/fixtures/orphan_postgap.cue", true);
537        assert!(cue.is_err());
538    }
539
540    #[test]
541    fn test_orphan_postgap_lenient() {
542        let cue = parse_from_file("test/fixtures/orphan_postgap.cue", false).unwrap();
543        assert_eq!(cue.files[0].tracks.len(), 1);
544        assert!(cue.files[0].tracks[0].pregap.is_none());
545    }
546
547    #[test]
548    fn test_missing_file() {
549        let cue = parse_from_file("test/fixtures/missing.cue.missing", true);
550        assert!(cue.is_err());
551    }
552
553    #[test]
554    fn test_bare_file() {
555        use std::io;
556
557        assert!(parse(&mut io::Cursor::new(b"FILE"), true).is_err());
558    }
559}