#!/usr/bin/perl # # Copyright © 2025 by David Gesswein # Copyright © 2015-2020 by Vincent Slyngstad # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS LISTED ABOVE BE LIABLE # FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF # CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # # Except as contained in this notice, the names of the authors above # shall not be used in advertising or otherwise to promote the sale, use # or other dealings in this Software without prior written authorization # from those authors. # # Implodes directory of files like created by dialxplode into tape image # # This program converts a directory of files like created by dialxplode # into a tape image of 12 bit words, zero extented to 16 bits. # It creates index for the files. # # See doc/dialimplode.md for usage # $SEEK_SET = 0; $SEEK_END = 2; #use Data::Dumper; sub ceil { if ($_[0] - int($_[0]) > 0) { return int($_[0]) + 1; } else { return $_[0]; } } sub first_index { local($str, @list) = @_; local $ret = -1; for (local $i = 0; $i < @list; $i++) { if (defined $list[$i] and $str eq $list[$i]) { $ret = $i; last; } } return $ret; } # This adds a file to index and tape image sub addfile { local($fn, $dirpath) = @_; local $fullfn = "$dirpath/$fn"; local $size = -s $fullfn; if ($fn =~ /\.bd$/) { local ($first, $last) = addindex($fn, $size, 0, 0); addbinfile($fullfn, $first, $last); } elsif ($fn =~ /\.b([0-9])$/) { local ($first, $last) = addindex($fn, $size, 0, $1); addbinfile($fullfn, $first, $last); } elsif ($fn =~ /\.tx$/) { $overhead = 0; # DIAL will be +0 # +5 for two words manuscript header and end of file character $overhead = 5 if $lap6; # LAP6 will be +5 local ($first, $last) = addindex($fn, $size+$overhead, 1, 0); addtxt($fullfn, $first, $last); } else { die "Unknown file extension $fn\n"; } } # This write a binary file to the tape image. Does not add file to # index. sub addbinfile { local($fn, $first, $last) = @_; #print "Adding bin $fn $first $last\n"; open(INPUT, "<$fn") || die "$fn: $!"; # Copy out the file. These are binary, so no reformatting needed for (; $first <= $last; $first++) { read(INPUT, $buf, $bsize) || die "$fn file read: $!"; # Now write the file. seek(OUTPUT, $first*$bsize, $SEEK_SET) || die "$fn file seek: $!"; print OUTPUT $buf || die "file write: $!"; } } # This write a text file to the tape image. Does not add file to # index. sub addtxt { local($fn, $first, $last) = @_; #print "Adding txt $fn $first $last\n"; open(INPUT, "<$fn") || die "$fn: $!"; seek(OUTPUT, $first*$bsize, $SEEK_SET) || die "$fn file seek: $!"; local($bytes); $bytes = $last - $first + 1; $bytes *= 512; local @sixbitchars = (); # Convert text while () { foreach $char (split('',$_)) { local $tmp = $sixbitrev[ord($char)]; if (defined($tmp)) { push(@sixbitchars, $tmp); $bytes--; } else { die("Undefined character $char in $fn\n"); } } } # Write header if LAP6 if ($lap6 == 1) { if (ceil((@sixbitchars + 4) / $bsize) + $first - 1 > $last) { die("File $fn too long\n"); } print OUTPUT pack("S1",02065); print OUTPUT pack("S1",05712); $bytes -= 4; } else { if (ceil(@sixbitchars / $bsize) + $first - 1 > $last) { die("File $fn too long\n"); } } # Pad the last block out with EOF characters. while ($bytes-- > 0) { push(@sixbitchars, $sixbitrev[032]); } # Write the bytes. for (local $i = 0; $i < @sixbitchars; $i += 2) { local $v = $sixbitchars[$i] << 6 | $sixbitchars[$i+1]; print OUTPUT pack("S1",$v) || die "file write: $!"; } } # This adds file to index sub addindex { local($fn, $bytes, $istxt, $quarter) = @_; local $blocks = ceil($bytes / $bsize); local $highspace = 1; if ($highfiles[1] - $highfiles[0] + 1 < $blocks) { $highspace = 0; } local $lowspace = 1; if ($lowfiles[1] - $lowfiles[0] + 1 < $blocks) { $lowspace = 0; } if ($highspace == 0 && $lowspace == 0) { die "No space for file $fn"; } if ($lowspace == 0 || ($highfiles[0] - $indexblock) < ($indexblock - $lowfiles[1]) && $highspace != 0) { $first = $highfiles[0]; $highfiles[0] += $blocks; } else { $first = $lowfiles[1] - $blocks + 1; $lowfiles[1] -= $blocks; } # convert .. deleted file name back to // $fn =~ s'^\.\.'//'; # remove extension $fn =~ s/\...$//; local $values; if (exists $index{$fn}) { $values = $index{$fn}; } else { # last value is which entry in index it should be in $values = [05757, 05757, 05757, 05757, (keys %index) + 1]; } if ($istxt) { ${$values}[0] = $first; ${$values}[1] = $blocks; } else { ${$values}[2] = $first | ($quarter << 9); ${$values}[3] = $blocks; } $index{$fn} = $values; return($first, $first + $blocks - 1); } sub indexsort { if ($sortindex) { return $a cmp $b; } else { return ${$index{$a}}[4] <=> ${$index{$b}}[4] } } # This writes index to tape image sub writeindex { seek(OUTPUT, $indexblock*$bsize, $SEEK_SET) || die "$fn file seek: $!"; print OUTPUT pack("S1",05757); print OUTPUT pack("S1",05757); print OUTPUT pack("S1",05757); print OUTPUT pack("S1",05757); print OUTPUT pack("S1",0) || die "file write: $!"; print OUTPUT pack("S1",0) || die "file write: $!"; print OUTPUT pack("S1",0) || die "file write: $!"; print OUTPUT pack("S1",0) || die "file write: $!"; local $entries = 0; # Get entries in order added to index foreach my $key (sort indexsort keys %index) { local $values = $index{$key}; local $padkey = $key; for ($i = length($padkey); $i < 8; $i++) { $padkey = $padkey . "\032"; } local @sixbitchars = (); foreach $char (split('',$padkey)) { local $tmp = $sixbitrev[ord(uc($char))]; if (defined($tmp)) { push(@sixbitchars, $tmp); } else { die("Undefined character $char in filename $key\n"); } } for (local $i = 0; $i < @sixbitchars; $i += 2) { local $v = $sixbitchars[$i] << 6 | $sixbitchars[$i+1]; print OUTPUT pack("S1",$v) || die "file write: $!"; } print OUTPUT pack("S1",${$values}[0]) || die "file write: $!"; print OUTPUT pack("S1",${$values}[1]) || die "file write: $!"; print OUTPUT pack("S1",${$values}[2]) || die "file write: $!"; print OUTPUT pack("S1",${$values}[3]) || die "file write: $!"; $entries = $entries + 1; } if ($entries > $indexentries) { die "Too many index entries $entries maximum $indexentries\n"; } print "$entries of $indexentries index entries used\n"; local $blocksleft = $lowfiles[1] - $lowfiles[0] + 1 + $highfiles[1] - $highfiles[0] + 1; print "$blocksleft blocks left\n"; # Fill unused entries for (; $entries < $indexentries; $entries++) { print OUTPUT pack("S1",05757); print OUTPUT pack("S1",05757); print OUTPUT pack("S1",05757); print OUTPUT pack("S1",05757); print OUTPUT pack("S1",0) || die "file write: $!"; print OUTPUT pack("S1",0) || die "file write: $!"; print OUTPUT pack("S1",0) || die "file write: $!"; print OUTPUT pack("S1",0) || die "file write: $!"; } } $tapeblocks = 512; $sortindex = 0; if ($ARGV[0] eq '-s') { shift(@ARGV); $sortindex = 1; } # Open output file open(OUTPUT,'>', $ARGV[1]) || die "$ARGV[1]: $!"; binmode(OUTPUT); # Get files in directory specified $dirpath = $ARGV[0]; opendir $dir, $dirpath or die "Cannot open directory $ARGV[0] $!"; @files = readdir $dir; closedir $dir; if (open(INPUT, "$dirpath/.config")) { if ($#ARGV >= 2) { die "Tape blocks not valid for LAP6 format"; } $lap6 = 1; @configvalues = split(' ',); @lowfiles = (oct($configvalues[0]), oct($configvalues[1])-1); @highfiles = (oct($configvalues[6]), 0777); $indexblock = oct($configvalues[4]); # Negative numbers indicate what field starting with 1 should # be used from .config. The second value will have one subtracted # from the field %specialfiles = ( ".manuscript" => [-4, -5, 0], ".lap6" => [-3, -4, 1], ".unused1" => [-2, -3, 1], ".unused2" => [-6, -7, 1] ); %ignorefiles = ( ".config" => "", ".index" => "", ".order" => "" ); @sixbit = ( "0", "1", "2", "3", "4", "5", "6", "7", # 00x "8", "9", "\n","\033"," ", "i", "p", "-", # 01x "+", "|", "#", "\031","A", "B", "C", "D", # 02x "E", "F", "G", "H", "I", "J", "K", "L", # 03x "M", "N", "O", "P", "Q", "R", "S", "T", # 04x "U", "V", "W", "X", "Y", "Z", "\r","/", # 05x "?", "=", "u", ",", ".", "\$","[", "_", # 06x '"', "'", "<", ">", "]", "*", ":", "\032",# 07x ); } else { if ($#ARGV >= 2) { $tapeblocks = $ARGV[2]; } $lap6 = 0; %specialfiles = ( ".edit" => [0300, 0321, 1], ".asmblr" => [0322, 0345, 1], ".index" => [0346, 0347, 1], ".filecoms" => [0350, 0353, 1], ".loader" => [0354, 0355, 1], ".svbinary" => [0356, 0360, 1], ".pxdx" => [0361, 0362, 1], ".tty" => [0363, 0363, 1], ".ps" => [0364, 0364, 1], ".unused" => [0365, 0366, 1], ".scratch" => [0367, 0367, 1] ); %ignorefiles = ( ".config" => "", ".index" => "", ".order" => "", ".unused1" => "", ".unused2" => "" ); @lowfiles = (0, 0267); @highfiles = (0470, $tapeblocks-1); $indexblock = 0346; @sixbit = ( "\032","A", "B", "C", "D", "E", "F", "G", # 00x "H", "I", "J", "K", "L", "M", "N", "O", # 01x "P", "Q", "R", "S", "T", "U", "V", "W", # 02x "X", "Y", "Z", "[", "\\","]", "^", "\r", # 03x " ", "!", '"', "\n","\$","%", "?", "\t", # 04x "(", ")", "*", "+", ",", "-", ".", "/", # 05x "0", "1", "2", "3", "4", "5", "6", "7", # 06x "8", "9", ":", ";", "<", "=", ">", "?", # 07x ); } %index = (); $indexentries = 63; $bsize = 0400 * 2; for $i (0 .. $#sixbit) { $sixbitrev[ord($sixbit[$i])] = $i; } # Read list of files to specify order to create files $fn = "$dirpath/.order"; if (-e $fn) { open(INPUT, "<$fn") || die "$fn: $!"; while () { chomp; push(@fileorder, $_); } } for $i (0 .. $#fileorder) { $fn = $fileorder[$i]; $entry = first_index($fn, @files); if ($entry >= 0) { addfile($fn, $dirpath); undef $files[$entry]; } else { die "Fileinfo file $fn not found\n"; } } for $i (0 .. $#files) { if (defined $files[$i] and exists $specialfiles{$files[$i]}) { $first = $specialfiles{$files[$i]}[0]; if ($first < 0) { # Convert negative values to index starting at 0. $first = oct($configvalues[-$first - 1]); } $last = $specialfiles{$files[$i]}[1]; if ($last < 0) { # Last configvalues is start of next area so need to subtract 1 $last = oct($configvalues[-$last - 1]) - 1; } if ($specialfiles{$files[$i]}[2]) { addbinfile("$dirpath/$files[$i]", $first, $last); } else { addtxt("$dirpath/$files[$i]", $first, $last); } undef $files[$i]; } } foreach (@files) { if (defined $_ and exists $ignorefiles{$_}) { next; } #print "$_\n"; if (defined $_) { $fn = $_; next if ($fn eq "."); next if ($fn eq ".."); addfile($fn, $dirpath); } } writeindex(); #Pad to correct size if needed seek(OUTPUT, 0, $SEEK_END); if (tell(OUTPUT) < $tapeblocks * $bsize) { seek(OUTPUT, $tapeblocks * $bsize - 1, $SEEK_SET) or die "Failed pad seek $!"; print OUTPUT chr(0); }