oggz-validate.c 15.7 KB
Newer Older
conrad's avatar
conrad committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
/*
   Copyright (C) 2003 Commonwealth Scientific and Industrial Research
   Organisation (CSIRO) Australia

   Redistribution and use in source and binary forms, with or without
   modification, are permitted provided that the following conditions
   are met:

   - Redistributions of source code must retain the above copyright
   notice, this list of conditions and the following disclaimer.

   - Redistributions in binary form must reproduce the above copyright
   notice, this list of conditions and the following disclaimer in the
   documentation and/or other materials provided with the distribution.

   - Neither the name of CSIRO Australia nor the names of its
   contributors may be used to endorse or promote products derived from
   this software without specific prior written permission.

   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
   ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
   LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
   PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE ORGANISATION OR
   CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
   EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
   PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
   PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
   LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
   NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
   SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

33 34
#include "config.h"

conrad's avatar
conrad committed
35 36
#include <stdio.h>
#include <stdlib.h>
37
#include <string.h>
conrad's avatar
conrad committed
38 39 40
#include <getopt.h>
#include <errno.h>

41 42 43 44 45 46
#ifdef HAVE_INTTYPES_H
#  include <inttypes.h>
#else
#  define PRId64 "I64d"
#endif

47
#include "oggz/oggz.h"
conrad's avatar
conrad committed
48 49 50 51 52 53 54

#include "oggz_tools.h"

#define MAX_ERRORS 10

#define SUBSECONDS 1000.0

55 56
/* #define DEBUG */

conrad's avatar
conrad committed
57 58
typedef ogg_int64_t timestamp_t;

59 60 61
typedef struct _OVData {
  OGGZ * writer;
  OggzTable * missing_eos;
62 63
  OggzTable * packetno;

64 65
  int theora_count;
  int audio_count;
66 67

  int chain_ended;
68 69
} OVData;

conrad's avatar
conrad committed
70 71 72 73 74 75
typedef struct {
  int error;
  char * description;
} error_text;

static error_text errors[] = {
76 77
  {-20, "Packet belongs to unknown serialno"},
  {-24, "Granulepos decreasing within track"},
78 79
  {-5, "Multiple bos pages"},
  {-6, "Multiple eos pages"},
conrad's avatar
conrad committed
80 81 82
  {0, NULL}
};

conrad's avatar
conrad committed
83
static char * progname;
84
static int max_errors = MAX_ERRORS;
conrad's avatar
conrad committed
85 86 87 88 89
static int multifile = 0;
static char * current_filename = NULL;
static timestamp_t current_timestamp = 0;
static int exit_status = 0;
static int nr_errors = 0;
90
static int prefix = 0, suffix = 0;
conrad's avatar
conrad committed
91 92

static void
93
list_errors (void)
conrad's avatar
conrad committed
94 95 96
{
  int i = 0;

97 98
  printf ("  File contains no Ogg packets\n");
  printf ("  Packets out of order\n");
conrad's avatar
conrad committed
99 100 101
  for (i = 0; errors[i].error; i++) {
    printf ("  %s\n", errors[i].description);
  }
102
  printf ("  eos marked but no bos\n");
103 104
  printf ("  Missing eos pages\n");
  printf ("  eos marked on page with no completed packets\n");
105 106
  printf ("  Granulepos on page with no completed packets\n");
  printf ("  Theora video bos page after audio bos page\n");
107 108 109
  printf ("  Terminal header page has non-zero granulepos\n");
  printf ("  Terminal header page contains non-header packet\n");
  printf ("  Terminal header page contains non-header segment\n");
110
}
111

112 113 114 115 116 117 118 119 120 121
static void
usage (char * progname)
{

  printf ("Usage: %s [options] filename ...\n", progname);
  printf ("Validate the Ogg framing of one or more files\n");
  printf ("\n%s detects the following errors in Ogg framing:\n", progname);

  list_errors ();

122 123 124 125
  printf ("\nError reporting options\n");
  printf ("  -M num, --max-errors num\n");
  printf ("                         Exit after the specified number of errors.\n");
  printf ("                         A value of 0 specifies no maximum. Default: %d\n", MAX_ERRORS);
126 127 128 129 130 131 132 133
  printf ("  -p, --prefix           Treat input as the prefix of a stream; suppress\n");
  printf ("                         warnings about missing end-of-stream markers\n");
  printf ("  -s, --suffix           Treat input as the suffix of a stream; suppress\n");
  printf ("                         warnings about missing beginning-of-stream markers\n");
  printf ("                         on the first chain\n");
  printf ("  -P, --partial          Treat input as a the middle portion of a stream;\n");
  printf ("                         equivalent to both --prefix and --suffix\n");

conrad's avatar
conrad committed
134 135
  printf ("\nMiscellaneous options\n");
  printf ("  -h, --help             Display this help and exit\n");
136
  printf ("  -E, --help-errors      List known types of error and exit\n");
conrad's avatar
conrad committed
137 138 139 140 141 142
  printf ("  -v, --version          Output version information and exit\n");
  printf ("\n");
  printf ("Exit status is 0 if all input files are valid, 1 otherwise.\n\n");
  printf ("Please report bugs to <ogg-dev@xiph.org>\n");
}

