diff options
Diffstat (limited to 'toolbox')
| -rw-r--r-- | toolbox/testdata/fe_max.golden | bin | 0 -> 41 bytes | |||
| -rw-r--r-- | toolbox/testdata/fe_normal.golden | bin | 0 -> 40 bytes | |||
| -rw-r--r-- | toolbox/testdata/fe_truncated.golden | bin | 0 -> 40 bytes | |||
| -rw-r--r-- | toolbox/toolbox.go | 68 | ||||
| -rw-r--r-- | toolbox/toolbox_linux.go | 181 | ||||
| -rw-r--r-- | toolbox/toolbox_test.go | 62 |
6 files changed, 311 insertions, 0 deletions
diff --git a/toolbox/testdata/fe_max.golden b/toolbox/testdata/fe_max.golden Binary files differnew file mode 100644 index 0000000..29479ee --- /dev/null +++ b/toolbox/testdata/fe_max.golden diff --git a/toolbox/testdata/fe_normal.golden b/toolbox/testdata/fe_normal.golden Binary files differnew file mode 100644 index 0000000..2658003 --- /dev/null +++ b/toolbox/testdata/fe_normal.golden diff --git a/toolbox/testdata/fe_truncated.golden b/toolbox/testdata/fe_truncated.golden Binary files differnew file mode 100644 index 0000000..5bdbd75 --- /dev/null +++ b/toolbox/testdata/fe_truncated.golden 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) + }) + } +} |
