| Category | Status | Notes |
|---|---|---|
| Core Infrastructure | ✅ Complete | DiskImage base, CRC, error handling |
| Apple II Formats | ✅ Complete | DO, PO, NIB, WOZ v1/v2 |
| MSX Formats | ✅ Complete | DSK, DMK, XSA (bidirectional) |
| Format Conversion | ✅ Complete | DO↔PO, DSK↔DMK, XSA↔DSK/DMK |
| CLI Application | ✅ Complete | All commands implemented |
| DOS 3.3 File System | ✅ Complete | Full CRUD, extended validation |
| ProDOS File System | ✅ Complete | Full CRUD, subdirectories |
| MSX-DOS File System | ✅ Complete | Full CRUD, subdirectories |
| Validation | ✅ Complete | All file systems |
| Category | Status | Notes |
|---|---|---|
| Bootdisk Mutation Guard | ✅ Complete | `--bootdisk-mode strict |
| Boot-Critical File Delete Guard | ✅ Complete | yes/no prompt by default, --force-system-file bypasses prompt |
| Invalid BPB Fail-Fast (MSX/Human68k) | ✅ Complete | corrupt BPB/filesystem init failure blocks mutate ops |
| Verbose Detection Reason | ✅ Complete | info -v reason includes invalid_bpb_or_filesystem_init_failed |
| Emulator Boot Smoke (disk add) | ✅ Complete | DOS33/ProDOS/MSX-DOS2/Human68k *_diskaddtest.sh 4/4 pass |
Operational policy now enforced:
- Boot area must remain untouched while allowing file add when safe verification passes.
- Existing non-target files must remain unchanged during safe add.
- Diskadd smoke scripts use copied boot disks to preserve originals.
AppleWin(Apple II) 및 openMSX(MSX) 에뮬레이터에서 사용하는 다양한 floppy disk image 포맷을 통합적으로 관리, 분석, 변환하는 도구 개발
- AppleWin (Apple II): 5.25" floppy disk emulation
- openMSX (MSX): 3.5" / 5.25" floppy disk emulation
- Executable:
rdedisktool(Retro Developer Environment Disk Tool) - Naming Convention:
rdeprefix for all RDE project binaries
- 디스크 이미지 읽기/쓰기
- 포맷 변환 (format conversion)
- 파일 시스템 분석 (DOS 3.3, ProDOS, MSX-DOS)
- 디스크 이미지 생성 (blank disk creation)
- 이미지 정보 표시 (metadata extraction)
- 섹터 수준 편집 (sector-level editing)
| File | Description | Lines |
|---|---|---|
source/DiskDefs.h |
디스크 상수 정의 | ~50 |
source/Disk.h/cpp |
Disk2InterfaceCard 클래스 | ~2,566 |
source/DiskImage.h/cpp |
이미지 조작 API | ~200 |
source/DiskImageHelper.h/cpp |
포맷별 구현 클래스 | ~2,374 |
source/DiskFormatTrack.h/cpp |
트랙 포맷팅 도우미 | ~300 |
| Format | Extension | Structure | Read | Write | Create |
|---|---|---|---|---|---|
| DOS Order | .do, .dsk | 35 tracks × 16 sectors × 256 bytes = 140KB | O | O | O |
| ProDOS Order | .po | 35 tracks × 16 sectors × 256 bytes = 140KB | O | O | X |
| Nibble 6656 | .nib | 35 tracks × 6,656 bytes = 232,960 bytes | O | O | O |
| Nibble 6384 | .nb2 | 35 tracks × 6,384 bytes = 223,440 bytes | O | O | X |
| WOZ v1 | .woz | Bit-level, variable track size | O | O | O |
| WOZ v2 | .woz | Bit-level, enhanced v1 | O | O | O |
| Hard Disk | .hdv | Block device | O | O | X |
NIBBLES_PER_TRACK_NIB = 0x1A00 // 6,656 nibbles
NIBBLES_PER_TRACK_WOZ2 = 0x1A18 // 6,680 nibbles (max)
NUM_SECTORS = 16
TRACKS_STANDARD = 35
TRACKS_MAX = 40
TRACK_DENIBBLIZED_SIZE = 4,096 // 16 × 256 bytes- Purpose: 256 bytes → 343 bytes (nibblized)
- Process:
- 256 bytes를 342개의 6-bit 값으로 변환 (3 bytes → 4 nibbles)
- XOR 체크섬 1 byte 추가
- 6-bit → 8-bit disk byte 변환 (lookup table)
[Gap1: 48 × 0xFF]
[Sector 0-15]:
├─ Address Field: D5 AA 96 | Vol(2) | Trk(2) | Sec(2) | Chk(2) | DE AA EB
├─ Gap2: 6 × 0xFF
├─ Data Field: D5 AA AD | Data(343) | DE AA EB
└─ Gap3: 27 × 0xFF
[Header: 12 bytes]
├─ ID1: 'WOZ1' or 'WOZ2'
├─ ID2: 0x0A0D0AFF
└─ CRC32: whole file checksum
[INFO Chunk]
├─ version, diskType, writeProtected
└─ creator (UTF-8, 32 bytes)
[TMAP Chunk: 160 bytes]
└─ Quarter-track mapping (40 tracks × 4)
[TRKS Chunk]
└─ Track data with bytesUsed, bitCount
| Logical | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| DOS Order | 0 | D | B | 9 | 7 | 5 | 3 | 1 | E | C | A | 8 | 6 | 4 | 2 | F |
| ProDOS | 0 | 8 | 1 | 9 | 2 | A | 3 | B | 4 | C | 5 | D | 6 | E | 7 | F |
| File | Description |
|---|---|
src/fdc/Disk.hh/cc |
Disk 추상 클래스 |
src/fdc/SectorAccessibleDisk.hh |
섹터 접근 인터페이스 |
src/fdc/SectorBasedDisk.hh/cc |
섹터 기반 디스크 |
src/fdc/DSKDiskImage.hh/cc |
.dsk 포맷 |
src/fdc/DMKDiskImage.hh/cc |
.dmk 포맷 |
src/fdc/XSADiskImage.hh/cc |
.xsa 압축 포맷 |
src/fdc/DiskFactory.hh/cc |
이미지 팩토리 |
src/fdc/RawTrack.hh |
raw track 데이터 구조 |
src/fdc/DiskImageUtils.hh/cc |
디스크 유틸리티 |
src/fdc/XSAExtractor.hh/cc |
XSA 압축 해제 |
| Format | Extension | Structure | Read | Write | Notes |
|---|---|---|---|---|---|
| DSK | .dsk, .di1, .di2, .fd1, .fd2 | Pure sector data | O | O | 720KB standard |
| DMK | .dmk | Raw track + IDAM table | O | O | TRS-80 origin |
| XSA | .xsa | LZ77 compressed | O | X | Xelasoft Archived |
| RamDSK | (virtual) | Memory-based | O | O | 720KB |
| DirAsDSK | (directory) | Host directory mapping | O | O/X | Sync mode |
SECTOR_SIZE = 512 bytes
STANDARD_TRACK_SIZE = 6,250 bytes (RawTrack)
SECTORS_PER_TRACK = 9 (720KB) or 18 (1.44MB)
NUM_TRACKS = 80
SECTORS_PER_CLUSTER = 2struct DmkHeader {
uint8_t writeProtected; // +0: 0x00 or 0xFF
uint8_t numTracks; // +1: track count
uint16_t trackLen; // +2-3: track length (LE)
uint8_t flags; // +4: FLAG_SINGLE_SIDED (0x10)
uint8_t reserved[7]; // +5-11: reserved
uint8_t format[4]; // +12-15: format info
};[IDAM Table: 128 bytes]
└─ 64 entries × 2 bytes (position + flags)
[Track Data: trackLen - 128 bytes]
└─ Raw MFM-encoded data
struct MSXBootSector {
uint8_t jumpCode[3]; // +0: 0xE5 or 0xEB
char name[8]; // +3: OEM name
uint16_t bytesPerSector; // +11: always 512
uint8_t sectorsPerCluster;// +13
uint16_t reservedSectors; // +14
uint8_t numFATs; // +16
uint16_t dirEntries; // +17
uint16_t totalSectors; // +19
uint8_t mediaDescriptor; // +21
uint16_t sectorsPerFAT; // +22
uint16_t sectorsPerTrack; // +24
uint16_t numSides; // +26
// ... additional fields
};| Descriptor | 0xF8 | 0xF9 | 0xFA | 0xFB | 0xFC | 0xFD | 0xFE | 0xFF |
|---|---|---|---|---|---|---|---|---|
| Sectors/Track | 9 | 9 | 8 | 8 | 9 | 9 | 8 | 8 |
| Sides | 1 | 2 | 1 | 2 | 1 | 2 | 1 | 2 |
| Tracks/Side | 80 | 80 | 80 | 80 | 40 | 40 | 40 | 40 |
- Header Magic:
'P' 'C' 'K' '\010' - Algorithm: LZ77 with Huffman coding
- Structure:
- 4 bytes: original length (LE)
- 4 bytes: compressed length (skipped)
- null-terminated filename (skipped)
- compressed data stream
DiskImage (abstract base)
├── AppleDiskImage (abstract)
│ ├── AppleDOImage (.do, .dsk - DOS order)
│ ├── ApplePOImage (.po - ProDOS order)
│ ├── AppleNibImage (.nib - 6656 bytes/track)
│ ├── AppleNib2Image (.nb2 - 6384 bytes/track)
│ ├── AppleWoz1Image (.woz - WOZ v1)
│ └── AppleWoz2Image (.woz - WOZ v2)
│
├── MSXDiskImage (abstract)
│ ├── MSXDSKImage (.dsk, .di1, .di2, .fd1, .fd2)
│ ├── MSXDMKImage (.dmk - raw track)
│ └── MSXXSAImage (.xsa - compressed)
│
└── Utilities
├── DiskImageFactory (format detection & creation)
├── NibbleEncoder (6-and-2 GCR for Apple II)
├── MFMEncoder (MFM encoding for MSX)
├── XSACompressor (LZ77 compression)
└── CRCCalculator (CRC-16, CRC-32)
class DiskImage {
public:
virtual ~DiskImage() = default;
// Identification
virtual std::string getFormatName() const = 0;
virtual std::string getPlatform() const = 0; // "AppleII" or "MSX"
// Properties
virtual size_t getTrackCount() const = 0;
virtual size_t getSideCount() const = 0;
virtual size_t getSectorsPerTrack() const = 0;
virtual size_t getBytesPerSector() const = 0;
virtual size_t getTotalSize() const = 0;
virtual bool isWriteProtected() const = 0;
// I/O Operations
virtual bool open(const std::string& path) = 0;
virtual bool save(const std::string& path) = 0;
virtual bool close() = 0;
// Sector Access
virtual bool readSector(int track, int side, int sector,
std::vector<uint8_t>& buffer) = 0;
virtual bool writeSector(int track, int side, int sector,
const std::vector<uint8_t>& buffer) = 0;
// Raw Track Access (optional)
virtual bool hasRawTrackSupport() const { return false; }
virtual bool readRawTrack(int track, int side,
std::vector<uint8_t>& buffer) { return false; }
virtual bool writeRawTrack(int track, int side,
const std::vector<uint8_t>& buffer) { return false; }
// Metadata
virtual std::string getMetadata() const = 0;
};class DiskImageFactory {
public:
// Auto-detect format from file content
static std::unique_ptr<DiskImage> open(const std::string& path);
// Create new blank disk
static std::unique_ptr<DiskImage> create(
const std::string& format, // "do", "po", "nib", "woz1", "woz2", "msx-dsk", "dmk"
const std::string& path,
size_t tracks = 0, // 0 = default for format
size_t sides = 0
);
// Format conversion
static bool convert(
const DiskImage& source,
const std::string& targetFormat,
const std::string& targetPath
);
// List supported formats
static std::vector<std::string> getSupportedFormats();
};Purpose: DO/PO 형식과 NIB/WOZ 형식 간 변환
Key Functions:
class AppleNibbleEncoder {
public:
// Sector encoding (256 bytes → 343 nibbles)
static std::vector<uint8_t> encodeSector(
const std::vector<uint8_t>& data, // 256 bytes
uint8_t volume,
uint8_t track,
uint8_t sector
);
// Sector decoding (343 nibbles → 256 bytes)
static std::vector<uint8_t> decodeSector(
const std::vector<uint8_t>& nibbles,
uint8_t& volume,
uint8_t& track,
uint8_t& sector
);
// Track encoding (4096 bytes → ~6600 nibbles)
static std::vector<uint8_t> encodeTrack(
const std::vector<uint8_t>& trackData, // 16 × 256 bytes
uint8_t volume,
uint8_t trackNum,
SectorOrder order
);
// Track decoding (~6600 nibbles → 4096 bytes)
static std::vector<uint8_t> decodeTrack(
const std::vector<uint8_t>& nibbles,
SectorOrder order
);
private:
static const uint8_t DISK_BYTE_TABLE[64]; // 6-bit to 8-bit
static const uint8_t REVERSE_TABLE[256]; // 8-bit to 6-bit
static std::vector<uint8_t> code62(const std::vector<uint8_t>& data);
static std::vector<uint8_t> decode62(const std::vector<uint8_t>& nibbles);
};Purpose: DSK 형식과 DMK raw track 간 변환
Key Functions:
class MSXMFMEncoder {
public:
// Generate raw track from sectors
static std::vector<uint8_t> encodeTrack(
const std::vector<std::vector<uint8_t>>& sectors, // 9 × 512 bytes
uint8_t track,
uint8_t side
);
// Extract sectors from raw track
static std::vector<std::vector<uint8_t>> decodeTrack(
const std::vector<uint8_t>& rawTrack,
std::vector<int>& idamPositions
);
// CRC calculation (CCITT)
static uint16_t calculateCRC(const std::vector<uint8_t>& data);
private:
static const size_t TRACK_SIZE = 6250;
static const size_t SECTOR_SIZE = 512;
static const size_t SECTORS_PER_TRACK = 9;
};Purpose: XSA 압축 형식 읽기/쓰기
Key Functions:
class XSACompressor {
public:
// Decompress XSA to raw sectors
static std::vector<uint8_t> decompress(const std::vector<uint8_t>& compressed);
// Compress raw sectors to XSA
static std::vector<uint8_t> compress(
const std::vector<uint8_t>& data,
const std::string& filename = ""
);
// Check if data is XSA format
static bool isXSAFormat(const std::vector<uint8_t>& data);
private:
static const uint8_t MAGIC[4]; // 'P', 'C', 'K', '\010'
// LZ77 implementation
static void lz77Decompress(/*...*/);
static void lz77Compress(/*...*/);
// Huffman coding
struct HuffNode { /*...*/ };
static void buildHuffTree(/*...*/);
};Purpose: WOZ v1/v2 파일 읽기/쓰기
Key Structures:
struct WOZHeader {
uint32_t id1; // 'WOZ1' or 'WOZ2'
uint32_t id2; // 0x0A0D0AFF
uint32_t crc32;
};
struct WOZInfoChunk {
uint8_t version;
uint8_t diskType; // 1 = 5.25", 2 = 3.5"
uint8_t writeProtected;
uint8_t synchronized;
uint8_t cleaned;
char creator[32];
};
struct WOZTrackV1 {
uint16_t bytesUsed;
uint16_t bitCount;
uint16_t splicePoint;
uint8_t spliceNibble;
uint8_t spliceBitCount;
// followed by track data
};
struct WOZTrackV2 {
uint16_t startBlock;
uint16_t blockCount;
uint32_t bitCount;
};
class WOZHandler {
public:
bool open(const std::string& path);
bool save(const std::string& path);
uint8_t getVersion() const;
bool isWriteProtected() const;
std::string getCreator() const;
// Track access (quarter-track precision)
bool readTrack(float quarterTrack, std::vector<uint8_t>& data,
uint32_t& bitCount);
bool writeTrack(float quarterTrack, const std::vector<uint8_t>& data,
uint32_t bitCount);
// Metadata
std::string getMetadata(const std::string& key) const;
void setMetadata(const std::string& key, const std::string& value);
private:
WOZHeader header;
WOZInfoChunk info;
std::vector<uint8_t> tmap; // 160 entries
std::vector<std::vector<uint8_t>> tracks;
};# Display disk image information
$ rdedisktool info game.dsk
Format: Apple II DOS Order (.dsk)
Tracks: 35, Sides: 1, Sectors/Track: 16
Sector Size: 256 bytes, Total: 143,360 bytes
Write Protected: No
# Convert formats
$ rdedisktool convert game.dsk game.woz --format=woz2
Converting: DOS Order → WOZ v2
Done: game.woz (245,760 bytes)
$ rdedisktool convert game.nib game.po --format=po
Converting: Nibble → ProDOS Order
Done: game.po (143,360 bytes)
# Create blank disk
$ rdedisktool create blank.dsk --format=do --tracks=35
Created: blank.dsk (143,360 bytes, DOS 3.3 formatted)
$ rdedisktool create msx.dsk --format=msx-dsk --size=720k
Created: msx.dsk (737,280 bytes, MSX-DOS formatted)
# Read/write sectors
$ rdedisktool read game.dsk 17 0 0 --output=sector.bin
Read track 17, side 0, sector 0 → sector.bin (256 bytes)
$ rdedisktool write game.dsk 17 0 0 --input=sector.bin
Wrote sector.bin → track 17, side 0, sector 0
# Hexdump sector
$ rdedisktool dump game.dsk 17 0 0
Track 17, Side 0, Sector 0:
0000: 11 0F 03 00 00 00 FE 00 00 00 00 00 00 00 00 00
0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
...
# List files (filesystem aware)
$ rdedisktool ls game.dsk
Volume: GAME DISK
Files:
HELLO A 002 $0801
GAME.OBJ B 045 $2000
DATA B 012 $6000
# Extract files
$ rdedisktool extract game.dsk HELLO --output=hello.bas
Extracted: HELLO → hello.bas (512 bytes)
# Compare images
$ rdedisktool compare game1.dsk game2.dsk
Comparing game1.dsk and game2.dsk...
Track 17, Sector 0: DIFFERENT
Track 17, Sector 1: DIFFERENT
2 sectors differ
# Verify integrity
$ rdedisktool verify game.woz
Checking WOZ integrity...
Header CRC: OK
Track 0: 6250 bits, OK
Track 1: 6248 bits, OK
...
All tracks verified.
# Show version
$ rdedisktool --version
rdedisktool v1.0.0 - Retro Developer Environment Disk Toolrdedisktool <command> [options] <arguments>
Commands:
info Display disk image information
convert Convert between disk image formats
create Create a new blank disk image
read Read a sector to file
write Write a file to sector
dump Hexdump a sector
ls List files (DOS 3.3, ProDOS, MSX-DOS)
extract Extract file from disk image
insert Insert file into disk image
rename Rename file or directory
compare Compare two disk images
verify Verify disk image integrity
format Format disk with filesystem
Options:
--format=<fmt> Target format (do, po, nib, woz1, woz2, msx-dsk, dmk)
--tracks=<n> Number of tracks (default: format-specific)
--sides=<n> Number of sides (1 or 2)
--size=<size> Disk size (360k, 720k, 1.44m)
--output=<file> Output file path
--input=<file> Input file path
--force Overwrite without confirmation
--verbose Verbose output
--version Show version information
--help Show help
- VTOC (Volume Table of Contents): Track 17, Sector 0
- Catalog: Track 17, Sectors 1-15
- File Types: T (Text), I (Integer BASIC), A (Applesoft BASIC), B (Binary), R (Relocatable), S (S-type), A/B (A/B)
- Volume Directory: Block 2-5
- File Types: TXT, BIN, BAS, SYS, etc.
- Block Size: 512 bytes
- Boot Sector: Sector 0
- FAT: Sector 1-2
- Root Directory: Sector 3-9
- 12-bit FAT entries
- Extended boot sector
- Subdirectory support
- Long filename support (optional)
- DiskImage abstract base class
- File I/O utilities
- CRC calculation (CRC-16 CCITT, CRC-32)
- Basic error handling
- DO/DSK format read/write
- PO format read/write
- 6-and-2 GCR nibble encoder/decoder
- NIB format read/write
- WOZ v1 format read/write
- WOZ v2 format read/write
- DSK format read/write
- DMK format read/write
- MFM encoder/decoder
- XSA decompression
- XSA compression (full bidirectional support)
- DO ↔ PO conversion
- DSK ↔ DMK conversion
- XSA ↔ DSK/DMK bidirectional conversion
- Command-line parser
- info command
- convert command
- create command
- read/write/dump commands
- list command
- add/extract/delete commands
- mkdir/rmdir commands
- validate command
- DOS 3.3 catalog parsing & file operations
- DOS 3.3 extended validation
- ProDOS directory parsing & file operations
- ProDOS subdirectory support
- MSX-DOS FAT parsing & file operations
- MSX-DOS subdirectory support
- File extraction (all file systems)
- File insertion (all file systems)
- File deletion (all file systems)
- File rename (all file systems)
- Integrity verification (validate command)
- Extended validation (DOS 3.3, ProDOS, MSX-DOS)
- Apple II: Little-endian
- MSX: Little-endian
- WOZ format: Little-endian
- DMK format: Little-endian
- C++17 or later
- Cross-platform (Windows, macOS, Linux)
- No external dependencies for core functionality
- Optional: zlib for gzip/zip support
enum class DiskError {
None,
FileNotFound,
InvalidFormat,
ReadError,
WriteError,
WriteProtected,
SectorNotFound,
CRCError,
UnsupportedFormat,
ConversionError
};
class DiskException : public std::exception {
DiskError error;
std::string message;
public:
DiskError getError() const;
const char* what() const noexcept override;
};- Use RAII and smart pointers
- Lazy loading for large images
- Memory-mapped I/O option for performance
- Sector caching for frequent access
- Nibble encoder/decoder
- MFM encoder/decoder
- CRC calculations
- Sector order conversions
- XSA compression/decompression
- Round-trip format conversion
- File extraction and verification
- Compatibility with AppleWin/openMSX
- Standard blank disks
- Known game images (public domain)
- Edge cases (protected disks, non-standard formats)
Emulator/AppleWin/source/DiskDefs.h- Core constantsEmulator/AppleWin/source/DiskImageHelper.h/cpp- Format implementationsEmulator/AppleWin/source/Disk.h/cpp- Drive emulation
Emulator/openMSX/src/fdc/Disk.hh/cc- Disk abstractionEmulator/openMSX/src/fdc/DMKDiskImage.hh/cc- DMK formatEmulator/openMSX/src/fdc/XSAExtractor.hh/cc- XSA decompression
- WOZ 2.0 Disk Image Reference
- DMK Format Documentation
- Apple II DOS 3.3 Technical Reference
- MSX-DOS Technical Handbook
RetroDeveloperEnvironmentDisktool/
├── include/
│ ├── rdedisktool/
│ │ ├── DiskImage.h
│ │ ├── DiskImageFactory.h
│ │ ├── AppleDiskImage.h
│ │ ├── MSXDiskImage.h
│ │ ├── NibbleEncoder.h
│ │ ├── MFMEncoder.h
│ │ ├── XSACompressor.h
│ │ ├── WOZHandler.h
│ │ ├── CRC.h
│ │ ├── Version.h
│ │ └── Exceptions.h
│ └── rdedisktool.h
├── src/
│ ├── core/
│ │ ├── DiskImage.cpp
│ │ ├── DiskImageFactory.cpp
│ │ └── CRC.cpp
│ ├── apple/
│ │ ├── AppleDOImage.cpp
│ │ ├── ApplePOImage.cpp
│ │ ├── AppleNibImage.cpp
│ │ ├── AppleWozImage.cpp
│ │ └── NibbleEncoder.cpp
│ ├── msx/
│ │ ├── MSXDSKImage.cpp
│ │ ├── MSXDMKImage.cpp
│ │ ├── MSXXSAImage.cpp
│ │ ├── MFMEncoder.cpp
│ │ └── XSACompressor.cpp
│ └── cli/
│ └── main.cpp
├── tests/
│ ├── test_nibble.cpp
│ ├── test_mfm.cpp
│ ├── test_woz.cpp
│ ├── test_xsa.cpp
│ └── test_conversion.cpp
├── docs/
│ └── Disktool_Development_Plan.md
├── CMakeLists.txt
└── README.md
cmake_minimum_required(VERSION 3.16)
project(rdedisktool VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Version information
configure_file(
"${CMAKE_SOURCE_DIR}/include/rdedisktool/Version.h.in"
"${CMAKE_SOURCE_DIR}/include/rdedisktool/Version.h"
)
# Library target
add_library(rdedisktool_lib STATIC
src/core/DiskImage.cpp
src/core/DiskImageFactory.cpp
src/core/CRC.cpp
src/apple/AppleDOImage.cpp
src/apple/ApplePOImage.cpp
src/apple/AppleNibImage.cpp
src/apple/AppleWozImage.cpp
src/apple/NibbleEncoder.cpp
src/msx/MSXDSKImage.cpp
src/msx/MSXDMKImage.cpp
src/msx/MSXXSAImage.cpp
src/msx/MFMEncoder.cpp
src/msx/XSACompressor.cpp
)
target_include_directories(rdedisktool_lib PUBLIC
${CMAKE_SOURCE_DIR}/include
)
# Executable target - Binary name: rdedisktool
add_executable(rdedisktool src/cli/main.cpp)
target_link_libraries(rdedisktool PRIVATE rdedisktool_lib)
# Installation
install(TARGETS rdedisktool DESTINATION bin)
install(DIRECTORY include/rdedisktool DESTINATION include)
# Testing (optional)
option(BUILD_TESTS "Build test suite" OFF)
if(BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()#ifndef RDEDISKTOOL_VERSION_H
#define RDEDISKTOOL_VERSION_H
#define RDEDISKTOOL_NAME "rdedisktool"
#define RDEDISKTOOL_FULL_NAME "Retro Developer Environment Disk Tool"
#define RDEDISKTOOL_VERSION "@PROJECT_VERSION@"
#define RDEDISKTOOL_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define RDEDISKTOOL_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define RDEDISKTOOL_VERSION_PATCH @PROJECT_VERSION_PATCH@
#endif // RDEDISKTOOL_VERSION_HLinux 환경에서 크로스 툴체인으로 빌드한 바이너리 파일이나 텍스트 파일 등을 Apple II 및 MSX 디스크 이미지에 삽입하는 기능
주요 사용 사례:
- cc65로 빌드한 Apple II 바이너리를 .dsk/.po/.woz 이미지에 삽입
- SDCC/z88dk로 빌드한 MSX 바이너리를 .dsk/.dmk 이미지에 삽입
- 어셈블러로 생성한 .bin 파일 삽입
- 텍스트/데이터 파일 삽입
| 파일 시스템 | 구현 상태 | 필요 작업 |
|---|---|---|
| ProDOS | 완전 구현됨 | AppleWin의 Util_ProDOS_AddFile() 참조 |
| DOS 3.3 | 부분 구현 | VTOC/Catalog 조작 함수 추가 필요 |
ProDOS 파일 삽입 (AppleWin 소스 참조):
- 핵심 함수:
ProDOS_Utils.cpp-Util_ProDOS_AddFile() - 블록 할당:
ProDOS_BlockGetFirstFree(),ProDOS_BlockSetUsed() - 저장 타입: SEED (≤512B), SAPL (≤128KB), TREE (>128KB)
DOS 3.3 파일 삽입 (추가 구현 필요):
- VTOC 비트맵 조작
- Catalog 엔트리 생성
- Track/Sector 체인 구성
| 파일 시스템 | 구현 상태 | 필요 작업 |
|---|---|---|
| MSX-DOS 1/2 | 완전 구현됨 | openMSX의 MSXtar 클래스 참조 |
| FAT12/FAT16 | 완전 구현됨 | DirAsDSK/MSXtar 로직 활용 |
MSX-DOS 파일 삽입 (openMSX 소스 참조):
- 핵심 클래스:
MSXtar.cc-addFileToDSK(),alterFileInDSK() - FAT 조작:
readFAT(),writeFAT() - 클러스터 할당:
findFirstFreeCluster(),findNextFreeCluster()
Offset Size Description
------ ------ ----------------------------------------
0x00 1 (unused)
0x01 1 Catalog Track (usually 0x11)
0x02 1 Catalog Sector (usually 0x0F)
0x03 1 DOS Version
0x06 1 Volume Number (default: 254)
0x27 1 Max Track/Sector pairs per T/S list
0x30 1 Last Track Allocated
0x31 1 Allocation Direction (+1 or -1)
0x34 1 Number of Tracks
0x35 1 Sectors per Track (16)
0x36-37 2 Bytes per Sector (256)
0x38+ 4*N Track Bitmap (4 bytes per track)
Track Bitmap (per track, 4 bytes):
Byte 0: Sectors F-8 free bitmap (bit=1: free)
Byte 1: Sectors 7-0 free bitmap (bit=1: free)
Byte 2-3: Reserved (0x00)
Offset Size Description
------ ------ ----------------------------------------
0x00 1 Track of first T/S list sector
0x01 1 Sector of first T/S list sector
0x02 1 File type + flags
Bit 7: Deleted flag
Bit 0-6: Type (T=0x00, I=0x01, A=0x02, B=0x04)
0x03-20 30 Filename (padded with spaces)
0x21-22 2 Length in sectors
File Types:
| Code | Type | Description |
|---|---|---|
| 0x00 | T | Text file |
| 0x01 | I | Integer BASIC |
| 0x02 | A | Applesoft BASIC |
| 0x04 | B | Binary file |
| 0x08 | S | S-type file |
| 0x10 | R | Relocatable |
| 0x20 | (new A) | New A file |
| 0x40 | (new B) | New B file |
Offset Size Description
------ ------ ----------------------------------------
0x00 1 (unused)
0x01 1 Next T/S list Track (0 if last)
0x02 1 Next T/S list Sector
0x05-06 2 Sector offset in file
0x0C+ 2*N Track/Sector pairs (122 max per sector)
Offset Size Description
------ ------ ----------------------------------------
0x00-01 2 Previous directory block
0x02-03 2 Next directory block
0x04 1 Storage type (0xF0) + name length
0x05-13 15 Volume name
0x1C-1D 2 Creation date
0x1E-1F 2 Creation time
0x20 1 Version
0x21 1 Min version
0x22 1 Access rights
0x23 1 Entry length (0x27 = 39 bytes)
0x24 1 Entries per block (13)
0x25-26 2 Active file count
0x27-28 2 Bitmap block pointer
0x29-2A 2 Total blocks
Offset Size Description
------ ------ ----------------------------------------
0x00 1 Storage type + name length
0x1: Seedling (1 block)
0x2: Sapling (≤256 blocks)
0x3: Tree (>256 blocks)
0xD: Subdirectory
0x01-0F 15 Filename
0x10 1 File type
0x11-12 2 Key block pointer
0x13-14 2 Blocks used
0x15-17 3 EOF (file size)
0x18-19 2 Creation date
0x1A-1B 2 Creation time
0x1C 1 Version
0x1D 1 Min version
0x1E 1 Access
0x1F-20 2 Aux type (load address for BIN)
0x21-22 2 Last modified date
0x23-24 2 Last modified time
0x25-26 2 Header pointer
ProDOS File Types (common):
| Code | Type | Description |
|---|---|---|
| 0x00 | UNK | Unknown |
| 0x04 | TXT | Text file |
| 0x06 | BIN | Binary file |
| 0xFC | BAS | BASIC program |
| 0xFF | SYS | System file |
Offset Size Description
------ ------ ----------------------------------------
0x00-02 3 Jump code (0xEB, 0xFE, 0x90)
0x03-0A 8 OEM name ("NMS 2.0P")
0x0B-0C 2 Bytes per sector (512)
0x0D 1 Sectors per cluster (2)
0x0E-0F 2 Reserved sectors (1)
0x10 1 Number of FATs (2)
0x11-12 2 Root directory entries (112)
0x13-14 2 Total sectors
0x15 1 Media descriptor (0xF9=2-sided, 0xF8=1-sided)
0x16-17 2 Sectors per FAT
0x18-19 2 Sectors per track (9)
0x1A-1B 2 Number of sides (2)
0x1C-1D 2 Hidden sectors (0)
Media Descriptor Table:
| Value | Sides | Tracks | Sectors/Track |
|---|---|---|---|
| 0xF8 | 1 | 80 | 9 |
| 0xF9 | 2 | 80 | 9 |
| 0xFA | 1 | 80 | 8 |
| 0xFB | 2 | 80 | 8 |
| 0xFC | 1 | 40 | 9 |
| 0xFD | 2 | 40 | 9 |
| 0xFE | 1 | 40 | 8 |
| 0xFF | 2 | 40 | 8 |
FAT Entry Encoding (12-bit):
// Reading FAT12 entry
offset = (cluster * 3) / 2;
if (cluster & 1) { // Odd cluster
value = (fat[offset] >> 4) | (fat[offset+1] << 4);
} else { // Even cluster
value = fat[offset] | ((fat[offset+1] & 0x0F) << 8);
}FAT Values:
| Value | Meaning |
|---|---|
| 0x000 | Free cluster |
| 0x001 | Reserved |
| 0x002-0xFF6 | Next cluster in chain |
| 0xFF7 | Bad cluster |
| 0xFF8-0xFFF | End of chain |
Offset Size Description
------ ------ ----------------------------------------
0x00-07 8 Filename (space-padded)
0x08-0A 3 Extension (space-padded)
0x0B 1 Attributes
0x01: Read-only
0x02: Hidden
0x04: System
0x08: Volume label
0x10: Directory
0x20: Archive
0x0C-15 10 Reserved
0x16-17 2 Time (DOS format)
Bits 0-4: Seconds/2
Bits 5-10: Minutes
Bits 11-15: Hours
0x18-19 2 Date (DOS format)
Bits 0-4: Day
Bits 5-8: Month
Bits 9-15: Year-1980
0x1A-1B 2 Start cluster
0x1C-1F 4 File size (bytes)
Special Filename Bytes:
| Value | Meaning |
|---|---|
| 0x00 | Entry never used |
| 0xE5 | Deleted entry |
| 0x05 | First char is 0xE5 |
class FileSystemHandler {
public:
virtual ~FileSystemHandler() = default;
// File operations
virtual bool addFile(const std::string& hostPath,
const std::string& targetName,
const FileMetadata& meta) = 0;
virtual bool deleteFile(const std::string& targetName) = 0;
virtual bool extractFile(const std::string& targetName,
const std::string& hostPath) = 0;
virtual std::vector<FileEntry> listFiles() = 0;
// Disk operations
virtual size_t getFreeSpace() const = 0;
virtual size_t getTotalSpace() const = 0;
virtual bool format(const FormatOptions& options) = 0;
};
struct FileMetadata {
std::string filename; // Target filename
FileType type; // Platform-specific file type
uint16_t loadAddress; // Load address (for binary)
uint16_t execAddress; // Execution address (optional)
bool readOnly; // Read-only flag
bool hidden; // Hidden flag
std::optional<time_t> timestamp; // File timestamp
};class AppleDOS33Handler : public FileSystemHandler {
private:
std::vector<uint8_t>& diskImage;
// VTOC operations
bool readVTOC(VTOC& vtoc);
bool writeVTOC(const VTOC& vtoc);
bool allocateSector(uint8_t& track, uint8_t& sector);
bool freeSector(uint8_t track, uint8_t sector);
// Catalog operations
int findFreeCatalogEntry();
bool writeCatalogEntry(int index, const CatalogEntry& entry);
// T/S List operations
bool buildTSList(const std::vector<uint8_t>& data,
std::vector<TSPair>& tsList);
public:
bool addFile(const std::string& hostPath,
const std::string& targetName,
const FileMetadata& meta) override;
};
class AppleProDOSHandler : public FileSystemHandler {
private:
std::vector<uint8_t>& diskImage;
// Block operations
int findFreeBlock();
bool markBlockUsed(int block);
bool markBlockFree(int block);
// Directory operations
int findFreeDirectoryEntry(int dirBlock);
bool writeDirectoryEntry(int dirBlock, int index,
const ProDOSEntry& entry);
// Index block operations
bool buildIndexBlocks(const std::vector<uint8_t>& data,
int& keyBlock, int& blocksUsed);
public:
bool addFile(const std::string& hostPath,
const std::string& targetName,
const FileMetadata& meta) override;
};class MSXDOSHandler : public FileSystemHandler {
private:
std::vector<uint8_t>& diskImage;
BootSector bootSector;
// FAT operations
uint16_t readFAT12(uint16_t cluster);
void writeFAT12(uint16_t cluster, uint16_t value);
uint16_t findFreeCluster();
uint16_t findNextFreeCluster(uint16_t current);
void freeClusterChain(uint16_t start);
// Directory operations
int findFreeDirEntry(int dirSector);
bool writeDirEntry(int sector, int index, const MSXDirEntry& entry);
// Cluster/Sector conversion
int clusterToSector(uint16_t cluster);
uint16_t sectorToCluster(int sector);
public:
bool addFile(const std::string& hostPath,
const std::string& targetName,
const FileMetadata& meta) override;
};# Add binary file to Apple II ProDOS disk
$ rdedisktool merge game.po myprogram.bin MYPROGRAM \
--type=BIN --load=0x2000 --exec=0x2000
Adding: myprogram.bin -> MYPROGRAM (BIN, load=$2000)
Allocated 12 blocks, file size: 5632 bytes
Done.
# Add binary file to Apple II DOS 3.3 disk
$ rdedisktool merge game.dsk hello.bin HELLO \
--type=B --load=0x0803
Adding: hello.bin -> HELLO (Binary)
Allocated 3 sectors (T:18,S:0 -> T:18,S:2)
Done.
# Add file to MSX disk
$ rdedisktool merge game.dsk program.com PROGRAM.COM
Adding: program.com -> PROGRAM.COM
Allocated clusters: 2-5 (4 clusters, 4096 bytes)
Done.
# Add text file
$ rdedisktool merge disk.dsk readme.txt README.TXT --type=TXT
Adding: readme.txt -> README.TXT (Text)
Done.# Merge multiple files from manifest
$ rdedisktool merge-batch game.po manifest.txt
# manifest.txt format:
# <host_file> <target_name> [options]
# myprogram.bin MYPROGRAM --type=BIN --load=0x2000
# data.dat DATA --type=BIN --load=0x4000
# readme.txt README --type=TXT# Sync host directory to MSX disk (like DirAsDSK)
$ rdedisktool sync ./build/ game.dsk --mode=full
Syncing directory: ./build/ -> game.dsk
Added: GAME.COM (8192 bytes)
Added: DATA.DAT (1024 bytes)
Updated: CONFIG.TXT (256 bytes)
Sync complete: 3 files, 9472 bytes total
# One-way sync (host -> disk only)
$ rdedisktool sync ./build/ game.dsk --mode=pushrdedisktool merge <disk_image> <host_file> <target_name> [options]
Options:
--type=<type> File type (platform-specific)
Apple II DOS 3.3: T, I, A, B, S, R
Apple II ProDOS: TXT, BIN, BAS, SYS, etc.
MSX: (auto-detected from extension)
--load=<addr> Load address (hex, e.g., 0x2000)
--exec=<addr> Execution address (hex, optional)
--readonly Mark file as read-only
--hidden Mark file as hidden
--force Overwrite if file exists
--timestamp=<ts> Set file timestamp (ISO 8601 or 'now')
rdedisktool merge-batch <disk_image> <manifest_file>
rdedisktool sync <host_dir> <disk_image> [options]
Options:
--mode=<mode> Sync mode: push, pull, full (default: push)
--delete Delete files not in source
--exclude=<pat> Exclude files matching pattern
1. Read VTOC (Track 17, Sector 0)
2. Find free Catalog entry
- Start from Track 17, Sector 15
- Follow chain until empty slot found
3. Calculate required sectors
- Data sectors: ceil(fileSize / 256)
- T/S List sectors: ceil(dataSectors / 122)
4. Allocate sectors
- Use VTOC bitmap to find free sectors
- Mark allocated sectors in VTOC
- Build T/S List chain
5. Write file data
- Copy data to allocated sectors
- For Binary: prepend 2-byte load address + 2-byte length
6. Create Catalog entry
- Set filename, type, first T/S List T/S
- Set sector count
7. Write VTOC back
1. Read Volume Header (Block 2)
2. Find free directory entry
- Scan directory blocks for empty slot (storage_type=0)
3. Determine storage type
- ≤512 bytes: Seedling (type 0x1)
- 513-131,072 bytes: Sapling (type 0x2)
- >131,072 bytes: Tree (type 0x3)
4. Allocate blocks
- Scan bitmap for free blocks
- Mark blocks as used
- For Sapling: create index block
- For Tree: create master index + sub-index blocks
5. Write file data
- Copy data to allocated blocks
- Update index blocks
6. Create directory entry
- Set storage_type, filename, file_type
- Set aux_type (load address for BIN)
- Set key_block, blocks_used, EOF
- Set timestamps
7. Update volume header
- Increment file count
1. Read Boot Sector
- Parse BPB for disk geometry
2. Find free directory entry
- Scan root directory (or subdirectory)
- Look for entry starting with 0x00 or 0xE5
3. Calculate required clusters
- clusters = ceil(fileSize / (sectorsPerCluster * 512))
4. Build FAT chain
- Find first free cluster
- For each additional cluster:
- Find next free cluster
- Link previous cluster to current
- Mark last cluster as EOF (0xFFF)
5. Write file data
- For each cluster:
- Calculate sector number
- Write data to sectors
6. Create directory entry
- Convert filename to 8.3 format (uppercase)
- Set start cluster
- Set file size
- Set timestamp (current or specified)
- Set attributes
7. Write FAT to both copies
# Build with cc65
$ cl65 -t apple2 -o myprogram.bin myprogram.c
# Merge to ProDOS disk
$ rdedisktool merge mydisk.po myprogram.bin MYPROGRAM \
--type=BIN --load=0x803
# Or with DOS 3.3
$ rdedisktool merge mydisk.dsk myprogram.bin MYPROGRAM \
--type=B --load=0x803# Build with SDCC for MSX
$ sdcc -mz80 --code-loc 0x100 --data-loc 0 -o myprogram.ihx myprogram.c
$ hex2bin myprogram.ihx
# Or with z88dk
$ zcc +msx -create-app -o myprogram.com myprogram.c
# Merge to MSX disk
$ rdedisktool merge mydisk.dsk myprogram.com MYPROGRAM.COM# Example Makefile for Apple II project
DISK = game.po
CC65 = cl65
RDEDISKTOOL = rdedisktool
all: $(DISK)
%.bin: %.c
$(CC65) -t apple2 -o $@ $<
$(DISK): game.bin data.bin
$(RDEDISKTOOL) create $(DISK) --format=po --size=140k
$(RDEDISKTOOL) merge $(DISK) game.bin GAME --type=BIN --load=0x2000
$(RDEDISKTOOL) merge $(DISK) data.bin DATA --type=BIN --load=0x4000
clean:
rm -f *.bin $(DISK)- FileSystemHandler abstract interface
- FileMetadata and FileEntry structures
- Platform detection from disk image
- Filename conversion utilities (8.3, ProDOS format)
- VTOC read/write
- Sector allocation/deallocation
- Catalog entry management
- T/S List chain building
- Binary file header handling (load address)
-
addcommand for DOS 3.3 - Extended validation (VTOC, catalog chain, T/S lists, bitmap consistency)
- Volume header read/write
- Block bitmap management
- Directory entry management
- Index block building (Seedling/Sapling/Tree)
- Timestamp conversion
-
addcommand for ProDOS - Subdirectory support (mkdir/rmdir)
- Boot sector parsing
- FAT12 read/write
- Cluster allocation/deallocation
- Directory entry management (root + subdirectories)
- DOS timestamp conversion
-
addcommand for MSX-DOS - Subdirectory support (mkdir/rmdir)
- File extraction (
extractcommand) - Subdirectory support (ProDOS, MSX-DOS)
- Integration tests with emulators (AppleWin, openMSX)
- Edge cases (full disk, file deletion, rename)
- ProDOS file deletion bug fixed
- DOS 3.3 sector count validation fixed
| Feature | File | Key Functions |
|---|---|---|
| ProDOS structures | ProDOS_FileSystem.h |
Struct definitions |
| ProDOS file add | ProDOS_Utils.cpp |
Util_ProDOS_AddFile() |
| Block allocation | ProDOS_Utils.cpp |
ProDOS_BlockGetFirstFree() |
| Block marking | ProDOS_Utils.cpp |
ProDOS_BlockSetUsed() |
| Index blocks | ProDOS_Utils.cpp |
ProDOS_PutIndexBlock() |
| File header | ProDOS_Utils.cpp |
ProDOS_PutFileHeader() |
| Feature | File | Key Functions |
|---|---|---|
| Boot sector | DiskImageUtils.hh |
MSXBootSector struct |
| Directory entry | DiskImageUtils.hh |
MSXDirEntry struct |
| FAT read/write | DirAsDSK.cc |
readFATHelper(), writeFATHelper() |
| Cluster allocation | DirAsDSK.cc |
findFirstFreeCluster() |
| File import | DirAsDSK.cc |
importHostFile() |
| MSXtar file add | MSXtar.cc |
addFileToDSK() |
| MSXtar FAT ops | MSXtar.cc |
readFAT(), writeFAT() |
| File data copy | MSXtar.cc |
alterFileInDSK() |
src/
├── filesystem/
│ ├── FileSystemHandler.h # Abstract interface
│ ├── FileSystemHandler.cpp
│ ├── FileMetadata.h # File metadata structures
│ ├── apple/
│ │ ├── AppleDOS33Handler.h
│ │ ├── AppleDOS33Handler.cpp # DOS 3.3 implementation
│ │ ├── AppleProDOSHandler.h
│ │ ├── AppleProDOSHandler.cpp # ProDOS implementation
│ │ ├── AppleDOS33Defs.h # DOS 3.3 constants & structures
│ │ └── ProDOSDefs.h # ProDOS constants & structures
│ └── msx/
│ ├── MSXDOSHandler.h
│ ├── MSXDOSHandler.cpp # MSX-DOS implementation
│ ├── MSXFATUtils.h # FAT12/FAT16 utilities
│ ├── MSXFATUtils.cpp
│ └── MSXDOSDefs.h # MSX-DOS constants & structures
├── cli/
│ ├── MergeCommand.h
│ ├── MergeCommand.cpp # merge command implementation
│ ├── MergeBatchCommand.h
│ ├── MergeBatchCommand.cpp # merge-batch command
│ ├── SyncCommand.h
│ └── SyncCommand.cpp # sync command implementation
└── utils/
├── FilenameConverter.h # 8.3, ProDOS name conversion
├── FilenameConverter.cpp
├── TimestampUtils.h # DOS/ProDOS timestamp conversion
└── TimestampUtils.cpp
To be determined based on project requirements. Consider:
- MIT License (permissive)
- GPL v2/v3 (copyleft, matching AppleWin)
- BSD 3-Clause
Document Version: 1.3
Created: 2025-12-31
Updated: 2025-12-31 - Binary name specified as rdedisktool
Updated: 2025-12-31 - Added File Merge Feature (Section 12) for cross-platform binary injection
Updated: 2026-01-02 - Updated implementation status (v1.3)
- All phases COMPLETE
- XSA compression: Full bidirectional support implemented
- Extended validation: DOS 3.3, ProDOS, MSX-DOS all implemented
- Bug fixes: ProDOS file deletion, DOS 3.3 sector count validation Based on source code analysis of AppleWin and openMSX