conrad's avatar
conrad committed
143 144 145 146 147 148 149
static void
exit_out_of_memory (void)
{
  fprintf (stderr, "%s: Out of memory\n", progname);
  exit (1);
}

conrad's avatar
conrad committed
150 151 152 153 154 155 156 157 158 159
static int
log_error (void)
{
  if (multifile && nr_errors == 0) {
    fprintf (stderr, "%s: Error:\n", current_filename);
  }

  exit_status = 1;

  nr_errors++;
160
  if (max_errors && nr_errors > max_errors)
conrad's avatar
conrad committed
161 162 163 164 165 166 167 168 169
    return OGGZ_STOP_ERR;

  return OGGZ_STOP_OK;
}

static ogg_int64_t
gp_to_granule (OGGZ * oggz, long serialno, ogg_int64_t granulepos)
{
  int granuleshift;
170
  ogg_int64_t iframe, pframe, granule;
conrad's avatar
conrad committed
171 172 173 174 175

  granuleshift = oggz_get_granuleshift (oggz, serialno);

  iframe = granulepos >> granuleshift;
  pframe = granulepos - (iframe << granuleshift);
176
  granule = iframe+pframe;
conrad's avatar
conrad committed
177

178 179 180 181
  if (oggz_stream_get_content (oggz, serialno) == OGGZ_CONTENT_DIRAC)
    granule >>= 9;

  return granule;
conrad's avatar
conrad committed
182 183 184 185 186 187 188 189 190 191 192 193 194
}

static timestamp_t
gp_to_time (OGGZ * oggz, long serialno, ogg_int64_t granulepos)
{
  ogg_int64_t gr_n, gr_d;
  ogg_int64_t granule;

  if (granulepos == -1) return -1.0;
  if (oggz_get_granulerate (oggz, serialno, &gr_n, &gr_d) != 0) return -1.0;

  granule = gp_to_granule (oggz, serialno, granulepos);

195
  return (timestamp_t)((double)(SUBSECONDS * granule * gr_d) / (double)gr_n);
conrad's avatar
conrad committed
196 197
}

198
static void
199 200
ovdata_init (OVData * ovdata)
{
201 202
  int flags;

203 204
  current_timestamp = 0;

205 206 207 208 209
  flags = OGGZ_WRITE|OGGZ_AUTO;
  if (prefix) flags |= OGGZ_PREFIX;
  if (suffix) flags |= OGGZ_SUFFIX;

  if ((ovdata->writer = oggz_new (flags)) == NULL) {
210 211 212 213
    fprintf (stderr, "oggz-validate: unable to create new writer\n");
    exit (1);
  }

conrad's avatar
conrad committed
214 215 216 217 218 219
  if ((ovdata->missing_eos = oggz_table_new ()) == NULL)
    exit_out_of_memory();

  if ((ovdata->packetno = oggz_table_new ()) == NULL)
    exit_out_of_memory();

220 221
  ovdata->theora_count = 0;
  ovdata->audio_count = 0;
222
  ovdata->chain_ended = 0;
223 224 225 226 227 228 229 230 231 232
}

static void
ovdata_clear (OVData * ovdata)
{
  long serialno;
  int i, nr_missing_eos = 0;

  oggz_close (ovdata->writer);

233
  if (!prefix && (max_errors == 0 || nr_errors <= max_errors)) {
234 235 236 237
    nr_missing_eos = oggz_table_size (ovdata->missing_eos);
    for (i = 0; i < nr_missing_eos; i++) {
      log_error ();
      oggz_table_nth (ovdata->missing_eos, i, &serialno);
conrad's avatar
conrad committed
238
      fprintf (stderr, "serialno %010lu: missing *** eos\n", serialno);
239 240 241 242
    }
  }

  oggz_table_delete (ovdata->missing_eos);
243
  oggz_table_delete (ovdata->packetno);
244 245
}

