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