1 /* Ogginfo
2 *
3 * A tool to describe ogg file contents and metadata.
4 *
5 * Copyright 2002-2005 Michael Smith <msmith@xiph.org>
6 * Copyright 2020-2021 Philipp Schafft <lion@lion.leolix.org>
7 * Licensed under the GNU GPL, distributed with this program.
8 */
9
10 #ifdef HAVE_CONFIG_H
11 #include <config.h>
12 #endif
13
14 #include <stdio.h>
15 #include <stdlib.h>
16 #include <errno.h>
17 #include <string.h>
18 #include <stdarg.h>
19 #include <stdbool.h>
20 #include <getopt.h>
21 #include <math.h>
22 #include <inttypes.h>
23
24 #include <ogg/ogg.h>
25
26 #include <locale.h>
27 #include "utf8.h"
28 #include "i18n.h"
29
30 #include "private.h"
31
32 #define CHUNK 4500
33
34 /* TODO:
35 *
36 * - detect violations of muxing constraints
37 * - detect granulepos 'gaps' (possibly vorbis-specific). (seperate from
38 * serial-number gaps)
39 */
40
41 typedef struct {
42 stream_processor *streams;
43 int allocated;
44 int used;
45
46 int in_headers;
47 } stream_set;
48
49 int printlots = 0;
50 static int printinfo = 1;
51 static int printwarn = 1;
52 static int verbose = 1;
53
54 static int flawed;
55
56 #define CONSTRAINT_PAGE_AFTER_EOS 1
57 #define CONSTRAINT_MUXING_VIOLATED 2
58
create_stream_set(void)59 static stream_set *create_stream_set(void) {
60 stream_set *set = calloc(1, sizeof(stream_set));
61
62 set->streams = calloc(5, sizeof(stream_processor));
63 set->allocated = 5;
64 set->used = 0;
65
66 return set;
67 }
68
info(const char * format,...)69 void info(const char *format, ...)
70 {
71 va_list ap;
72
73 if (!printinfo)
74 return;
75
76 va_start(ap, format);
77 vfprintf(stdout, format, ap);
78 va_end(ap);
79 }
80
warn(const char * format,...)81 void warn(const char *format, ...)
82 {
83 va_list ap;
84
85 flawed = 1;
86 if (!printwarn)
87 return;
88
89 va_start(ap, format);
90 vfprintf(stdout, format, ap);
91 va_end(ap);
92 }
93
error(const char * format,...)94 void error(const char *format, ...)
95 {
96 va_list ap;
97
98 flawed = 1;
99
100 va_start(ap, format);
101 vfprintf(stdout, format, ap);
102 va_end(ap);
103 }
104
print_summary(stream_processor * stream,size_t bytes,double time)105 void print_summary(stream_processor *stream, size_t bytes, double time)
106 {
107 long minutes, seconds, milliseconds;
108 double bitrate;
109
110 minutes = (long)time / 60;
111 seconds = (long)time - minutes*60;
112 milliseconds = (long)((time - minutes*60 - seconds)*1000);
113 bitrate = bytes*8 / time / 1000.0;
114
115 info(_("%s stream %d:\n"
116 "\tTotal data length: %" PRId64 " bytes\n"
117 "\tPlayback length: %ldm:%02ld.%03lds\n"
118 "\tAverage bitrate: %f kb/s\n"),
119 stream->type, stream->num, bytes, minutes, seconds, milliseconds, bitrate);
120 }
121
free_stream_set(stream_set * set)122 static void free_stream_set(stream_set *set)
123 {
124 int i;
125 for (i=0; i < set->used; i++) {
126 if (!set->streams[i].end) {
127 warn(_("WARNING: EOS not set on stream %d\n"),
128 set->streams[i].num);
129 if (set->streams[i].process_end)
130 set->streams[i].process_end(&set->streams[i]);
131 }
132 ogg_stream_clear(&set->streams[i].os);
133 }
134
135 free(set->streams);
136 free(set);
137 }
138
streams_open(stream_set * set)139 static int streams_open(stream_set *set)
140 {
141 int i;
142 int res=0;
143 for (i=0; i < set->used; i++) {
144 if (!set->streams[i].end)
145 res++;
146 }
147
148 return res;
149 }
150
find_stream_processor(stream_set * set,ogg_page * page)151 static stream_processor *find_stream_processor(stream_set *set, ogg_page *page)
152 {
153 ogg_uint32_t serial = ogg_page_serialno(page);
154 int i;
155 int invalid = 0;
156 int constraint = 0;
157 stream_processor *stream;
158
159 for (i=0; i < set->used; i++) {
160 if (serial == set->streams[i].serial) {
161 /* We have a match! */
162 stream = &(set->streams[i]);
163
164 set->in_headers = 0;
165 /* if we have detected EOS, then this can't occur here. */
166 if (stream->end) {
167 stream->isillegal = 1;
168 stream->constraint_violated = CONSTRAINT_PAGE_AFTER_EOS;
169 return stream;
170 }
171
172 stream->isnew = 0;
173 stream->start = ogg_page_bos(page);
174 stream->end = ogg_page_eos(page);
175 stream->serial = serial;
176 return stream;
177 }
178 }
179
180 /* If there are streams open, and we've reached the end of the
181 * headers, then we can't be starting a new stream.
182 * XXX: might this sometimes catch ok streams if EOS flag is missing,
183 * but the stream is otherwise ok?
184 */
185 if (streams_open(set) && !set->in_headers) {
186 constraint = CONSTRAINT_MUXING_VIOLATED;
187 invalid = 1;
188 }
189
190 set->in_headers = 1;
191
192 if (set->allocated < set->used) {
193 stream = &set->streams[set->used];
194 } else {
195 set->allocated += 5;
196 set->streams = realloc(set->streams, sizeof(stream_processor)*
197 set->allocated);
198 stream = &set->streams[set->used];
199 }
200 set->used++;
201 stream->num = set->used; /* We count from 1 */
202
203 stream->isnew = 1;
204 stream->isillegal = invalid;
205 stream->constraint_violated = constraint;
206
207 {
208 int res;
209 ogg_packet packet;
210
211 /* We end up processing the header page twice, but that's ok. */
212 ogg_stream_init(&stream->os, serial);
213 ogg_stream_pagein(&stream->os, page);
214 res = ogg_stream_packetout(&stream->os, &packet);
215 if (res <= 0) {
216 warn(_("WARNING: Invalid header page, no packet found\n"));
217 invalid_start(stream);
218 } else if (packet.bytes >= 7 && memcmp(packet.packet, "\x01vorbis", 7)==0) {
219 vorbis_start(stream);
220 } else if (packet.bytes >= 7 && memcmp(packet.packet, "\x80theora", 7)==0) {
221 theora_start(stream);
222 } else if (packet.bytes >= 8 && memcmp(packet.packet, "OggMIDI\0", 8)==0) {
223 other_start(stream, "MIDI");
224 } else if (packet.bytes >= 5 && memcmp(packet.packet, "\177FLAC", 5)==0) {
225 flac_start(stream);
226 } else if (packet.bytes == 4 && memcmp(packet.packet, "fLaC", 4)==0) {
227 other_start(stream, "FLAC (legacy)");
228 } else if (packet.bytes >= 8 && memcmp(packet.packet, "Speex ", 8)==0) {
229 speex_start(stream);
230 } else if (packet.bytes >= 8 && memcmp(packet.packet, "fishead\0", 8)==0) {
231 skeleton_start(stream);
232 } else if (packet.bytes >= 5 && memcmp(packet.packet, "BBCD\0", 5)==0) {
233 other_start(stream, "dirac");
234 } else if (packet.bytes >= 8 && memcmp(packet.packet, "KW-DIRAC", 8)==0) {
235 other_start(stream, "dirac (legacy)");
236 } else if (packet.bytes >= 8 && memcmp(packet.packet, "OpusHead", 8)==0) {
237 opus_start(stream);
238 } else if (packet.bytes >= 8 && memcmp(packet.packet, "\x80kate\0\0\0", 8)==0) {
239 kate_start(stream);
240 } else {
241 other_start(stream, NULL);
242 }
243
244 res = ogg_stream_packetout(&stream->os, &packet);
245 if (res > 0) {
246 warn(_("WARNING: Invalid header page in stream %d, "
247 "contains multiple packets\n"), stream->num);
248 }
249
250 /* re-init, ready for processing */
251 ogg_stream_clear(&stream->os);
252 ogg_stream_init(&stream->os, serial);
253 }
254
255 stream->start = ogg_page_bos(page);
256 stream->end = ogg_page_eos(page);
257 stream->serial = serial;
258
259 if (stream->serial == 0 || stream->serial == -1) {
260 info(_("Note: Stream %d has serial number %d, which is legal but may "
261 "cause problems with some tools.\n"), stream->num,
262 stream->serial);
263 }
264
265 return stream;
266 }
267
get_next_page(FILE * f,ogg_sync_state * sync,ogg_page * page,ogg_int64_t * written)268 static int get_next_page(FILE *f, ogg_sync_state *sync, ogg_page *page,
269 ogg_int64_t *written)
270 {
271 int ret;
272 char *buffer;
273 int bytes;
274
275 while ((ret = ogg_sync_pageseek(sync, page)) <= 0) {
276 if (ret < 0) {
277 /* unsynced, we jump over bytes to a possible capture - we don't need to read more just yet */
278 warn(_("WARNING: Hole in data (%d bytes) found at approximate offset %" PRId64 " bytes. Corrupted Ogg.\n"), -ret, *written);
279 continue;
280 }
281
282 /* zero return, we didn't have enough data to find a whole page, read */
283 buffer = ogg_sync_buffer(sync, CHUNK);
284 bytes = fread(buffer, 1, CHUNK, f);
285 if (bytes <= 0) {
286 ogg_sync_wrote(sync, 0);
287 return 0;
288 }
289 ogg_sync_wrote(sync, bytes);
290 *written += bytes;
291 }
292
293 return 1;
294 }
295
process_file(const char * filename)296 static void process_file(const char *filename) {
297 FILE *file = fopen(filename, "rb");
298 ogg_sync_state sync;
299 ogg_page page;
300 stream_set *processors = create_stream_set();
301 int gotpage = 0;
302 ogg_int64_t written = 0;
303
304 if (!file) {
305 error(_("Error opening input file \"%s\": %s\n"), filename,
306 strerror(errno));
307 return;
308 }
309
310 printf(_("Processing file \"%s\"...\n\n"), filename);
311
312 ogg_sync_init(&sync);
313
314 while (get_next_page(file, &sync, &page, &written)) {
315 stream_processor *p = find_stream_processor(processors, &page);
316 gotpage = 1;
317
318 if (!p) {
319 error(_("Could not find a processor for stream, bailing\n"));
320 return;
321 }
322
323 if (p->isillegal && !p->shownillegal) {
324 char *constraint;
325 switch(p->constraint_violated) {
326 case CONSTRAINT_PAGE_AFTER_EOS:
327 constraint = _("Page found for stream after EOS flag");
328 break;
329 case CONSTRAINT_MUXING_VIOLATED:
330 constraint = _("Ogg muxing constraints violated, new "
331 "stream before EOS of all previous streams");
332 break;
333 default:
334 constraint = _("Error unknown.");
335 }
336
337 warn(_("WARNING: illegally placed page(s) for logical stream %d\n"
338 "This indicates a corrupt Ogg file: %s.\n"),
339 p->num, constraint);
340 p->shownillegal = 1;
341 /* If it's a new stream, we want to continue processing this page
342 * anyway to suppress additional spurious errors
343 */
344 if (!p->isnew)
345 continue;
346 }
347
348 if (p->isnew) {
349 info(_("New logical stream (#%d, serial: %08x): type %s\n"),
350 p->num, p->serial, p->type);
351 if (!p->start)
352 warn(_("WARNING: stream start flag not set on stream %d\n"),
353 p->num);
354 } else if (p->start) {
355 warn(_("WARNING: stream start flag found in mid-stream "
356 "on stream %d\n"), p->num);
357 }
358
359 if (p->seqno++ != ogg_page_pageno(&page)) {
360 if (!p->lostseq)
361 warn(_("WARNING: sequence number gap in stream %d. Got page "
362 "%ld when expecting page %ld. Indicates missing data.\n"
363 ), p->num, ogg_page_pageno(&page), p->seqno - 1);
364 p->seqno = ogg_page_pageno(&page);
365 p->lostseq = 1;
366 } else {
367 p->lostseq = 0;
368 }
369
370 if (!p->isillegal) {
371 p->process_page(p, &page);
372
373 if (p->end) {
374 if (p->process_end)
375 p->process_end(p);
376 info(_("Logical stream %d ended\n"), p->num);
377 p->isillegal = 1;
378 p->constraint_violated = CONSTRAINT_PAGE_AFTER_EOS;
379 }
380 }
381 }
382
383 if (!gotpage)
384 error(_("ERROR: No Ogg data found in file \"%s\".\n"
385 "Input probably not Ogg.\n"), filename);
386
387 free_stream_set(processors);
388
389 ogg_sync_clear(&sync);
390
391 fclose(file);
392 }
393
version(void)394 static void version (void) {
395 printf (_("ogginfo from %s %s\n"), PACKAGE, VERSION);
396 }
397
usage(void)398 static void usage(void) {
399 version ();
400 printf (_(" by the Xiph.Org Foundation (https://www.xiph.org/)\n\n"));
401 printf(_("(c) 2003-2005 Michael Smith <msmith@xiph.org>\n"
402 "\n"
403 "Usage: ogginfo [flags] file1.ogg [file2.ogx ... fileN.ogv]\n"
404 "Flags supported:\n"
405 "\t-h Show this help message\n"
406 "\t-q Make less verbose. Once will remove detailed informative\n"
407 "\t messages, two will remove warnings\n"
408 "\t-v Make more verbose. This may enable more detailed checks\n"
409 "\t for some stream types.\n"));
410 printf (_("\t-V Output version information and exit\n"));
411 }
412
main(int argc,char ** argv)413 int main(int argc, char **argv) {
414 int f, ret;
415
416 setlocale(LC_ALL, "");
417 bindtextdomain(PACKAGE, LOCALEDIR);
418 textdomain(PACKAGE);
419
420 if (argc < 2) {
421 fprintf(stdout,
422 _("Usage: ogginfo [flags] file1.ogg [file2.ogx ... fileN.ogv]\n"
423 "\n"
424 "ogginfo is a tool for printing information about Ogg files\n"
425 "and for diagnosing problems with them.\n"
426 "Full help shown with \"ogginfo -h\".\n"));
427 exit(1);
428 }
429
430 while ((ret = getopt(argc, argv, "hqvV")) >= 0) {
431 switch(ret) {
432 case 'h':
433 usage();
434 return 0;
435 case 'V':
436 version();
437 return 0;
438 case 'v':
439 verbose++;
440 break;
441 case 'q':
442 verbose--;
443 break;
444 }
445 }
446
447 if (verbose > 1)
448 printlots = 1;
449 if (verbose < 1)
450 printinfo = 0;
451 if (verbose < 0)
452 printwarn = 0;
453
454 if (optind >= argc) {
455 fprintf(stderr,
456 _("No input files specified. \"ogginfo -h\" for help\n"));
457 return 1;
458 }
459
460 ret = 0;
461
462 for (f=optind; f < argc; f++) {
463 flawed = 0;
464 process_file(argv[f]);
465 if (flawed != 0)
466 ret = flawed;
467 }
468
469 return ret;
470 }
471