Disk Storage (MSC)¶
A mass-storage function is a block backend plus the class's SCSI / Bulk-Only Transport state machine. The class does no filesystem access of its own: you supply the backend - an image file, a real partition, your own block source - and the OS mounts it like a USB drive.
An image-backed drive¶
import sys, time, usbip
from usbip.device import USBDevice
from usbip.classes.device import MSC
class ImageStore: # the backend is yours
block_size = 512
def __init__(self, path):
self._f = open(path, "r+b", buffering=0) # an existing image, as it is
self.num_blocks = self._f.seek(0, 2) // self.block_size
if not self.num_blocks:
raise SystemExit(f"{path}: smaller than one {self.block_size} B block")
def read(self, lba, count):
self._f.seek(lba * self.block_size)
return self._f.read(count * self.block_size)
def write(self, lba, count, data):
self._f.seek(lba * self.block_size)
self._f.write(data)
store = ImageStore(sys.argv[1]) # capacity = the image's size
dev = USBDevice(0x1209, 0x0008, product="USBIP MSC")
dev.add(MSC(store, vendor="USB-IP", product="DISK"))
dev.plug() # serve it; -> a drive
time.sleep(3600) # serve (Ctrl-C to stop)
#include <stdio.h>
#include <unistd.h>
#include "usbip-device.h"
#include "classes/msc.h"
#define BLOCK_SIZE 512
static uint32_t num_of_blocks; /* the image's size, in blocks */
static uint32_t get_num_of_blocks(void *u) {
(void)u;
return num_of_blocks;
}
static uint32_t get_block_size(void *u) {
(void)u;
return BLOCK_SIZE;
}
static int disk_read(void *u, uint32_t lba, uint32_t n, uint8_t *b) {
FILE *fp = u;
if (fseek(fp, (long)lba * BLOCK_SIZE, SEEK_SET) != 0)
return -1;
if (fread(b, 1, n * BLOCK_SIZE, fp) == n * BLOCK_SIZE)
return 0;
else
return -1;
}
static int disk_write(void *u, uint32_t lba, uint32_t n, const uint8_t *b) {
FILE *fp = u;
if (fseek(fp, (long)lba * BLOCK_SIZE, SEEK_SET) != 0)
return -1;
if (fwrite(b, 1, n * BLOCK_SIZE, fp) == n * BLOCK_SIZE)
return 0;
else
return -1;
}
int main(int argc, char **argv) {
if (argc != 2) {
fprintf(stderr, "usage: %s <disk.img>\n", argv[0]);
return 1;
}
FILE *fp = fopen(argv[1], "r+b"); /* an existing image, as it is */
if (!fp) {
perror(argv[1]);
return 1;
}
setvbuf(fp, NULL, _IONBF, 0); /* reads and writes alternate */
fseek(fp, 0, SEEK_END); /* capacity = the image's size */
num_of_blocks = (uint32_t)(ftell(fp) / BLOCK_SIZE);
if (num_of_blocks == 0) {
fprintf(stderr, "%s: smaller than one %d B block\n", argv[1], BLOCK_SIZE);
return 1;
}
usbip_device *g = usbip_device_create(0x1209, 0x0008);
msc_opts opts = {
.num_blocks = get_num_of_blocks,
.block_size = get_block_size,
.read = disk_read,
.write = disk_write,
.user = fp, /* passed back to disk_read/disk_write */
};
msc_add(g, &opts);
usbip_device_plug(g, NULL); /* serve it; -> a drive */
for (;;) sleep(1);
}
Mount it¶
Once a client has imported the device, the host's in-box mass-storage driver binds it and it behaves like any other USB drive.
usb-storage binds it as /dev/sd*:
USBSTOR binds it and it gets a drive letter - browse it in Explorer. A
floppy-declared device (see Floppy drives) shows up as a floppy
drive instead of a removable disk.
Blocks, LBAs and media types¶
Both take the image path on the command line and serve exactly what is already there:
the capacity is the file's own size, and neither creates, grows nor truncates it, so
pointing either at a prepared image - a formatted disk, an ISO - cannot damage it. Make
a blank image of whatever size you want and format it with your OS's own tools, or just
let the host format the drive after it attaches. (On Linux: truncate -s 1M disk.img
then mkfs.vfat disk.img.)
A block is a fixed-size chunk of storage (here 512 bytes), addressed by its LBA
(logical block address). The class translates the host's SCSI read/write commands into
calls to your read/write over a range of blocks - you never parse SCSI yourself.
Present a write-protected medium with read_only=True / .read_only = 1. What the
medium is comes from one field: medium="disk" / .medium = MSC_MEDIUM_DISK (the
default), "cdrom" / MSC_MEDIUM_CDROM for a read-only CD-ROM, or "floppy" /
MSC_MEDIUM_FLOPPY for a floppy.
Several drives on one interface (LUNs)¶
A mass-storage interface can carry up to 16 logical units - what a card reader's
slots are, or a disk shipped beside its install CD. They share one pair of bulk pipes
and answer one command at a time; the host picks the unit per command, having asked
GET_MAX_LUN how many there are. Each unit is a store of its own, and can be its own
kind of medium:
disk = ImageStore("disk.img")
cd = ImageStore("install.iso")
cd.medium, cd.read_only = "cdrom", True # a store says what it is
dev.add(MSC([disk, cd])) # two drives, one interface
A store may carry medium, read_only, product and serial; anything it does
not carry comes from the MSC(...) arguments.
msc_opts opts = {
.num_blocks = get_num_of_blocks, /* one backend, called per unit */
.block_size = get_block_size,
.read = disk_read,
.write = disk_write,
.n_luns = 2,
.luns = {
{ .user = &disk }, /* unit 0: a plain read-write disk */
{ .user = &cd, .medium = MSC_MEDIUM_CDROM }, /* unit 1: its install CD */
},
.on_command = log_scsi,
.user = &app, /* on_command's context */
};
msc_add(g, &opts);
Each msc_lun field left zero or NULL falls back to the device-wide msc_opts
one, and with n_luns unset (the everyday case) there is a single unit backed by
opts.user - which is why every single-LUN program above needs none of this.
The host treats the units as separate drives: Linux gives each its own /dev/sd*,
Windows its own drive letter. Both examples take the images on the command line,
comma-separated, and a tag on an entry says what that unit is:
msc_device --file disk.img,install.iso:cdrom,key.img:ro # a disk, a CD and a locked key
msc_device --file a.img,b.img --size 8M,64M # two disks, sized separately
The tags are :cdrom, :floppy, :disk and :ro; an entry without one takes the
device-wide flags. --size and --block-size take one value for every image, or one
per image, and only ever size an image that is not there yet - an existing image keeps
its own size, as always.
Floppy drives¶
What makes a host treat a disk as a floppy is its geometry. Give the store one of the standard capacities and the class answers MODE SENSE page 0x05 (Flexible Disk) with that format's cylinders, heads and sectors, and offers the whole set through READ FORMAT CAPACITIES - so Windows can format it:
| Format | Code | Blocks (512 B) | C/H/S |
|---|---|---|---|
| 2.88M | FLOPPY_2_88M / MSC_FLOPPY_2_88M |
5760 | 80 / 2 / 36 |
| 1.44M | FLOPPY_1_44M / MSC_FLOPPY_1_44M |
2880 | 80 / 2 / 18 |
| 1.2M | FLOPPY_1_2M / MSC_FLOPPY_1_2M |
2400 | 80 / 2 / 15 |
| 720K | FLOPPY_720K / MSC_FLOPPY_720K |
1440 | 80 / 2 / 9 |
| 360K | FLOPPY_360K / MSC_FLOPPY_360K |
720 | 40 / 2 / 9 |
Size the image with floppy_format_size(FLOPPY_1_44M) /
msc_floppy_format_size(MSC_FLOPPY_1_44M).
A capacity matching none of these still works - the class reports the real block count
with 1.44M's layout - but no host will call it a standard floppy.
Add ufi=True / .ufi = 1 to declare the UFI command set (bInterfaceSubClass
0x04, SFF-8070i) instead of SCSI transparent (0x06): the subclass a real USB floppy
drive reports, and what makes Windows show a floppy drive rather than a removable disk.
The transport stays Bulk-Only either way. The UFI commands themselves - FORMAT UNIT,
READ(12)/WRITE(12), VERIFY, SEEK, REZERO UNIT - are answered whatever the subclass.
Full program: examples/msc_device.py; in C, examples/device/msc_device.c. API:
MSC.