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
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/>.
19 nrgtool - reads and converts/splits Nero optical disc image files (.nrg)
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> ]>
33 Getopt
::Long
::Configure
('bundling', 'no_auto_abbrev', 'auto_version', 'auto_help');
35 use String
::Escape
qw(printable);
36 use Data
::Hexdumper
qw(hexdump);
39 $main::VERSION
= "0.2";
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)
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,
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);
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
} = {
69 fields
=> [ [ 'iff_offset', 8, $Q ] ],
71 $chunk_types->{sinf
} = {
73 fields
=> [ [ 'size', 4, 'N' ], [ 'num_tracks', 4, 'N' ] ],
75 $chunk_types->{etnf
} = {
77 fields
=> [ [ 'size', 4, 'N' ] ],
81 [ 'mode', 4, 'N', [ '0x%08X', '%d' ] ],
83 [ 'unknown', 4, 'N', [ '0x%08X (always 0)' ] ],
84 [ 'length', 4, 'N' ] ],
86 $chunk_types->{daoi
} = {
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' ] ],
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
} = {
102 fields
=> [ [ 'size', 4, 'N' ] ],
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
} = {
110 fields
=> [ [ 'size', 4, 'N' ], [ 'unknown', 4, 'N', [ '0x%08X', '%d' ] ] ],
112 $chunk_types->{toct
} = {
114 fields
=> [ [ 'size', 4, 'N' ], [ 'unknown', 2, 'n', [ '0x%04X', '%d' ] ] ],
116 $chunk_types->{dinf
} = {
118 fields
=> [ [ 'size', 4, 'N' ], [ 'unknown', 4, 'N', [ '0x%08X', '%d' ] ] ],
120 $chunk_types->{cdtx
} = {
122 fields
=> [ [ 'size', 4, 'N' ] ],
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
} = {
130 fields
=> [ [ 'size', 4, 'N' ], [ 'type', 4, 'N' ] ],
132 $chunk_types->{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
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";
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";
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)) {
167 warn "ERROR while reading an IFF chunk; aborting\n";
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]
178 my ($nrg, $iff) = map shift, (0..1);
179 my $code = read_fixed
($nrg, 4);
180 my $type = typeofcode
($code);
182 warn "ERROR Unknown chunk code '" . printable
($code) . "'\n";
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}));
189 my $fields = $chunk_types->{$type}->{fields
};
192 fields
=> [ map { unpack($_->[2], read_fixed
($nrg, $_->[1])) } @
$fields ],
194 push(@
{$iff->{$type}}, $chunk);
195 if ($fields->[0]->[0] eq 'size') {
197 my $size = $chunk->{fields
}->[0];
198 $pos += $fields->[0]->[1]+$size;
199 print " Chunk ends at at offset $pos\n" if ($verbose > 1);
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
213 warn "WARNING Remaining data ($left bytes) at the end of the chunk $code\n"
215 $pos++ if ($pos % 2);
216 seek $nrg, $pos, SEEK_SET
;
218 return 0 if ($type eq 'end');
223 for (keys %{$chunk_types}) {
224 defined($chunk_types->{$_}->{code
}) or next;
225 return $_ if ($code eq $chunk_types->{$_}->{code
});
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";
237 print "IFF offset: $offset\n" if ($verbose > 1);
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";
245 unless ($no_act or open $dst, ">", $dst_name) {
246 warn "ERROR Cannot open file '$dst_name' for writing: $!\n";
249 binmode $dst unless ($no_act);
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);
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
(@_) },
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 };
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 }, @_);
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
(
286 -sections
=> "SYNOPSIS|COMMANDS",
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(@_));
305 warn "ERROR Unknown command '$cmd'\n";
310 unless (process_cmd
(\
@ARGV, $nrg_name)) {
311 print "There was an error.\n" if ($verbose);
319 my ($process, $filter) = @_;
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;
333 my ($type, $chunk) = 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);
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] );
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";
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) );
373 my ($process, $filter) = @_;
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;
392 my ($sess, $trck, $chunk) = map shift, (0..2);
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
}
400 # Extract the track at position $trck in session $sess from the image
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";
413 unless (defined($sect_size)) {
414 warn "ERROR Cannot find sector size of track $track_num\n";
417 printf " start: $start\tend: $end\n" if ($verbose > 1);
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";
425 unless ($no_act or open $dst, ">", $dst_name) {
426 warn "ERROR Cannot open file '$dst_name' for writing: $!\n";
429 binmode $dst unless ($no_act);
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);
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";
442 my $buf = read_fixed
($src, $left);
443 print $dst $buf unless ($no_act);
445 close $dst unless ($no_act);
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];
461 $chunk->{etnf
} = $iff->{etnf
}->[$etnf_n];
462 $chunk->{etnf_first
} = 1; # TODO
463 $chunk->{first
} = 1; # TODO
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)
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]
495 $chunk->{end
} = ($trck)
496 ?
$chunk->{start
} + $chunk->{etnf
}->{lfields
}->[$trck-1]->[1]
504 my ($file, $size) = @_;
506 my $n = read $file, $buf, $size;
507 die "ERROR Partial read of $n instead of $size\n" unless ($n == $size);
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
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
523 A track at position 0 is the lead-in of the session. Tracks of DAO sessions
524 are split at index 1 points.
529 The default command is "info".
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).
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.
560 Extract the IFF header to a file.
564 List the known chunk types.
571 Options can be negated by prefixing them with "--no-" instead of "--".
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
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).
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
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.
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.
637 No environment variables are used.
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
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
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
684 The NRG version 1 format is not supported because of the lack of
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>.
706 L<nero(1)>, L<raw96cdconv(1)>, L<sox(1)>