summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
l---------LICENSE1
-rw-r--r--LICENSES/CC0-1.0.txt121
-rw-r--r--LICENSES/EUPL-1.2.txt190
-rw-r--r--README.md28
-rw-r--r--REUSE.toml6
-rw-r--r--go.mod14
-rw-r--r--go.sum8
-rw-r--r--go.sum.license2
-rw-r--r--main.go55
-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
15 files changed, 736 insertions, 0 deletions
diff --git a/LICENSE b/LICENSE
new file mode 120000
index 0000000..cd4eb7f
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1 @@
+LICENSES/EUPL-1.2.txt \ No newline at end of file
diff --git a/LICENSES/CC0-1.0.txt b/LICENSES/CC0-1.0.txt
new file mode 100644
index 0000000..0e259d4
--- /dev/null
+++ b/LICENSES/CC0-1.0.txt
@@ -0,0 +1,121 @@
+Creative Commons Legal Code
+
+CC0 1.0 Universal
+
+ CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+ LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
+ ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+ INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+ REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
+ PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+ THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
+ HEREUNDER.
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator
+and subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for
+the purpose of contributing to a commons of creative, cultural and
+scientific works ("Commons") that the public can reliably and without fear
+of later claims of infringement build upon, modify, incorporate in other
+works, reuse and redistribute as freely as possible in any form whatsoever
+and for any purposes, including without limitation commercial purposes.
+These owners may contribute to the Commons to promote the ideal of a free
+culture and the further production of creative, cultural and scientific
+works, or to gain reputation or greater distribution for their Work in
+part through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any
+expectation of additional consideration or compensation, the person
+associating CC0 with a Work (the "Affirmer"), to the extent that he or she
+is an owner of Copyright and Related Rights in the Work, voluntarily
+elects to apply CC0 to the Work and publicly distribute the Work under its
+terms, with knowledge of his or her Copyright and Related Rights in the
+Work and the meaning and intended legal effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+protected by copyright and related or neighboring rights ("Copyright and
+Related Rights"). Copyright and Related Rights include, but are not
+limited to, the following:
+
+ i. the right to reproduce, adapt, distribute, perform, display,
+ communicate, and translate a Work;
+ ii. moral rights retained by the original author(s) and/or performer(s);
+iii. publicity and privacy rights pertaining to a person's image or
+ likeness depicted in a Work;
+ iv. rights protecting against unfair competition in regards to a Work,
+ subject to the limitations in paragraph 4(a), below;
+ v. rights protecting the extraction, dissemination, use and reuse of data
+ in a Work;
+ vi. database rights (such as those arising under Directive 96/9/EC of the
+ European Parliament and of the Council of 11 March 1996 on the legal
+ protection of databases, and under any national implementation
+ thereof, including any amended or successor version of such
+ directive); and
+vii. other similar, equivalent or corresponding rights throughout the
+ world based on applicable law or treaty, and any national
+ implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention
+of, applicable law, Affirmer hereby overtly, fully, permanently,
+irrevocably and unconditionally waives, abandons, and surrenders all of
+Affirmer's Copyright and Related Rights and associated claims and causes
+of action, whether now known or unknown (including existing as well as
+future claims and causes of action), in the Work (i) in all territories
+worldwide, (ii) for the maximum duration provided by applicable law or
+treaty (including future time extensions), (iii) in any current or future
+medium and for any number of copies, and (iv) for any purpose whatsoever,
+including without limitation commercial, advertising or promotional
+purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
+member of the public at large and to the detriment of Affirmer's heirs and
+successors, fully intending that such Waiver shall not be subject to
+revocation, rescission, cancellation, termination, or any other legal or
+equitable action to disrupt the quiet enjoyment of the Work by the public
+as contemplated by Affirmer's express Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason
+be judged legally invalid or ineffective under applicable law, then the
+Waiver shall be preserved to the maximum extent permitted taking into
+account Affirmer's express Statement of Purpose. In addition, to the
+extent the Waiver is so judged Affirmer hereby grants to each affected
+person a royalty-free, non transferable, non sublicensable, non exclusive,
+irrevocable and unconditional license to exercise Affirmer's Copyright and
+Related Rights in the Work (i) in all territories worldwide, (ii) for the
+maximum duration provided by applicable law or treaty (including future
+time extensions), (iii) in any current or future medium and for any number
+of copies, and (iv) for any purpose whatsoever, including without
+limitation commercial, advertising or promotional purposes (the
+"License"). The License shall be deemed effective as of the date CC0 was
+applied by Affirmer to the Work. Should any part of the License for any
+reason be judged legally invalid or ineffective under applicable law, such
+partial invalidity or ineffectiveness shall not invalidate the remainder
+of the License, and in such case Affirmer hereby affirms that he or she
+will not (i) exercise any of his or her remaining Copyright and Related
+Rights in the Work or (ii) assert any associated claims and causes of
+action with respect to the Work, in either case contrary to Affirmer's
+express Statement of Purpose.
+
+4. Limitations and Disclaimers.
+
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
+ surrendered, licensed or otherwise affected by this document.
+ b. Affirmer offers the Work as-is and makes no representations or
+ warranties of any kind concerning the Work, express, implied,
+ statutory or otherwise, including without limitation warranties of
+ title, merchantability, fitness for a particular purpose, non
+ infringement, or the absence of latent or other defects, accuracy, or
+ the present or absence of errors, whether or not discoverable, all to
+ the greatest extent permissible under applicable law.
+ c. Affirmer disclaims responsibility for clearing rights of other persons
+ that may apply to the Work or any use thereof, including without
+ limitation any person's Copyright and Related Rights in the Work.
+ Further, Affirmer disclaims responsibility for obtaining any necessary
+ consents, permissions or other rights required for any use of the
+ Work.
+ d. Affirmer understands and acknowledges that Creative Commons is not a
+ party to this document and has no duty or obligation with respect to
+ this CC0 or use of the Work.
diff --git a/LICENSES/EUPL-1.2.txt b/LICENSES/EUPL-1.2.txt
new file mode 100644
index 0000000..6d8cea4
--- /dev/null
+++ b/LICENSES/EUPL-1.2.txt
@@ -0,0 +1,190 @@
+EUROPEAN UNION PUBLIC LICENCE v. 1.2
+EUPL © the European Union 2007, 2016
+
+This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined below) which is provided under the
+terms of this Licence. Any use of the Work, other than as authorised under this Licence is prohibited (to the extent such
+use is covered by a right of the copyright holder of the Work).
+The Work is provided under the terms of this Licence when the Licensor (as defined below) has placed the following
+notice immediately following the copyright notice for the Work:
+ Licensed under the EUPL
+or has expressed by any other means his willingness to license under the EUPL.
+
+1.Definitions
+In this Licence, the following terms have the following meaning:
+— ‘The Licence’:this Licence.
+— ‘The Original Work’:the work or software distributed or communicated by the Licensor under this Licence, available
+as Source Code and also as Executable Code as the case may be.
+— ‘Derivative Works’:the works or software that could be created by the Licensee, based upon the Original Work or
+modifications thereof. This Licence does not define the extent of modification or dependence on the Original Work
+required in order to classify a work as a Derivative Work; this extent is determined by copyright law applicable in
+the country mentioned in Article 15.
+— ‘The Work’:the Original Work or its Derivative Works.
+— ‘The Source Code’:the human-readable form of the Work which is the most convenient for people to study and
+modify.
+— ‘The Executable Code’:any code which has generally been compiled and which is meant to be interpreted by
+a computer as a program.
+— ‘The Licensor’:the natural or legal person that distributes or communicates the Work under the Licence.
+— ‘Contributor(s)’:any natural or legal person who modifies the Work under the Licence, or otherwise contributes to
+the creation of a Derivative Work.
+— ‘The Licensee’ or ‘You’:any natural or legal person who makes any usage of the Work under the terms of the
+Licence.
+— ‘Distribution’ or ‘Communication’:any act of selling, giving, lending, renting, distributing, communicating,
+transmitting, or otherwise making available, online or offline, copies of the Work or providing access to its essential
+functionalities at the disposal of any other natural or legal person.
+
+2.Scope of the rights granted by the Licence
+The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, sublicensable licence to do the following, for
+the duration of copyright vested in the Original Work:
+— use the Work in any circumstance and for all usage,
+— reproduce the Work,
+— modify the Work, and make Derivative Works based upon the Work,
+— communicate to the public, including the right to make available or display the Work or copies thereof to the public
+and perform publicly, as the case may be, the Work,
+— distribute the Work or copies thereof,
+— lend and rent the Work or copies thereof,
+— sublicense rights in the Work or copies thereof.
+Those rights can be exercised on any media, supports and formats, whether now known or later invented, as far as the
+applicable law permits so.
+In the countries where moral rights apply, the Licensor waives his right to exercise his moral right to the extent allowed
+by law in order to make effective the licence of the economic rights here above listed.
+The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to any patents held by the Licensor, to the
+extent necessary to make use of the rights granted on the Work under this Licence.
+
+3.Communication of the Source Code
+The Licensor may provide the Work either in its Source Code form, or as Executable Code. If the Work is provided as
+Executable Code, the Licensor provides in addition a machine-readable copy of the Source Code of the Work along with
+each copy of the Work that the Licensor distributes or indicates, in a notice following the copyright notice attached to
+the Work, a repository where the Source Code is easily and freely accessible for as long as the Licensor continues to
+distribute or communicate the Work.
+
+4.Limitations on copyright
+Nothing in this Licence is intended to deprive the Licensee of the benefits from any exception or limitation to the
+exclusive rights of the rights owners in the Work, of the exhaustion of those rights or of other applicable limitations
+thereto.
+
+5.Obligations of the Licensee
+The grant of the rights mentioned above is subject to some restrictions and obligations imposed on the Licensee. Those
+obligations are the following:
+
+Attribution right: The Licensee shall keep intact all copyright, patent or trademarks notices and all notices that refer to
+the Licence and to the disclaimer of warranties. The Licensee must include a copy of such notices and a copy of the
+Licence with every copy of the Work he/she distributes or communicates. The Licensee must cause any Derivative Work
+to carry prominent notices stating that the Work has been modified and the date of modification.
+
+Copyleft clause: If the Licensee distributes or communicates copies of the Original Works or Derivative Works, this
+Distribution or Communication will be done under the terms of this Licence or of a later version of this Licence unless
+the Original Work is expressly distributed only under this version of the Licence — for example by communicating
+‘EUPL v. 1.2 only’. The Licensee (becoming Licensor) cannot offer or impose any additional terms or conditions on the
+Work or Derivative Work that alter or restrict the terms of the Licence.
+
+Compatibility clause: If the Licensee Distributes or Communicates Derivative Works or copies thereof based upon both
+the Work and another work licensed under a Compatible Licence, this Distribution or Communication can be done
+under the terms of this Compatible Licence. For the sake of this clause, ‘Compatible Licence’ refers to the licences listed
+in the appendix attached to this Licence. Should the Licensee's obligations under the Compatible Licence conflict with
+his/her obligations under this Licence, the obligations of the Compatible Licence shall prevail.
+
+Provision of Source Code: When distributing or communicating copies of the Work, the Licensee will provide
+a machine-readable copy of the Source Code or indicate a repository where this Source will be easily and freely available
+for as long as the Licensee continues to distribute or communicate the Work.
+Legal Protection: This Licence does not grant permission to use the trade names, trademarks, service marks, or names
+of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and
+reproducing the content of the copyright notice.
+
+6.Chain of Authorship
+The original Licensor warrants that the copyright in the Original Work granted hereunder is owned by him/her or
+licensed to him/her and that he/she has the power and authority to grant the Licence.
+Each Contributor warrants that the copyright in the modifications he/she brings to the Work are owned by him/her or
+licensed to him/her and that he/she has the power and authority to grant the Licence.
+Each time You accept the Licence, the original Licensor and subsequent Contributors grant You a licence to their contributions
+to the Work, under the terms of this Licence.
+
+7.Disclaimer of Warranty
+The Work is a work in progress, which is continuously improved by numerous Contributors. It is not a finished work
+and may therefore contain defects or ‘bugs’ inherent to this type of development.
+For the above reason, the Work is provided under the Licence on an ‘as is’ basis and without warranties of any kind
+concerning the Work, including without limitation merchantability, fitness for a particular purpose, absence of defects or
+errors, accuracy, non-infringement of intellectual property rights other than copyright as stated in Article 6 of this
+Licence.
+This disclaimer of warranty is an essential part of the Licence and a condition for the grant of any rights to the Work.
+
+8.Disclaimer of Liability
+Except in the cases of wilful misconduct or damages directly caused to natural persons, the Licensor will in no event be
+liable for any direct or indirect, material or moral, damages of any kind, arising out of the Licence or of the use of the
+Work, including without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, loss
+of data or any commercial damage, even if the Licensor has been advised of the possibility of such damage. However,
+the Licensor will be liable under statutory product liability laws as far such laws apply to the Work.
+
+9.Additional agreements
+While distributing the Work, You may choose to conclude an additional agreement, defining obligations or services
+consistent with this Licence. However, if accepting obligations, You may act only on your own behalf and on your sole
+responsibility, not on behalf of the original Licensor or any other Contributor, and only if You agree to indemnify,
+defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against such Contributor by
+the fact You have accepted any warranty or additional liability.
+
+10.Acceptance of the Licence
+The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ placed under the bottom of a window
+displaying the text of this Licence or by affirming consent in any other similar way, in accordance with the rules of
+applicable law. Clicking on that icon indicates your clear and irrevocable acceptance of this Licence and all of its terms
+and conditions.
+Similarly, you irrevocably accept this Licence and all of its terms and conditions by exercising any rights granted to You
+by Article 2 of this Licence, such as the use of the Work, the creation by You of a Derivative Work or the Distribution
+or Communication by You of the Work or copies thereof.
+
+11.Information to the public
+In case of any Distribution or Communication of the Work by means of electronic communication by You (for example,
+by offering to download the Work from a remote location) the distribution channel or media (for example, a website)
+must at least provide to the public the information requested by the applicable law regarding the Licensor, the Licence
+and the way it may be accessible, concluded, stored and reproduced by the Licensee.
+
+12.Termination of the Licence
+The Licence and the rights granted hereunder will terminate automatically upon any breach by the Licensee of the terms
+of the Licence.
+Such a termination will not terminate the licences of any person who has received the Work from the Licensee under
+the Licence, provided such persons remain in full compliance with the Licence.
+
+13.Miscellaneous
+Without prejudice of Article 9 above, the Licence represents the complete agreement between the Parties as to the
+Work.
+If any provision of the Licence is invalid or unenforceable under applicable law, this will not affect the validity or
+enforceability of the Licence as a whole. Such provision will be construed or reformed so as necessary to make it valid
+and enforceable.
+The European Commission may publish other linguistic versions or new versions of this Licence or updated versions of
+the Appendix, so far this is required and reasonable, without reducing the scope of the rights granted by the Licence.
+New versions of the Licence will be published with a unique version number.
+All linguistic versions of this Licence, approved by the European Commission, have identical value. Parties can take
+advantage of the linguistic version of their choice.
+
+14.Jurisdiction
+Without prejudice to specific agreement between parties,
+— any litigation resulting from the interpretation of this License, arising between the European Union institutions,
+bodies, offices or agencies, as a Licensor, and any Licensee, will be subject to the jurisdiction of the Court of Justice
+of the European Union, as laid down in article 272 of the Treaty on the Functioning of the European Union,
+— any litigation arising between other parties and resulting from the interpretation of this License, will be subject to
+the exclusive jurisdiction of the competent court where the Licensor resides or conducts its primary business.
+
+15.Applicable Law
+Without prejudice to specific agreement between parties,
+— this Licence shall be governed by the law of the European Union Member State where the Licensor has his seat,
+resides or has his registered office,
+— this licence shall be governed by Belgian law if the Licensor has no seat, residence or registered office inside
+a European Union Member State.
+
+
+ Appendix
+
+‘Compatible Licences’ according to Article 5 EUPL are:
+— GNU General Public License (GPL) v. 2, v. 3
+— GNU Affero General Public License (AGPL) v. 3
+— Open Software License (OSL) v. 2.1, v. 3.0
+— Eclipse Public License (EPL) v. 1.0
+— CeCILL v. 2.0, v. 2.1
+— Mozilla Public Licence (MPL) v. 2
+— GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
+— Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for works other than software
+— European Union Public Licence (EUPL) v. 1.1, v. 1.2
+— Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong Reciprocity (LiLiQ-R+).
+
+The European Commission may update this Appendix to later versions of the above licences without producing
+a new version of the EUPL, as long as they provide the rights granted in Article 2 of this Licence and protect the
+covered Source Code from exclusive appropriation.
+All other changes or additions to this Appendix require the production of a new EUPL version.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f37f3a9
--- /dev/null
+++ b/README.md
@@ -0,0 +1,28 @@
+<!--
+SPDX-FileCopyrightText: 2026 Terin Stock <terinjokes@gmail.com>
+SPDX-License-Identifier: EUPL-1.2
+-->
+# SCSI Toolbox for (Optical) Drive Emulators
+
+An implementation of the "SCSI Toolbox" for SCSI emulators, written in Go and targetting modern operating systems.
+The project provides a Go library and a simple CLI interface.
+
+This project has been tested with:
+
+* [USBODE](https://github.com/danifunker/usbode-circle)
+
+It may also work with the BlueSCSI and ZuluSCSI projects, but I do not currently have these devices to test with.
+
+## Status
+
+The project currently only supports devices on Linux with the "SCSI Generic" interface.
+
+* Listing images
+* Switching active image
+* Enumeate emulated device types
+
+There is currently no support for uploading or downloading files from the emulator.
+
+## Related Projects
+
+* [escsitoolbox](https://github.com/nielsmh/escsitoolbox): Supports MS-DOS, Windows 3.x, and Windows 98.
diff --git a/REUSE.toml b/REUSE.toml
new file mode 100644
index 0000000..bafd3be
--- /dev/null
+++ b/REUSE.toml
@@ -0,0 +1,6 @@
+version = 1
+
+[[annotations]]
+path = "toolbox/testdata/*.golden"
+SPDX-FileCopyrightText = "2026 Terin Stock <terinjokes@gmail.com>"
+SPDX-License-Identifier = "CC0-1.0"
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..b0caff9
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,14 @@
+// SPDX-FileCopyrightText: NONE
+// SPDX-License-Identifier: CC0-1.0
+
+module go.terinstock.com/odescsitoolbox
+
+go 1.25.5
+
+require (
+ github.com/dustin/go-humanize v1.0.1
+ golang.org/x/sys v0.39.0
+ gotest.tools/v3 v3.5.2
+)
+
+require github.com/google/go-cmp v0.6.0 // indirect
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..cf9ed4e
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,8 @@
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
+golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
+gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
diff --git a/go.sum.license b/go.sum.license
new file mode 100644
index 0000000..cf53d2b
--- /dev/null
+++ b/go.sum.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: None
+SPDX-License-Identifier: CC0-1.0
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..676ae29
--- /dev/null
+++ b/main.go
@@ -0,0 +1,55 @@
+// SPDX-FileCopyrightText: 2026 Terin Stock <terinjokes@gmail.com>
+// SPDX-License-Identifier: EUPL-1.2
+
+package main
+
+import (
+ "flag"
+ "fmt"
+ "os"
+
+ humanize "github.com/dustin/go-humanize"
+ "go.terinstock.com/odescsitoolbox/toolbox"
+)
+
+func listCDs(d *toolbox.Device) {
+ fileEntries, err := d.ListCDs()
+ if err != nil {
+ fmt.Printf("unable to fetch CD list: %v\n", err)
+ os.Exit(1)
+ }
+
+ fmt.Printf("index,name,size\n")
+
+ for _, entry := range fileEntries {
+ fmt.Printf("%d,%s,%s\n", entry.Index(), entry.Name(), humanize.IBytes(entry.Size()))
+ }
+}
+
+func setCD(d *toolbox.Device, idx int) {
+ err := d.SetCDByIndex(idx)
+ if err != nil {
+ fmt.Printf("unable to set CD index: %v\n", err)
+ os.Exit(1)
+ }
+}
+
+func main() {
+ device := flag.String("dev", "/dev/sr0", "the ODE device")
+ index := flag.Int("index", -1, "the index to set")
+ flag.Parse()
+
+ d, err := toolbox.Open(*device)
+ if err != nil {
+ fmt.Printf("unable to open device: %v\n", err)
+ os.Exit(1)
+ }
+ defer d.Close()
+
+ if *index >= 0 {
+ setCD(d, *index)
+ } else {
+ listCDs(d)
+ }
+
+}
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)
+ })
+ }
+}