version/0.3
[cdimgtools.git] / nrgtool
blob2de96192d36f822d04ac8e929636e4d61173138b
1 #!/usr/bin/perl -w
2 # Copyright © 2012,2013 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 # This program is distributed in the hope that it will be useful, but
8 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
9 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
10 # for more details.
12 # You should have received a copy of the GNU General Public License along
13 # with this program. If not, see <http://www.gnu.org/licenses/>.
15 =encoding utf-8
17 =head1 NAME
19 nrgtool - reads and converts/splits Nero optical disc image files (.nrg)
21 =head1 SYNOPSIS
23 B<nrgtool> S<{ B<-h> | B<--version> }>
25 B<nrgtool> S<[ B<-v> ]> S<[ B<-n> ]> S<[ B<-f> ]> S<[ B<-1> ]> S<[ B<-I> ]> I<nrg> S<[ I<cmd> I<args> ]>
27 =cut
29 use strict;
30 use integer;
32 use Getopt::Long;
33 Getopt::Long::Configure('bundling', 'no_auto_abbrev', 'auto_version', 'auto_help');
34 use Pod::Usage;
35 use String::Escape qw(printable);
36 use Data::Hexdumper qw(hexdump);
37 use Fcntl qw(:seek);
39 $main::VERSION = "0.2";
41 # Command line
42 my ($verbose, $no_act, $force) = (0, undef, undef);
43 my ($nrgv, $ext, $iff_ext, $trk_ext) = (2, '', '.iff', '.s%02d-t%02d');
44 my ($iff_only, $smallint) = (0, 0);
45 die Getopt::Long::HelpMessage(128)
46 unless GetOptions(
47 'h|help' => sub { Pod::Usage::pod2usage(-exitval => 0, -verbose => 2) },
48 'v|verbose+' => \$verbose,
49 'q|quiet' => sub { $verbose = 0 },
50 'n|no-act!' => \$no_act,
51 'f|force!' => \$force,
52 '2|nrgv2' => sub { $nrgv = 2 },
53 '1|nrgv1' => sub { $nrgv = 1 },
54 'e|extension=s' => \$ext,
55 't|track-ext=s' => \$trk_ext,
56 'i|iff-ext=s' => \$iff_ext,
57 'I|iff' => \$iff_only,
58 '3|32' => \$smallint,
59 ) and my $nrg_name = shift;
60 $ARGV[0] = "info" unless (@ARGV);
62 # Data pertaining to the NRG file format IFF structure
63 die "ERROR For now only NRG version 2 files are supported" if ($nrgv != 2);
64 my $chunk_types;
65 my $Q = 'Q>'; # unpack template for 64-bit values
66 $Q = 'xxxxN' if ($smallint); # skip the most significant part for 32-bit values
67 $chunk_types->{nero} = {
68 code => "NER5",
69 fields => [ [ 'iff_offset', 8, $Q ] ],
71 $chunk_types->{sinf} = {
72 code => "SINF",
73 fields => [ [ 'size', 4, 'N' ], [ 'num_tracks', 4, 'N' ] ],
75 $chunk_types->{etnf} = {
76 code => "ETN2",
77 fields => [ [ 'size', 4, 'N' ] ],
78 lfields => [
79 [ 'start', 8, $Q ],
80 [ 'tsize', 8, $Q ],
81 [ 'mode', 4, 'N', [ '0x%08X', '%d' ] ],
82 [ 'lba', 4, 'N' ],
83 [ 'unknown', 4, 'N', [ '0x%08X (always 0)' ] ],
84 [ 'length', 4, 'N' ] ],
86 $chunk_types->{daoi} = {
87 code => "DAOX",
88 fields => [
89 [ 'size', 4, 'N' ], [ 'le_size', 4, 'N' ],
90 [ 'upc', 13, 'a13' ], [ 'null', 1, 'C', '0x%02X' ],
91 [ 'toc_type', 2, 'n', [ '0x%04x', '%d' ] ],
92 [ 'first_track', 1, 'C' ], [ 'last_track', 1, 'C' ] ],
93 lfields => [
94 [ 'isrc', 12, 'a12' ],
95 [ 'sector_size', 2, 'n' ], [ 'mode', 2, 'n', '0x%04x' ],
96 [ 'unknown', 2, 'n', '0x%04X (always 1)' ],
97 [ 'index0', 8, $Q ], [ 'index1', 8, $Q ],
98 [ 'next_index', 8, $Q ] ],
100 $chunk_types->{cues} = {
101 code => "CUEX",
102 fields => [ [ 'size', 4, 'N' ] ],
103 lfields => [
104 [ 'mode', 1, 'C', [ '0x%02X', '%d' ] ],
105 [ 'track', 1, 'C', '%02X' ], [ 'index', 1, 'C', '%02X' ],
106 [ 'null', 1, 'C', '0x%02X' ], [ 'lba', 4, 'l>' ] ],
108 $chunk_types->{relo} = {
109 code => "RELO",
110 fields => [ [ 'size', 4, 'N' ], [ 'unknown', 4, 'N', [ '0x%08X', '%d' ] ] ],
112 $chunk_types->{toct} = {
113 code => "TOCT",
114 fields => [ [ 'size', 4, 'N' ], [ 'unknown', 2, 'n', [ '0x%04X', '%d' ] ] ],
116 $chunk_types->{dinf} = {
117 code => "DINF",
118 fields => [ [ 'size', 4, 'N' ], [ 'unknown', 4, 'N', [ '0x%08X', '%d' ] ] ],
120 $chunk_types->{cdtx} = {
121 code => "CDTX",
122 fields => [ [ 'size', 4, 'N' ] ],
123 lfields => [
124 [ 'pack_type', 1, 'C', '%02X' ], [ 'track', 1, 'C' ],
125 [ 'pack_num', 1, 'C' ], [ 'block_char', 1, 'C', [ '%08B', '0x%02x' ] ],
126 [ 'text', 12, 'a12', '"%s"' ], [ 'crc', 2, 'n', '0x%04x' ] ],
128 $chunk_types->{mtyp} = {
129 code => "MTYP",
130 fields => [ [ 'size', 4, 'N' ], [ 'type', 4, 'N' ] ],
132 $chunk_types->{end} = {
133 code => "END!",
134 fields => [ [ 'size', 4, 'N' ] ],
136 $chunk_types->{unknown} = {
137 fields => [ [ 'size', 4, 'N' ] ],
139 # { "NERO", "NER5" },
140 # { "CUES", "CUEX" },
141 # { "ETNF", "ETN2" },
142 # { "DAOI", "DAOX" },
143 # { "SINF", "SINF" },
144 # { "END!", "END!" },
146 # File parsing & processing
147 sub process_file {
148 my ($process_iff, $nrg_name) = map shift, (0..1);
149 my ($nrg, $iff, $rc) = (undef, { }, 1);
150 unless (open $nrg, "<", $nrg_name . $ext) {
151 warn "ERROR Cannot open file '$nrg_name$ext' for reading: $!\n";
152 return;
154 binmode $nrg;
155 print "Locating the header at the end of the file\n" if ($verbose > 1);
156 seek $nrg, -4-$chunk_types->{nero}->{fields}->[0]->[1], SEEK_END;
157 read_chunk($nrg, $iff);
158 my $offset = $iff->{nero}->[0]->{fields}->[0];
159 unless (defined $offset) {
160 warn "ERROR The offset of the IFF chunk list in '$nrg_name' could not be determined\n";
161 return;
163 seek $nrg, 0, SEEK_SET if ($iff_only);
164 seek $nrg, $offset, SEEK_SET unless ($iff_only);
165 while (my $_ = read_chunk($nrg, $iff)) {
166 if ($_ < -1) {
167 warn "ERROR while reading an IFF chunk; aborting\n";
168 return;
170 $rc = 0 if ($_ == -1); # report non fatal error
172 &$process_iff($iff, $nrg, $nrg_name) and ($rc);
174 # Read an IFF chunk at the current location in $nrg and set the fields' values in $iff
175 # $iff->{$type}->[$index]->{fields}->[$values]
176 # $iff->{$type}->[$index]->{lfields}->[$index]->[$values]
177 sub read_chunk {
178 my ($nrg, $iff) = map shift, (0..1);
179 my $code = read_fixed($nrg, 4);
180 my $type = typeofcode($code);
181 unless ($type) {
182 warn "ERROR Unknown chunk code '" . printable($code) . "'\n";
183 return -2;
185 my ($rc, $pos) = (1, tell($nrg));
186 print "Reading chunk '$code' at offset $pos\n" if ($verbose > 1);
187 $iff->{$type} = [ ] unless (defined($iff->{$type}));
188 # read the fields
189 my $fields = $chunk_types->{$type}->{fields};
190 my $chunk = {
191 offset => $pos-4,
192 fields => [ map { unpack($_->[2], read_fixed($nrg, $_->[1])) } @$fields ],
194 push(@{$iff->{$type}}, $chunk);
195 if ($fields->[0]->[0] eq 'size') {
196 # find the chunk end
197 my $size = $chunk->{fields}->[0];
198 $pos += $fields->[0]->[1]+$size;
199 print " Chunk ends at at offset $pos\n" if ($verbose > 1);
200 # read the lfields
201 if (defined($chunk_types->{$type}->{lfields})) {
202 $chunk->{lfields} = [ ] unless (defined($chunk->{lfields}));
203 my $lfields = $chunk_types->{$type}->{lfields};
204 until (tell($nrg) >= $pos) {
205 my $vals = [ map { unpack($_->[2], read_fixed($nrg, $_->[1])) } @$lfields ];
206 push(@{$chunk->{lfields}}, $vals)
209 # check that all the chunk has been processed
210 my $left = $pos-tell($nrg);
211 warn "ERROR Data ($left bytes) read after the end of the chunk $code\n", $rc = -1
212 if ($left < 0);
213 warn "WARNING Remaining data ($left bytes) at the end of the chunk $code\n"
214 if ($left > 0);
215 $pos++ if ($pos % 2);
216 seek $nrg, $pos, SEEK_SET;
218 return 0 if ($type eq 'end');
219 return $rc;
221 sub typeofcode {
222 my $code = shift;
223 for (keys %{$chunk_types}) {
224 defined($chunk_types->{$_}->{code}) or next;
225 return $_ if ($code eq $chunk_types->{$_}->{code});
227 return undef;
229 sub extract_iff {
230 my $iff = shift;
231 my ($src, $src_name) = map shift, (0..1);
232 my $offset = $iff->{nero}->[0]->{fields}->[0];
233 unless (defined($offset)) {
234 warn "ERROR Cannot find offset of the IFF\n";
235 return;
237 print "IFF offset: $offset\n" if ($verbose > 1);
238 # Dest file
239 my $dst_name = $src_name . $iff_ext;
240 unless ($force or !-e $dst_name) {
241 warn "Not extracting the IFF because '$dst_name' already exists\n";
242 return;
244 my $dst;
245 unless ($no_act or open $dst, ">", $dst_name) {
246 warn "ERROR Cannot open file '$dst_name' for writing: $!\n";
247 return;
249 binmode $dst unless ($no_act);
250 # Copy
251 my $rc = 1;
252 seek $src, $offset, SEEK_SET;
253 my $size = (stat($src))[7]-$offset;
254 my $buf = read_fixed($src, $size);
255 print $dst $buf or $rc = 0 unless ($no_act);
256 print "$size bytes written in '$dst_name'\n" if ($verbose);
257 close $dst unless ($no_act);
258 return $rc;
261 # Command processing
262 sub process_cmd {
263 my $args = shift; my $cmd = shift @$args;
264 my $chunk_filter = (@$args) ? sub { grep { $_[0] eq $_ } @$args } : undef;
265 local *print_iff = walk_chunks(\&print_chunk, $chunk_filter);
266 local *hexdump_iff = walk_chunks(
267 sub { print "\n$chunk_types->{$_[0]}->{code}\n"; hexdump_chunk(@_) },
268 $chunk_filter
270 #*printhexdump_iff = walk_chunks(sub { print_chunk(@_); hexdump_chunk(@_) });
271 my $track_filter = sub {
272 for (@$args[0,1]) { defined($_) or next; ($_ == shift) or return };
273 return 1;
275 my $iff_only_warn = sub {
276 return $iff_only unless ($iff_only);
277 print "WARNING Command $cmd on a bare IFF file ignored\n";
278 process_file(sub { return 1 }, @_);
279 return $iff_only;
281 local *list_tracks = walk_tracks(\&list_track, $track_filter);
282 local *extract_tracks = walk_tracks(\&extract_track, $track_filter);
283 if ($cmd eq "help") {
284 Pod::Usage::pod2usage(
285 -verbose => 99,
286 -sections => "SYNOPSIS|COMMANDS",
287 -exitval => 0
289 } elsif ($cmd eq "types") {
290 for (keys %$chunk_types) { print "$_\n" unless ($_ eq 'unknown') };
292 } elsif ($cmd eq "nop") {
293 process_file(sub { return 1 }, @_);
294 } elsif ($cmd eq "info") {
295 process_file(\&print_iff, @_);
296 } elsif ($cmd eq "hexdump") {
297 process_file(\&hexdump_iff, @_);
298 } elsif ($cmd eq "list") {
299 process_file(\&list_tracks, @_);
300 } elsif ($cmd eq "track") {
301 process_file(\&extract_tracks, @_) unless (&$iff_only_warn(@_));
302 } elsif ($cmd eq "iff") {
303 process_file(\&extract_iff, @_) unless (&$iff_only_warn(@_));
304 } else {
305 warn "ERROR Unknown command '$cmd'\n";
309 # Main
310 unless (process_cmd(\@ARGV, $nrg_name)) {
311 print "There was an error.\n" if ($verbose);
312 exit 1;
313 } else {
314 exit;
317 # Chunks & Fields
318 sub walk_chunks {
319 my ($process, $filter) = @_;
320 sub {
321 my $iff = shift;
322 my $rc = 1;
323 for my $type (keys %$iff) {
324 CHUNK: for my $chunk (@{$iff->{$type}}) {
325 next CHUNK if (defined($filter) and not &$filter($type, $chunk));
326 &$process($type, $chunk, @_) or $rc = 0;
329 return $rc;
332 sub print_chunk {
333 my ($type, $chunk) = map shift, (0..1);
334 map shift, (0..1);
335 print "$chunk_types->{$type}->{code} (at offset $chunk->{offset})\n";
336 print_fields($chunk_types->{$type}->{fields}, $chunk->{fields}) if ($verbose);
337 if (defined($chunk->{lfields})) {
338 for (my $i = 0; $i < @{$chunk->{lfields}}; $i++) {
339 printf "%s[%d]\n", $chunk_types->{$type}->{code}, $i+1;
340 print_fields($chunk_types->{$type}->{lfields}, $chunk->{lfields}->[$i]) if ($verbose);
343 print "\n" if ($verbose);
344 return 1;
346 sub print_fields {
347 my ($fields, $vals) = map shift, (0..1);
348 for (my $i = 0; $i < @{$vals}; $i++) {
349 my $field = $fields->[$i];
350 my @val = ( $vals->[$i] );
351 my $str = $val[0];
352 if (defined($field->[3])) {
353 my $fmts = (ref($field->[3]) eq 'ARRAY') ? $field->[3] : [ $field->[3] ];
354 $str = join(" ", map { sprintf($_, @val) } @$fmts);
356 print "($field->[1])$field->[0]: $str\n";
358 return 1;
360 sub hexdump_chunk {
361 my ($type, $chunk) = map shift, (0..1);
362 my ($src) = map shift, (0..1);
363 $type = $chunk_types->{$type};
364 seek $src, $chunk->{offset}, SEEK_SET;
365 my $first_f = $type->{fields}->[0];
366 my $size = 4+$first_f->[1];
367 $size += $chunk->{fields}->[0] if ($first_f->[0] eq 'size');
368 print hexdump( read_fixed($src, $size) );
371 # Sessions & Tracks
372 sub walk_tracks {
373 my ($process, $filter) = @_;
374 sub {
375 my $iff = shift;
376 my $rc = 1;
377 for (my $sess = 1; $sess <= @{$iff->{sinf}}; $sess++) {
378 my $numoftrcks = $iff->{sinf}->[$sess-1]->{fields}->[1];
379 TRACK: for (my $trck = 0; $trck <= $numoftrcks; $trck++) {
380 # $trck is the ordinal index of the track in the current session, not the track number
381 # the track 0 is for the lead-in
382 my @params = ($sess, $trck, track_chunk($iff, $sess, $trck));
383 next TRACK if ($trck == 0 and defined($params[2]->{etnf}));
384 next TRACK if (defined($filter) and not &$filter(@params));
385 &$process(@params, @_) or $rc = 0;
388 return $rc;
391 sub list_track {
392 my ($sess, $trck, $chunk) = map shift, (0..2);
393 map shift, (0..1);
394 printf "SESSION %d TRACK %02d [%02d]\n", $sess, $chunk->{track}, $trck;
395 printf " mode $chunk_types->{daoi}->{lfields}->[2]->[3] %db/s %db\n",
396 $chunk->{mode}, $chunk->{sect_size}, $chunk->{end}-$chunk->{start}
397 if ($verbose);
398 return 1;
400 # Extract the track at position $trck in session $sess from the image
401 sub extract_track {
402 list_track(@_) if ($verbose);
403 my ($sess, $trck, $chunk) = map shift, (0..2);
404 my ($src, $src_name) = map shift, (0..1);
405 my $track_num = $chunk->{track};
406 my $start = $chunk->{start};
407 my $end = $chunk->{end};
408 my $sect_size = $chunk->{sect_size};
409 unless (defined($start) and defined($end)) {
410 warn "ERROR Cannot find start/end of track $track_num\n";
411 return;
413 unless (defined($sect_size)) {
414 warn "ERROR Cannot find sector size of track $track_num\n";
415 return;
417 printf " start: $start\tend: $end\n" if ($verbose > 1);
418 # Dest file
419 my $dst_name = $src_name . sprintf($trk_ext, $sess, $track_num, $sess);
420 unless ($force or !-e $dst_name) {
421 warn "Skipping track $track_num because '$dst_name' already exists\n";
422 return;
424 my $dst;
425 unless ($no_act or open $dst, ">", $dst_name) {
426 warn "ERROR Cannot open file '$dst_name' for writing: $!\n";
427 return;
429 binmode $dst unless ($no_act);
430 # Copy
431 seek $src, $start, SEEK_SET;
432 my ($rc, $count) = (1, 0);
433 SECTOR: while (tell($src) + $sect_size <= $end) {
434 my $buf = read_fixed($src, $sect_size);
435 print $dst $buf unless ($no_act);
436 $count++;
438 print "$count sectors of size $sect_size from track $track_num written in '$dst_name'\n" if ($verbose);
439 if (my $left = $end-tell($src)) {
440 warn "ERROR Partial sector of size $left/$sect_size found in track $track_num\n";
441 $rc = 0;
442 my $buf = read_fixed($src, $left);
443 print $dst $buf unless ($no_act);
445 close $dst unless ($no_act);
446 return $rc;
448 sub session_chunk {
449 my ($iff, $sess) = map shift, @_;
450 my ($daoi_n, $etnf_n) = (0, 0);
451 my $next_session_chunk = sub {
452 my $chunk = { tracks => $iff->{sinf}->[$sess-1]->{fields}->[1] };
453 my $is_daoi = defined($iff->{daoi}->[$daoi_n]);
454 my $is_etnf = defined($iff->{etnf}->[$etnf_n]);
455 if ( $is_daoi and (not $is_etnf
456 or ($iff->{daoi}->[$daoi_n]->{offset} < $iff->{etnf}->[$etnf_n]->{offset})) ) {
457 $chunk->{daoi} = $iff->{daoi}->[$daoi_n];
458 $chunk->{first} = $iff->{daoi}->[$daoi_n]->{fields}->[5];
459 $daoi_n++;
460 } elsif ($is_etnf) {
461 $chunk->{etnf} = $iff->{etnf}->[$etnf_n];
462 $chunk->{etnf_first} = 1; # TODO
463 $chunk->{first} = 1; # TODO
464 $etnf_n++;
466 $chunk;
468 ( map &$next_session_chunk, (1..$sess) )[-1];
470 # (using index1 of the next track as the end because Nero records the end as the index0 of the next track)
471 sub track_chunk () {
472 my ($iff, $sess, $trck) = map shift, @_;
473 my $chunk = session_chunk($iff, $sess);
474 $chunk->{track} = $chunk->{first} + $trck - 1;
475 if (defined $chunk->{daoi}) {
476 $chunk->{sect_size} = $chunk->{daoi}->{lfields}->[($trck) ? $trck-1 : $trck]->[1];
477 $chunk->{mode} = $chunk->{daoi}->{lfields}->[($trck) ? $trck-1 : $trck]->[2];
478 $chunk->{start} = ($trck)
479 ? $chunk->{daoi}->{lfields}->[$trck-1]->[5]
480 : $chunk->{daoi}->{lfields}->[$trck]->[4];
481 # track 0 starts at index 0
482 $chunk->{end} = (defined($chunk->{daoi}->{lfields}->[$trck]))
483 ? $chunk->{daoi}->{lfields}->[$trck]->[5]
484 : $chunk->{daoi}->{lfields}->[$trck-1]->[6];
485 # last track ends at next_index
486 } elsif (defined($chunk->{etnf})) {
487 warn "WARNING Only DAO discs are fully supported for now\n";
488 $chunk->{track_size} = $chunk->{etnf}->{lfields}->[($trck) ? $trck-1 : $trck]->[1];
489 $chunk->{track_length} = $chunk->{etnf}->{lfields}->[($trck) ? $trck-1 : $trck]->[5];
490 $chunk->{sect_size} = $chunk->{track_size} / $chunk->{track_length};
491 $chunk->{mode} = $chunk->{etnf}->{lfields}->[($trck) ? $trck-1 : $trck]->[2];
492 $chunk->{start} = ($trck)
493 ? $chunk->{etnf}->{lfields}->[$trck-1]->[0]
494 : 0;
495 $chunk->{end} = ($trck)
496 ? $chunk->{start} + $chunk->{etnf}->{lfields}->[$trck-1]->[1]
497 : 0;
499 $chunk;
502 # Misc
503 sub read_fixed {
504 my ($file, $size) = @_;
505 my $buf;
506 my $n = read $file, $buf, $size;
507 die "ERROR Partial read of $n instead of $size\n" unless ($n == $size);
508 return $buf;
511 __END__
513 =head1 DESCRIPTION
515 C<nrgtool> processes a NRG disc image file. It finds and parses the IFF
516 footer, which contains the metadata; then it prints information or extracts
517 some data.
519 Images of all types of discs can be processed; in particular audio CDs (CDDA
520 and CD-extra) and CD-ROM/DVD-ROM (including multi-session discs) have been
521 tested.
523 A track at position 0 is the lead-in of the session. Tracks of DAO sessions
524 are split at index 1 points.
527 =head1 COMMANDS
529 The default command is "info".
531 =over 8
533 =item B<help>
535 Print a short usage message.
537 =item B<info> S<[ I<chunk_types> ]>
539 Print the chunks' structure resulting of the parsing of the IFF. Enbale
540 verbose mode to view the fields' values.
542 =item B<hexdump> S<[ I<chunk_types> ]>
544 Print a hexdump for each chunk (of the selected types).
546 =item B<nop>
548 No op: only parse the IFF.
550 =item B<list> S<[ I<session> [ I<track_pos> ] ]>
552 List the selected tracks. In verbose mode, give some track attributes.
554 =item B<track> S<[ I<session> [ I<track_pos> ] ]>
556 Extract the (raw) data of the selected tracks to files.
558 =item B<iff>
560 Extract the IFF header to a file.
562 =item B<types>
564 List the known chunk types.
566 =back
569 =head1 OPTIONS
571 Options can be negated by prefixing them with "--no-" instead of "--".
573 =over 8
575 =item B<--version>
577 Print the version information and exit.
579 =item B<-h>, B<--help>
581 Display part of the manual.
583 =item B<-v>, B<--verbose>
585 Verbose: print some or more information, depending on the command.
587 If it is given twice, also print debugging information, notably during the
588 parsing.
590 =item B<-I>, B<--iff>
592 IFF only: process a bare IFF file previously extracted by the command B<iff>.
594 =item B<-n>, B<--no-act>
596 No Action: test the reading of files but do not write any files.
598 =item B<-f>, B<--force>
600 Force: overwrite existing files.
602 =item B<-1>, B<--nrgv1>
604 NRG Version: Treat the file as a first version (prior to Nero 5.5) NRG; the
605 default is to treat the file as a version 2 (Nero 5.5 and later).
606 Unimplemented.
608 =item B<-e>, B<--extension> I<ext>
610 NRG Extension (empty by default): extension appended to the I<nrg> file name
611 given on the command line to obtain the actual file name used; with this
612 option, the generated files' names can be controlled somewhat.
614 =item B<-t>, B<--track-ext> I<ext_fmt>
616 Track Extension: printf format template for the tracks' file name extensions
617 (appended to the S<nrg> file name); the first argument to printf is the session
618 number, the second the track number, the third the session number; the default
619 is ".s%02d-t%02d".
621 =item B<-i>, B<--iff-ext> I<ext>
623 IFF Extension: file name extension of the IFF file (appended to the S<nrg> file
624 name given on the command line); it is ".iff" by default.
626 =item B<-3>, B<--32>
628 32-bit integer mode: if Perl does not have support for 64-bit integers, use
629 this option to parse 64-bit integers as 32-bit (without using the 'Q' template
630 of unpack()). When this option is set, the image size cannot be as large.
632 =back
635 =head1 ENVIRONMENT
637 No environment variables are used.
640 =head1 EXAMPLES
642 To view the result of the parsing:
644 nrgtool -v image.nrg | less
646 To show the debugging output of the parsing:
648 nrgtool -vv image.nrg nop
650 To view a hexdump of the CD-text chunk:
652 nrgtool image.nrg hexdump cdtx | less
654 To list the tracks:
656 nrgtool image.nrg list
658 To extract all the audio tracks of F<image.nrg> to F<image-XX.cdda>:
660 nrgtool -v -e .nrg -t '-%n%02d.cdda' image track 1
662 To extract a data disc to an iso file F<image.nrg.iso>:
664 nrgtool -v -t .iso image.nrg track 1 1
666 If a track contains sub-channel data, you can extract the audio/data with one
667 of the following commands:
669 raw96cdconv -v image.nrg.s01-t16
670 raw96cdconv -v --iso image.nrg.s01-t01
671 raw96cdconv -v --sector-size 2336 image.nrg.s01-t01
673 An audio track can be converted to WAV by sox(1):
675 sox -t .cdda -L image.nrg.s01-t16 image-16.wav
678 =head1 BUGS
680 C<nrgtool> has been tested with images created by Nerolinux 4.0 only. Any
681 resulting incompatibilties with other versions of Nero may be reported to the
682 author.
684 The NRG version 1 format is not supported because of the lack of
685 sample image files.
687 If you can reconstruct the same NRG from the extracted files, most probably
688 C<nrgtool> can handle the particular image:
690 nrgtool image.nrg iff
691 nrgtool image.nrg track
692 cat image.nrg.s??-t?? image.nrg.iff >image.nrg.nrgtool
693 cmp image.nrg image.nrg.nrgtool && echo "nrgtool can extract all the data"
695 In 32-bit mode, the size of images that can be processed is limited.
697 See also the CDimg|tools distribution file F<BUGS>.
700 =head1 AUTHOR
702 G.raud Meyer
704 =head1 SEE ALSO
706 L<nero(1)>, L<raw96cdconv(1)>, L<sox(1)>
708 =cut