conrad's avatar
conrad committed
246
static int
247
read_page (OGGZ * oggz, const ogg_page * og, long serialno, void * user_data)
conrad's avatar
conrad committed
248
{
249
  OVData * ovdata = (OVData *)user_data;
250
  ogg_int64_t gpos = ogg_page_granulepos((ogg_page *)og);
251
  OggzStreamContent content_type;
252
  int packets, packetno, headers, ret = 0;
conrad's avatar
conrad committed
253

254 255 256 257 258 259 260 261
  if (ovdata->chain_ended) {
    ovdata_clear (ovdata);
    ovdata_init (ovdata);
    suffix = 0;
  }

  if (ogg_page_bos ((ogg_page *)og)) {
    /* Register this serialno as needing eos */
conrad's avatar
conrad committed
262 263
    if (oggz_table_insert (ovdata->missing_eos, serialno, (void *)0x1) == NULL)
      exit_out_of_memory();
264 265

    /* Handle ordering of Theora vs. audio packets */
266
    content_type = oggz_stream_get_content (oggz, serialno);
267

268 269
    switch (content_type) {
      case OGGZ_CONTENT_THEORA:
270 271 272
	ovdata->theora_count++;
	if (ovdata->audio_count > 0) {
	  log_error ();
conrad's avatar
conrad committed
273
	  fprintf (stderr, "serialno %010lu: Theora video bos page after audio bos page\n", serialno);
274
	}
275 276 277 278 279 280 281
        break;
      case OGGZ_CONTENT_VORBIS:
      case OGGZ_CONTENT_SPEEX:
      case OGGZ_CONTENT_PCM:
      case OGGZ_CONTENT_FLAC0:
      case OGGZ_CONTENT_FLAC:
      case OGGZ_CONTENT_CELT:
282
	ovdata->audio_count++;
283 284 285
        break;
      default:
        break;
286
    }
287 288
  }

289 290 291 292 293 294
  packets = ogg_page_packets ((ogg_page *)og);

  /* Check header constraints */
  if (!suffix) {
    if (oggz_table_lookup (ovdata->missing_eos, serialno) == NULL) {
      ret = log_error ();
conrad's avatar
conrad committed
295
      fprintf (stderr, "serialno %010lu: missing *** bos\n", serialno);
296 297 298 299
    }

    packetno = (int)oggz_table_lookup (ovdata->packetno, serialno);
    headers = oggz_stream_get_numheaders (oggz, serialno);
300
    if (packetno < headers-1) {
301 302
      /* The previous page was headers, and more are expected */
      packetno += packets;
conrad's avatar
conrad committed
303 304 305
      if (oggz_table_insert (ovdata->packetno, serialno, (void *)packetno) == NULL)
        exit_out_of_memory();

306 307
      if (packetno == headers && gpos != 0) {
        ret = log_error ();
conrad's avatar
conrad committed
308
        fprintf (stderr, "serialno %010lu: Terminal header page has non-zero granulepos\n", serialno);
309 310
      } else if (packetno > headers) {
        ret = log_error ();
conrad's avatar
conrad committed
311
        fprintf (stderr, "serialno %010lu: Terminal header page contains non-header packet\n", serialno);
312 313 314 315 316
      }
    } else if (packetno == headers) {
      /* This is the next page after the page on which the last header finished */
      if (ogg_page_continued (og)) {
        ret = log_error ();
conrad's avatar
conrad committed
317
        fprintf (stderr, "serialno %010lu: Terminal header page contains non-header segment\n", serialno);
318 319 320
      }

      /* Mark packetno as greater than headers to avoid these checks for this serialno */
conrad's avatar
conrad committed
321 322
      if (oggz_table_insert (ovdata->packetno, serialno, (void *)(headers+1)) == NULL)
        exit_out_of_memory();
323 324
    }

325 326
  }

327
  /* Check EOS */
328 329 330
  if (ogg_page_eos((ogg_page *)og)) {
    int removed = oggz_table_remove (ovdata->missing_eos, serialno);
    if (!suffix && removed == -1) {
331
      ret = log_error ();
conrad's avatar
conrad committed
332
      fprintf (stderr, "serialno %010lu: *** eos marked but no bos\n",
333 334 335
  	       serialno);
    }

336
    if (packets == 0) {
337
      ret = log_error ();
conrad's avatar
conrad committed
338
      fprintf (stderr, "serialno %010lu: *** eos marked on page with no completed packets\n",
339 340 341 342 343
  	       serialno);
    }

    if (oggz_table_size (ovdata->missing_eos) == 0) {
      ovdata->chain_ended = 1;
344 345 346
    }
  }

347

348
  if(gpos != -1 && packets == 0) {
349
    ret = log_error ();
conrad's avatar
conrad committed
350
    fprintf (stderr, "serialno %010lu: granulepos %" PRId64 " on page with no completed packets, must be -1\n", serialno, gpos);
351 352 353 354 355 356 357 358 359 360 361 362 363
  }

  return ret;
}

