MMB/SSD Utils in perl
A tar file with the files
GitHub repo. PRs welcome!
Last modified: Tue Mar 21 18:25:16 EDT 2023
Beeb Utilities to manipulate MMB and SSD files
Copyright (C) 2012-2015 Stephen Harris
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.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
===================================================================
HOW TO INSTALL
Something like
mkdir /usr/local/beeb
cd /usr/local/beeb
tar xvf ..../mmb_utils.tar
ln -s /usr/local/beeb/beeb /usr/local/bin/beeb
===================================================================
For all the commands that work on MMB files, the default MMB is called
"BEEB.MMB" but this can be changed with "-f otherfile" if used as the
first parameter, or by setting $BBC_FILE
So
beeb dinfo 2
would do a "*INFO" for all files in image 2 in BEEB.MMB
beeb dinfo -f other.MMB 2
would do a "*INFO" for all files in image 2 in other.MMB
export BBC_FILE=myfile.MMB
beeb dinfo 2
would do a "*INFO" for all files in image 2 in myfile.MMB
For greater control of the MMB file used, see later around $BEEB_UTILS_DFS
Other commands that work on normal files do not need "-f" as the first
option because they typically always require an filename (no default)
e.g
beeb info filename.ssd
beeb list MYPROG
SSD filenames may be entered as type:filename (eg stl:mydisk.ssd). See
the following section on BEEB_UTILS_DFS for the meaning of "type".
We never allow disk images in the MMB to be referred to by name; you
must always use the slot number.
One environment variable may be used:
BEEB_UTILS_DFS
This alters how the catalgue is read. The format is type:filename
Default type is Acorn
Default filename is BEEB.MMB
If there is no : in the value then it is read as a filename with type "acorn"
examples
stl:/mnt/beeb/beeb.mmb
/mnt/beeb/beeb.mmb
watford:
Allowed values for type are
SOLIDISK (or STL)
WATFORD
OPUS (or DDOS) ---- UNTESTED!!
DISKDOCTOR (or DISCDOCTOR)
ACORN (same as empty)
What this changes is how the "extra byte" information and extra catalogues
are read.
This value may also be set in $HOME/.beeb_utils_dfs
If the environment variable is set then it overrides the contents of the
file, thus a default may be set but overridden as necessary
Be careful if setting it wrong; eg if reading a Solidisk disk with
a deleted file in the first catalogue then you will see a file with &7F
as the directory if you're not in SOLIDISK mode!
===================================================================
MMB Impacting commands
===================================================================
* daccess
Does similar to *DLOCK and *DUNLOCK but using "*ACCESS" type syntax
lock disk 40: beeb daccess 40 L
unlock disk 40: beeb daccess 40
* dblank_mmb
Creates blank 128Mb MMB file
If an additional paramter is added (between 1 and 15) then this will
create an Extended MMB (see below) with that number of additional
catalogues. Note 15 additional catalogues will result in a 1.7Gb
file capable of handling 8176 disk images.
beeb dblank_mmb -f NEW.MMB 15
* dcat
Catalogue: Show disk images stored in the MMB. The -a flag will also
show unformatted disks
* dextend
Adds an additional catalogue to the MMB file, effectively adding an extra
511 disc slots to the image. Will convert a normal MMB to a Extended MMB
* dget_ssd
Extracts an SSD from an MMB
* dgetfile
Extracts all the files from an SSD stored in an MMB
A few characters are renamed to an _
:<>|`'/\*?"
to keep the extracted filename "sane". The leading "$." is removed
(other BBC directory names are retained).
If two files would be extracted with the same name then a -### count is
added to the end.
For each file a .inf is also created in the form
Filename LOAD EXEC [Locked] CRC=####
The Filename is the original BBC Filename.
This "inf" file is compatble to the bbcim extraction tool from
W.H.Scholten
* dform
Puts a blank SSD onto the MMB. Allows you to title the disk at the
same time
beeb dform 20 NewDisk
* dinfo
Does an effective *INFO *.* for an SSD stored in an MMB
(If the disk is multi-catalogue then the catalogue sector is also
shown)
* dkill
Marks a disk in a slot as Unformatted (*DKILL)
If the optional R flag is passed then it "restores" the image
(*DRESTORE).
beeb dkill 10
beeb dkill 10 R
A -y flag will force answer "yes" to override locked disks and won't
ask you if you're sure; use with care
beeb dkill -y 10
* dlabel
Changes the label for disk in slot that shows with with *DCAT
Note: this does not change the SSD title
* dmerge_mmb
Will merge multiple MMB files together to create an Extended MMB
beeb dmerge_mmb -f RESULTS.MMB part1.mmb part2.mmb part3.mmb
* dmmb_info
Report some basic statistics on the MMB file (number of extents,
number of disks, number unformatted)
* donboot
Shows the current "boot disk" settings, and lets you change them.
Will not let you set an unformatted disk unless -y flag is used
eg
beeb donboot -y 1 300
* dput_ssd
Writes an SSD to an MMB. You can't write into a slot that's in
use (so dkill it first if you want to replace it). The MMB
catalogue name for this disk is set to the SSDs name
* dreplace_mmb
Will replace a complete MMB image in an Extended MMB. This may
be useful if you have an Extended MMB built from multiple sources
(eg a Games image, a Z80 image, a PanOS image) and you want to
update just the Z80 image. It will update the catalogue and the
associated 511 disk images in the target Extended MMB with the
requested MMB
beeb dreplace_mmb new_z80.MMB 1
* dsplit_mmb
Will split a Extended MMB into a collection of single extent MMBs
numbered 0.MMB -> F.MMB. If an optional number is given then only
that extent will be extracted
% beeb dsplit_mmb RESULTS 13
Created RESULTS/D.MMB
* drecat
Performs similar to *DRECAT; for each disk marked as formatted
the title of the SSD will be read and the master catalogue updated.
===================================================================
SSD Impacting commands
===================================================================
Commands that update SSDs (eg delete, access, compact, putfile)
will not work on multi-catalogue disks. The update commands
are mostly meant to create new SSDs for putting into MMBs. Read commands
should work on multi-catalogues.
* access
*ACCESS equivalent
e.g.
beeb access mydisk.ssd A.*
beeb access mydisk.ssd B.* L
Defaults to '$.' if no directory specified.
Remember to quote shell characters if necessary!
* blank_ssd
Creates a new blank 200Kb SSD image
* compact
*COMPACT equivalent.
* delete
Deletes files from an SSD. e.g
beeb delete mydisk.ssd A.TESTFIL
Multiple filenames can be used. Directory defaults to '$'.
We attempt to handle BBC wildcards as well. e.g.
beeb delete mydisk.ssd E.*
It will ask a y/n question for each file before deleting.
Optional parameter "-y" will skip the asking and will, instead,
display the files deleted
beeb delete Foo/RAM_Manager_2.ssd -y E.*
Deleted E.OE00
Deleted E.ASM
Deleted E.CONVERT
Deleted E.SE00
Deleted E.E1770
Deleted E.EXMON
Remember you might need to quote on the command line to prevent the
shell doing filename expansion!
beeb delete Foo/dd.ssd \*
* getfile
Extracts all the files from an SSD
See dgetfile for details.
* info
Does an effective *INFO *.* for an SSD
* merge_dsd
Will take two SSD image and interleave them into a single DSD image
If the -concat option is used then it assumes the DSD is two SSDs
stacked after each other (side0 then side 2). Without this option
it assumes the disk tracks are interleaved (side0 track 0, side 2
track 0, side 0 track 1... etc). Interleaving is more normal.
* opt4
*OPT4 equivalent
* putfile
Puts all specified files onto an SSD.
beeb putfile myssd file1 file2 file3 file4...
Can use wildcards, eg
beeb putfile myssd mydir/*
This will attempt to read .inf files to work out load/exec/locked and
filename. If inf file doesn't exist then it'll use the name of the
file with load/exec values of 0.
e.g
% ls -l mydir
total 16
-rw-r--r-- 1 sweh sweh 6 Mar 20 21:12 Test1
-rw-r--r-- 1 sweh sweh 15 Mar 20 21:12 X.Test2
-rw-r--r-- 1 sweh sweh 13 Mar 20 21:13 foo
-rw-r--r-- 1 sweh sweh 39 Mar 20 21:13 foo.inf
We can see there are three files, but one has a .inf file
% cat mydir/foo.inf
$.FOO FF1900 FF8023 Locked CRC=AB7A
This means that "foo" is really called $.FOO
So lets add these to a new SSD
% beeb blank_ssd myssd
Blank myssd created
% beeb putfile myssd mydir/*
% beeb info myssd
Disk title: (1) Disk size: &320 - 200K
Boot Option: 0 (None) File count: 3
Filename: Lck Lo.add Ex.add Length Sct
$.FOO L FF1900 FF8023 00000D 004
X.Test2 000000 000000 00000F 003
$.Test1 000000 000000 000006 002
note that case was preserved, and Test1 was put into $.
There is an optional -c flag which will *COMPACT the disk before
adding files.
The SSD is only saved if all the files get added properly.
* rename
*RENAME
beeb rename myfile.ssd o.oldname n.newname
* split_dsd
Will take a DSD image and de-interleave it into two SSD images
If the -concat option is used then it assumes the DSD is two SSDs
stacked after each other (side0 then side 2). Without this option
it assumes the disk tracks are interleaved (side0 track 0, side 2
track 0, side 0 track 1... etc). Interleaving is more normal.
* title
Does an effective *TITLE for an SSD
* to_stdout
Will take an SSD and a filename on that disk and send it to stdout.
So could be used in a pipeline, such as:
beeb to_stdout mydisk.ssd '$.!BOOT' | beeb type -
===================================================================
Utility Programs (file commands)
===================================================================
* beeb
Simple wrapper so you can do "beeb dcat" or similar. In this
way you just need to symlink this "beeb" program into your PATH
(eg $HOME/bin) and that's it.
You may need to set the one variable in the file:
# $INSTALL_DIR="/where/you/installed/the/program";
if the program can't work out the symlink target properly
In good BBC style, a "." will act as a wildcard. So "beeb i."
might match "beeb info". If multiple commands might match then an
"Ambiguous" error message is returned.
* dump
*DUMP
* list
Lists a basic program.
"-o #" applies LISTO options
"-t XXX" uses the XXX decoder
basic2 == BASIC 2 (default)
basic4 == BASIC 4
z80 == BASIC 4 for Z80
arm == BASIC from the Arc
b4w == BASIC for Windows
eg
beeb list myfile -o 7
(Extra LISTO option "8" adds a space after each token)
If you want a prettier lister (eg html, colour etc) then bbclist
from W.H.Scholten produces nice output.
* type
*TYPE (converts BBC to Unix line endings)
===================================================================
Disk format
===================================================================
MMB consists of 32 sectors (8Kb) of data, split into 16 byte blocks.
The first block of 8 chars is is in the format
aa bb cc dd AA BB CC DD
where AAaa BBbb CCcc DDdd are the images inserted at boot time. Default
is 00 01 02 03 00 00 00 00
"*ONBOOT 3 500" would make dd=F4 DD=01 (&01F4=500)
The next 8 bytes (completing the 16 byte block) are unused and are
typically all zero (see "Extended MMB", below).
After that comes 511 blocks which consist of
DISKNAME(12 chars)
unused (3 chars)
STATUS(1 char) 0 ==> locked (readonly); 15=>Readwrite; 240=>Unformatted
255 => invalid
In theory the DISKNAME should match the disk TITLE in each place, but it
can get out of sync. *RECAT on the MMC Utils ROM reads the title from
each SSD and updates this name.
Then each SSD is a chunk of 200Kb data following. So disk 'n' starts
at 'n'*204800+8192. That's all there is to an MMB.
An Extended MMB is pretty much just multiple MMBs concatenated on top
of each other. This can extend the number of disks an MMB can hold in
steps of 511. A previously unused byte (offset 8) in the table header
indicates how many additional MMBs there are. It consists of 0xA# where
# is the number of additional entries.
In this format we have
<Disk table Header for disks 0-510>
<511 SSD images>
<Disk table Header for disks 511->1021>
<511 SSD images>
<Disk table Header for disks 1022->1532>
<511 SSD images>
up to a total of 15 additional headers, which results in disk numbers 0->8175.
If this form of MMB is used in a BBC which doesn't understand the extended
format then that machine should be able to read the first 511 disks, so
is backwards compatible.
An SSD is a simple 200K (80track 10 sector) image. Since it's literally
an image it follows the standard Acorn DFS layout.
Sector 0 is split into 32 * 8byte records. Record 0 is the first 8
characters of the disk title. Records 1->31 have filename (7 chars)
and directory(1 char). If the high bit of the directory is set then
the file is locked.
Sector 1 is similarly split into 32*8 bytes but a lot more complicated.
Bytes 0->3 are the last 4 characters of the disk title. The title is NULL
terminated if it's shorter than 12 chars.
Byte 4 is (BCD) the "write cycle". In theory every write should update
that, but really... who cares?
Byte 5 is "number of files"*8
Byte 6: bits 4 and 5 encode the "*OPT 4" value
bits 0 and 1 are the high bits for "number of sectors"
Byte 7 is the low 8 bits of "number of sectors"
Now for a "double density" disk, you need 11 bits to encode the disk
size, so Solidisk and others used byte 6 bit 2 (unused) for this.
This code will always assume this bit is part of the sector size.
See "Solidisk chained catalgues" (below) for a minor variation on this.
Now we have the remaining 31 records, which match file equivalent
files in sector 0. They are laid out like this:
LL LL EE EE SS SS XX YY
where "LLLL" is the load address, "EEEE" is the exec address, "SSSS" is
the size, and "YY" is the start sector. "XX" is the fun one.
bits 0+1 are high bits of sector start
bits 2+3 are high bits of load address
bits 4+5 are high bits of size
bits 6+7 are high bits of exec address
That makes 10 bits for start sector and 18 bits for size.
But on a 320K disk you need 11 bits for stat sector and 19 bits for size.
So Solidisk steals bits 2 and 3 ("load address"). bit 2 is added to the
sector, bit 3 to the size. This means we have now only have 8 bits for
the load address. So, by trial and error (and disk sector editing -
Solidisk DDFS comes with *DZAP :-)) I found that Solidisk reuses the
high bits of the "exec" address as the high bits of the "load" address.
Solidisk chained catalogues
---------------------------
Solidisk also has the concept of "chained catalogues". If
byte 2 & 192 == 192 then the low nibble of byte 2 and byte 3 point to
the next catalogue, where another 30 files might be stored.
So looking at a hex dump of the first 8 bytes of sector 1:
00000100 00 00 C0 21 35 F8 03 20 ...!5..
You'll notice &102 and &103 have odd values in them. We can see that
&102 AND 192 == 192 so &102 AND 63 and &103 point to the next
catalogue. In this case we have a second catalogue at sector &021.
Yes, this means disk titles are be limited to 10 characters.
And here it is:
00002100 3F 3F 3F 3F 3F 3F 3F 3F ????????
00002108 46 49 4C 45 36 30 20 24 FILE60 $
00002110 46 49 4C 45 35 39 20 24 FILE59 $
Obviously the "title" doesn't mean anything in secondary catalogues!
We note a funky entry in the 2nd catalogue:
000021f8 3F 3F 3F 3F 3F 3F 3F BF ???????.
and its associated data:
000022f8 00 00 00 00 00 21 00 02 .....!..
Basically this an "invisible" file that pretends to fill up the disk from
sector 2 to where this new catalogue starts. It's a cheap kludge so that
programs like *COMPACT can work on this catalogue without worrying about
what is before. Everything in earlier catalogues is now frozen. So what
happens if you delete or change a file in an earlier catalogue? That
value gets marked "deleted", thus:
000000a8 46 49 4C 45 31 30 20 FF FILE10 .
(Basically the directory is set to &FF). The space isn't reclaimed, but
DDFS now ignores it.
Looking, again at the 2nd catalogue, at the "data" sector:
00002200 00 21 C0 41 30 F8 03 20 .!.A0..
Oops, a third catalog at sector &41. This follows the same pattern, but
we get:
000041d0 46 49 4C 45 36 32 20 24 FILE62 $
000041d8 46 49 4C 45 36 31 20 24 FILE61 $
000041e0 3F 3F 3F 3F 3F 3F 3F BF ???????.
000041e8 3F 3F 3F 3F 3F 3F 3F 3F ????????
Ah ha, the locked ?.??????? entry is the locked file again, and all entries
after that are just ?.??????? because they haven't been used yet. This
means that the locked ?.??????? entry is a good "end of catalogue" marker
because it'll always be the first entry written (so the last in the sector)
when a 2ndary catalogue is created.
===================================================================
OPUS DDOS
===================================================================
Opus does things differently to everyone else. It's a little bizarre,
but...
Basically, for a double-density disk (only!) they split the drive into
sub-drives. Drive 0 is really split into drive 0A to 0H. Now some of
these drives may be of zero length and so empty. Each of these drives
can be a maximum of 252K in size (&3F0 sectors). See what they did
there? They avoided the whole problem of handling sector sizes over
2^10 long, or file sizes over 2^18 long so the catalogues are Acorn
standard. To add more space, the catalogues actually live outside
of the "drive", so files in the drive are allocated from sector zero.
Now an Opus disk is 18 sectors per track, and allocations are done
per track.
Track 0
Sector 0, 1 : catalogue A
Sector 2, 3 : catalogue B
Sector 4, 5 : catalogue C
Sector 6, 7 : catalogue D
Sector 8, 9 : catalogue E
Sector 10,11 : catalogue F
Sector 12,13 : catalogue G
Sector 14,15 : catalogue H
Sector 16 : Disk allocation table
Sector 17 : unused
Tracks 1 to 79 are the data.
The disk allocation table is small. http://beebwiki.jonripley.com/Opus_DDOS
has some details, but #4 doesn't seem to match. We see that bytes 1,2,3,4
allow for different format of disks (eg 40 track, 35 track) and even
different densities. But the standard 80-track double density disk will
be as follows:
byte 0: &20
byte 1,2: disk size (&5A0)
byte 3: sector per track (&12)
byte 4: &50 (but I've also seen &FF in testing!)
byte 8, 9: track start for A
byte 10,11: track start for B
byte 12,13: track start for C
byte 14,15: track start for D
byte 16,17: track start for E
byte 18,19: track start for F
byte 20,21: track start for G
byte 22,23: track start for H
So
00001000 20 05 A0 12 50 00 00 00 ...P...
00001008 01 00 39 00 00 00 00 00 ..9.....
00001010 00 00 00 00 00 00 00 00 ........
00001018 00 00 00 00 00 00 00 00 ........
We have two disks (A, starting at track 1, and B, starting at track &39).
The other disks are not defined.
This is wasteful of data (a whole unused sector!) but given we have 360K
of data rather than 200K of the original Acorn format, I guess Opus figured
this was a fair trade off.
Changelog
=========
2015/05/30 - Make opt4 a function (lazy lazy; it should always have been!)
- split add_file_to_ssd into a function that loads inf as
necessary and into add_content_to_ssd()
add_content_to_ssd($image,$fname,$data,$load,$exec,$locked);
e.g.
add_content_to_ssd($image,"!BOOT","*BASIC\r",0,0,1);
2015/06/01 - Add some binmode() calls to let this work better on Windows
- safety check on $ENV{HOME} since Windows doesn't define this
2015/07/11 - merge_dsd program added
2018/09/15 - Change FindBin::Bin to FindBin::RealBin to handle relative
symlinks. Thanks to Ray Bellis
2019/09/06 - Allow BBC_FILE to specify the MMB file
2019/09/17 - Strip out bad characers in titles in MMB catalogue
2020/10/03 - Add drecat
2020/11/11 - Add -concat options to split_dsd/merge_dsd
2021/08/26 - Allow for "-" to be specified as an SSD name eg for things like
unzip -q -c myssd.zip | beeb info -
Add new command "to_stdout"
2021/09/28 - Allow "-a" for dcat to show all disk slots, even unformatted ones
Add the ability to handle Extended MMBs
Add dextend to add an extent to an MMB
Add dmmb_info to report on some basic MMB statistics
Added dsplit_mmb
Added dmerge_mmb
Added dreplace_mmb
2021/09/30 - Added dbase command and report BASE value in dmmb_info
2021/10/02 - Allow dmmb_info to report on current base MMB onboot settings
Fix critical offset calculation error for first disk in each
extent
Add bdiag
Fix blank_mmb image size so extensions start correctly blank
Have put_ssd abort if request disk is out of range
2023/03/21 - Handle z80 basic and basic 4 windows; they code lines differently