/*
* CHU - driver for CH375 in disk mode
* Copyright (C) 2024 Javier S. Pedro
*
* This program 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.
* This program 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 this program. If not, see .
*/
#include
#include
#include
#include
#include
#include
#include
#include "inc/mbr.h"
#include "inc/dosdrv.h"
#include "chudisk.h"
uint16_t ch375_port;
bool read_only;
bool show_fat32;
uint8_t drive;
uint32_t partition_start;
uint32_t partition_len;
#define MAX_PARTITIONS 32
struct partinfo {
uint32_t start_sector;
uint32_t num_sectors;
uint8_t type;
} partinfo[MAX_PARTITIONS];
unsigned int num_parts;
static int ioctl_read_control(uint8_t drive, void *buffer, int len)
{
union REGS regs;
regs.h.ah = DOS_IOCTL;
regs.h.al = 0x04; // Read control string from block device
regs.x.bx = drive;
regs.x.cx = len;
regs.x.dx = FP_OFF(buffer);
intdos(®s, ®s);
if (regs.x.cflag) {
return -1;
} else {
return regs.x.ax;
}
}
static int ioctl_write_control(uint8_t drive, void *buffer, int len)
{
union REGS regs;
regs.h.ah = DOS_IOCTL;
regs.h.al = 0x05; // Write control string to block device
regs.x.bx = drive;
regs.x.cx = len;
regs.x.dx = FP_OFF(buffer);
intdos(®s, ®s);
if (regs.x.cflag) {
return -1;
} else {
return regs.x.ax;
}
}
static int ioctl_read_abs_sector(uint8_t drive, void *buffer, uint32_t sector, int sectors)
{
CHUDISK_IOCTL_DISK_DATA ciod;
union REGS regs;
ciod.rwabs.buffer = buffer;
ciod.rwabs.sector = sector;
ciod.rwabs.sectors = sectors;
regs.h.ah = DOS_IOCTL;
regs.h.al = 0x0D; // IOCTL for block devices
regs.x.bx = drive;
regs.h.ch = DOS_IOCTL_DISK;
regs.h.cl = CHUDISK_IOCTL_DISK_READ_ABS_SECTOR;
regs.x.dx = FP_OFF(&ciod);
intdos(®s, ®s);
return regs.x.cflag ? -1 : 0;
}
static void dos_disk_reset()
{
union REGS regs;
regs.h.ah = 0x0D; // DOS DISK RESET
intdos(®s, ®s);
}
static int parse_chudisk_control_string(const char *b, int len)
{
// Not sure why I'm doing it this way instead of just sharing a pointer.
int i = 0;
#if 0
printf("control string:\n");
for (i = 0; i < len; i+=8) {
printf(" %hx %hx %hx %hx %hx %hx %hx %hx\n",
b[i+0], b[i+1], b[i+2], b[i+3], b[i+4], b[i+5], b[i+6], b[i+7]);
}
i = 0;
#endif
while (i < len) {
char c = b[i++];
switch (c) {
case 'B':
memcpy(&ch375_port, &b[i], sizeof(ch375_port));
i+=sizeof(ch375_port);
break;
case '3':
show_fat32 = b[i++];
break;
case 'R':
read_only = b[i++];
break;
case 'S':
memcpy(&partition_start, &b[i], sizeof(partition_start));
i+=sizeof(partition_start);
break;
case 'L':
memcpy(&partition_len, &b[i], sizeof(partition_len));
i+=sizeof(partition_len);
break;
case '\0':
return 0;
default:
fprintf(stderr, "Unidentified control string parameter: '%c'\n", c);
return -1;
}
}
return 0;
}
static bool is_selectable_partition(const MBR_PARTENTRY *partentry)
{
return partentry->type == MBR_FAT12 || partentry->type == MBR_FAT16_32MB
|| partentry->type == MBR_FAT16 || partentry->type == MBR_FAT16_LBA
|| (show_fat32 && (partentry->type == MBR_FAT32 || partentry->type == MBR_FAT32_LBA));
}
static const char *get_partition_type(const MBR_PARTENTRY *partentry)
{
static char buffer[8];
switch (partentry->type) {
case MBR_EMPTY:
return "invalid";
case MBR_FAT12:
return "fat12";
case MBR_FAT16_32MB:
case MBR_FAT16:
case MBR_FAT16_LBA:
return "fat16";
case MBR_IFS:
return "ntfs";
case MBR_FAT32:
case MBR_FAT32_LBA:
return "fat32";
case MBR_EXT:
case MBR_EXT_LBA:
return "extended";
case MBR_LINUX:
case MBR_LINUX_SWAP:
return "linux";
default:
sprintf(buffer, "%#X", partentry->type);
return buffer;
}
}
static const char * show_partition_size(const MBR_PARTENTRY *partentry)
{
static char buffer[12];
uint32_t size = partentry->lbaLen / 2;
STATIC_ASSERT(SECTOR_SIZE == 512);
if (size <= 10000) {
sprintf(buffer, "%lu KB", size);
return buffer;
}
size /= 1024;
if (size <= 10000) {
sprintf(buffer, "%lu MB", size);
return buffer;
}
size /= 1024;
sprintf(buffer, "%lu GB", size);
return buffer;
}
static const char *get_partition_label(const MBR_PARTENTRY *partentry, const uint32_t start_sector)
{
static char buffer[SECTOR_SIZE];
DOS_BPB *bpb = (DOS_BPB*) &buffer[DOS_BPB_OFFSET_ON_DISK];
switch (partentry->type) {
// Focus on well-known types for now
case MBR_FAT12:
case MBR_FAT16_32MB:
case MBR_FAT16:
case MBR_FAT16_LBA:
case MBR_FAT32:
case MBR_FAT32_LBA:
break;
default:
return " ";
}
if (ioctl_read_abs_sector(drive, buffer, start_sector, 1) != 0) {
fprintf(stderr, "while reading partition boot record at %lu: %s\n", start_sector, strerror(errno));
return " ???? ";
}
if (bpb->sectorsPerFAT != 0) {
// Assume FAT16
return bpb->fat16.volumeLabel;
} else {
// Assume FAT32
return bpb->fat32.volumeLabel;
}
}
static void print_partition(const uint32_t mbr_sector, const MBR_PARTENTRY *partentry, bool selectable)
{
const bool ebr = mbr_sector != 0;
const uint32_t start_sector = mbr_sector + partentry->lbaFirst;
const bool is_current = partition_start == start_sector;
const char *typeLabel = get_partition_type(partentry);
const char *volumeLabel = get_partition_label(partentry, start_sector);
const char *sizeLabel = show_partition_size(partentry);
if (partentry->type == MBR_EMPTY) {
// Skip empty partitions no matter what
return;
}
if (ebr && (partentry->type == MBR_EXT || partentry->type == MBR_EXT_LBA)) {
// Skip displaying EXT partitions EBR, noisy
return;
}
if (selectable) {
const unsigned index = num_parts + 1;
printf(" %c[%2u] %8s %-11.11s %10s \n",
is_current ? '*' : ' ', index, typeLabel, volumeLabel, sizeLabel);
} else {
printf(" %c[ ] %8s %-11.11s %10s \n",
is_current ? '*' : ' ', typeLabel, volumeLabel, sizeLabel);
}
}
static int scan_disk_partitions()
{
static char buffer[SECTOR_SIZE];
uint32_t next_sector = 0;
printf(" Index %8s %-11.11s %10s \n",
"Type", "Label", "Size");
// Start with MBR at sector 0
next_sector = 0;
do {
const uint32_t this_sector = next_sector;
const MBR_PARTENTRY *partentry;
int i;
// Try to read sector
if (ioctl_read_abs_sector(drive, buffer, this_sector, 1) != 0) {
fprintf(stderr, "while reading %s at disk sector %lu: %s\n",
this_sector == 0 ? "MBR" : "EBR",
this_sector, strerror(errno));
// Fatal for MBR, non-fatal for EBR
if (this_sector == 0) {
fprintf(stderr, "is the disk present?\n");
return -1;
}
break;
}
// Check for signature
if (*(uint16_t*)&buffer[MBR_SIGNATURE_OFFSET_ON_DISK] != 0xAA55U) {
fprintf(stderr, "on disk sector %lu: invalid MBR signature %x\n",
this_sector, *(uint16_t*)&buffer[MBR_SIGNATURE_OFFSET_ON_DISK]);
if (this_sector == 0) return -1;
break;
}
// Parse the MBR
partentry = (MBR_PARTENTRY*)&buffer[MBR_PARTENTRY_OFFSET_ON_DISK];
next_sector = 0; // If there is another EBR we will store it here
for (i = 0; i < 4 && num_parts < MAX_PARTITIONS; i++) {
bool is_selectable = is_selectable_partition(&partentry[i]);
print_partition(this_sector, &partentry[i], is_selectable);
if (is_selectable) {
partinfo[num_parts].type = partentry[i].type;
partinfo[num_parts].start_sector = this_sector + partentry[i].lbaFirst;
partinfo[num_parts].num_sectors = partentry[i].lbaLen;
num_parts++;
}
if (partentry[i].type == MBR_EXT || partentry[i].type == MBR_EXT_LBA) {
// Go to the EBR next
next_sector = this_sector + partentry[i].lbaFirst;
}
}
} while (next_sector != 0 && num_parts < MAX_PARTITIONS);
return 0;
}
static void usage()
{
printf("Usage: chud X:, where X is a drive letter handled by CHUDISK.SYS\n");
}
int main(int argc, const char *argv[])
{
static char buffer[16];
int i;
unsigned part;
for (i = 1; i < argc; i++) {
// Drive letter?
if (argv[i][0] >= 'A' && argv[i][0] <= 'Z' && argv[i][1] == ':') {
drive = 1 + argv[i][0] - 'A';
} else if (argv[i][0] >= 'a' && argv[i][0] <= 'z' && argv[i][1] == ':') {
drive = 1 + argv[i][0] - 'a';
}
}
// Now read the driver settings and current partition
i = ioctl_read_control(drive, buffer, sizeof(buffer));
if (i < 0) {
perror("ioctl_read_control");
usage();
return EXIT_FAILURE;
}
if (parse_chudisk_control_string(buffer, i)) {
fprintf(stderr, "wrong IOCTL string returned by driver\n");
usage();
return EXIT_FAILURE;
}
if (scan_disk_partitions() != 0) {
return EXIT_FAILURE;
}
if (num_parts == 0) {
fprintf(stderr, "No partitions found on the disk\n");
return EXIT_FAILURE;
}
printf("Select partition in [1..%u] range: ", num_parts);
i = scanf("%u", &part);
if (i != 1) {
// Assume cancel
return EXIT_FAILURE;
}
if (part == 0 || part > num_parts) {
fprintf(stderr, "Invalid partition %u\n", part);
return EXIT_FAILURE;
}
printf("Switching drive to partition %u at offset %lu size %lu\n", part,
partinfo[part-1].start_sector, partinfo[part-1].num_sectors);
dos_disk_reset(); // Flush caches before we swap the drive...
i = 0;
buffer[i++] = 'S';
memcpy(&buffer[i], &partinfo[part-1].start_sector, sizeof(uint32_t));
i+=sizeof(uint32_t);
buffer[i++] = 'L';
memcpy(&buffer[i], &partinfo[part-1].num_sectors, sizeof(uint32_t));
i+=sizeof(uint32_t);
buffer[i] = '\0';
if (ioctl_write_control(drive, buffer, i) < i) {
perror("ioctl_write_control");
return EXIT_FAILURE;
}
// The above IOCTL will also cause the next mediacheck from DOS to return changed.
return EXIT_SUCCESS;
}