#!/usr/bin/perl -w
use strict;
# blorbtar 0.1 - tarlike interface to Blorb files
# Copyright (c) 2000 Evin Robertson
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# See
http://www.gnu.org/copyleft/gpl.html for the terms of the
# GNU General Public License. If you don't have internet access, write to
# the Free Software Foundation, Inc., 59 Temple Place, Suite 330,
# Boston, MA 02111, USA.
#
# The author can be reached at
[email protected]
#
# Blorb is a file format for storing resources for Interactive Fictions
# games. The specification is available at
http://www.eblong.com/zarf/blorb/
#
# The blorb specification recommends interpreters allow resources to be
# spread around a directory instead of packaged to make development easier.
# Blorbtar takes these files and generates a Blorb file without requiring you
# first to write a 'blurb' file.
#
# All input files are expected to be standalone equivalents of what's inside
# blorb files.
#
# TODO:
# - allow text versions of chunks (especially RESOL and RELEASE)
# - IDENT chunk autogeneration
# - 'SONG' type support (this is a MOD file which lacks samples of its own)
# - Perhaps make the extension option ('e') use extensions for type
# detection when creating Blorb files.
# - maybe support the append ('r') and diff ('d') commands
# - perhaps an option to be tar-compatible in the double verbose listing for
# tar wrapping programs
#
# base filename => [ Usage, Chunk type ]
my %namelist = ( PIC => [ "Pict", \&PicType ],
SND => [ "Snd ", \&SndType ],
STORY => [ "Exec", \&GamType ],
PALETTE => [ 0, "Plte" ],
RESOL => [ 0, "Reso" ],
LOOPING => [ 0, "Loop" ],
RELEASE => [ 0, "RelN" ],
IDENT => [ 0, "IFhd" ],
COPY => [ 0, "(c) " ],
AUTH => [ 0, "AUTH" ],
ANNO => [ 0, "ANNO" ],
SAGL => [ 0, "SAGL" ],
);
# chunk type => common extension
my %extlist = ( "JPEG" => "jpg",
"PNG " => "png",
"FORM" => \&FORM_ext,
"MOD " => "mod",
"Glul" => "ulx",
"TADG" => "gam",
"MSRL" => "mag",
"ZCOD" => \&ZCOD_ext,
"HUGO" => "hex",
"ALAN" => "acd",
"SAAI" => "dat",
);
sub ZCOD_ext
{
read BLORB, $_, 1;
seek BLORB, -1, 1;
return "z" . ord($_);
}
sub FORM_ext
{
read BLORB, $_, 4;
seek BLORB, -4, 1;
return "aiff" if $_ eq "AIFF";
return $_;
}
sub PicType
{
my $magic;
seek CHUNK, 0, 0;
read CHUNK, $magic, 10;
if($magic =~ /^\xff\xd8....JFIF/) {
return "JPEG";
}
if($magic =~ /^\x89PNG\x0d\x0a\x1a\x0a/) {
return "PNG ";
}
die "Unknown picture type!\n";
}
sub SndType
{
my $magic;
seek CHUNK, 0, 0;
read CHUNK, $magic, 12;
if($magic =~ /^FORM....AIFF/) {
return "AIFF";
}
seek CHUNK, 1080, 0;
read CHUNK, $magic, 4;
if($magic eq "M.K." || $magic eq "M!K!") {
return "MOD ";
}
die "Unknown sound type!\n";
}
sub GamType
{
my $magic;
seek CHUNK, 0, 0;
read CHUNK, $magic, 64;
if($magic =~ /^Glul/) {
return "Glul";
}
if($magic =~ /^TADS2 bin\x0a\x0d\x1a\x00\x76/s) {
return "TADG";
}
if($magic =~ /^MaSc/) {
return "MSRL";
}
if($magic =~ /^(\x01|\x02|\x03|\x04|\x05|\x06|\x07|\x08).................[0-9][0-9][0-9][0-9][0-9][0-9]/s) {
return "ZCOD";
}
if($magic =~ /^...[0-9][0-9]-[0-9][0-9]-[0-9][0-9].....................................\x00\x00\x00\x00\x00\x00\x00\x00/s) {
return "HUGO";
}
if($magic =~ /^\x02[\x00-\x09][\x00-\x09][\x00-\x09]....\x00\x00\x00[\x00-\x01]........\x00\x00\x00[\x00-\x01]/s) {
return "ALAN";
}
if($magic =~ /^\s*[0-9]+\s+[0-9]+\s+[0-9]+\s+[0-9]+\s+[0-9]+\s+/) {
return "SAAI";
}
die "Unknown game type!\n";
}
sub bin32_num
{
return vec($_[0], 0, 32);
}
sub num_bin32
{
my $number = '';
vec($number, 0, 32) = $_[0];
return $number;
}
my $option_verbose = 0;
my $option_extension = 0;
sub list_file
{
my ($usage, $number, $type, $length, $name) = @_;
if($option_verbose) {
if($option_verbose >= 2) {
if($usage) {
printf "$usage %4d", $number;
} else {
print " ";
}
printf " $type %10d ", $length;
}
print "$name\n";
}
}
sub archive
{
my @files = @_;
my @usagelist;
my @chunklist;
my $usagesize = 0;
my $chunksize = 0;
my $offset = 0;
if(!@files) {
die "Cowardly refusing to create an empty blorb file.\n";
}
foreach my $f (@files) {
my @chunkinfo;
open CHUNK, "<$f";
seek CHUNK, 0, 2;
my $length = tell CHUNK;
if($f =~ /^(.*\/)?(.*?)([0-9]+)?(\..*)?$/) {
if($namelist{uc $2}) {
my $name = uc $2;
my $usage = $namelist{$name}->[0];
my $type;
my $number;
my $is_IFF;
if(ref $namelist{$name}->[1]) {
$type = $namelist{$name}->[1]->();
} else {
$type = $namelist{$name}->[1];
}
if(defined $3) {
$number = int($3);
} else {
$number = 0;
}
if($usage) {
push @usagelist, [ $usage, $number, $offset ];
$usagesize += 12;
}
if($type eq "AIFF") {
$is_IFF = 1;
$type = "FORM";
}
list_file($usage, $number, $type, $length, $f);
if($is_IFF) {
$length -= 8;
}
push @chunklist, [ $f, $type, $length, $is_IFF ];
if($length % 2) {
$length++;
}
$chunksize += 8 + $length;
$offset += 8 + $length;
} else {
print "Unknown resource type name: $f\n";
}
} else {
print "Unparseable filename: $f\n";
}
close CHUNK;
}
print BLORB "FORM", num_bin32(4 + 12 + $usagesize + $chunksize), "IFRS";
print BLORB "RIdx", num_bin32(4 + $usagesize), num_bin32($#usagelist + 1);
foreach my $f (@usagelist) {
print BLORB $f->[0], num_bin32($f->[1]),
num_bin32(24 + $usagesize + $f->[2]);
}
foreach my $f (@chunklist) {
my $buffer;
# If it's not an IFF file, create a chunk header for it
if(!($f->[3])) {
print BLORB $f->[1], num_bin32($f->[2]);
}
open CHUNK, "<$f->[0]";
seek CHUNK, 0, 0;
while(read CHUNK, $buffer, 16384) {
print BLORB $buffer;
}
if(($f->[2]) % 2) {
print BLORB "\x0";
}
}
}
sub load_blorb
{
my %file_mask;
foreach $_ (@_) {
$file_mask{lc $_} = 1;
}
my %usagelist;
my @chunklist;
my $total_size;
read BLORB, $_, 4;
if($_ ne "FORM") { die "Not an IFF file\n"; }
read BLORB, $_, 4;
$total_size = bin32_num($_);
read BLORB, $_, 4;
if($_ ne "IFRS") { die "IFF type $_ instead of IFRS\n"; }
$total_size -= 4;
read BLORB, $_, 4;
if($_ ne "RIdx") { die "First chunk $_ instead of RIdx\n"; }
read BLORB, $_, 4;
my $index_size = bin32_num($_);
read BLORB, $_, 4;
my $index_count = bin32_num($_);
if($index_size != 4 + $index_count * 12) { die "Bad RIdx size\n"; }
$total_size -= 12 + $index_size;
if($total_size < 0) { die "RIdx broken"; }
for my $i (1..$index_count) {
my ($usage, $number, $start);
read BLORB, $usage, 4;
read BLORB, $_, 4; $number = bin32_num($_);
read BLORB, $_, 4; $start = bin32_num($_);
$usagelist{$start} = [ $usage, $number ];
}
while($total_size > 0) {
my ($type, $size, $name, $location, $ext);
$location = tell BLORB;
read BLORB, $type, 4;
read BLORB, $_, 4; $size = bin32_num($_);
if($option_extension && $extlist{$type}) {
if(ref $extlist{$type}) {
$ext = "." . $extlist{$type}->();
} else {
$ext = "." . $extlist{$type};
}
} else {
$ext = "";
}
seek BLORB, $size, 1;
$total_size -= 8 + $size;
if((tell BLORB) % 2) {
seek BLORB, 1, 1;
$total_size--;
}
if($usagelist{$location}) {
foreach my $pname (keys %namelist) {
if($namelist{$pname}->[0] eq $usagelist{$location}->[0]) {
$name = $pname . $usagelist{$location}->[1] . $ext;
}
}
} else {
foreach my $pname (keys %namelist) {
if($namelist{$pname}->[1] eq $type) {
$name = $pname . $ext;
}
}
}
if(!%file_mask || $file_mask{lc $name}) {
push @chunklist, [ $usagelist{$location}, $type, $size, $name, $location + 8 ];
}
}
return @chunklist;
}
sub unarchive
{
my @chunklist = load_blorb(@_);
foreach my $c (@chunklist) {
my ($name, $location, $length) = ($c->[3], $c->[4], $c->[2]);
my $buffer;
list_file($c->[0]->[0], $c->[0]->[1], $c->[1], $c->[2], $c->[3]);
open CHUNK, ">$name";
if($c->[1] eq "FORM") {
print CHUNK "FORM", num_bin32($length);
}
seek BLORB, $location, 0;
read BLORB, $buffer, $length;
print CHUNK $buffer;
close CHUNK;
}
}
sub listarchive
{
my @chunklist = load_blorb(@_);
foreach my $c (@chunklist) {
list_file($c->[0]->[0], $c->[0]->[1], $c->[1], $c->[2], $c->[3]);
}
}
my $command = "";
my $commandoptions = $ARGV[0];
shift @ARGV;
if(!defined $commandoptions || $commandoptions eq "--help") {
die
"blorbtar 0.1 - gives a tarlike interface to blorb archives.
Usage:
blorbtar.pl command blorbfile [files]
The following commands and options are available
c create a blorb file
x extract files from a blorb file
t list files in blorb file
v do this verbosely (repeating makes it doubly so)
e Append appropriate extensions to filenames
";
}
while($_ = chop $commandoptions) {
if($_ eq "-") { } # Ignore so they can do -x if they like
elsif($_ eq "f") { } # Backward compatibility with your fingers
elsif($_ eq "c") { $command = "c"; }
elsif($_ eq "x") { $command = "x"; }
elsif($_ eq "t") { $command = "t"; }
elsif($_ eq "v") { $option_verbose++; }
elsif($_ eq "e") { $option_extension = 1; }
else { die "Unknown option or command: $_\n"; }
}
my $blorbfile = $ARGV[0];
shift @ARGV;
if(!defined $blorbfile) {
die "You must specify a Blorb file.\n";
}
if($command eq "c") {
open BLORB, ">$blorbfile" || die "Cannot open \"$blorbfile\" for writing.\n";
archive(@ARGV);
close BLORB;
} elsif($command eq "x") {
open BLORB, "<$blorbfile" || die "Cannot open \"$blorbfile\" for reading.\n";
unarchive(@ARGV);
close BLORB;
} elsif($command eq "t") {
open BLORB, "<$blorbfile" || die "Cannot open \"$blorbfile\" for reading.\n";
$option_verbose++;
listarchive(@ARGV);
close BLORB;
} else {
die "You must give a command (one of c, x, or t).\n";
}