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#[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#[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 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}