mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2025-04-02 11:02:17 -04:00
318 lines
8 KiB
Go
318 lines
8 KiB
Go
// This file is part of Gopher2600.
|
|
//
|
|
// Gopher2600 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 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// Gopher2600 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 Gopher2600. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package cartridgeloader
|
|
|
|
import (
|
|
"crypto/sha1"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
|
|
"github.com/jetsetilly/gopher2600/curated"
|
|
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/mapper"
|
|
"github.com/jetsetilly/gopher2600/logger"
|
|
)
|
|
|
|
// Loader is used to specify the cartridge to use when Attach()ing to
|
|
// the VCS. it also permits the called to specify the mapping of the cartridge
|
|
// (if necessary. fingerprinting is pretty good).
|
|
type Loader struct {
|
|
// filename of cartridge to load.
|
|
Filename string
|
|
|
|
// empty string or "AUTO" indicates automatic fingerprinting
|
|
Mapping string
|
|
|
|
// expected hash of the loaded cartridge. empty string indicates that the
|
|
// hash is unknown and need not be validated. after a load operation the
|
|
// value will be the hash of the loaded data
|
|
//
|
|
// in the case of sound data (IsSoundData is true) then the hash is of the
|
|
// original binary file not he decoded PCM data
|
|
Hash string
|
|
|
|
// copy of the loaded data. subsequenct calls to Load() will return a copy
|
|
// of this data
|
|
Data []byte
|
|
|
|
// does the Data field consist of sound (PCM) data
|
|
IsSoundData bool
|
|
|
|
// callback function when cartridge has been successfully inserted/loaded.
|
|
// not all cartridge formats support this
|
|
//
|
|
// !!TODO: all cartridge formats to support OnLoaded() callback (for completeness)
|
|
OnLoaded func(cart mapper.CartMapper) error
|
|
|
|
// for some file types streaming is necessary.
|
|
//
|
|
// note that we have a pointer to a pointer of an os.File. this is so we
|
|
// can record a change of streamHandle even if the Loader{} struct is
|
|
// passed by value.
|
|
//
|
|
// tricky to handle but it works well for our use case.
|
|
streamHandle **os.File
|
|
}
|
|
|
|
// NewLoader is the preferred method of initialisation for the Loader type.
|
|
//
|
|
// The mapping argument will be used to set the Mapping field, unless the
|
|
// argument is either "AUTO" or the empty string. In which case the file
|
|
// extension is used to set the field.
|
|
//
|
|
// File extensions should be the same as the ID of the intended mapper, as
|
|
// defined in the cartridge package. The exception is the DPC+ format which
|
|
// requires the file extension "DP+"
|
|
//
|
|
// File extensions ".BIN" and "A26" will set the Mapping field to "AUTO".
|
|
//
|
|
// Alphabetic characters in file extensions can be in upper or lower case or a
|
|
// mixture of both.
|
|
func NewLoader(filename string, mapping string) Loader {
|
|
cl := Loader{
|
|
Filename: filename,
|
|
Mapping: "AUTO",
|
|
streamHandle: new(*os.File),
|
|
}
|
|
|
|
mapping = strings.TrimSpace(strings.ToUpper(mapping))
|
|
if mapping != "AUTO" && mapping != "" {
|
|
cl.Mapping = mapping
|
|
} else {
|
|
ext := strings.ToUpper(path.Ext(filename))
|
|
switch ext {
|
|
case ".BIN":
|
|
fallthrough
|
|
case ".ROM":
|
|
fallthrough
|
|
case ".A26":
|
|
cl.Mapping = "AUTO"
|
|
case ".2k":
|
|
fallthrough
|
|
case ".4k":
|
|
fallthrough
|
|
case ".F8":
|
|
fallthrough
|
|
case ".F6":
|
|
fallthrough
|
|
case ".F4":
|
|
fallthrough
|
|
case ".2k+":
|
|
fallthrough
|
|
case ".4k+":
|
|
fallthrough
|
|
case ".F8+":
|
|
fallthrough
|
|
case ".F6+":
|
|
fallthrough
|
|
case ".F4+":
|
|
fallthrough
|
|
case ".FA":
|
|
fallthrough
|
|
case ".FE":
|
|
fallthrough
|
|
case ".E0":
|
|
fallthrough
|
|
case ".E7":
|
|
fallthrough
|
|
case ".3F":
|
|
fallthrough
|
|
case ".AR":
|
|
fallthrough
|
|
case ".DF":
|
|
fallthrough
|
|
case ".3E":
|
|
fallthrough
|
|
case ".3E+":
|
|
fallthrough
|
|
case ".SB":
|
|
fallthrough
|
|
case ".DPC":
|
|
cl.Mapping = ext[1:]
|
|
case ".DP+":
|
|
cl.Mapping = "DPC+"
|
|
case "CDF":
|
|
cl.Mapping = "CDF"
|
|
case ".WAV":
|
|
fallthrough
|
|
case ".MP3":
|
|
cl.Mapping = "AR"
|
|
cl.IsSoundData = true
|
|
case ".MVC":
|
|
cl.Mapping = "MVC"
|
|
}
|
|
}
|
|
|
|
return cl
|
|
}
|
|
|
|
// FileExtensions is the list of file extensions that are recognised by the
|
|
// cartridgeloader package.
|
|
var FileExtensions = [...]string{".BIN", ".ROM", ".A26", ".2k", ".4k", ".F8", ".F6", ".F4", ".2k+", ".4k+", ".F8+", ".F6+", ".F4+", ".FA", ".FE", ".E0", ".E7", ".3F", ".AR", ".DF", "3E", "3E+", "SB", ".DPC", ".DP+", "CDF", ".WAV", ".MP3", ".MVC"}
|
|
|
|
// ShortName returns a shortened version of the CartridgeLoader filename.
|
|
func (cl Loader) ShortName() string {
|
|
// return the empty string if filename is undefined
|
|
if len(strings.TrimSpace(cl.Filename)) == 0 {
|
|
return ""
|
|
}
|
|
|
|
sn := path.Base(cl.Filename)
|
|
sn = strings.TrimSuffix(sn, path.Ext(cl.Filename))
|
|
return sn
|
|
}
|
|
|
|
// HasLoaded returns true if Load() has been successfully called.
|
|
func (cl Loader) HasLoaded() bool {
|
|
return len(cl.Data) > 0
|
|
}
|
|
|
|
// Load the cartridge data and return as a byte array. Loader filenames with a
|
|
// valid schema will use that method to load the data. Currently supported
|
|
// schemes are HTTP and local files.
|
|
func (cl *Loader) Load() error {
|
|
if cl.Mapping == "MVC" {
|
|
if (*cl.streamHandle) == nil {
|
|
var err error
|
|
*cl.streamHandle, err = os.Open(cl.Filename)
|
|
if err != nil {
|
|
return curated.Errorf("cartridgeloader: %v", err)
|
|
}
|
|
logger.Logf("cartridgeloader", "stream open (%s)", (*cl.streamHandle).Name())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if len(cl.Data) > 0 {
|
|
// !!TODO: already-loaded error?
|
|
return nil
|
|
}
|
|
|
|
scheme := "file"
|
|
|
|
url, err := url.Parse(cl.Filename)
|
|
if err == nil {
|
|
scheme = url.Scheme
|
|
}
|
|
|
|
switch scheme {
|
|
case "http":
|
|
fallthrough
|
|
case "https":
|
|
resp, err := http.Get(cl.Filename)
|
|
if err != nil {
|
|
return curated.Errorf("cartridgeloader: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
cl.Data, err = ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return curated.Errorf("cartridgeloader: %v", err)
|
|
}
|
|
|
|
case "file":
|
|
fallthrough
|
|
|
|
case "":
|
|
fallthrough
|
|
|
|
default:
|
|
f, err := os.Open(cl.Filename)
|
|
if err != nil {
|
|
return curated.Errorf("cartridgeloader: %v", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
// get file info. not using Stat() on the file handle because the
|
|
// windows version (when running under wine) does not handle that
|
|
cfi, err := os.Stat(cl.Filename)
|
|
if err != nil {
|
|
return curated.Errorf("cartridgeloader: %v", err)
|
|
}
|
|
size := cfi.Size()
|
|
|
|
cl.Data = make([]byte, size)
|
|
_, err = f.Read(cl.Data)
|
|
if err != nil {
|
|
return curated.Errorf("cartridgeloader: %v", err)
|
|
}
|
|
}
|
|
|
|
// generate hash
|
|
hash := fmt.Sprintf("%x", sha1.Sum(cl.Data))
|
|
|
|
// check for hash consistency
|
|
if cl.Hash != "" && cl.Hash != hash {
|
|
return curated.Errorf("cartridgeloader: %v", "unexpected hash value")
|
|
}
|
|
|
|
// not generated hash
|
|
cl.Hash = hash
|
|
|
|
return nil
|
|
}
|
|
|
|
func (cl *Loader) isStreaming() bool {
|
|
return cl.streamHandle != nil && *cl.streamHandle != nil
|
|
}
|
|
|
|
// Close ends a cartridge loader session.
|
|
func (cl *Loader) Close() error {
|
|
if cl.isStreaming() {
|
|
fn := (*cl.streamHandle).Name()
|
|
err := (*cl.streamHandle).Close()
|
|
if err != nil {
|
|
return curated.Errorf("cartridgeloader: %v", err)
|
|
}
|
|
*cl.streamHandle = nil
|
|
logger.Logf("cartridgeloader", "stream closed (%s)", fn)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Streamer exposes only the Stream() function for use.
|
|
type Streamer interface {
|
|
Stream(offset int64, buffer []byte) (int, error)
|
|
}
|
|
|
|
// Stream enough data to fill buffer from position offset.
|
|
func (cl Loader) Stream(offset int64, buffer []byte) (int, error) {
|
|
if !cl.isStreaming() {
|
|
return 0, curated.Errorf("cartridgeloader: stream: no stream open")
|
|
}
|
|
|
|
if _, err := (*cl.streamHandle).Seek(offset, io.SeekStart); err != nil {
|
|
return 0, curated.Errorf("cartridgeloader: stream: %v", err)
|
|
}
|
|
|
|
n, err := (*cl.streamHandle).Read(buffer)
|
|
if err != nil {
|
|
if err != io.EOF {
|
|
return 0, curated.Errorf("cartridgeloader: stream: %v", err)
|
|
}
|
|
}
|
|
|
|
if n > 0 && n != len(buffer) {
|
|
return 0, curated.Errorf("cartridgeloader: stream: buffer underrun")
|
|
}
|
|
|
|
return n, nil
|
|
}
|