1/* grabbag - Convenience lib for various routines common to several tools
2 * Copyright (C) 2002,2003,2004,2005,2006,2007  Josh Coalson
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2.1 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with this library; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
17 */
18
19#if HAVE_CONFIG_H
20#  include <config.h>
21#endif
22
23#include "share/grabbag.h"
24#include "FLAC/assert.h"
25#include <stdio.h>
26#include <stdlib.h>
27#include <string.h>
28
29unsigned grabbag__cuesheet_msf_to_frame(unsigned minutes, unsigned seconds, unsigned frames)
30{
31	return ((minutes * 60) + seconds) * 75 + frames;
32}
33
34void grabbag__cuesheet_frame_to_msf(unsigned frame, unsigned *minutes, unsigned *seconds, unsigned *frames)
35{
36	*frames = frame % 75;
37	frame /= 75;
38	*seconds = frame % 60;
39	frame /= 60;
40	*minutes = frame;
41}
42
43/* since we only care about values >= 0 or error, returns < 0 for any illegal string, else value */
44static int local__parse_int_(const char *s)
45{
46	int ret = 0;
47	char c;
48
49	if(*s == '\0')
50		return -1;
51
52	while('\0' != (c = *s++))
53		if(c >= '0' && c <= '9')
54			ret = ret * 10 + (c - '0');
55		else
56			return -1;
57
58	return ret;
59}
60
61/* since we only care about values >= 0 or error, returns < 0 for any illegal string, else value */
62static FLAC__int64 local__parse_int64_(const char *s)
63{
64	FLAC__int64 ret = 0;
65	char c;
66
67	if(*s == '\0')
68		return -1;
69
70	while('\0' != (c = *s++))
71		if(c >= '0' && c <= '9')
72			ret = ret * 10 + (c - '0');
73		else
74			return -1;
75
76	return ret;
77}
78
79/* accept '[0-9]+:[0-9][0-9]?:[0-9][0-9]?', but max second of 59 and max frame of 74, e.g. 0:0:0, 123:45:67
80 * return sample number or <0 for error
81 */
82static FLAC__int64 local__parse_msf_(const char *s)
83{
84	FLAC__int64 ret, field;
85	char c;
86
87	c = *s++;
88	if(c >= '0' && c <= '9')
89		field = (c - '0');
90	else
91		return -1;
92	while(':' != (c = *s++)) {
93		if(c >= '0' && c <= '9')
94			field = field * 10 + (c - '0');
95		else
96			return -1;
97	}
98
99	ret = field * 60 * 44100;
100
101	c = *s++;
102	if(c >= '0' && c <= '9')
103		field = (c - '0');
104	else
105		return -1;
106	if(':' != (c = *s++)) {
107		if(c >= '0' && c <= '9') {
108			field = field * 10 + (c - '0');
109			c = *s++;
110			if(c != ':')
111				return -1;
112		}
113		else
114			return -1;
115	}
116
117	if(field >= 60)
118		return -1;
119
120	ret += field * 44100;
121
122	c = *s++;
123	if(c >= '0' && c <= '9')
124		field = (c - '0');
125	else
126		return -1;
127	if('\0' != (c = *s++)) {
128		if(c >= '0' && c <= '9') {
129			field = field * 10 + (c - '0');
130			c = *s++;
131		}
132		else
133			return -1;
134	}
135
136	if(c != '\0')
137		return -1;
138
139	if(field >= 75)
140		return -1;
141
142	ret += field * (44100 / 75);
143
144	return ret;
145}
146
147static char *local__get_field_(char **s, FLAC__bool allow_quotes)
148{
149	FLAC__bool has_quote = false;
150	char *p;
151
152	FLAC__ASSERT(0 != s);
153
154	if(0 == *s)
155		return 0;
156
157	/* skip leading whitespace */
158	while(**s && 0 != strchr(" \t\r\n", **s))
159		(*s)++;
160
161	if(**s == 0) {
162		*s = 0;
163		return 0;
164	}
165
166	if(allow_quotes && (**s == '"')) {
167		has_quote = true;
168		(*s)++;
169		if(**s == 0) {
170			*s = 0;
171			return 0;
172		}
173	}
174
175	p = *s;
176
177	if(has_quote) {
178		*s = strchr(*s, '\"');
179		/* if there is no matching end quote, it's an error */
180		if(0 == *s)
181			p = *s = 0;
182		else {
183			**s = '\0';
184			(*s)++;
185		}
186	}
187	else {
188		while(**s && 0 == strchr(" \t\r\n", **s))
189			(*s)++;
190		if(**s) {
191			**s = '\0';
192			(*s)++;
193		}
194		else
195			*s = 0;
196	}
197
198	return p;
199}
200
201static FLAC__bool local__cuesheet_parse_(FILE *file, const char **error_message, unsigned *last_line_read, FLAC__StreamMetadata *cuesheet, FLAC__bool is_cdda, FLAC__uint64 lead_out_offset)
202{
203#if defined _MSC_VER || defined __MINGW32__ || defined __EMX__
204#define FLAC__STRCASECMP stricmp
205#else
206#define FLAC__STRCASECMP strcasecmp
207#endif
208	char buffer[4096], *line, *field;
209	unsigned forced_leadout_track_num = 0;
210	FLAC__uint64 forced_leadout_track_offset = 0;
211	int in_track_num = -1, in_index_num = -1;
212	FLAC__bool disc_has_catalog = false, track_has_flags = false, track_has_isrc = false, has_forced_leadout = false;
213	FLAC__StreamMetadata_CueSheet *cs = &cuesheet->data.cue_sheet;
214
215	cs->lead_in = is_cdda? 2 * 44100 /* The default lead-in size for CD-DA */ : 0;
216	cs->is_cd = is_cdda;
217
218	while(0 != fgets(buffer, sizeof(buffer), file)) {
219		(*last_line_read)++;
220		line = buffer;
221
222		{
223			size_t linelen = strlen(line);
224			if((linelen == sizeof(buffer)-1) && line[linelen-1] != '\n') {
225				*error_message = "line too long";
226				return false;
227			}
228		}
229
230		if(0 != (field = local__get_field_(&line, /*allow_quotes=*/false))) {
231			if(0 == FLAC__STRCASECMP(field, "CATALOG")) {
232				if(disc_has_catalog) {
233					*error_message = "found multiple CATALOG commands";
234					return false;
235				}
236				if(0 == (field = local__get_field_(&line, /*allow_quotes=*/true))) {
237					*error_message = "CATALOG is missing catalog number";
238					return false;
239				}
240				if(strlen(field) >= sizeof(cs->media_catalog_number)) {
241					*error_message = "CATALOG number is too long";
242					return false;
243				}
244				if(is_cdda && (strlen(field) != 13 || strspn(field, "0123456789") != 13)) {
245					*error_message = "CD-DA CATALOG number must be 13 decimal digits";
246					return false;
247				}
248				strcpy(cs->media_catalog_number, field);
249				disc_has_catalog = true;
250			}
251			else if(0 == FLAC__STRCASECMP(field, "FLAGS")) {
252				if(track_has_flags) {
253					*error_message = "found multiple FLAGS commands";
254					return false;
255				}
256				if(in_track_num < 0 || in_index_num >= 0) {
257					*error_message = "FLAGS command must come after TRACK but before INDEX";
258					return false;
259				}
260				while(0 != (field = local__get_field_(&line, /*allow_quotes=*/false))) {
261					if(0 == FLAC__STRCASECMP(field, "PRE"))
262						cs->tracks[cs->num_tracks-1].pre_emphasis = 1;
263				}
264				track_has_flags = true;
265			}
266			else if(0 == FLAC__STRCASECMP(field, "INDEX")) {
267				FLAC__int64 xx;
268				FLAC__StreamMetadata_CueSheet_Track *track = &cs->tracks[cs->num_tracks-1];
269				if(in_track_num < 0) {
270					*error_message = "found INDEX before any TRACK";
271					return false;
272				}
273				if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) {
274					*error_message = "INDEX is missing index number";
275					return false;
276				}
277				in_index_num = local__parse_int_(field);
278				if(in_index_num < 0) {
279					*error_message = "INDEX has invalid index number";
280					return false;
281				}
282				FLAC__ASSERT(cs->num_tracks > 0);
283				if(track->num_indices == 0) {
284					/* it's the first index point of the track */
285					if(in_index_num > 1) {
286						*error_message = "first INDEX number of a TRACK must be 0 or 1";
287						return false;
288					}
289				}
290				else {
291					if(in_index_num != track->indices[track->num_indices-1].number + 1) {
292						*error_message = "INDEX numbers must be sequential";
293						return false;
294					}
295				}
296				if(is_cdda && in_index_num > 99) {
297					*error_message = "CD-DA INDEX number must be between 0 and 99, inclusive";
298					return false;
299				}
300				/*@@@ search for duplicate track number? */
301				if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) {
302					*error_message = "INDEX is missing an offset after the index number";
303					return false;
304				}
305				xx = local__parse_msf_(field);
306				if(xx < 0) {
307					if(is_cdda) {
308						*error_message = "illegal INDEX offset (not of the form MM:SS:FF)";
309						return false;
310					}
311					xx = local__parse_int64_(field);
312					if(xx < 0) {
313						*error_message = "illegal INDEX offset";
314						return false;
315					}
316				}
317				if(is_cdda && cs->num_tracks == 1 && cs->tracks[0].num_indices == 0 && xx != 0) {
318					*error_message = "first INDEX of first TRACK must have an offset of 00:00:00";
319					return false;
320				}
321				if(is_cdda && track->num_indices > 0 && (FLAC__uint64)xx <= track->indices[track->num_indices-1].offset) {
322					*error_message = "CD-DA INDEX offsets must increase in time";
323					return false;
324				}
325				/* fill in track offset if it's the first index of the track */
326				if(track->num_indices == 0)
327					track->offset = (FLAC__uint64)xx;
328				if(is_cdda && cs->num_tracks > 1) {
329					const FLAC__StreamMetadata_CueSheet_Track *prev = &cs->tracks[cs->num_tracks-2];
330					if((FLAC__uint64)xx <= prev->offset + prev->indices[prev->num_indices-1].offset) {
331						*error_message = "CD-DA INDEX offsets must increase in time";
332						return false;
333					}
334				}
335				if(!FLAC__metadata_object_cuesheet_track_insert_blank_index(cuesheet, cs->num_tracks-1, track->num_indices)) {
336					*error_message = "memory allocation error";
337					return false;
338				}
339				track->indices[track->num_indices-1].offset = (FLAC__uint64)xx - track->offset;
340				track->indices[track->num_indices-1].number = in_index_num;
341			}
342			else if(0 == FLAC__STRCASECMP(field, "ISRC")) {
343				char *l, *r;
344				if(track_has_isrc) {
345					*error_message = "found multiple ISRC commands";
346					return false;
347				}
348				if(in_track_num < 0 || in_index_num >= 0) {
349					*error_message = "ISRC command must come after TRACK but before INDEX";
350					return false;
351				}
352				if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) {
353					*error_message = "ISRC is missing ISRC number";
354					return false;
355				}
356				/* strip out dashes */
357				for(l = r = field; *r; r++) {
358					if(*r != '-')
359						*l++ = *r;
360				}
361				*l = '\0';
362				if(strlen(field) != 12 || strspn(field, "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") < 5 || strspn(field+5, "1234567890") != 7) {
363					*error_message = "invalid ISRC number";
364					return false;
365				}
366				strcpy(cs->tracks[cs->num_tracks-1].isrc, field);
367				track_has_isrc = true;
368			}
369			else if(0 == FLAC__STRCASECMP(field, "TRACK")) {
370				if(cs->num_tracks > 0) {
371					const FLAC__StreamMetadata_CueSheet_Track *prev = &cs->tracks[cs->num_tracks-1];
372					if(
373						prev->num_indices == 0 ||
374						(
375						 	is_cdda &&
376							(
377								(prev->num_indices == 1 && prev->indices[0].number != 1) ||
378								(prev->num_indices == 2 && prev->indices[0].number != 1 && prev->indices[1].number != 1)
379							)
380						)
381					) {
382						*error_message = is_cdda?
383							"previous TRACK must specify at least one INDEX 01" :
384							"previous TRACK must specify at least one INDEX";
385						return false;
386					}
387				}
388				if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) {
389					*error_message = "TRACK is missing track number";
390					return false;
391				}
392				in_track_num = local__parse_int_(field);
393				if(in_track_num < 0) {
394					*error_message = "TRACK has invalid track number";
395					return false;
396				}
397				if(in_track_num == 0) {
398					*error_message = "TRACK number must be greater than 0";
399					return false;
400				}
401				if(is_cdda) {
402					if(in_track_num > 99) {
403						*error_message = "CD-DA TRACK number must be between 1 and 99, inclusive";
404						return false;
405					}
406				}
407				else {
408					if(in_track_num == 255) {
409						*error_message = "TRACK number 255 is reserved for the lead-out";
410						return false;
411					}
412					else if(in_track_num > 255) {
413						*error_message = "TRACK number must be between 1 and 254, inclusive";
414						return false;
415					}
416				}
417				if(is_cdda && cs->num_tracks > 0 && in_track_num != cs->tracks[cs->num_tracks-1].number + 1) {
418					*error_message = "CD-DA TRACK numbers must be sequential";
419					return false;
420				}
421				/*@@@ search for duplicate track number? */
422				if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) {
423					*error_message = "TRACK is missing a track type after the track number";
424					return false;
425				}
426				if(!FLAC__metadata_object_cuesheet_insert_blank_track(cuesheet, cs->num_tracks)) {
427					*error_message = "memory allocation error";
428					return false;
429				}
430				cs->tracks[cs->num_tracks-1].number = in_track_num;
431				cs->tracks[cs->num_tracks-1].type = (0 == FLAC__STRCASECMP(field, "AUDIO"))? 0 : 1; /*@@@ should we be more strict with the value here? */
432				in_index_num = -1;
433				track_has_flags = false;
434				track_has_isrc = false;
435			}
436			else if(0 == FLAC__STRCASECMP(field, "REM")) {
437				if(0 != (field = local__get_field_(&line, /*allow_quotes=*/false))) {
438					if(0 == strcmp(field, "FLAC__lead-in")) {
439						FLAC__int64 xx;
440						if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) {
441							*error_message = "FLAC__lead-in is missing offset";
442							return false;
443						}
444						xx = local__parse_int64_(field);
445						if(xx < 0) {
446							*error_message = "illegal FLAC__lead-in offset";
447							return false;
448						}
449						if(is_cdda && xx % 588 != 0) {
450							*error_message = "illegal CD-DA FLAC__lead-in offset, must be even multiple of 588 samples";
451							return false;
452						}
453						cs->lead_in = (FLAC__uint64)xx;
454					}
455					else if(0 == strcmp(field, "FLAC__lead-out")) {
456						int track_num;
457						FLAC__int64 offset;
458						if(has_forced_leadout) {
459							*error_message = "multiple FLAC__lead-out commands";
460							return false;
461						}
462						if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) {
463							*error_message = "FLAC__lead-out is missing track number";
464							return false;
465						}
466						track_num = local__parse_int_(field);
467						if(track_num < 0) {
468							*error_message = "illegal FLAC__lead-out track number";
469							return false;
470						}
471						forced_leadout_track_num = (unsigned)track_num;
472						/*@@@ search for duplicate track number? */
473						if(0 == (field = local__get_field_(&line, /*allow_quotes=*/false))) {
474							*error_message = "FLAC__lead-out is missing offset";
475							return false;
476						}
477						offset = local__parse_int64_(field);
478						if(offset < 0) {
479							*error_message = "illegal FLAC__lead-out offset";
480							return false;
481						}
482						forced_leadout_track_offset = (FLAC__uint64)offset;
483						if(forced_leadout_track_offset != lead_out_offset) {
484							*error_message = "FLAC__lead-out offset does not match end-of-stream offset";
485							return false;
486						}
487						has_forced_leadout = true;
488					}
489				}
490			}
491		}
492	}
493
494	if(cs->num_tracks == 0) {
495		*error_message = "there must be at least one TRACK command";
496		return false;
497	}
498	else {
499		const FLAC__StreamMetadata_CueSheet_Track *prev = &cs->tracks[cs->num_tracks-1];
500		if(
501			prev->num_indices == 0 ||
502			(
503				is_cdda &&
504				(
505					(prev->num_indices == 1 && prev->indices[0].number != 1) ||
506					(prev->num_indices == 2 && prev->indices[0].number != 1 && prev->indices[1].number != 1)
507				)
508			)
509		) {
510			*error_message = is_cdda?
511				"previous TRACK must specify at least one INDEX 01" :
512				"previous TRACK must specify at least one INDEX";
513			return false;
514		}
515	}
516
517	if(!has_forced_leadout) {
518		forced_leadout_track_num = is_cdda? 170 : 255;
519		forced_leadout_track_offset = lead_out_offset;
520	}
521	if(!FLAC__metadata_object_cuesheet_insert_blank_track(cuesheet, cs->num_tracks)) {
522		*error_message = "memory allocation error";
523		return false;
524	}
525	cs->tracks[cs->num_tracks-1].number = forced_leadout_track_num;
526	cs->tracks[cs->num_tracks-1].offset = forced_leadout_track_offset;
527
528	if(!feof(file)) {
529		*error_message = "read error";
530		return false;
531	}
532	return true;
533#undef FLAC__STRCASECMP
534}
535
536FLAC__StreamMetadata *grabbag__cuesheet_parse(FILE *file, const char **error_message, unsigned *last_line_read, FLAC__bool is_cdda, FLAC__uint64 lead_out_offset)
537{
538	FLAC__StreamMetadata *cuesheet;
539
540	FLAC__ASSERT(0 != file);
541	FLAC__ASSERT(0 != error_message);
542	FLAC__ASSERT(0 != last_line_read);
543
544	*last_line_read = 0;
545	cuesheet = FLAC__metadata_object_new(FLAC__METADATA_TYPE_CUESHEET);
546
547	if(0 == cuesheet) {
548		*error_message = "memory allocation error";
549		return 0;
550	}
551
552	if(!local__cuesheet_parse_(file, error_message, last_line_read, cuesheet, is_cdda, lead_out_offset)) {
553		FLAC__metadata_object_delete(cuesheet);
554		return 0;
555	}
556
557	return cuesheet;
558}
559
560void grabbag__cuesheet_emit(FILE *file, const FLAC__StreamMetadata *cuesheet, const char *file_reference)
561{
562	const FLAC__StreamMetadata_CueSheet *cs;
563	unsigned track_num, index_num;
564
565	FLAC__ASSERT(0 != file);
566	FLAC__ASSERT(0 != cuesheet);
567	FLAC__ASSERT(cuesheet->type == FLAC__METADATA_TYPE_CUESHEET);
568
569	cs = &cuesheet->data.cue_sheet;
570
571	if(*(cs->media_catalog_number))
572		fprintf(file, "CATALOG %s\n", cs->media_catalog_number);
573	fprintf(file, "FILE %s\n", file_reference);
574
575	for(track_num = 0; track_num < cs->num_tracks-1; track_num++) {
576		const FLAC__StreamMetadata_CueSheet_Track *track = cs->tracks + track_num;
577
578		fprintf(file, "  TRACK %02u %s\n", (unsigned)track->number, track->type == 0? "AUDIO" : "DATA");
579
580		if(track->pre_emphasis)
581			fprintf(file, "    FLAGS PRE\n");
582		if(*(track->isrc))
583			fprintf(file, "    ISRC %s\n", track->isrc);
584
585		for(index_num = 0; index_num < track->num_indices; index_num++) {
586			const FLAC__StreamMetadata_CueSheet_Index *index = track->indices + index_num;
587
588			fprintf(file, "    INDEX %02u ", (unsigned)index->number);
589			if(cs->is_cd) {
590				const unsigned logical_frame = (unsigned)((track->offset + index->offset) / (44100 / 75));
591				unsigned m, s, f;
592				grabbag__cuesheet_frame_to_msf(logical_frame, &m, &s, &f);
593				fprintf(file, "%02u:%02u:%02u\n", m, s, f);
594			}
595			else
596#ifdef _MSC_VER
597				fprintf(file, "%I64u\n", track->offset + index->offset);
598#else
599				fprintf(file, "%llu\n", (unsigned long long)(track->offset + index->offset));
600#endif
601		}
602	}
603
604#ifdef _MSC_VER
605	fprintf(file, "REM FLAC__lead-in %I64u\n", cs->lead_in);
606	fprintf(file, "REM FLAC__lead-out %u %I64u\n", (unsigned)cs->tracks[track_num].number, cs->tracks[track_num].offset);
607#else
608	fprintf(file, "REM FLAC__lead-in %llu\n", (unsigned long long)cs->lead_in);
609	fprintf(file, "REM FLAC__lead-out %u %llu\n", (unsigned)cs->tracks[track_num].number, (unsigned long long)cs->tracks[track_num].offset);
610#endif
611}
612