mirror of
https://github.com/ivanizag/izapple2.git
synced 2026-10-25 03:02:24 +00:00
The zip reader took the first entry recognised as a diskette, so a zip holding a hard disk image found nothing and the raw bytes of the zip were handed on as if they were the disk. Nothing recognises a hard disk by its contents, they are plain blocks, so with no diskette inside the biggest entry is taken now, which on the zip of a hard disk is the image and not the readme beside it. This is why the core declares zip among the extensions it takes even though the frontend unpacks archives itself: an izapple2 that reads its own zips has to read all of them.
231 lines
5.6 KiB
Go
231 lines
5.6 KiB
Go
package izapple2
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"embed"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/ivanizag/izapple2/storage"
|
|
)
|
|
|
|
const (
|
|
internalPrefix = "<internal>/"
|
|
embedPrefix = "resources/"
|
|
httpPrefix = "http://"
|
|
httpsPrefix = "https://"
|
|
)
|
|
|
|
//go:embed resources
|
|
var internalFiles embed.FS
|
|
|
|
func isInternalResource(filename string) bool {
|
|
return strings.HasPrefix(filename, internalPrefix)
|
|
}
|
|
|
|
func isHTTPResource(filename string) bool {
|
|
return strings.HasPrefix(filename, httpPrefix) ||
|
|
strings.HasPrefix(filename, httpsPrefix)
|
|
}
|
|
|
|
func normalizeFilename(filename string) string {
|
|
// Remove quotes if surrounded by them
|
|
if strings.HasPrefix(filename, "\"") && strings.HasSuffix(filename, "\"") {
|
|
filename = filename[1 : len(filename)-1]
|
|
}
|
|
|
|
// Expand the tilde if prefixed by it
|
|
if strings.HasPrefix(filename, "~") {
|
|
home, err := os.UserHomeDir()
|
|
if err == nil {
|
|
filename = home + filename[1:]
|
|
}
|
|
|
|
}
|
|
return filename
|
|
}
|
|
|
|
// LoadResource loads in memory a file from the filesystem, http or embedded
|
|
func LoadResource(filename string) ([]uint8, bool, error) {
|
|
filename = normalizeFilename(filename)
|
|
|
|
var writeable bool
|
|
var file io.Reader
|
|
if isInternalResource(filename) {
|
|
// load from embedded resource
|
|
resource := embedPrefix + strings.TrimPrefix(filename, internalPrefix)
|
|
resourceFile, err := internalFiles.Open(resource)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
defer resourceFile.Close()
|
|
file = resourceFile
|
|
writeable = false
|
|
|
|
} else if isHTTPResource(filename) {
|
|
response, err := http.Get(filename)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
defer response.Body.Close()
|
|
file = response.Body
|
|
writeable = false
|
|
|
|
} else {
|
|
diskFile, err := os.Open(filename)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
defer diskFile.Close()
|
|
file = diskFile
|
|
writeable = true
|
|
}
|
|
|
|
data, err := io.ReadAll(file)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
contentType := http.DetectContentType(data)
|
|
if contentType == "application/x-gzip" {
|
|
writeable = false
|
|
gz, err := gzip.NewReader(bytes.NewReader(data))
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
defer gz.Close()
|
|
data, err = io.ReadAll(gz)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
} else if contentType == "application/zip" {
|
|
writeable = false
|
|
z, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
data, err = pickFromZip(z)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
}
|
|
|
|
return data, writeable, nil
|
|
}
|
|
|
|
/*
|
|
pickFromZip returns the disk image of a zip file.
|
|
|
|
A diskette is taken as soon as it is recognised by its contents, which is what
|
|
tells a disk image from the readme or the box scan that often travels with it.
|
|
Nothing recognises a hard disk by its contents, they are plain blocks, so with
|
|
no diskette inside the biggest file is taken: on the zips of a hard disk image
|
|
that is the image.
|
|
*/
|
|
func pickFromZip(z *zip.Reader) ([]uint8, error) {
|
|
var biggest []uint8
|
|
for _, zf := range z.File {
|
|
if zf.FileInfo().IsDir() {
|
|
continue
|
|
}
|
|
|
|
f, err := zf.Open()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
content, err := io.ReadAll(f)
|
|
f.Close()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if storage.IsDiskette(content) {
|
|
return content, nil
|
|
}
|
|
if len(content) > len(biggest) {
|
|
biggest = content
|
|
}
|
|
}
|
|
|
|
if biggest == nil {
|
|
return nil, errors.New("the zip file is empty")
|
|
}
|
|
return biggest, nil
|
|
}
|
|
|
|
/*
|
|
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,
|
|
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, "")
|
|
}
|
|
|
|
// 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
|
|
readOnly := false
|
|
file, err := os.OpenFile(filename, os.O_RDWR, 0)
|
|
if os.IsPermission(err) {
|
|
// Retry in read-only mode
|
|
readOnly = true
|
|
file, _ = os.OpenFile(filename, os.O_RDONLY, 0)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 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))
|
|
}
|