dvdimgdecss.c: main(): also copy the non-decrypted parts
[cdimgtools.git] / nrgtool
blob5e10b386b02aad24d248b554817217b292daca02
1 #!/usr/bin/perl -w
2 # Copyright © 2012 Géraud Meyer <graud@gmx.com>
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License version 2 as
5 # published by the Free Software Foundation.
7 =encoding utf-8
9 =head1 NAME
11 nrgtool - reads and converts/splits Nero CD images (.nrg)
13 =head1 SYNOPSIS
15 B<nrgtool> B<-h>
17 B<nrgtool> S<[ B<-v> ]> S<[ B<-n> ]> S<[ B<-f> ]> S<[ B<-1> ]> S<[ B<-I> ]> I<nrg> S<[ I<cmd> I<args> ]>
19 =cut
21 use strict;
22 use integer;
24 use Getopt::Long;
25 Getopt::Long::Configure('bundling', 'no_auto_abbrev', 'auto_version', 'auto_help');
26 use Pod::Usage;
27 use String::Escape qw(printable);
28 use Data::Hexdumper qw(hexdump);
29 use Fcntl qw(:seek);
31 $main::VERSION = "0.1";
33 # Command line
34 my ($verbose, $no_act, $force) = (0, undef, undef);
35 my ($nrgv, $ext, $iff_ext, $trk_ext, $iff_only) = (2, '', '.iff', '.s%02d-t%02d', 0);
36 die Getopt::Long::HelpMessage(128)
37 unless GetOptions(
38 'h|help' => sub { Pod::Usage::pod2usage(-exitval => 0, -verbose => 2) },
39 'v|verbose+' => \$verbose,
40 'q|quiet' => sub { $verbose = 0 },
41 'n|no-act!' => \$no_act,
42 'f|force!' => \$force,
43 '2|nrgv2' => sub { $nrgv = 2 },
44 '1|nrgv1' => sub { $nrgv = 1 },
45 'e|extension=s' => \$ext,
46 't|track-ext=s' => \$trk_ext,
47 'i|iff-ext=s' => \$iff_ext,
48 'I|iff' => \$iff_only,
49 ) and my $nrg_name = shift;
50 $ARGV[0] = "info" unless (@ARGV);
52 # Data pertaining to the NRG file format IFF structure
53 die "ERROR For now only NRG version 2 files are supported" if ($nrgv != 2);
54 my $chunk_types;
55 my $Q = 'Q>'; # unpack template for 64-bit values
56 $chunk_types->{nero} = {
57 code => "NER5",
58 fields => [ [ 'iff_offset', 8, $Q ] ],
60 $chunk_types->{sinf} = {
61 code => "SINF",
62 fields => [ [ 'size', 4, 'N' ], [ 'num_tracks', 4, 'N' ] ],
64 $chunk_types->{etnf} = {
65 code => "ETN2",
66 fields => [ [ 'size', 4, 'N' ] ],
67 lfields => [
68 [ 'start', 8, $Q ],
69 [ 'tsize', 8, $Q ],
70 [ 'mode', 4, 'N', [ '0x%08X', '%d' ] ],
71 [ 'lba', 4, 'N' ],
72 [ 'unknown', 4, 'N', [ '0x%08X (always 0)' ] ],
73 [ 'length', 4, 'N' ] ],
75 $chunk_types->{daoi} = {
76 code => "DAOX",
77 fields => [
78 [ 'size', 4, 'N' ], [ 'le_size', 4, 'N' ],
79 [ 'upc', 13, 'a13' ], [ 'null', 1, 'C', '0x%02X' ],
80 [ 'toc_type', 2, 'n', [ '0x%04x', '%d' ] ],
81 [ 'first_track', 1, 'C' ], [ 'last_track', 1, 'C' ] ],
82 lfields => [
83 [ 'isrc', 12, 'a12' ],
84 [ 'sector_size', 2, 'n' ], [ 'mode', 2, 'n', '0x%04x' ],
85 [ 'unknown', 2, 'n', '0x%04X (always 1)' ],
86 [ 'index0', 8, $Q ], [ 'index1', 8, $Q ],
87 [ 'next_index', 8, $Q ] ],
89 $chunk_types->{cues} = {
90 code => "CUEX",
91 fields => [ [ 'size', 4, 'N' ] ],
92 lfields => [
93 [ 'mode', 1, 'C', [ '0x%02X', '%d' ] ],
94 [ 'track', 1, 'C', '%02X' ], [ 'index', 1, 'C', '%02X' ],
95 [ 'null', 1, 'C', '0x%02X' ], [ 'lba', 4, 'l>' ] ],
97 $chunk_types->{relo} = {
98 code => "RELO",
99 fields => [ [ 'size', 4, 'N' ], [ 'unknown', 4, 'N', [ '0x%08X', '%d' ] ] ],
101 $chunk_types->{toct} = {
102 code => "TOCT",
103 fields => [ [ 'size', 4, 'N' ], [ 'unknown', 2, 'n', [ '0x%04X', '%d' ] ] ],
105 $chunk_types->{dinf} = {
106 code => "DINF",
107 fields => [ [ 'size', 4, 'N' ], [ 'unknown', 4, 'N', [ '0x%08X', '%d' ] ] ],
109 $chunk_types->{cdtx} = {
110 code => "CDTX",
111 fields => [ [ 'size', 4, 'N' ] ],
112 lfields => [
113 [ 'pack_type', 1, 'C', '%02X' ], [ 'track', 1, 'C' ],
114 [ 'pack_num', 1, 'C' ], [ 'block_char', 1, 'C', [ '%08B', '0x%02x' ] ],
115 [ 'text', 12, 'a12', '"%s"' ], [ 'crc', 2, 'n', '0x%04x' ] ],
117 $chunk_types->{mtyp} = {
118 code => "MTYP",
119 fields => [ [ 'size', 4, 'N' ], [ 'type', 4, 'N' ] ],
121 $chunk_types->{end} = {
122 code => "END!",
123 fields => [ [ 'size', 4, 'N' ] ],
125 $chunk_types->{unknown} = {
126 fields => [ [ 'size', 4, 'N' ] ],
128 # { "NERO", "NER5" },
129 # { "CUES", "CUEX" },
130 # { "ETNF", "ETN2" },
131 # { "DAOI", "DAOX" },
132 # { "SINF", "SINF" },
133 # { "END!", "END!" },
135 # File parsing & processing
136 sub process_file {
137 my ($process_iff, $nrg_name) = map shift, (0..1);
138 my ($nrg, $iff, $rc) = (undef, { }, 1);
139 unless (open $nrg, "<", $nrg_name . $ext) {
140 warn "ERROR Cannot open file '$nrg_name$ext' for reading: $!\n";
141 return;
143 binmode $nrg;
144 print "Locating the header at the end of the file\n" if ($verbose > 1);
145 seek $nrg, -4-$chunk_types->{nero}->{fields}->[0]->[1], SEEK_END;
146 read_chunk($nrg, $iff);
147 my $offset = $iff->{nero}->[0]->{fields}->[0];
148 unless (defined $offset) {
149 warn "ERROR The offset of the IFF chunk list in '$nrg_name' could not be determined\n";
150 return;
152 seek $nrg, 0, SEEK_SET if ($iff_only);
153 seek $nrg, $offset, SEEK_SET unless ($iff_only);
154 while (my $_ = read_chunk($nrg, $iff)) {
155 if ($_ < -1) {
156 warn "ERROR while reading an IFF chunk; aborting\n";
157 return;
159 $rc = 0 if ($_ == -1); # report non fatal error
161 &$process_iff($iff, $nrg, $nrg_name) and ($rc);
163 # Read an IFF chunk at the current location in $nrg and set the fields' values in $iff
164 # $iff->{$type}->[$index]->{fields}->[$values]
165 # $iff->{$type}->[$index]->{lfields}->[$index]->[$values]
166 sub read_chunk {
167 my ($nrg, $iff) = map shift, (0..1);
168 my $code = read_fixed($nrg, 4);
169 my $type = typeofcode($code);
170 unless ($type) {
171 warn "ERROR Unknown chunk code '" . printable($code) . "'\n";
172 return -2;
174 my ($rc, $pos) = (1, tell($nrg));
175 print "Reading chunk '$code' at offset $pos\n" if ($verbose > 1);
176 $iff->{$type} = [ ] unless (defined($iff->{$type}));
177 # read the fields
178 my $fields = $chunk_types->{$type}->{fields};
179 my $chunk = {
180 offset => $pos-4,
181 fields => [ map { unpack($_->[2], read_fixed($nrg, $_->[1])) } @$fields ],
183 push(@{$iff->{$type}}, $chunk);
184 if ($fields->[0]->[0] eq 'size') {
185 # find the chunk end
186 my $size = $chunk->{fields}->[0];
187 $pos += $fields->[0]->[1]+$size;
188 print " Chunk ends at at offset $pos\n" if ($verbose > 1);
189 # read the lfields
190 if (defined($chunk_types->{$type}->{lfields})) {
191 $chunk->{lfields} = [ ] unless (defined($chunk->{lfields}));
192 my $lfields = $chunk_types->{$type}->{lfields};
193 until (tell($nrg) >= $pos) {
194 my $vals = [ map { unpack($_->[2], read_fixed($nrg, $_->[1])) } @$lfields ];
195 push(@{$chunk->{lfields}}, $vals)
198 # check that all the chunk has been processed
199 my $left = $pos-tell($nrg);
200 warn "ERROR Data ($left bytes) read after the end of the chunk $code\n", $rc = -1
201 if ($left < 0);
202 warn "WARNING Remaining data ($left bytes) at the end of the chunk $code\n"
203 if ($left > 0);
204 $pos++ if ($pos % 2);
205 seek $nrg, $pos, SEEK_SET;
207 return 0 if ($type eq 'end');
208 return $rc;
210 sub typeofcode {
211 my $code = shift;
212 for (keys %{$chunk_types}) {
213 defined($chunk_types->{$_}->{code}) or next;
214 return $_ if ($code eq $chunk_types->{$_}->{code});
216 return undef;
218 sub extract_iff {
219 my $iff = shift;
220 my ($src, $src_name) = map shift, (0..1);
221 my $offset = $iff->{nero}->[0]->{fields}->[0];
222 unless (defined($offset)) {
223 warn "ERROR Cannot find offset of the IFF\n";
224 return;
226 print "IFF offset: $offset\n" if ($verbose > 1);
227 # Dest file
228 my $dst_name = $src_name . $iff_ext;
229 unless ($force or !-e $dst_name) {
230 warn "Not extracting the IFF because '$dst_name' already exists\n";
231 return;
233 my $dst;
234 unless ($no_act or open $dst, ">", $dst_name) {
235 warn "ERROR Cannot open file '$dst_name' for writing: $!\n";
236 return;
238 binmode $dst unless ($no_act);
239 # Copy
240 my $rc = 1;
241 seek $src, $offset, SEEK_SET;
242 my $size = (stat($src))[7]-$offset;
243 my $buf = read_fixed($src, $size);
244 print $dst $buf or $rc = 0;
245 print "$size bytes written in '$dst_name'\n" if ($verbose);
246 close $dst unless ($no_act);
247 return $rc;
250 # Command processing
251 sub process_cmd {
252 my $args = shift; my $cmd = shift @$args;
253 my $chunk_filter = (@$args) ? sub { grep { $_[0] eq $_ } @$args } : undef;
254 local *print_iff = walk_chunks(\&print_chunk, $chunk_filter);
255 local *hexdump_iff = walk_chunks(
256 sub { print "\n$chunk_types->{$_[0]}->{code}\n"; hexdump_chunk(@_) },
257 $chunk_filter
259 #*printhexdump_iff = walk_chunks(sub { print_chunk(@_); hexdump_chunk(@_) });
260 my $track_filter = sub {
261 for (@$args[0,1]) { defined($_) or next; ($_ == shift) or return };
262 return 1;
264 my $iff_only_warn = sub {
265 return $iff_only unless ($iff_only);
266 print "WARNING Command $cmd on a bare IFF file ignored\n";
267 process_file(sub { return 1 }, @_);
268 return $iff_only;
270 local *list_tracks = walk_tracks(\&list_track, $track_filter);
271 local *extract_tracks = walk_tracks(\&extract_track, $track_filter);
272 if ($cmd eq "help") {
273 Pod::Usage::pod2usage(
274 -verbose => 99,
275 -sections => "SYNOPSIS|COMMANDS",
276 -exitval => 0
278 } elsif ($cmd eq "types") {
279 for (keys %$chunk_types) { print "$_\n" unless ($_ eq 'unknown') };
281 } elsif ($cmd eq "nop") {
282 process_file(sub { return 1 }, @_);
283 } elsif ($cmd eq "info") {
284 process_file(\&print_iff, @_);
285 } elsif ($cmd eq "hexdump") {
286 process_file(\&hexdump_iff, @_);
287 } elsif ($cmd eq "list") {
288 process_file(\&list_tracks, @_);
289 } elsif ($cmd eq "track") {
290 process_file(\&extract_tracks, @_) unless (&$iff_only_warn(@_));
291 } elsif ($cmd eq "iff") {
292 process_file(\&extract_iff, @_) unless (&$iff_only_warn(@_));
293 } else {
294 warn "ERROR Unknown command '$cmd'\n";
298 # Main
299 unless (process_cmd(\@ARGV, $nrg_name)) {
300 print "There was an error.\n" if ($verbose);
301 exit 1;
302 } else {
303 exit;
306 # Chunks & Fields
307 sub walk_chunks {
308 my ($process, $filter) = @_;
309 sub {
310 my $iff = shift;
311 my $rc = 1;
312 for my $type (keys %$iff) {
313 CHUNK: for my $chunk (@{$iff->{$type}}) {
314 next CHUNK if (defined($filter) and not &$filter($type, $chunk));
315 &$process($type, $chunk, @_) or $rc = 0;
318 return $rc;
321 sub print_chunk {
322 my ($type, $chunk) = map shift, (0..1);
323 map shift, (0..1);
324 print "$chunk_types->{$type}->{code} (at offset $chunk->{offset})\n";
325 print_fields($chunk_types->{$type}->{fields}, $chunk->{fields}) if ($verbose);
326 if (defined($chunk->{lfields})) {
327 for (my $i = 0; $i < @{$chunk->{lfields}}; $i++) {
328 printf "%s[%d]\n", $chunk_types->{$type}->{code}, $i+1;
329 print_fields($chunk_types->{$type}->{lfields}, $chunk->{lfields}->[$i]) if ($verbose);
332 print "\n" if ($verbose);
333 return 1;
335 sub print_fields {
336 my ($fields, $vals) = map shift, (0..1);
337 for (my $i = 0; $i < @{$vals}; $i++) {
338 my $field = $fields->[$i];
339 my @val = ( $vals->[$i] );
340 my $str = $val[0];
341 if (defined($field->[3])) {
342 my $fmts = (ref($field->[3]) eq 'ARRAY') ? $field->[3] : [ $field->[3] ];
343 $str = join(" ", map { sprintf($_, @val) } @$fmts);
345 print "($field->[1])$field->[0]: $str\n";
347 return 1;
349 sub hexdump_chunk {
350 my ($type, $chunk) = map shift, (0..1);
351 my ($src) = map shift, (0..1);
352 $type = $chunk_types->{$type};
353 seek $src, $chunk->{offset}, SEEK_SET;
354 my $first_f = $type->{fields}->[0];
355 my $size = 4+$first_f->[1];
356 $size += $chunk->{fields}->[0] if ($first_f->[0] eq 'size');
357 print hexdump( read_fixed($src, $size) );
360 # Sessions & Tracks
361 sub walk_tracks {
362 my ($process, $filter) = @_;
363 sub {
364 my $iff = shift;
365 my $rc = 1;
366 for (my $sess = 1; $sess <= @{$iff->{sinf}}; $sess++) {
367 my $numoftrcks = $iff->{sinf}->[$sess-1]->{fields}->[1];
368 TRACK: for (my $trck = 0; $trck <= $numoftrcks; $trck++) {
369 # $trck is the ordinal index of the track in the current session, not the track number
370 # the track 0 is for the lead-in
371 my @params = ($sess, $trck, track_chunk($iff, $sess, $trck));
372 next TRACK if ($trck == 0 and defined($params[2]->{etnf}));
373 next TRACK if (defined($filter) and not &$filter(@params));
374 &$process(@params, @_) or $rc = 0;
377 return $rc;
380 sub list_track {
381 my ($sess, $trck, $chunk) = map shift, (0..2);
382 map shift, (0..1);
383 printf "SESSION %d TRACK %02d [%02d]\n", $sess, $chunk->{track}, $trck;
384 printf " mode $chunk_types->{daoi}->{lfields}->[2]->[3] %db/s %db\n",
385 $chunk->{mode}, $chunk->{sect_size}, $chunk->{end}-$chunk->{start}
386 if ($verbose);
388 # Extract the track at position $trck in session $sess from the image
389 sub extract_track {
390 list_track(@_) if ($verbose);
391 my ($sess, $trck, $chunk) = map shift, (0..2);
392 my ($src, $src_name) = map shift, (0..1);
393 my $track_num = $chunk->{track};
394 my $start = $chunk->{start};
395 my $end = $chunk->{end};
396 my $sect_size = $chunk->{sect_size};
397 unless (defined($start) and defined($end)) {
398 warn "ERROR Cannot find start/end of track $track_num\n";
399 return;
401 unless (defined($sect_size)) {
402 warn "ERROR Cannot find sector size of track $track_num\n";
403 return;
405 printf " start: $start\tend: $end\n" if ($verbose > 1);
406 # Dest file
407 my $dst_name = $src_name . sprintf($trk_ext, $sess, $track_num, $sess);
408 unless ($force or !-e $dst_name) {
409 warn "Skipping track $track_num because '$dst_name' already exists\n";
410 return;
412 my $dst;
413 unless ($no_act or open $dst, ">", $dst_name) {
414 warn "ERROR Cannot open file '$dst_name' for writing: $!\n";
415 return;
417 binmode $dst unless ($no_act);
418 # Copy
419 seek $src, $start, SEEK_SET;
420 my ($rc, $count) = (1, 0);
421 SECTOR: while (tell($src) + $sect_size <= $end) {
422 my $buf = read_fixed($src, $sect_size);
423 print $dst $buf unless ($no_act);
424 $count++;
426 print "$count sectors of size $sect_size from track $track_num written in '$dst_name'\n" if ($verbose);
427 if (my $left = $end-tell($src)) {
428 warn "ERROR Partial sector of size $left/$sect_size found in track $track_num\n";
429 $rc = 0;
430 my $buf = read_fixed($src, $left);
431 print $dst $buf unless ($no_act);
433 close $dst unless ($no_act);
434 return $rc;
436 sub session_chunk {
437 my ($iff, $sess) = map shift, @_;
438 my ($daoi_n, $etnf_n) = (0, 0);
439 my $next_session_chunk = sub {
440 my $chunk = { tracks => $iff->{sinf}->[$sess-1]->{fields}->[1] };
441 my $is_daoi = defined($iff->{daoi}->[$daoi_n]);
442 my $is_etnf = defined($iff->{etnf}->[$etnf_n]);
443 if ( $is_daoi and (not $is_etnf
444 or ($iff->{daoi}->[$daoi_n]->{offset} < $iff->{etnf}->[$etnf_n]->{offset})) ) {
445 $chunk->{daoi} = $iff->{daoi}->[$daoi_n];
446 $chunk->{first} = $iff->{daoi}->[$daoi_n]->{fields}->[5];
447 $daoi_n++;
448 } elsif ($is_etnf) {
449 $chunk->{etnf} = $iff->{etnf}->[$etnf_n];
450 $chunk->{etnf_first} = 1; # TODO
451 $chunk->{first} = 1; # TODO
452 $etnf_n++;
454 $chunk;
456 ( map &$next_session_chunk, (1..$sess) )[-1];
458 # (using index1 of the next track as the end because Nero records the end as the index0 of the next track)
459 sub track_chunk () {
460 my ($iff, $sess, $trck) = map shift, @_;
461 my $chunk = session_chunk($iff, $sess);
462 $chunk->{track} = $chunk->{first} + $trck - 1;
463 if (defined $chunk->{daoi}) {
464 $chunk->{sect_size} = $chunk->{daoi}->{lfields}->[($trck) ? $trck-1 : $trck]->[1];
465 $chunk->{mode} = $chunk->{daoi}->{lfields}->[($trck) ? $trck-1 : $trck]->[2];
466 $chunk->{start} = ($trck)
467 ? $chunk->{daoi}->{lfields}->[$trck-1]->[5]
468 : $chunk->{daoi}->{lfields}->[$trck]->[4];
469 # track 0 starts at index 0
470 $chunk->{end} = (defined($chunk->{daoi}->{lfields}->[$trck]))
471 ? $chunk->{daoi}->{lfields}->[$trck]->[5]
472 : $chunk->{daoi}->{lfields}->[$trck-1]->[6];
473 # last track ends at next_index
474 } elsif (defined($chunk->{etnf})) {
475 warn "WARNING Only DAO discs are fully supported for now\n";
476 $chunk->{track_size} = $chunk->{etnf}->{lfields}->[($trck) ? $trck-1 : $trck]->[1];
477 $chunk->{track_length} = $chunk->{etnf}->{lfields}->[($trck) ? $trck-1 : $trck]->[5];
478 $chunk->{sect_size} = $chunk->{track_size} / $chunk->{track_length};
479 $chunk->{mode} = $chunk->{etnf}->{lfields}->[($trck) ? $trck-1 : $trck]->[2];
480 $chunk->{start} = ($trck)
481 ? $chunk->{etnf}->{lfields}->[$trck-1]->[0]
482 : 0;
483 $chunk->{end} = ($trck)
484 ? $chunk->{start} + $chunk->{etnf}->{lfields}->[$trck-1]->[1]
485 : 0;
487 $chunk;
490 # Misc
491 sub read_fixed {
492 my ($file, $size) = @_;
493 my $buf;
494 my $n = read $file, $buf, $size;
495 die "ERROR Partial read of $n instead of $size\n" unless ($n == $size);
496 return $buf;
499 __END__
501 =head1 DESCRIPTION
503 C<nrgtool> processes one NRG CD image file. It finds and parses the IFF
504 footer, which contains the metadata; then it prints information or extracts
505 some data.
507 A track at position 0 is the lead-in of the session. Tracks of DAO sessions
508 are split at index 1 points.
510 =head1 EXAMPLES
512 To view the result of the parsing:
514 nrgtool -v image.nrg | less
516 To show the debugging output of the parsing:
518 nrgtool -vv image.nrg nop
520 To view a hexdump of the CD-text chunk:
522 nrgtool image.nrg hexdump cdtx | less
524 To list the tracks:
526 nrgtool image.nrg list
528 To extract all the audio tracks of F<image.nrg> to F<image-XX.cdda>:
530 nrgtool -v -e .nrg -t '-%n%02d.cdda' image track 1
532 To extract a data disc to an iso file F<image.nrg.iso>:
534 nrgtool -v -t .iso image.nrg track 1 1
536 If a track contains sub-channel data, you may extract the audio/data with one
537 of the following commands:
539 raw96cdconv -v image.nrg.s01-t16
540 raw96cdconv -v --iso image.nrg.s01-t01
541 raw96cdconv -v --sector-size 2336 image.nrg.s01-t01
543 An audio track may be converted to WAV by sox(1):
545 sox -t .cdda -L image.nrg.s01-t16 image-16.wav
547 =head1 COMMANDS
549 The default command is "info".
551 =over 8
553 =item B<help>
555 =item B<info> S<[ I<chunk_types> ]>
557 Print the chunks' structure resulting of the parsing of the IFF. Enbale
558 verbose mode to view the fields' values.
560 =item B<hexdump> S<[ I<chunk_types> ]>
562 =item B<nop>
564 Only parse the IFF.
566 =item B<list> S<[ I<session> S<[ I<track_pos> ]> ]>
568 List the tracks. In verbose mode, give some track attributes.
570 =item B<track> S<[ I<session> S<[ I<track_pos> ]> ]>
572 Extract the track (raw) data to file(s).
574 =item B<iff>
576 Extract the IFF header to a file.
578 =item B<types>
580 List the known chunk types.
582 =back
584 =head1 OPTIONS
586 Options may be negated by prefixing them with "--no-" instead of "--".
588 =over 8
590 =item B<-h>, B<--help>
592 =item B<-v>, B<--verbose>
594 Verbose: print some or more information, depending on the command.
596 If it is given twice, also print debugging information, notably during the
597 parsing.
599 =item B<-I>, B<--iff>
601 IFF only: process a bare IFF file, previously extracted by the command B<iff>.
603 =item B<-n>, B<--no-act>
605 No Action: test the reading of files but do not write any files.
607 =item B<-f>, B<--force>
609 Force: overwrite existing files.
611 =item B<-1>, B<--nrgv1>
613 NRG Version: Treat the file as a first version (prior to Nero 5.5) NRG; the
614 default is to treat the file as a version 2 (Nero 5.5 and later).
615 Unimplemented.
617 =item B<-e>, B<--extension> I<ext>
619 NRG extension: appended to the given file name to obtain the NRG file name;
620 this way the generated files' names can be controlled somewhat.
622 =item B<-t>, B<--track-ext> I<ext_fmt>
624 Track Extension: printf format template for the file name extensions of the
625 tracks; the first argument to printf is the session number, the second the
626 track number, the third the session number; the default is ".s%02d-t%02d".
628 =item B<-i>, B<--iff-ext> I<ext>
630 IFF Extension: file name extension of the IFF.
632 =back
634 =head1 ENVIRONMENT
636 No environment variables are used.
638 =head1 BUGS
640 C<nrgtool> has been tested with images created by Nerolinux 4.0 only. Any
641 resulting incompatibilties with other versions of Nero may be reported to the
642 author.
644 The NRG version 1 format is not supported because of the lack of
645 sample image files.
647 If you can reconstruct the same NRG from the extracted files, most probably
648 C<nrgtool> can handle the particular image:
650 nrgtool image.nrg iff
651 nrgtool image.nrg track
652 cat image.nrg.s??-t?? image.nrg.iff >image.nrg.nrgtool
653 cmp image.nrg image.nrg.nrgtool && echo "nrgtool can extract all the data"
655 =head1 AUTHOR
657 G.raud <graud@gmx.com>
659 =head1 SEE ALSO
661 L<nero(1)>, L<raw96cdconv(1)>, L<sox(1)>
663 =cut