diff --git a/README.md b/README.md index 8b614d21..35340c75 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ Portable emulator of an Apple II+ or //e. Written in Go. - Mouse support. No mouse capture needed - Adjustable speed - Fast disk mode to set max speed while using the disks + - Save directory with `-saveDir`: what the software writes goes to an overlay file per disk, keeping the images untouched and making writable the disks loaded from a compressed file, an URL or the embedded resources - Single file executable with embedded ROMs and DOS 3.3 - Pause (thanks a2geek) - Passes the [A2AUDIT 1.06](https://github.com/zellyn/a2audit) tests as II+, //e, and //e Enhanced. diff --git a/cardBuilder.go b/cardBuilder.go index 6ac1d2e8..0307356d 100644 --- a/cardBuilder.go +++ b/cardBuilder.go @@ -26,6 +26,32 @@ type cardBuilder struct { const noCardName = "empty" +const saveDirParamName = "savedir" + +/* +saveDirParamSpec is declared by the cards that write to their disk images, so +that the changes can be kept apart and the images left alone. + +Left empty, the card uses the save directory of the machine, the saveDir +command line option. Set to 'none' it writes back to the image even when the +machine has one. +*/ +var saveDirParamSpec = paramSpec{ + saveDirParamName, + "directory to keep what is written to the disks of this card, leaving the images unmodified. Defaults to the saveDir of the machine, 'none' to write back to the images", + "", +} + +// paramsGetSaveDir returns the directory a card keeps the changes to its disks +// in, empty when they go back to the images +func paramsGetSaveDir(params map[string]string) string { + value := paramsGetPath(params, saveDirParamName) + if value == "none" { + return "" + } + return value +} + var commonParams = []paramSpec{ {"trace", "Enable debug messages", "false"}, {"tracess", "Trace softswitches", "false"}, @@ -91,7 +117,9 @@ func (cb *cardBuilder) fullDefaultParams() map[string]string { return finalParams } -func setupCard(a *Apple2, slot int, paramString string) (Card, error) { +// setupCard builds the card of a slot. saveDir is the one of the machine, used +// by the cards that write to their disk images and do not name one themselves. +func setupCard(a *Apple2, slot int, paramString string, saveDir string) (Card, error) { actualArgs := splitConfigurationString(paramString, ',') cardName := actualArgs[0] @@ -126,6 +154,12 @@ func setupCard(a *Apple2, slot int, paramString string) (Card, error) { } } + // A card that writes to its disks and does not name a save directory of its + // own uses the one of the machine + if value, ok := finalParams[saveDirParamName]; ok && value == "" { + finalParams[saveDirParamName] = saveDir + } + card, err := builder.buildFunc(finalParams) if err != nil { return nil, err diff --git a/cardDisk2.go b/cardDisk2.go index fb20e29f..3815b08b 100644 --- a/cardDisk2.go +++ b/cardDisk2.go @@ -44,6 +44,11 @@ type drive interface { } type cardDisk2Drive struct { + // saveDirectory keeps the changes written to the diskette apart from the + // image, when it is not empty. It comes from the savedir parameter of the + // card and is kept here for the diskettes inserted later on. + saveDirectory string + name string diskette storage.Diskette phases uint8 // q3, q2, q1 and q0 with q0 on the LSB. Magnets that are active on the stepper motor @@ -60,11 +65,16 @@ func newCardDisk2Builder() *cardBuilder { {"tracktracer", "Trace how the disk head moves between tracks", "false"}, {"fast", "Enable CPU burst when accessing the disk", "true"}, {"sectors13", "Use 13 sectors per track ROM", "false"}, + saveDirParamSpec, }, buildFunc: func(params map[string]string) (Card, error) { var c CardDisk2 c.sectors13 = paramsGetBool(params, "sectors13") + saveDirectory := paramsGetSaveDir(params) + c.drive[0].saveDirectory = saveDirectory + c.drive[1].saveDirectory = saveDirectory + disk1 := paramsGetPath(params, "disk1") if disk1 != "" { err := c.drive[0].insertDiskette(disk1) @@ -292,7 +302,7 @@ func (c *CardDisk2) processQ6Q7(in uint8) { } func (d *cardDisk2Drive) insertDiskette(name string) error { - diskette, err := LoadDiskette(name) + diskette, err := LoadDiskette(name, d.saveDirectory) if err != nil { return err } diff --git a/cardSmartport.go b/cardSmartport.go index 41eb9497..b3cc5895 100644 --- a/cardSmartport.go +++ b/cardSmartport.go @@ -43,15 +43,17 @@ func newCardSmartPortStorageBuilder() *cardBuilder { {"image8", "Disk image for unit 8", ""}, {"tracesp", "Trace SmartPort calls", "false"}, {"tracehd", "Trace image accesses", "false"}, + saveDirParamSpec, }, buildFunc: func(params map[string]string) (Card, error) { var c CardSmartPort c.trace = paramsGetBool(params, "tracesp") traceHD := paramsGetBool(params, "tracehd") + saveDirectory := paramsGetSaveDir(params) for i := 1; i <= 8; i++ { image := paramsGetPath(params, "image"+strconv.Itoa(i)) if image != "" { - err := c.LoadImage(image, traceHD) + err := c.LoadImage(image, traceHD, saveDirectory) if err != nil { return nil, err } @@ -70,14 +72,16 @@ func newCardProDOSBlockStorageBuilder() *cardBuilder { {"image1", "Disk image for unit 1", ""}, {"image2", "Disk image for unit 2", ""}, {"tracehd", "Trace image accesses", "false"}, + saveDirParamSpec, }, buildFunc: func(params map[string]string) (Card, error) { var c CardSmartPort traceHD := paramsGetBool(params, "tracehd") + saveDirectory := paramsGetSaveDir(params) for i := 1; i <= 8; i++ { image := paramsGetPath(params, "image"+strconv.Itoa(i)) if image != "" { - err := c.LoadImage(image, traceHD) + err := c.LoadImage(image, traceHD, saveDirectory) if err != nil { return nil, err } @@ -122,9 +126,10 @@ func (c *CardSmartPort) GetInfo() map[string]string { return info } -// LoadImage loads a disk image -func (c *CardSmartPort) LoadImage(filename string, trace bool) error { - device, err := NewSmartPortHardDisk(c, filename) +// LoadImage loads a disk image. An empty saveDirectory writes the changes back +// to the image. +func (c *CardSmartPort) LoadImage(filename string, trace bool, saveDirectory string) error { + device, err := NewSmartPortHardDisk(c, filename, saveDirectory) if err == nil { device.trace = trace c.devices = append(c.devices, device) diff --git a/configs/_base.cfg b/configs/_base.cfg index 7030d00c..346cb02f 100644 --- a/configs/_base.cfg +++ b/configs/_base.cfg @@ -8,6 +8,7 @@ ramworks: none nsc: none mods: tape: none +saveDir: none rgb: false romx: false chargenmap: 2e diff --git a/configuration.go b/configuration.go index 75c92f71..327969f8 100644 --- a/configuration.go +++ b/configuration.go @@ -36,6 +36,7 @@ const ( confRomx = "romx" confMods = "mods" confTape = "tape" + confSaveDir = "saveDir" confS0 = "s0" confS1 = "s1" @@ -232,6 +233,7 @@ func setupFlags(models *configurationModels, configuration *configuration) error confRgb: "emulate the RGB modes of the 80col RGB card for DHGR", confRomx: "emulate a RomX", confTape: "WAV file with a tape recording for the cassette input", + confSaveDir: "directory to keep what the software writes to the disks, leaving the images unmodified. 'none' to write back to the images", confS0: "slot 0 configuration.", confS1: "slot 1 configuration.", confS2: "slot 2 configuration.", @@ -306,7 +308,7 @@ func applyDiskAliases(filename string) string { // Returns true if the file is a diskette, false if it's a block device func classifyFile(filename string) bool { filename = applyDiskAliases(filename) - _, err := LoadDiskette(filename) + _, err := LoadDiskette(filename, "") return err == nil } @@ -435,7 +437,7 @@ func expandSlotConfiguration(configString string) (string, error) { } // Try to load as diskette - _, err := LoadDiskette(filename) + _, err := LoadDiskette(filename, "") if err == nil { diskettes = append(diskettes, part) // Keep original part (may have quotes) } else { diff --git a/doc/command_line.md b/doc/command_line.md index ba2eaf50..807ec16a 100644 --- a/doc/command_line.md +++ b/doc/command_line.md @@ -7,6 +7,7 @@ This guide explains how to configure the izapple2 emulator using command line op - [Basic Usage](#basic-usage) - [Models](#models) - [Slot Configuration](#slot-configuration) +- [Keeping the Disk Images Unmodified](#keeping-the-disk-images-unmodified) - [Simplified Filename Configuration](#simplified-filename-configuration) - [Reference](#reference) - [Examples](#examples) @@ -115,6 +116,41 @@ The default model (`2enh`) configures slots as follows: - **Slot 6:** Disk II with DOS 3.3 - **Slot 7:** Empty +## Keeping the Disk Images Unmodified + +When the software saves, the emulator writes to the disk it was given, in the file +it was loaded from. With `-saveDir` the changes go to an overlay file in that +directory instead, and the image is only ever read: + +```bash +izapple2 -saveDir ~/.izapple2/saves mygame.dsk +``` + +The overlay is named after the disk with `.ovl` added, and holds only the parts +the software wrote, so it is small: a few kilobytes for a diskette, and for a +32Mb hard disk only the blocks that were saved. Loading the same disk again puts +them back on top of the image. + +Because the image is only read, this also makes writable the disks that could +not be written at all: the ones inside a zip or a gzip, the ones loaded from an +URL, and the ones embedded in the executable. + +An overlay carries a checksum of the image it was made from and is refused if it +does not match, so it is never applied to a different disk that happens to have +the same name. + +The cards that write to their disks, `diskii`, `smartport` and `prodos`, take a +`savedir` parameter of their own. It defaults to the `-saveDir` of the machine, +and `none` writes back to the image even when the machine has one: + +```bash +# The hard disk keeps its saves apart, the diskette is written in place +izapple2 -saveDir ~/saves -s7 smartport,image1=big.hdv -s6 diskii,disk1=work.dsk,savedir=none +``` + +WOZ and NIB diskettes are read only by nature and never write, with or without a +save directory. + ## Simplified Filename Configuration ### Positional Filename Arguments @@ -254,6 +290,8 @@ Usage: izapple2 [file] slot 6 configuration. (default "diskii,disk1=/dos33.dsk") -s7 string slot 7 configuration. (default "empty") + -saveDir string + directory to keep what the software writes to the disks, leaving the images unmodified. 'none' to write back to the images (default "none") -showConfig show the calculated configuration and exit -speed string diff --git a/doc/usage.txt b/doc/usage.txt index 7dafbaf1..062f9fd5 100644 --- a/doc/usage.txt +++ b/doc/usage.txt @@ -39,6 +39,8 @@ Usage: izapple2 [file] slot 6 configuration. (default "diskii,disk1=/dos33.dsk") -s7 string slot 7 configuration. (default "empty") + -saveDir string + directory to keep what the software writes to the disks, leaving the images unmodified. 'none' to write back to the images (default "none") -showConfig show the calculated configuration and exit -speed string diff --git a/resources.go b/resources.go index ecd1c440..a086c3ea 100644 --- a/resources.go +++ b/resources.go @@ -8,6 +8,7 @@ import ( "io" "net/http" "os" + "path/filepath" "strings" "github.com/ivanizag/izapple2/storage" @@ -129,23 +130,42 @@ func LoadResource(filename string) ([]uint8, bool, error) { return data, writeable, nil } -// LoadDiskette returns a Diskette by detecting the format -func LoadDiskette(filename string) (storage.Diskette, error) { +/* +overlayFilename returns where to keep the changes a disk gets, or an empty +string when they go back to the image. + +With a save directory, the changes the software writes go to an overlay file +there instead of to the image, which is then never modified. It also makes +writable the disks that could not be written at all, the ones loaded from a +compressed file, an URL or the embedded resources. +*/ +func overlayFilename(filename string, saveDirectory string) string { + if saveDirectory == "" { + return "" + } + return filepath.Join(saveDirectory, filepath.Base(filename)+".ovl") +} + +// LoadDiskette returns a Diskette by detecting the format. An empty +// saveDirectory writes the changes back to the image. +func LoadDiskette(filename string, saveDirectory string) (storage.Diskette, error) { data, writeable, err := LoadResource(filename) if err != nil { return nil, err } - return storage.MakeDiskette(data, filename, writeable) + return storage.MakeDiskette(data, filename, writeable, + overlayFilename(filename, saveDirectory)) } // LoadDisketteFromBytes returns a Diskette from byte array (useful for WASM/browser file loading) func LoadDisketteFromBytes(data []byte, filename string, writeable bool) (storage.Diskette, error) { - return storage.MakeDiskette(data, filename, writeable) + return storage.MakeDiskette(data, filename, writeable, "") } -// LoadBlockDisk returns a BlockDisk -func LoadBlockDisk(filename string) (storage.BlockDisk, error) { +// LoadBlockDisk returns a BlockDisk. An empty saveDirectory writes the changes +// back to the image. +func LoadBlockDisk(filename string, saveDirectory string) (storage.BlockDisk, error) { filename = normalizeFilename(filename) // Try to open as a file @@ -156,15 +176,25 @@ func LoadBlockDisk(filename string) (storage.BlockDisk, error) { readOnly = true file, _ = os.OpenFile(filename, os.O_RDONLY, 0) } - if file != nil { - return storage.NewBlockDiskFile(file, readOnly) - } - // Load as a resource - data, _, err := LoadResource(filename) + var blockDisk storage.BlockDisk + if file != nil { + blockDisk, err = storage.NewBlockDiskFile(file, readOnly) + } else { + // Load as a resource + var data []uint8 + data, _, err = LoadResource(filename) + if err != nil { + return nil, err + } + blockDisk, err = storage.NewBlockDiskMemory(data) + } if err != nil { return nil, err } - return storage.NewBlockDiskMemory(data) + // With a save directory, the changes go to an overlay and the image is + // left as it is, even the ones opened read only + return storage.NewBlockDiskOverlay(blockDisk, + overlayFilename(filename, saveDirectory)) } diff --git a/setup.go b/setup.go index aa310884..46a19c14 100644 --- a/setup.go +++ b/setup.go @@ -80,11 +80,18 @@ func configure(configuration *configuration) (*Apple2, error) { return nil, err } + // Where the cards that write to their disks keep the changes, unless they + // name a directory of their own + saveDir := configuration.get(confSaveDir) + if saveDir == "none" { + saveDir = "" + } + // Add cards on the slots for i := range 8 { cardConfig := configuration.get(fmt.Sprintf("s%v", i)) if cardConfig != "" { - _, err := setupCard(&a, i, cardConfig) + _, err := setupCard(&a, i, cardConfig, saveDir) if err != nil { return nil, err } diff --git a/smartPortHardDisk.go b/smartPortHardDisk.go index 8cb3c340..883f04af 100644 --- a/smartPortHardDisk.go +++ b/smartPortHardDisk.go @@ -23,13 +23,14 @@ type SmartPortHardDisk struct { disk storage.BlockDisk } -// NewSmartPortHardDisk creates a new hard disk with the smartPort interface -func NewSmartPortHardDisk(host *CardSmartPort, filename string) (*SmartPortHardDisk, error) { +// NewSmartPortHardDisk creates a new hard disk with the smartPort interface. +// An empty saveDirectory writes the changes back to the image. +func NewSmartPortHardDisk(host *CardSmartPort, filename string, saveDirectory string) (*SmartPortHardDisk, error) { var d SmartPortHardDisk d.host = host d.filename = filename - hd, err := LoadBlockDisk(filename) + hd, err := LoadBlockDisk(filename, saveDirectory) if err != nil { return nil, err } diff --git a/storage/blockDisk.go b/storage/blockDisk.go index 5fb371ee..ea4f3adf 100644 --- a/storage/blockDisk.go +++ b/storage/blockDisk.go @@ -4,6 +4,7 @@ import ( "bytes" "errors" "fmt" + "hash/crc64" "io" "os" ) @@ -150,3 +151,78 @@ func (bd *blockDiskFile) Write(block uint32, data []uint8) error { func (bd *blockDiskMemory) Write(block uint32, data []uint8) error { return errors.New("can't write in a readonly disk") } + +/* +blockDiskOverlay sends the blocks written to an overlay, leaving the image +untouched, and reads back from it the ones already saved. The blocks are taken +one at a time as ProDOS asks for them, so a 32 Mb hard disk costs nothing to +open however few blocks a game saves. +*/ +type blockDiskOverlay struct { + base BlockDisk + overlay *overlay +} + +// NewBlockDiskOverlay wraps a block device so that the writes go to the +// overlay kept in overlayFilename instead of to the image. It also makes +// writable a device that was not, as the image is only read. An empty +// overlayFilename returns the device untouched. +func NewBlockDiskOverlay(base BlockDisk, overlayFilename string) (BlockDisk, error) { + if overlayFilename == "" { + return base, nil + } + + checksum, err := checksumOfBlockDisk(base) + if err != nil { + return nil, err + } + + o, err := openOverlay(overlayFilename, int(ProDosBlockSize), + int(base.GetSizeInBlocks()), checksum) + if err != nil { + return nil, err + } + return &blockDiskOverlay{base: base, overlay: o}, nil +} + +// checksumOfBlockDisk identifies the image an overlay was made from. It reads +// the whole device, which for a 32 Mb hard disk is the price of opening it +// once with the overlays in use. +func checksumOfBlockDisk(base BlockDisk) (uint64, error) { + hash := crc64.New(overlayChecksumTable) + for block := range base.GetSizeInBlocks() { + data, err := base.Read(block) + if err != nil { + return 0, err + } + hash.Write(data) + } + return hash.Sum64(), nil +} + +func (bd *blockDiskOverlay) GetSizeInBlocks() uint32 { + return bd.base.GetSizeInBlocks() +} + +func (bd *blockDiskOverlay) IsReadOnly() bool { + return false +} + +func (bd *blockDiskOverlay) Read(block uint32) ([]uint8, error) { + if !bd.overlay.has(int(block)) { + return bd.base.Read(block) + } + + buf := make([]uint8, ProDosBlockSize) + if err := bd.overlay.read(int(block), buf); err != nil { + return nil, err + } + return buf, nil +} + +func (bd *blockDiskOverlay) Write(block uint32, data []uint8) error { + if block >= bd.base.GetSizeInBlocks() { + return errors.New("disk block number is too big") + } + return bd.overlay.write(int(block), data) +} diff --git a/storage/diskette.go b/storage/diskette.go index 1a1ebce2..ea4f143e 100644 --- a/storage/diskette.go +++ b/storage/diskette.go @@ -18,8 +18,16 @@ func IsDiskette(data []byte) bool { return isFileNib(data) || isFileDsk(data) || isFileWoz(data) } -// MakeDiskette returns a Diskette by detecting the format -func MakeDiskette(data []byte, filename string, writeable bool) (Diskette, error) { +/* +MakeDiskette returns a Diskette by detecting the format. + +With an overlayFilename, the changes the software writes are kept in that file +and the image loaded is never modified. It also makes writable the diskettes +that could not be written otherwise, the ones loaded from a compressed file, an +URL or the embedded resources. An empty overlayFilename writes the changes back +to the image, which only works for the DSK and PO files on disk. +*/ +func MakeDiskette(data []byte, filename string, writeable bool, overlayFilename string) (Diskette, error) { if isFileD13(data) { return nil, errors.New("files with .d13 format are not supported for 13 sectors disk, use .nib or .woz") } @@ -31,9 +39,23 @@ func MakeDiskette(data []byte, filename string, writeable bool) (Diskette, error } if isFileDsk(data) { + // The checksum is of the image as it was loaded, before anything + // saved before is put back on top of it + overlay, err := openOverlay(overlayFilename, bytesPerTrack, numberOfTracks, + checksumOfBytes(data)) + if err != nil { + return nil, err + } + if overlay != nil { + if err := overlay.applyTo(data); err != nil { + return nil, err + } + } + var d disketteNibWritable d.nib = newFileDsk(data, filename) - d.nib.supportsWrite = d.nib.supportsWrite && writeable + d.nib.overlay = overlay + d.nib.supportsWrite = overlay != nil || (d.nib.supportsWrite && writeable) return &d, nil } diff --git a/storage/fileNib.go b/storage/fileNib.go index 1a73bb91..3f18f711 100644 --- a/storage/fileNib.go +++ b/storage/fileNib.go @@ -33,6 +33,9 @@ type fileNib struct { supportsWrite bool filename string logicalOrder *[16]int + + // When set, the changes go to the overlay and the image is left alone + overlay *overlay } func isFileNib(data []uint8) bool { @@ -78,26 +81,40 @@ func newFileDsk(data []uint8, filename string) *fileNib { } func (f *fileNib) saveTrack(track int) { - if f.supportsWrite { - file, err := os.OpenFile(f.filename, os.O_RDWR, 0) - if err != nil { - // We can't open the file for writing" - f.supportsWrite = false - fmt.Printf("Data can't be written for %v\n", f.filename) - } + if !f.supportsWrite { + return + } - data, err := nibDecodeTrack(f.track[track], f.logicalOrder) - if err != nil { - f.supportsWrite = false - fmt.Printf("Data written can't be decoded from nibbles\n") - } + data, err := nibDecodeTrack(f.track[track], f.logicalOrder) + if err != nil { + f.supportsWrite = false + fmt.Printf("Data written can't be decoded from nibbles\n") + return + } - offset := int64(track * bytesPerTrack) - _, err = file.WriteAt(data, offset) - if err != nil { + if f.overlay != nil { + // The changes are kept apart, the image loaded is not modified + if err := f.overlay.write(track, data); err != nil { f.supportsWrite = false - fmt.Printf("Data can't be written\n") + fmt.Printf("Data can't be written to the overlay: %v\n", err) } + return + } + + file, err := os.OpenFile(f.filename, os.O_RDWR, 0) + if err != nil { + // We can't open the file for writing + f.supportsWrite = false + fmt.Printf("Data can't be written for %v\n", f.filename) + return + } + defer file.Close() + + offset := int64(track * bytesPerTrack) + _, err = file.WriteAt(data, offset) + if err != nil { + f.supportsWrite = false + fmt.Printf("Data can't be written\n") } } diff --git a/storage/overlay.go b/storage/overlay.go new file mode 100644 index 00000000..5cf45d76 --- /dev/null +++ b/storage/overlay.go @@ -0,0 +1,210 @@ +package storage + +import ( + "encoding/binary" + "fmt" + "hash/crc64" + "os" +) + +/* +An overlay keeps the changes made to a disk apart from the image it was loaded +from, so that saving a game does not modify the file the user has. + +Only the parts the software writes are stored, indexed by a bitmap in the +header. Nothing is written for the rest, so the file has holes and takes on +disk as much as what was actually saved and not the size of the disk. That +matters for the hard disks, where an image can be 32 Mb and a saved game a +handful of blocks. + +The unit is whatever the disk writes at a time: a track of 4096 bytes for the +diskettes, a block of 512 bytes for the ProDOS devices. + +The header holds a checksum of the image the overlay was made from. An overlay +belongs to one image, and applying the one of another disk would corrupt it, so +a mismatch is refused instead of guessed. +*/ + +const ( + // overlayMagic is 24 bytes, the version is the last character + overlayMagic = "IZAPPLE2 DISK OVERLAY 2\n" + + // overlayHeaderSize leaves room after the magic for the shape of the disk + // and the checksum, and keeps the bitmap aligned + overlayHeaderSize = 64 +) + +var overlayChecksumTable = crc64.MakeTable(crc64.ECMA) + +type overlay struct { + filename string + unitSize int + units int + checksum uint64 + + // present has a bit set for each unit stored in the overlay + present []byte + + // file is kept open, opened on the first write. There is one overlay per + // disk in use, so the handles are few. + file *os.File +} + +/* +openOverlay returns the overlay kept in filename for a disk of the given shape +and content. An empty filename means no overlay. The file is not created until +something is written to it, and an overlay of another disk is refused. +*/ +func openOverlay(filename string, unitSize int, units int, checksum uint64) (*overlay, error) { + if filename == "" { + return nil, nil + } + + o := &overlay{ + filename: filename, + unitSize: unitSize, + units: units, + checksum: checksum, + present: make([]byte, (units+7)/8), + } + + file, err := os.Open(filename) + if os.IsNotExist(err) { + // Nothing saved yet + return o, nil + } + if err != nil { + return nil, err + } + defer file.Close() + + header := make([]byte, overlayHeaderSize) + if _, err := file.ReadAt(header, 0); err != nil { + return nil, fmt.Errorf("the overlay %s is damaged: %w", filename, err) + } + if string(header[:len(overlayMagic)]) != overlayMagic { + return nil, fmt.Errorf("%s is not a disk overlay", filename) + } + + storedUnitSize := binary.LittleEndian.Uint32(header[24:]) + storedUnits := binary.LittleEndian.Uint32(header[28:]) + storedChecksum := binary.LittleEndian.Uint64(header[32:]) + + if int(storedUnitSize) != unitSize || int(storedUnits) != units { + return nil, fmt.Errorf( + "the overlay %s was made for a disk of a different shape, delete it to start over", + filename) + } + if storedChecksum != checksum { + return nil, fmt.Errorf( + "the overlay %s belongs to another disk image, delete it to start over", + filename) + } + + if _, err := file.ReadAt(o.present, overlayBitmapOffset); err != nil { + return nil, fmt.Errorf("the overlay %s is damaged: %w", filename, err) + } + + return o, nil +} + +// has returns whether the overlay holds the unit +func (o *overlay) has(unit int) bool { + return o.present[unit/8]&(1< 64*1024 { + t.Errorf("the overlay of one block grew to %v bytes", info.Size()) + } +}