static int
read_packet (OGGZ * oggz, ogg_packet * op, long serialno, void * user_data)
{
  OVData * ovdata = (OVData *)user_data;
  timestamp_t timestamp;
  int flush;
  int ret = 0, feed_err = 0, i;

conrad's avatar
conrad committed
364
  timestamp = gp_to_time (oggz, serialno, op->granulepos);
365
  if (timestamp != -1.0 && oggz_stream_get_content (oggz, serialno) != OGGZ_CONTENT_DIRAC) {
conrad's avatar
conrad committed
366 367 368
    if (timestamp < current_timestamp) {
      ret = log_error();
      ot_fprint_time (stderr, (double)timestamp/SUBSECONDS);
conrad's avatar
conrad committed
369
      fprintf (stderr, ": serialno %010lu: Packet out of order (previous ",
conrad's avatar
conrad committed
370 371 372 373 374 375 376 377 378 379 380 381 382
	       serialno);
      ot_fprint_time (stderr, (double)current_timestamp/SUBSECONDS);
      fprintf (stderr, ")\n");
    }
    current_timestamp = timestamp;
  }

  if (op->granulepos == -1) {
    flush = 0;
  } else {
    flush = OGGZ_FLUSH_AFTER;
  }

383
  if ((feed_err = oggz_write_feed (ovdata->writer, op, serialno, flush, NULL)) != 0) {
conrad's avatar
conrad committed
384 385
    ret = log_error ();
    if (timestamp == -1.0) {
386
      fprintf (stderr, "%" PRId64 , oggz_tell (oggz));
conrad's avatar
conrad committed
387 388 389
    } else {
      ot_fprint_time (stderr, (double)timestamp/SUBSECONDS);
    }
conrad's avatar
conrad committed
390
    fprintf (stderr, ": serialno %010lu: ", serialno);
391 392 393 394 395 396 397 398 399 400 401
    for (i = 0; errors[i].error; i++) {
      if (errors[i].error == feed_err) {
	fprintf (stderr, "%s\n", errors[i].description);
	break;
      }
    }
    if (errors[i].error == 0) {
      fprintf (stderr,
	       "Packet violates Ogg framing constraints: %d\n",
	       feed_err);
    }
conrad's avatar
conrad committed
402 403 404 405 406 407 408 409
  }

  return ret;
}

static int
validate (char * filename)
{
410 411
  OGGZ * reader;
  OVData ovdata;
conrad's avatar
conrad committed
412
  unsigned char buf[1024];
413 414
  long n, nout = 0, bytes_written = 0;
  int active = 1;
conrad's avatar
conrad committed
415 416

  current_filename = filename;
417
  current_timestamp = 0;
conrad's avatar
conrad committed
418 419 420
  nr_errors = 0;

  /*printf ("oggz-validate: %s\n", filename);*/
421

422 423 424 425 426 427
  if (!strncmp (filename, "-", 2)) {
    if ((reader = oggz_open_stdio (stdin, OGGZ_READ|OGGZ_AUTO)) == NULL) {
      fprintf (stderr, "oggz-validate: unable to open stdin\n");
      return -1;
    }
  } else if ((reader = oggz_open (filename, OGGZ_READ|OGGZ_AUTO)) == NULL) {
conrad's avatar
conrad committed
428
    fprintf (stderr, "oggz-validate: unable to open file %s\n", filename);
429
    return -1;
conrad's avatar
conrad committed
430 431
  }

432
  ovdata_init (&ovdata);
conrad's avatar
conrad committed
433

434
  oggz_set_read_callback (reader, -1, read_packet, &ovdata);
435
  oggz_set_read_page (reader, -1, read_page, &ovdata);
conrad's avatar
conrad committed
436 437

  while (active && (n = oggz_read (reader, 1024)) != 0) {
438 439 440 441
#ifdef DEBUG
      fprintf (stderr, "validate: read %ld bytes\n", n);
#endif
    
442
    if (max_errors && nr_errors > max_errors) {
conrad's avatar
conrad committed
443
      fprintf (stderr,
444 445
	       "oggz-validate --max-errors %d: maximum error count reached, bailing out ...\n",
               max_errors);
conrad's avatar
conrad committed
446
      active = 0;
447
    } else while ((nout = oggz_write_output (ovdata.writer, buf, n)) > 0) {
448 449 450
#ifdef DEBUG
      fprintf (stderr, "validate: wrote %ld bytes\n", nout);
#endif
451 452
      bytes_written += nout;
    }
conrad's avatar
conrad committed
453 454 455 456
  }

  oggz_close (reader);

457 458 459 460 461
  if (bytes_written == 0) {
    log_error ();
    fprintf (stderr, "File contains no Ogg packets\n");
  }

462
  ovdata_clear (&ovdata);
463

conrad's avatar
conrad committed
464 465 466 467 468 469 470 471 472
  return active ? 0 : -1;
}

