summaryrefslogtreecommitdiff
path: root/toolbox
diff options
context:
space:
mode:
Diffstat (limited to 'toolbox')
-rw-r--r--toolbox/testdata/fe_max.goldenbin0 -> 41 bytes
-rw-r--r--toolbox/testdata/fe_normal.goldenbin0 -> 40 bytes
-rw-r--r--toolbox/testdata/fe_truncated.goldenbin0 -> 40 bytes
-rw-r--r--toolbox/toolbox.go68
-rw-r--r--toolbox/toolbox_linux.go181
-rw-r--r--toolbox/toolbox_test.go62
6 files changed, 311 insertions, 0 deletions
diff --git a/toolbox/testdata/fe_max.golden b/toolbox/testdata/fe_max.golden
new file mode 100644
index 0000000..29479ee
--- /dev/null
+++ b/toolbox/testdata/fe_max.golden
Binary files differ
diff --git a/toolbox/testdata/fe_normal.golden b/toolbox/testdata/fe_normal.golden
new file mode 100644
index 0000000..2658003
--- /dev/null
+++ b/toolbox/testdata/fe_normal.golden
Binary files differ
diff --git a/toolbox/testdata/fe_truncated.golden b/toolbox/testdata/fe_truncated.golden
new file mode 100644
index 0000000..5bdbd75
--- /dev/null
+++ b/toolbox/testdata/fe_truncated.golden
Binary files differ
diff --git a/toolbox/toolbox.go b/toolbox/toolbox.go
new file mode 100644
index 0000000..535fe9f
--- /dev/null
+++ b/toolbox/toolbox.go
@@ -0,0 +1,68 @@
+// SPDX-FileCopyrightText: 2026 Terin Stock <terinjokes@gmail.com>
+// SPDX-License-Identifier: EUPL-1.2
+
+package toolbox
+
+import (
+ "strings"
+)
+
+const (
+ TOOLBOX_COUNT_CDS = 0xDA
+ TOOLBOX_LIST_CDS = 0xD7
+ TOOLBOX_SET_NEXT_CD = 0xD8
+ TOOLBOX_LIST_DEVICES = 0xD9
+)
+
+// FileType represents the type of file in a [FileEntry].
+type FileType uint8
+
+const (
+ File FileType = iota
+ Directory
+)
+
+// DeviceType represents the type of emulated device.
+type DeviceType uint8
+
+const (
+ Fixed DeviceType = iota
+ Removeable
+ Optical
+ Floppy14MB
+ MO
+ Sequential
+ Network
+ Zip100
+
+ None DeviceType = 0xFF
+)
+
+// FileEntry describes each file recognized by the ODE as
+// emulatable media.
+type FileEntry [40]byte
+
+// Index is the file's index, to be used with [Device.SetCDByIndex].
+func (e *FileEntry) Index() int {
+ return int(e[0])
+}
+
+// FileType returns the entry's file type.
+func (e *FileEntry) FileType() FileType {
+ return FileType(e[1])
+}
+
+// Name is the entry's name, truncated to 32 characters.
+func (e *FileEntry) Name() string {
+ str := string(e[2:35])
+ idx := strings.IndexByte(str, 0)
+ if idx < 0 {
+ idx = 33
+ }
+ return str[:idx]
+}
+
+// Size returns the size of the entry in bytes.
+func (e *FileEntry) Size() uint64 {
+ return uint64(e[36])<<24 | uint64(e[37])<<16 | uint64(e[38])<<8 | uint64(e[39])
+}
diff --git a/toolbox/toolbox_linux.go b/toolbox/toolbox_linux.go
new file mode 100644
index 0000000..d35425e
--- /dev/null
+++ b/toolbox/toolbox_linux.go
@@ -0,0 +1,181 @@
+// SPDX-FileCopyrightText: 2026 Terin Stock <terinjokes@gmail.com>
+// SPDX-License-Identifier: EUPL-1.2
+
+// Package toolbox is implemented on Linux using the SCSI Generic (sg) driver, version 3. This
+// driver has been included since Linux 2.4, and harmonizes access to SCSI devices. This
+// implementation supports access via the high level driver paths (eg, /dev/sr0, /dev/st0) as well
+// as the generic paths (eg, /dev/sg0). Raw SCSI commands are sent to the device using the SCSI
+// Generic SG_IO ioctl.
+package toolbox
+
+import (
+ "fmt"
+ "os"
+ "structs"
+ "unsafe"
+
+ "golang.org/x/sys/unix"
+)
+
+const (
+ SG_GET_VERSION_NUM = 0x2282
+ SG_IO = 0x2285
+)
+
+type sgIoHdr struct {
+ _ structs.HostLayout
+
+ interfaceID int32
+ dxferDirection int32
+ cmdLen uint8
+ mxSbLen uint8
+ iovecCount uint16
+ dxferLen uint32
+ dxferp *byte
+ cmdp *uint8
+ sbp *byte
+ timeout uint32
+ flags uint32
+ packId int32
+ userPtr *byte
+ status uint8
+ maskedStatus uint8
+ msgStatus uint8
+ sbLenWr uint8
+ hostStatus uint16
+ driverStatus uint16
+ resId int32
+ duration uint32
+ info uint32
+}
+
+func ioctlSgIo(fd uintptr, hdr *sgIoHdr) error {
+ _, _, errNo := unix.Syscall(unix.SYS_IOCTL, fd, SG_IO, uintptr(unsafe.Pointer(hdr)))
+ if errNo != 0 {
+ return errNo
+ }
+ return nil
+}
+
+// Device holds an open reference to teh sg device.
+type Device struct {
+ f *os.File
+}
+
+// Open creats a connection to the sg device at devpath, which can be a high level
+// device path (such as /dev/sr0) or the generic device path (such as /dev/sg0).
+// Returns an error if the device cannot be opened, is not a SCSI device, or if
+// the sg driver is too old (older than Linux 2.4).
+func Open(devpath string) (*Device, error) {
+ f, err := os.OpenFile(devpath, os.O_RDONLY, 0)
+ if err != nil {
+ return nil, err
+ }
+
+ version, err := unix.IoctlGetUint32(int(f.Fd()), SG_GET_VERSION_NUM)
+ if err != nil {
+ return nil, err
+ }
+ if version < 30_000 {
+ return nil, fmt.Errorf("requires a SCSI device or newer sg dervier")
+ }
+
+ return &Device{f: f}, nil
+}
+
+// Close closes the refernce to the sg driver.
+func (d *Device) Close() error {
+ return d.f.Close()
+}
+
+// CountCDs returns the count of files recognized by the emulator.
+func (d *Device) CountCDs() (int, error) {
+ dxferBuf := byte(0)
+ cmdp := []byte{TOOLBOX_COUNT_CDS, 0, 0, 0, 0, 0}
+
+ ioHdr := &sgIoHdr{
+ interfaceID: int32('S'),
+ cmdLen: uint8(len(cmdp)),
+ mxSbLen: 0,
+ dxferDirection: -3,
+ cmdp: &cmdp[0],
+ dxferp: &dxferBuf,
+ dxferLen: 1,
+ timeout: 20_000,
+ }
+
+ err := ioctlSgIo(d.f.Fd(), ioHdr)
+ if err != nil {
+ return 0, err
+ }
+ return int(dxferBuf), nil
+}
+
+// ListCDs returns [FileEntry] instances corresponding to recognized
+// files on the emulator.
+func (d *Device) ListCDs() ([]FileEntry, error) {
+ cdCount, err := d.CountCDs()
+ if err != nil {
+ return nil, err
+ }
+
+ fileEntries := make([]FileEntry, cdCount)
+ cmdp := []byte{TOOLBOX_LIST_CDS, 0, 0, 0, 0, 0}
+
+ ioHdr := &sgIoHdr{
+ interfaceID: int32('S'),
+ cmdLen: uint8(len(cmdp)),
+ mxSbLen: 0,
+ dxferDirection: -3,
+ cmdp: &cmdp[0],
+ dxferp: (*byte)(unsafe.Pointer(&fileEntries[0])),
+ dxferLen: uint32(int(unsafe.Sizeof(FileEntry{})) * cdCount),
+ timeout: 20_000,
+ }
+
+ err = ioctlSgIo(d.f.Fd(), ioHdr)
+ if err != nil {
+ return nil, err
+ }
+
+ return fileEntries, nil
+}
+
+// SetCDByIndex request the emulator to switch media to the specified index.
+func (d *Device) SetCDByIndex(idx int) error {
+ cmdp := []byte{TOOLBOX_SET_NEXT_CD, byte(idx), 0, 0, 0, 0}
+
+ ioHdr := &sgIoHdr{
+ interfaceID: int32('S'),
+ cmdLen: uint8(len(cmdp)),
+ mxSbLen: 0,
+ dxferDirection: -3,
+ cmdp: &cmdp[0],
+ timeout: 20_000,
+ }
+
+ return ioctlSgIo(d.f.Fd(), ioHdr)
+}
+
+// ListDevices returns the device types of each emulated SCSI device.
+func (d *Device) ListDevices() ([]DeviceType, error) {
+ devices := make([]DeviceType, 8)
+ cmdp := []byte{TOOLBOX_LIST_DEVICES, 0, 0, 0, 0, 0}
+ ioHdr := &sgIoHdr{
+ interfaceID: int32('S'),
+ cmdLen: uint8(len(cmdp)),
+ mxSbLen: 0,
+ dxferDirection: -3,
+ cmdp: &cmdp[0],
+ dxferp: (*byte)(unsafe.Pointer(&devices[0])),
+ dxferLen: 8,
+ timeout: 20_000,
+ }
+
+ err := ioctlSgIo(d.f.Fd(), ioHdr)
+ if err != nil {
+ return nil, err
+ }
+
+ return devices, nil
+}
diff --git a/toolbox/toolbox_test.go b/toolbox/toolbox_test.go
new file mode 100644
index 0000000..7548be3
--- /dev/null
+++ b/toolbox/toolbox_test.go
@@ -0,0 +1,62 @@
+// SPDX-FileCopyrightText: 2026 Terin Stock <terinjokes@gmail.com>
+// SPDX-License-Identifier: EUPL-1.2
+
+package toolbox
+
+import (
+ "testing"
+
+ "gotest.tools/v3/assert"
+ "gotest.tools/v3/golden"
+)
+
+func TestParseFileEntry(t *testing.T) {
+ type testcase struct {
+ name string
+ fileEntry FileEntry
+ expectedIndex int
+ expectedName string
+ expectedType FileType
+ expectedSize uint64
+ }
+
+ run := func(t *testing.T, tc testcase) {
+ assert.Equal(t, tc.fileEntry.Index(), tc.expectedIndex)
+ assert.Equal(t, tc.fileEntry.Name(), tc.expectedName)
+ assert.Equal(t, tc.fileEntry.FileType(), tc.expectedType)
+ assert.Equal(t, tc.fileEntry.Size(), tc.expectedSize)
+ }
+
+ testcases := []testcase{
+ {
+ name: "normal entry",
+ fileEntry: FileEntry(golden.Get(t, "fe_normal.golden")),
+ expectedIndex: 0,
+ expectedName: "9front-11321.386.iso",
+ expectedType: File,
+ expectedSize: 479795200,
+ },
+ {
+ name: "truncated name entry",
+ fileEntry: FileEntry(golden.Get(t, "fe_truncated.golden")),
+ expectedIndex: 3,
+ expectedName: "gentoo-x86-minimal-20251223T1631",
+ expectedType: File,
+ expectedSize: 635064320,
+ },
+ {
+ name: "maximium size",
+ fileEntry: FileEntry(golden.Get(t, "fe_max.golden")),
+ expectedIndex: 0,
+ expectedName: "max.iso",
+ expectedType: File,
+ expectedSize: 4294967295,
+ },
+ }
+
+ for _, tc := range testcases {
+ t.Run(tc.name, func(t *testing.T) {
+ run(t, tc)
+ })
+ }
+}