DOS33-perl/DOS33.pm

1188 lines
38 KiB
Perl

#!/usr/bin/perl -w
package DOS33;
#
# DOS33.pm:
#
# Module to access Apple II DOS 3.3 disk images.
#
# 20190115 LSH
#
use strict;
use POSIX;
use DSK;
use Exporter::Auto;
my $debug = 0;
my $min_trk = 0; # Minimum track number
my $max_trk = 34; # Maximum track number
my $min_sec = 0; # Minimum sector number
my $max_sec = 15; # Maximum sector number
my $sec_size = 256; # Sector size
my $vtoc_trk = 0x11; # Default VTOC track
my $vtoc_sec = 0x00; # Default VTOC sector
# DOS 3.3 file types
my %file_types = (
0x00 => ' T', # Text file
0x01 => ' I', # INTBASIC file
0x02 => ' A', # Applesoft file
0x04 => ' B', # Binary file
0x08 => ' S', # Special file
0x10 => ' R', # Relocatable file
0x20 => ' A', # A file
0x40 => ' B', # B file
);
# DOS 3.3 file types (display)
my %disp_file_types = (
0x00 => ' T', # Unlocked Text file
0x01 => ' I', # Unlocked INTBASIC file
0x02 => ' A', # Unlocked Applesoft file
0x04 => ' B', # Unlocked Binary file
0x08 => ' S', # Unlocked Special file
0x10 => ' R', # Unlocked Relocatable file
0x20 => ' A', # Unlocked A file
0x40 => ' B', # Unlocked B file
0x80 => '*T', # Locked text file
0x81 => '*I', # Locked INTBASIC file
0x82 => '*A', # Locked Applesoft file
0x84 => '*B', # Locked Binary file
0x88 => '*S', # Locked Special file
0x90 => '*R', # Locked Relocatable file
0xa0 => '*A', # Locked A file
0xb0 => '*B', # Locked B file
);
# For free space counts.
my %ones_count = (
0x00 => 0, # 0000
0x01 => 1, # 0001
0x02 => 1, # 0010
0x03 => 2, # 0011
0x04 => 1, # 0100
0x05 => 2, # 0101
0x06 => 2, # 0110
0x07 => 3, # 0111
0x08 => 1, # 1000
0x09 => 2, # 1001
0x0a => 2, # 1010
0x0b => 3, # 1011
0x0c => 2, # 1100
0x0d => 3, # 1101
0x0e => 3, # 1110
0x0f => 4, # 1111
);
#
# Volume Table of Contents (VTOC) Format
#
# 00 Not used
# 01 Track number of first catalog sector
# 02 Sector number of first catalog sector
# 03 Release number of DOS used to INIT this diskette
# 04-05 Not used
# 06 Diskette volume number
# 07-26 Not used
# 27 Maximum number of track/sector list sector (122 for 256 byte sectors)
# 28-2f Not used
# 30 Last track where sectors were allocated
# 31 Direction of track allocation (+1 or -1)
# 32-33 Not used
# 34 Number of tracks per diskette (normally 35)
# 35 Number of sectors per track (13 or 16)
# 36-37 Number of bytes per sector (LO/HI format)
# 38-3b Bit map of free sectors in track 0
# 3c-3f Bit map of free sectors in track 1
# 40-43 Bit map of free sectors in track 2
# ...
# bc-bf Bit map of free sectors in track 33
# c0-c3 Bit map of free sectors in track 34
# c4-cf Bit maps for additional tracks if there are more than 35 tracks per diskette
#
my $vtoc_fmt_tmpl = 'xCCCx2Cx32Cx8CCx2CCva140';
#
# Bit maps of free sectors on a given track
#
# BYTE SECTORS
# 0 FDEC BA98
# 1 7654 3210
# 2 .... .... (not used)
# 3 .... .... (not used)
#
my $bit_map_free_sec_tmpl = 'nx2';
#
#
# Catalog Sector Format
#
# 00 Not used
# 01 Track number of next catalog sector (usually 11 hex)
# 02 Sector number of next catalog sector
# 03-0a Not used
# 0b-2d First file descriptive entry
# 2e-50 Second file descriptive entry
# 51-73 Third file descriptive entry
# 74-96 Fourth file descriptive entry
# 97-b9 Fifth file descriptive entry
# ba-dc Sixth file descriptive entry
# dd-ff Seventh file descriptive entry
#
my $cat_sec_fmt_tmpl = 'xCCx8a35a35a35a35a35a35a35';
#
# File Descriptive Entry Format
#
# 00 Track of first track/sector list sector.
# 01 Sector of first track/sector list sector.
# 02 File type and flags:
# 03-20 File name (30 characters)
# 21-22 Length of file in sectors (LO/HI format).
#
my $file_desc_ent_dmt_tmpl = 'CCCa30C';
#
# Track/Sector ListFormat
#
# 00 Not used
# 01 Track number of next T/S List sector if one was needed or zero if no more T/S List sectors.
# 02 Sector number of next T/S List sector (if present).
# 03-04 Not used
# 05-06 Sector offset in file of the first sector described by this list.
# 07-0b Not used
# 0c-0d Track and sector of first data sector or zeros
# 0e-0f Track and sector of second data sector or zeros
# 10-ff Up to 120 more Track/Sector pairs
#
my $tslist_fmt_tmpl = 'xCCx2vx5a122';
my %dsk = (); # Memory for disk image.
#
# Display a file entry in DOS 3.3 catalog format.
#
sub display_file_entry {
my ($file_type, $filename, $file_length) = @_;
print sprintf("%-2s %03d %s\n", $disp_file_types{$file_type}, $file_length, $filename);
}
# Parse a file entry
sub parse_file_entry {
my ($file_desc_entry) = @_;
my ($first_tslist_trk, $first_tslist_sec, $file_type, $filename, $file_length) = unpack $file_desc_ent_dmt_tmpl, $file_desc_entry;
return undef if $first_tslist_trk eq '';
return undef if $first_tslist_trk == 0xff; # Deleted
return undef if $first_tslist_trk == 0x00; # Never used
$file_length = 0 unless defined $file_length;
if (defined $first_tslist_trk && $first_tslist_trk ne '') {
# Convert Apple ASCII to normal (clear the high bit)
my $fname = '';
my @bytes = unpack "C*", $filename;
foreach my $byte (@bytes) {
$fname .= sprintf("%c", $byte & 0x7f);
}
return $first_tslist_trk, $first_tslist_sec, $file_type, $fname, $file_length;
}
return 0;
}
# Parse a catalog sector
sub parse_cat_sec {
my ($buf) = @_;
my ($trk_num_nxt_cat_sec, $sec_num_nxt_cat_sec, $first_file_desc_ent, $second_file_desc_ent, $third_file_desc_ent, $fourth_file_desc_ent, $fifth_file_desc_ent, $sixth_file_desc_ent, $seventh_file_desc_ent) = unpack $cat_sec_fmt_tmpl, $buf;
my @files = ();
my $empty_file_entry = 0;
my ($first_tslist_trk, $first_tslist_sec, $file_type, $filename, $file_length);
($first_tslist_trk, $first_tslist_sec, $file_type, $filename, $file_length) = parse_file_entry($first_file_desc_ent);
if (defined $first_tslist_trk && $first_tslist_trk ne '') {
push @files, { 'file_type' => $file_type, 'filename' => $filename, 'file_length' => $file_length, 'trk' => $first_tslist_trk, 'sec' => $first_tslist_sec, 'cat_offset' => 1 };
} else {
$empty_file_entry = 1;
}
($first_tslist_trk, $first_tslist_sec, $file_type, $filename, $file_length) = parse_file_entry($second_file_desc_ent);
if (defined $first_tslist_trk && $first_tslist_trk ne '') {
push @files, { 'file_type' => $file_type, 'filename' => $filename, 'file_length' => $file_length, 'trk' => $first_tslist_trk, 'sec' => $first_tslist_sec, 'cat_offset' => 2 };
} else {
$empty_file_entry = 2 if $empty_file_entry == 0;
}
($first_tslist_trk, $first_tslist_sec, $file_type, $filename, $file_length) = parse_file_entry($third_file_desc_ent);
if (defined $first_tslist_trk && $first_tslist_trk ne '') {
push @files, { 'file_type' => $file_type, 'filename' => $filename, 'file_length' => $file_length, 'trk' => $first_tslist_trk, 'sec' => $first_tslist_sec, 'cat_offset' => 3 };
} else {
$empty_file_entry = 3 if $empty_file_entry == 0;
}
($first_tslist_trk, $first_tslist_sec, $file_type, $filename, $file_length) = parse_file_entry($fourth_file_desc_ent);
if (defined $first_tslist_trk && $first_tslist_trk ne '') {
push @files, { 'file_type' => $file_type, 'filename' => $filename, 'file_length' => $file_length, 'trk' => $first_tslist_trk, 'sec' => $first_tslist_sec, 'cat_offset' => 4 };
} else {
$empty_file_entry = 4 if $empty_file_entry == 0;
}
($first_tslist_trk, $first_tslist_sec, $file_type, $filename, $file_length) = parse_file_entry($fifth_file_desc_ent);
if (defined $first_tslist_trk && $first_tslist_trk ne '') {
push @files, { 'file_type' => $file_type, 'filename' => $filename, 'file_length' => $file_length, 'trk' => $first_tslist_trk, 'sec' => $first_tslist_sec, 'cat_offset' => 5 };
} else {
$empty_file_entry = 5 if $empty_file_entry == 0;
}
($first_tslist_trk, $first_tslist_sec, $file_type, $filename, $file_length) = parse_file_entry($sixth_file_desc_ent);
if (defined $first_tslist_trk && $first_tslist_trk ne '') {
push @files, { 'file_type' => $file_type, 'filename' => $filename, 'file_length' => $file_length, 'trk' => $first_tslist_trk, 'sec' => $first_tslist_sec, 'cat_offset' => 6 };
} else {
$empty_file_entry = 6 if $empty_file_entry == 0;
}
($first_tslist_trk, $first_tslist_sec, $file_type, $filename, $file_length) = parse_file_entry($seventh_file_desc_ent);
if (defined $first_tslist_trk && $first_tslist_trk ne '') {
push @files, { 'file_type' => $file_type, 'filename' => $filename, 'file_length' => $file_length, 'trk' => $first_tslist_trk, 'sec' => $first_tslist_sec, 'cat_offset' => 7 };
} else {
$empty_file_entry = 7 if $empty_file_entry == 0;
}
return $trk_num_nxt_cat_sec, $sec_num_nxt_cat_sec, $empty_file_entry, @files;
}
# Get catalog sector
sub get_cat_sec {
my ($dskfile, $cat_trk, $cat_sec) = @_;
my $buf;
if (rts($dskfile, $cat_trk, $cat_sec, \$buf)) {
return $buf, parse_cat_sec($buf);
}
return 0;
}
#
# Display disk catalog
#
sub catalog {
my ($dskfile, $dbg) = @_;
if (defined $dbg && $dbg) {
$debug = 1;
}
my ($trk_num_1st_cat_sec, $sec_num_1st_cat_sec, $rel_num_dos, $dsk_vol_num, $max_tslist_secs, $last_trk_secs_alloc, $dir_trk_alloc, $num_trks_dsk, $bit_map_free_secs) = get_vtoc_sec($dskfile);
if (defined $trk_num_1st_cat_sec && $trk_num_1st_cat_sec ne '') {
print sprintf("DISK VOLUME %d\n\n", $dsk_vol_num);
my $cat_buf;
my ($next_cat_trk, $next_cat_sec) = ($trk_num_1st_cat_sec, $sec_num_1st_cat_sec);
my @files = ();
my $empty_file_entry;
do {
($cat_buf, $next_cat_trk, $next_cat_sec, $empty_file_entry, @files) = get_cat_sec($dskfile, $next_cat_trk, $next_cat_sec);
#if (defined $next_cat_trk && $next_cat_trk ne '') {
if (scalar @files) {
foreach my $file (@files) {
display_file_entry($file->{'file_type'}, $file->{'filename'}, $file->{'file_length'});
}
}
} while ($next_cat_trk != 0);
} else {
return 0;
}
return 1;
}
#
# Calculate free space
#
sub freespace {
my ($dskfile, $dbg) = @_;
if (defined $dbg && $dbg) {
$debug = 1;
}
my ($trk_num_1st_cat_sec, $sec_num_1st_cat_sec, $rel_num_dos, $dsk_vol_num, $max_tslist_secs, $last_trk_secs_alloc, $dir_trk_alloc, $num_trks_dsk, $num_secs_dsk, $num_bytes_sec, $bit_map_free_secs) = get_vtoc_sec($dskfile);
my $tmpl = '';
for (my $t = $min_trk; $t <= $max_trk; $t++) {
$tmpl .= $bit_map_free_sec_tmpl;
}
my $free_sectors = 0;
my @flds = unpack $tmpl, $bit_map_free_secs;
for (my $t = $min_trk; $t <= $max_trk; $t++) {
$free_sectors += $ones_count{($flds[$t] >> 12)}; # Sectors fdec
$free_sectors += $ones_count{($flds[$t] >> 8) & 0x0f}; # Sectors ba98
$free_sectors += $ones_count{($flds[$t] >> 4) & 0x0f}; # Sectors 7654
$free_sectors += $ones_count{$flds[$t] & 0x0f}; # Sectors 3210
}
#print "$free_sectors sectors free\n";
return $free_sectors;
}
#
# Display sector free map
#
sub freemap {
my ($dskfile, $dbg) = @_;
if (defined $dbg && $dbg) {
$debug = 1;
}
my ($trk_num_1st_cat_sec, $sec_num_1st_cat_sec, $rel_num_dos, $dsk_vol_num, $max_tslist_secs, $last_trk_secs_alloc, $dir_trk_alloc, $num_trks_dsk, $num_secs_dsk, $num_bytes_sec, $bit_map_free_secs) = get_vtoc_sec($dskfile);
print " 0123456789abcdef\n";
print " +----------------\n";
my $tmpl = '';
for (my $t = $min_trk; $t <= $max_trk; $t++) {
$tmpl .= $bit_map_free_sec_tmpl;
}
print "tmpl=$tmpl\n" if $debug;
my $free_sectors = 0;
my @flds = unpack $tmpl, $bit_map_free_secs;
for (my $t = $min_trk; $t <= $max_trk; $t++) {
print sprintf("%2d %04x\n", $t, $flds[$t]) if $debug;
print sprintf("%2d %016b\n", $t, $flds[$t]) if $debug;
my $fr = sprintf("%016b", $flds[$t]);
print "fr=$fr\n" if $debug;
my $fm = reverse $fr;
print "fm=$fm\n" if $debug;
$fm =~ s/0/ /g;
$fm =~ s/1/*/g;
print "fm=$fm\n" if $debug;
print sprintf("%2d|%s\n", $t, $fm);
$free_sectors += $ones_count{($flds[$t] >> 12)}; # Sectors fdec
$free_sectors += $ones_count{($flds[$t] >> 8) & 0x0f}; # Sectors ba98
$free_sectors += $ones_count{($flds[$t] >> 4) & 0x0f}; # Sectors 7654
$free_sectors += $ones_count{$flds[$t] & 0x0f}; # Sectors 3210
}
#print "bit_map_free_secs=";
#my @bytes = unpack "C*", $bit_map_free_secs;
#foreach my $byte (@bytes) {
# print sprintf("%02x ", $byte);
#}
print "\n";
print "$free_sectors sectors free\n";
}
# Parse a VTOC sector
sub parse_vtoc_sec {
my ($buf) = @_;
my ($trk_num_1st_cat_sec, $sec_num_1st_cat_sec, $rel_num_dos, $dsk_vol_num, $max_tslist_secs, $last_trk_secs_alloc, $dir_trk_alloc, $num_trks_dsk, $num_secs_dsk, $num_bytes_sec, $bit_map_free_secs) = unpack $vtoc_fmt_tmpl, $buf;
if ($debug) {
print sprintf("trk_num_1st_cat_sec=%02x\n", $trk_num_1st_cat_sec);
print sprintf("sec_num_1st_cat_sec=%02x\n", $sec_num_1st_cat_sec);
print sprintf("rel_num_dos=%02x\n", $rel_num_dos);
print sprintf("dsk_vol_num=%02x\n", $dsk_vol_num);
print sprintf("max_tslist_secs=%02x\n", $max_tslist_secs);
print sprintf("last_trk_secs_alloc=%02x\n", $last_trk_secs_alloc);
print sprintf("dir_trk_alloc=%02x\n", $dir_trk_alloc);
print sprintf("num_trks_dsk=%02x\n", $num_trks_dsk);
print sprintf("num_secs_dsk=%02x\n", $num_secs_dsk);
print sprintf("num_bytes_sec=%04x\n", $num_bytes_sec);
print "bit_map_free_secs=";
my @bytes = unpack "C*", $bit_map_free_secs;
foreach my $byte (@bytes) {
print sprintf("%02x ", $byte);
}
print "\n";
}
return $trk_num_1st_cat_sec, $sec_num_1st_cat_sec, $rel_num_dos, $dsk_vol_num, $max_tslist_secs, $last_trk_secs_alloc, $dir_trk_alloc, $num_trks_dsk, $num_secs_dsk, $num_bytes_sec, $bit_map_free_secs;
}
#
# Get VTOC Sector
#
sub get_vtoc_sec {
my ($dskfile) = @_;
my $buf;
if (rts($dskfile, $vtoc_trk, $vtoc_sec, \$buf)) {
dump_sec($buf) if $debug;
return parse_vtoc_sec($buf);
}
return 0;
}
sub write_vtoc {
my ($dskfile, $trk_num_1st_cat_sec, $sec_num_1st_cat_sec, $rel_num_dos, $dsk_vol_num, $max_tslist_secs, $last_trk_secs_alloc, $dir_trk_alloc, $num_trks_dsk, $num_secs_dsk, $num_bytes_sec, $bit_map_free_secs) = @_;
# Re-pack vtoc sector, the double pack is to pad the sector with zero bytes.
my $buf = pack "a$sec_size", pack $vtoc_fmt_tmpl, ($trk_num_1st_cat_sec, $sec_num_1st_cat_sec, $rel_num_dos, $dsk_vol_num, $max_tslist_secs, $last_trk_secs_alloc, $dir_trk_alloc, $num_trks_dsk, $num_secs_dsk, $num_bytes_sec, $bit_map_free_secs);
print "Writing vtoc\n" if $debug;
if ($debug) {
print "vtoc=";
my @bytes = unpack "C*", $buf;
foreach my $byte (@bytes) {
print sprintf("%02x ", $byte);
}
print "\n";
}
dump_sec($buf) if $debug;
# Write back vtoc sector.
if (!wts($dskfile, $vtoc_trk, $vtoc_sec, $buf)) {
print "Failed to write vtoc sector $vtoc_trk $vtoc_sec!\n";
return 0;
}
return 1;
}
#
# Parse a sector of a track/sector list
#
sub parse_tslist_sec {
my ($buf, $num_secs) = @_;
#dump_sec($buf);
# Track/Sector ListFormat
#
# 00 Not used
# 01 Track number of next T/S List sector if one was needed or zero if no more T/S List sectors.
# 02 Sector number of next T/S List sector (if present).
# 03-04 Not used
# 05-06 Sector offset in file of the first sector described by this list.
# 07-0b Not used
# 0c-0d Track and sector of first data sector or zeros
# 0e-0f Track and sector of second data sector or zeros
# 10-ff Up to 120 more Track/Sector pairs
#
#$tslist_fmt_tmpl = 'xCCx2vx5a122';
my @secs = ();
##FIXME -- tslist_fmt_tmpl should not have 122 hard coded, that value shoulc come from vtoc.
my ($next_tslist_trk, $next_tslist_sec, $soffset, $tslist) = unpack $tslist_fmt_tmpl, $buf;
#if ($debug) {
print "num_secs=$num_secs\n";
print "tslist=";
my @bytes = unpack "C*", $tslist;
foreach my $byte (@bytes) {
print sprintf("%02x ", $byte);
}
print "\n";
#}
my $tmpl = '';
for (my $ts = 0; $ts < 122; $ts++) {
$tmpl .= 'CC';
}
my (@tsl) = unpack $tmpl, $tslist;
for (my $ts = 0; $ts < 122; $ts++) {
my $sec = pop @tsl;
my $trk = pop @tsl;
last unless defined $trk;
last if $trk eq '';
next if $trk == 0 && $sec == 0;
print "Adding trk=$trk sec=$sec to tslist\n";
unshift @secs, { 'trk' => $trk, 'sec' => $sec };
}
return $next_tslist_trk, $next_tslist_sec, @secs;
}
#
# Get a sector of a track/sector list
#
sub get_tslist_sec {
my ($dskfile, $tslist_trk, $tslist_sec, $num_secs) = @_;
my $buf;
if (rts($dskfile, $tslist_trk, $tslist_sec, \$buf)) {
#dump_sec($buf) if $debug;
dump_sec($buf);
return parse_tslist_sec($buf, $num_secs);
}
return 0;
}
#
# Get a track/sector list
#
sub get_tslist {
my ($dskfile, $tslist_trk, $tslist_sec, $num_secs) = @_;
my ($next_tslist_trk, $next_tslist_sec) = ($tslist_trk, $tslist_sec);
my @secs = ();
do {
($next_tslist_trk, $next_tslist_sec, @secs) = get_tslist_sec($dskfile, $next_tslist_trk, $next_tslist_sec, $num_secs);
if (defined $next_tslist_trk && $next_tslist_trk ne '') {
print "pushing trk $next_tslist_trk sec $next_tslist_sec\n";
push @secs, { 'trk' => $next_tslist_trk, 'sec', $next_tslist_sec };
}
} while ($next_tslist_trk != 0);
return @secs;
}
#
# Find a file in the catalog
#
sub find_file {
my ($dskfile, $filename) = @_;
my ($trk_num_1st_cat_sec, $sec_num_1st_cat_sec, $rel_num_dos, $dsk_vol_num, $max_tslist_secs, $last_trk_secs_alloc, $dir_trk_alloc, $num_trks_dsk, $bit_map_free_secs) = get_vtoc_sec($dskfile);
if (defined $trk_num_1st_cat_sec && $trk_num_1st_cat_sec ne '') {
my ($next_cat_trk, $next_cat_sec) = ($trk_num_1st_cat_sec, $sec_num_1st_cat_sec);
my $cat_buf;
my @files = ();
do {
my $cur_cat_trk = $next_cat_trk;
my $cur_cat_sec = $next_cat_sec;
my $empty_file_entry;
($cat_buf, $next_cat_trk, $next_cat_sec, $empty_file_entry, @files) = get_cat_sec($dskfile, $next_cat_trk, $next_cat_sec);
#if (defined $next_cat_trk && $next_cat_trk ne '') {
if (scalar @files) {
foreach my $file (@files) {
my $fn = $file->{'filename'};
$fn =~ s/\s+$//g;
if ($fn eq $filename) {
#print "trk=$file->{'trk'} sec=$file->{'sec'}\n";
return $file, $cur_cat_trk, $cur_cat_sec, $cat_buf;
}
}
}
} while ($next_cat_trk != 0);
} else {
return 0;
}
print "File $filename NOT FOUND\n";
return 0;
}
#
# Read a file
#
sub read_file {
my ($dskfile, $filename, $mode, $conv, $dbg) = @_;
$mode = '' unless defined $mode;
$conv = 0 unless defined $conv;
$debug = 1 if (defined $dbg && $dbg);
my ($file, $cat_trk, $cat_sec, $cat_buf) = find_file($dskfile, $filename);
if (defined $file && $file && $file->{'trk'}) {
my $buf;
my @secs = get_tslist($dskfile, $file->{'trk'}, $file->{'sec'}, $file->{'file_length'});
foreach my $sec (@secs) {
next if $sec->{'trk'} == 0 && $sec->{'sec'} == 0;
#print "**** trk=$sec->{'trk'} sec=$sec->{'sec'}\n";
if (rts($dskfile, $sec->{'trk'}, $sec->{'sec'}, \$buf)) {
dump_sec($buf) if $debug;
#my @bytes = unpack "C$sec_size", $buf;
my @bytes = unpack "C*", $buf;
foreach my $byte (@bytes) {
# For text file translation.
last if $byte == 0x00 && $mode eq 'T';
# Translate \r to \n
$byte = 0x0a if $byte == 0x8d && $conv;
# Convert Apple II ASCII to standard ASCII (clear high bit)
$byte &= 0x7f if $mode eq 'T';
#print sprintf("%c", $byte & 0x7f);
print sprintf("%c", $byte);
}
##FIXME -- need to handle additional file types + handle incomplete last sectors properly here.
}
}
}
}
#
# Unlock a file
#
sub unlock_file {
my ($dskfile, $filename, $dbg) = @_;
$debug = 1 if (defined $dbg && $dbg);
my ($file, $cat_trk, $cat_sec, $cat_buf) = find_file($dskfile, $filename);
if (defined $file && $file && $file->{'trk'}) {
print "cat_trk=$cat_trk cat_sec=$cat_sec\n" if $debug;
dump_sec($cat_buf) if $debug;
my @bytes = unpack "C*", $cat_buf;
# 12 is number of bytes before file descriptive entries, 35 is length of file descriptive entry.
my $file_type = $bytes[13 + (($file->{'cat_offset'} - 1) * 35)];
print sprintf("cat_offset=%d\n", $file->{'cat_offset'}) if $debug;
print sprintf("file_type=%02x\n", $file_type) if $debug;
# Mark file as unlocked.
my $new_file_type = $bytes[13 + (($file->{'cat_offset'} - 1) * 35)] & 0x7f;
print sprintf("new_file_type=%02x\n", $new_file_type) if $debug;
$bytes[13 + (($file->{'cat_offset'} - 1) * 35)] = $new_file_type;
# Re-pack the data in the catalog sector.
$cat_buf = pack "a$sec_size", pack "C*", @bytes;
dump_sec($cat_buf) if $debug;
# Write back catalog sector.
if (!wts($dskfile, $cat_trk, $cat_sec, $cat_buf)) {
print "Failed to write catalog sector $cat_trk $cat_sec!\n";
}
}
}
#
# Lock a file
#
sub lock_file {
my ($dskfile, $filename, $dbg) = @_;
$debug = 1 if (defined $dbg && $dbg);
my ($file, $cat_trk, $cat_sec, $cat_buf) = find_file($dskfile, $filename);
if (defined $file && $file && $file->{'trk'}) {
print "cat_trk=$cat_trk cat_sec=$cat_sec\n" if $debug;
dump_sec($cat_buf) if $debug;
my @bytes = unpack "C*", $cat_buf;
# 12 is number of bytes before file descriptive entries, 35 is length of file descriptive entry.
my $file_type = $bytes[13 + (($file->{'cat_offset'} - 1) * 35)];
print sprintf("cat_offset=%d\n", $file->{'cat_offset'}) if $debug;
print sprintf("file_type=%02x\n", $file_type) if $debug;
# Mark file as locked.
my $new_file_type = $bytes[13 + (($file->{'cat_offset'} - 1) * 35)] | 0x80;
print sprintf("new_file_type=%02x\n", $new_file_type) if $debug;
$bytes[13 + (($file->{'cat_offset'} - 1) * 35)] = $new_file_type;
# Re-pack the data in the sector.
$cat_buf = pack "a$sec_size", pack "C*", @bytes;
dump_sec($cat_buf) if $debug;
# Write back catalog sector.
if (!wts($dskfile, $cat_trk, $cat_sec, $cat_buf)) {
print "Failed to write catalog sector $cat_trk $cat_sec!\n";
}
}
}
#
# Delete a file
#
sub delete_file {
my ($dskfile, $filename, $dbg) = @_;
$debug = 1 if (defined $dbg && $dbg);
my ($file, $cat_trk, $cat_sec, $cat_buf) = find_file($dskfile, $filename);
if (defined $file && $file && $file->{'trk'}) {
print "cat_trk=$cat_trk cat_sec=$cat_sec\n" if $debug;
dump_sec($cat_buf) if $debug;
my @bytes = unpack "C*", $cat_buf;
# Mark file as deleted.
# 11 is first tslist sector track
my $first_tslist_sec_trk = $bytes[11 + (($file->{'cat_offset'} - 1) * 35)];
print sprintf("first_tslist_sec_trk=%02x\n", $first_tslist_sec_trk) if $debug;
$bytes[11 + (($file->{'cat_offset'} - 1) * 35)] = 0x00;
# Set last byte of filename to first tslist sector track
$bytes[43 + (($file->{'cat_offset'} - 1) * 35)] = $first_tslist_sec_trk;
# Re-pack the data in the sector.
$cat_buf = pack "a$sec_size", pack "C*", @bytes;
dump_sec($cat_buf) if $debug;
# Write back catalog sector.
if (!wts($dskfile, $cat_trk, $cat_sec, $cat_buf)) {
print "Failed to write catalog sector $cat_trk $cat_sec!\n";
}
my ($trk_num_1st_cat_sec, $sec_num_1st_cat_sec, $rel_num_dos, $dsk_vol_num, $max_tslist_secs, $last_trk_secs_alloc, $dir_trk_alloc, $num_trks_dsk, $num_secs_dsk, $num_bytes_sec, $bit_map_free_secs) = get_vtoc_sec($dskfile);
my $tmpl = '';
for (my $t = $min_trk; $t <= $max_trk; $t++) {
$tmpl .= $bit_map_free_sec_tmpl;
}
print "tmpl=$tmpl\n" if $debug;
my @flds = unpack $tmpl, $bit_map_free_secs;
if ($debug) {
for (my $t = $min_trk; $t <= $max_trk; $t++) {
print sprintf("%2d %04x\n", $t, $flds[$t]) if $debug;
print sprintf("%2d %016b\n", $t, $flds[$t]) if $debug;
my $fr = sprintf("%016b", $flds[$t]);
print "fr=$fr\n" if $debug;
my $fm = reverse $fr;
print "fm=$fm\n" if $debug;
$fm =~ s/0/ /g;
$fm =~ s/1/*/g;
print "fm=$fm\n" if $debug;
print sprintf("%2d|%s\n", $t, $fm);
}
}
# get the files t/s list and free those sectors
my @secs = get_tslist($dskfile, $file->{'trk'}, $file->{'sec'}, $file->{'file_length'});
foreach my $sec (@secs) {
next if $sec->{'trk'} == 0 && $sec->{'sec'} == 0;
print "Freeing trk=$sec->{'trk'} sec=$sec->{'sec'}\n";
my $fr = sprintf("%016b", $flds[$sec->{'trk'}]);
#print "fr=$fr\n";
$flds[$sec->{'trk'}] |= (1 << $sec->{'sec'});
my $fr2 = sprintf("%016b", $flds[$sec->{'trk'}]);
#print "fr=$fr2\n";
}
##FIXME -- may need to free additional tslist sectors.
$flds[$file->{'trk'}] |= (1 << $file->{'sec'});
if ($debug) {
for (my $t = $min_trk; $t <= $max_trk; $t++) {
print sprintf("%2d %04x\n", $t, $flds[$t]) if $debug;
print sprintf("%2d %016b\n", $t, $flds[$t]) if $debug;
my $fr = sprintf("%016b", $flds[$t]);
print "fr=$fr\n" if $debug;
my $fm = reverse $fr;
print "fm=$fm\n" if $debug;
$fm =~ s/0/ /g;
$fm =~ s/1/*/g;
print "fm=$fm\n" if $debug;
print sprintf("%2d|%s\n", $t, $fm);
}
}
$bit_map_free_secs = pack $tmpl, @flds;
# Write back vtoc
if (!write_vtoc($dskfile, $trk_num_1st_cat_sec, $sec_num_1st_cat_sec, $rel_num_dos, $dsk_vol_num, $max_tslist_secs, $last_trk_secs_alloc, $dir_trk_alloc, $num_trks_dsk, $num_secs_dsk, $num_bytes_sec, $bit_map_free_secs)) {
print "I/O ERROR!\n";
}
}
}
#
# Rename a file
#
sub rename_file {
my ($dskfile, $filename, $new_filename, $dbg) = @_;
$debug = 1 if (defined $dbg && $dbg);
if (length($new_filename) > 30) {
print "Filename $new_filename too long\n";
return;
}
my ($file, $cat_trk, $cat_sec, $cat_buf) = find_file($dskfile, $filename);
if (defined $file && $file && $file->{'trk'}) {
print "cat_trk=$cat_trk cat_sec=$cat_sec\n" if $debug;
dump_sec($cat_buf) if $debug;
my @bytes = unpack "C*", $cat_buf;
my $fname_start = 14 + (($file->{'cat_offset'} - 1) * 35);
print sprintf("fname_start=%02x\n", $fname_start) if $debug;
# Change filename
for (my $i = 0; $i < length($new_filename); $i++) {
# Set the high bit
$bytes[$fname_start + $i] = ord(substr($new_filename, $i, 1)) | 0x80;
}
# Make sure new filename is space padded
for (my $i = length($new_filename); $i < 30; $i++) {
# 0xa0 is Apple II space (high bit set)
$bytes[$fname_start + $i] = 0xa0;
}
# Re-pack the data in the catalog sector.
$cat_buf = pack "a$sec_size", pack "C*", @bytes;
dump_sec($cat_buf) if $debug;
# Write back catalog sector.
if (!wts($dskfile, $cat_trk, $cat_sec, $cat_buf)) {
print "Failed to write catalog sector $cat_trk $cat_sec!\n";
}
}
}
#
# Find empty file descriptive entry for writing a file.
#
sub find_empty_file_desc_ent {
my ($dskfile, $dbg) = @_;
$debug = 1 if (defined $dbg && $dbg);
my ($trk_num_1st_cat_sec, $sec_num_1st_cat_sec, $rel_num_dos, $dsk_vol_num, $max_tslist_secs, $last_trk_secs_alloc, $dir_trk_alloc, $num_trks_dsk, $bit_map_free_secs) = get_vtoc_sec($dskfile);
if (defined $trk_num_1st_cat_sec && $trk_num_1st_cat_sec ne '') {
my ($next_cat_trk, $next_cat_sec) = ($trk_num_1st_cat_sec, $sec_num_1st_cat_sec);
my $cat_buf;
my @files = ();
my $empty_file_entry;
do {
my $cur_cat_trk = $next_cat_trk;
my $cur_cat_sec = $next_cat_sec;
($cat_buf, $next_cat_trk, $next_cat_sec, $empty_file_entry, @files) = get_cat_sec($dskfile, $next_cat_trk, $next_cat_sec);
return ($cat_buf, $cur_cat_trk, $cur_cat_sec, $empty_file_entry) if $empty_file_entry > 0;
} while ($next_cat_trk != 0);
} else {
print "I/O ERROR!\n";
return 0;
}
print "DISK FULL!\n";
return 0;
}
#
# Get a list of free sectors
#
sub find_free_sectors {
my ($dskfile, $dbg) = @_;
$debug = 1 if (defined $dbg && $dbg);
my ($trk_num_1st_cat_sec, $sec_num_1st_cat_sec, $rel_num_dos, $dsk_vol_num, $max_tslist_secs, $last_trk_secs_alloc, $dir_trk_alloc, $num_trks_dsk, $num_secs_dsk, $num_bytes_sec, $bit_map_free_secs) = get_vtoc_sec($dskfile);
my @secs = ();
my $tmpl = '';
for (my $t = $min_trk; $t <= $max_trk; $t++) {
$tmpl .= $bit_map_free_sec_tmpl;
}
print "tmpl=$tmpl\n" if $debug;
my @flds = unpack $tmpl, $bit_map_free_secs;
for (my $t = $min_trk; $t <= $max_trk; $t++) {
for (my $s = 0; $s < 16; $s++) {
if ($flds[$t] & 1 << $s) {
print "Free $t $s\n" if $debug;
push @secs, { 'trk' => $t, 'sec' => $s };
}
}
}
return @secs;
}
#
# Copy a file
#
sub copy_file {
my ($dskfile, $filename, $new_filename, $dbg) = @_;
$debug = 1 if (defined $dbg && $dbg);
my ($file, $cat_trk, $cat_sec, $cat_buf) = find_file($dskfile, $filename);
if (defined $file && $file && $file->{'trk'}) {
##FIXME
}
}
#
# Write a file to disk image.
#
sub write_file {
my ($dskfile, $filename, $new_filename, $mode, $conv, $dbg) = @_;
$debug = 1 if (defined $dbg && $dbg);
$debug = 1;
# Find empty catalog file descriptive entry.
my ($cat_buf, $cat_trk, $cat_sec, $empty_file_entry) = find_empty_file_desc_ent($dskfile);
print "cat_trk=$cat_trk cat_sec=$cat_sec empty_file_entry=$empty_file_entry\n" if $debug;
if ($empty_file_entry) {
# Find free sectors.
my @used_secs = ();
my @free_secs = find_free_sectors($dskfile, $debug);
if (scalar @free_secs) {
my $sectors_used = 0;
my $buf;
# Read input file a sector worth at a time.
my $file_length = 0;
my $ifh;
if (open($ifh, "<$filename")) {
my $done = 0;
my $error = 0;
while (! $done) {
# Initialize sector buffer.
$buf = pack "C*", 0x00 x $sec_size;
# Read a sectors worth of data.
my $bytes_read = read($ifh, $buf, $sec_size);
print "Read $bytes_read bytes\n" if $debug;
if ($bytes_read < $sec_size) {
# Last sector
$done = 1;
}
# Keep track of file size.
$file_length += $bytes_read;
# Pop a sector from the free sector list.
my $next_sec;
if (scalar @free_secs) {
$next_sec = pop @free_secs;
print "Next free sector is trk $next_sec->{'trk'} sec $next_sec->{'sec'}\n" if $debug;
# Push it onto the used sector list.
push @used_secs, { 'trk' => $next_sec->{'trk'}, 'sec' => $next_sec->{'sec'} };
# Write the data to the next sector.
print "Writing trk $next_sec->{'trk'} sec $next_sec->{'sec'}\n" if $debug;
if (!wts($dskfile, $next_sec->{'trk'}, $next_sec->{'sec'}, $buf)) {
print "Failed to write sector $next_sec->{'trk'} $next_sec->{'sec'}!\n";
}
$sectors_used++;
} else {
# Disk full.
print "DISK FULL!\n";
$error = 1;
$done = 1;
}
}
print sprintf("sectors_used=%04x\n", $sectors_used);
print sprintf("num_used_secs=%d\n", scalar @used_secs);
# Number of tslists is number of sectors used / 121.
my $num_tslists = ceil($sectors_used / 121);
print "Need $num_tslists tslist sector(s)\n" if $debug;
# Create t/s list(s).
my $first_tslist_trk = 0;
my $first_tslist_sec = 0;
my @tslist_secs = ();
my $cur_tslist = 1;
for (my $ts = 0; $ts < $num_tslists; $ts++) {
my $next_sec;
if (scalar @free_secs) {
$next_sec = pop @free_secs;
print "Next free sector is trk $next_sec->{'trk'} sec $next_sec->{'sec'}\n" if $debug;
if ($cur_tslist++ == 1) {
$first_tslist_trk = $next_sec->{'trk'};
$first_tslist_sec = $next_sec->{'sec'};
}
print "Writing tslist $ts\n" if $debug;
my $next_tslist_trk = 0x00;
my $next_tslist_sec = 0x00;
if ($ts < $num_tslists) {
$next_tslist_trk = $free_secs[0]->{'trk'};
$next_tslist_sec = $free_secs[0]->{'sec'};
}
my $soffset = pack "CCC", (0x00, 0x00, 0x00);
if ($ts > 0) {
# Calculate soffset for this sector.
my $off = $ts * 121;
my $of1 = ($off & 0xff0000) >> 16;
my $of2 = ($off & 0x00ff00) >> 8;
my $of3 = $off & 0x0000ff;
$soffset = pack "CCC", ($of1, $of2, $of3);
}
# Make data for this tslist sector -- hunks of 121 t/s pairs.
#print sprintf("\*\*\*\* num_used_secs=%d\n", scalar @used_secs);
#my @temp_used_secs = map { [@$_] } @used_secs;
#my @tsl = splice @temp_used_secs, ($ts * 121), 121;
#print sprintf("\*\*\*\* num_used_secs=%d\n", scalar @used_secs);
#print sprintf("num_secs_in_tsl_tsl=%d\n", scalar @tsl);
my @tsl = ();
for (my $cur_ts = $ts * 121; $cur_ts < ($ts * 121) + 121; $cur_ts++) {
print "cur_ts=$cur_ts\n";
last if $cur_ts > scalar @used_secs;
print "tsl trk $used_secs[$cur_ts]->{'trk'} sec $used_secs[$cur_ts]->{'sec'}\n";
push @tsl, { 'trk' => $used_secs[$cur_ts]->{'trk'}, 'sec' => $used_secs[$cur_ts]->{'sec'} };
}
my $tslst_fmt = 'C' x 242;
my $tslist = pack $tslst_fmt, @tsl;
my $tslist_buf = pack "a$sec_size", pack $tslist_fmt_tmpl, ($next_tslist_trk, $next_tslist_sec, $soffset, $tslist);
print "Writing tslist\n" if $debug;
if (!wts($dskfile, $next_sec->{'trk'}, $next_sec->{'sec'}, $tslist_buf)) {
print "Failed to write catalog sector $cat_trk $cat_sec!\n";
}
push @tslist_secs, { 'trk' => $next_sec->{'trk'}, 'sec' => $next_sec->{'sec'} };
} else {
print "DISK FULL!\n";
return;
}
}
print "first tslist trk $first_tslist_trk sec $first_tslist_sec\n" if $debug;
dump_sec($cat_buf) if $debug;
my @bytes = unpack "C*", $cat_buf;
# Create file descriptive entry in catalog.
# Set first tslist track.
$bytes[11 + (($empty_file_entry - 1) * 35)] = $first_tslist_trk;
# Set first tslist sector.
$bytes[12 + (($empty_file_entry - 1) * 35)] = $first_tslist_sec;
# Handle file type.
my $file_type = 0x00; # Default T
if ($mode eq "I") {
$file_type = 0x01;
} elsif ($mode eq "A") {
$file_type = 0x02;
} elsif ($mode eq "B") {
$file_type = 0x04;
}
# Set file type
$bytes[13 + (($empty_file_entry - 1) * 35)] = $file_type;
# Handle Filename
my $fname_start = 14 + (($empty_file_entry - 1) * 35);
print sprintf("fname_start=%02x\n", $fname_start) if $debug;
# Put in the filename
for (my $i = 0; $i < length($filename); $i++) {
# Set the high bit
$bytes[$fname_start + $i] = ord(substr($filename, $i, 1)) | 0x80;
}
# Make sure new filename is space padded
for (my $i = length($filename); $i < 30; $i++) {
# 0xa0 is Apple II space (high bit set)
$bytes[$fname_start + $i] = 0xa0;
}
print sprintf("sectors_used=%04x\n", $sectors_used);
my $file_secs_lo = $sectors_used & 0x00ff;
print sprintf("file_secs_lo=%02x\n", $file_secs_lo);
my $file_secs_hi = ($sectors_used & 0xff00) >> 8;
print sprintf("file_secs_hi=%02x\n", $file_secs_hi);
print sprintf("num_used_secs=%d\n", scalar @used_secs);
# Set file length in sectors.
my $file_length_secs = ceil($file_length / $sec_size);
$bytes[44 + (($empty_file_entry - 1) * 35)] = $file_secs_lo;
$bytes[45 + (($empty_file_entry - 1) * 35)] = $file_secs_hi;
# Re-pack the data in the catalog sector.
$cat_buf = pack "a$sec_size", pack "C*", @bytes;
dump_sec($cat_buf);
# Write back catalog sector with new file descriptive entry.
print "Writing catalog sector $cat_trk $cat_sec\n" if $debug;
if (!wts($dskfile, $cat_trk, $cat_sec, $cat_buf)) {
print "Failed to write catalog sector $cat_trk $cat_sec!\n";
}
print sprintf("num_used_secs=%d\n", scalar @used_secs);
# Mark sectors used.
my ($trk_num_1st_cat_sec, $sec_num_1st_cat_sec, $rel_num_dos, $dsk_vol_num, $max_tslist_secs, $last_trk_secs_alloc, $dir_trk_alloc, $num_trks_dsk, $num_secs_dsk, $num_bytes_sec, $bit_map_free_secs) = get_vtoc_sec($dskfile);
my $tmpl = '';
for (my $t = $min_trk; $t <= $max_trk; $t++) {
$tmpl .= $bit_map_free_sec_tmpl;
}
print "tmpl=$tmpl\n" if $debug;
my @flds = unpack $tmpl, $bit_map_free_secs;
print sprintf("num_used_secs=%d\n", scalar @used_secs);
# Mark sectors used
foreach my $sec (@used_secs) {
#next unless defined $sec;
next unless defined $sec->{'trk'};
#next if $sec->{'trk'} == 0 && $sec->{'sec'} == 0;
print "Marking trk $sec->{'trk'} sec $sec->{'sec'} used\n" if $debug;
$flds[$sec->{'trk'}] &= ~(1 << $sec->{'sec'});
}
# Mark tslist sectors used.
foreach my $sec (@tslist_secs) {
print "Marking tslist sector used trk $sec->{'trk'} sec $sec->{'sec'}\n" if $debug;
$flds[$sec->{'trk'}] &= ~(1 << $sec->{'sec'});
}
$bit_map_free_secs = pack $tmpl, @flds;
# Write back vtoc
if (!write_vtoc($dskfile, $trk_num_1st_cat_sec, $sec_num_1st_cat_sec, $rel_num_dos, $dsk_vol_num, $max_tslist_secs, $last_trk_secs_alloc, $dir_trk_alloc, $num_trks_dsk, $num_secs_dsk, $num_bytes_sec, $bit_map_free_secs)) {
print "I/O ERROR!\n";
}
close $ifh;
} else {
print "Can't open $filename\n";
}
} else {
print "DISK FULL\n";
}
} else {
print "DISK FULL\n";
}
}
#
# Undelete a file
#
sub undelete_file {
my ($dskfile, $filename, $dbg) = @_;
$debug = 1 if (defined $dbg && $dbg);
print "dskfile=$dskfile filename=$filename\n";
return 1;
}
#
# Initialize a disk image.
#
sub init {
my ($dskfile, $volume, $dbg) = @_;
$debug = 1 if (defined $dbg && $dbg);
print "dskfile=$dskfile volume=$volume\n";
return 1;
}
1;