int
main (int argc, char ** argv)
{
  int show_version = 0;
  int show_help = 0;

473 474 475 476 477
  /* Cache the --prefix, --suffix options and reset before validating
   * each input file */
  int opt_prefix = 0;
  int opt_suffix = 0;

conrad's avatar
conrad committed
478 479 480
  char * filename;
  int i = 1;

481 482 483 484 485 486 487 488 489 490 491 492 493 494 495
  char * optstring = "M:psPhvE";

#ifdef HAVE_GETOPT_LONG
  static struct option long_options[] = {
    {"max-errors", required_argument, 0, 'M'},
    {"prefix", no_argument, 0, 'p'},
    {"suffix", no_argument, 0, 's'},
    {"partial", no_argument, 0, 'P'},
    {"help", no_argument, 0, 'h'},
    {"help-errors", no_argument, 0, 'E'},
    {"version", no_argument, 0, 'v'},
    {0,0,0,0}
  };
#endif

conrad's avatar
conrad committed
496 497 498 499 500 501 502 503 504
  ot_init();

  progname = argv[0];

  if (argc < 2) {
    usage (progname);
    return (1);
  }

505
  if (!strncmp (argv[1], "-?", 2)) {
conrad's avatar
conrad committed
506
#ifdef HAVE_GETOPT_LONG
507 508 509 510 511 512
    ot_print_options (long_options, optstring);
#else
    ot_print_short_options (optstring);
#endif
    exit (0);
  }
conrad's avatar
conrad committed
513

514 515
  while (1) {
#ifdef HAVE_GETOPT_LONG
conrad's avatar
conrad committed
516 517 518 519 520 521 522 523 524 525 526 527 528
    i = getopt_long(argc, argv, optstring, long_options, NULL);
#else
    i = getopt (argc, argv, optstring);
#endif
    if (i == -1) {
      break;
    }
    if (i == ':') {
      usage (progname);
      goto exit_err;
    }

    switch (i) {
529 530 531
    case 'M': /* max-errors */
      max_errors = atoi (optarg);
      break;
532 533 534 535 536 537 538 539 540 541
    case 'p': /* prefix */
      opt_prefix = 1;
      break;
    case 's': /* suffix */
      opt_suffix = 1;
      break;
    case 'P': /* partial */
      opt_prefix = 1;
      opt_suffix = 1;
      break;
conrad's avatar
conrad committed
542 543 544 545 546 547
    case 'h': /* help */
      show_help = 1;
      break;
    case 'v': /* version */
      show_version = 1;
      break;
548 549 550
    case 'E': /* list errors */
      show_help = 2;
      break;
conrad's avatar
conrad committed
551 552 553 554 555 556 557 558 559
    default:
      break;
    }
  }

  if (show_version) {
    printf ("%s version " VERSION "\n", progname);
  }

560
  if (show_help == 1) {
conrad's avatar
conrad committed
561
    usage (progname);
562 563
  } else if (show_help == 2) {
    list_errors ();
conrad's avatar
conrad committed
564 565 566 567 568 569
  }

  if (show_version || show_help) {
    goto exit_out;
  }

570 571 572 573 574
  if (max_errors < 0) {
    printf ("%s: Error: [-M num, --max-errors num] option must be non-negative\n", progname);
    goto exit_err;
  }

conrad's avatar
conrad committed
575 576 577 578 579 580 581
  if (optind >= argc) {
    usage (progname);
    goto exit_err;
  }

  if (argc-i > 2) multifile = 1;

582
  for (i = optind; i < argc; i++) {
conrad's avatar
conrad committed
583
    filename = argv[i];
584 585
    prefix = opt_prefix;
    suffix = opt_suffix;
586 587
    if (validate (filename) == -1)
      exit_status = 1;
conrad's avatar
conrad committed
588 589 590 591 592 593 594 595
  }

 exit_out:
  exit (exit_status);

 exit_err:
  exit (1);
}