mirror of
				https://github.com/davidgiven/fluxengine.git
				synced 2025-10-24 11:11:02 -07:00 
			
		
		
		
	Compare commits
	
		
			37 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 500fcde21b | ||
|  | eb363a4b2a | ||
|  | 8a78e609b0 | ||
|  | 15c67b8cc1 | ||
|  | 00e9c5a07f | ||
|  | 7643457374 | ||
|  | 78d5584e21 | ||
|  | 1d1143a893 | ||
|  | 91093e1304 | ||
|  | 1175a06f3d | ||
|  | 6e5abd1189 | ||
|  | 34f97384e7 | ||
|  | 653a6a0189 | ||
|  | f0b1b61eac | ||
|  | c0fd121bdf | ||
|  | b805b86ddb | ||
|  | 654e7e750c | ||
|  | 7501fcfe8b | ||
|  | fdb7837e03 | ||
|  | 1c57cea483 | ||
|  | 0c8e8d4d69 | ||
|  | 8876aae2cc | ||
|  | 3e053b32e2 | ||
|  | 0611728537 | ||
|  | a84cf83ce5 | ||
|  | c064aa7862 | ||
|  | 195f7126cc | ||
|  | 50d466c9c1 | ||
|  | 5763574634 | ||
|  | 2da568b3e8 | ||
|  | 2732d9aec8 | ||
|  | 15d34aff15 | ||
|  | af3e257c78 | ||
|  | c2248c7e4a | ||
|  | a7967b6dc3 | ||
|  | c1f47921e6 | ||
|  | cda93d516b | 
							
								
								
									
										16
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										16
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -33,20 +33,21 @@ jobs: | ||||
|     - uses: actions/checkout@v4 | ||||
|       with: | ||||
|         repository: 'davidgiven/fluxengine' | ||||
|         path: 'fluxengine' | ||||
|  | ||||
|     - name: run | ||||
|       run: | | ||||
|         wsl sh -c 'make BUILDTYPE=windows -j$(nproc)' | ||||
|         wsl sh -c 'cd fluxengine && make BUILDTYPE=windows -j$(nproc)' | ||||
|  | ||||
|     - name: nsis | ||||
|       run: | | ||||
|         wsl sh -c 'strip fluxengine.exe -o fluxengine-stripped.exe' | ||||
|         wsl sh -c 'strip fluxengine-gui.exe -o fluxengine-gui-stripped.exe' | ||||
|         wsl sh -c 'makensis -v2 -nocd -dOUTFILE=fluxengine-installer.exe extras/windows-installer.nsi' | ||||
|         wsl sh -c 'cd fluxengine && strip fluxengine.exe -o fluxengine-stripped.exe' | ||||
|         wsl sh -c 'cd fluxengine && strip fluxengine-gui.exe -o fluxengine-gui-stripped.exe' | ||||
|         wsl sh -c 'cd fluxengine && makensis -v2 -nocd -dOUTFILE=fluxengine-installer.exe extras/windows-installer.nsi' | ||||
|  | ||||
|     - name: zip | ||||
|       run: | | ||||
|         wsl sh -c 'zip -9 fluxengine-windows.zip fluxengine.exe fluxengine-gui.exe upgrade-flux-file.exe brother120tool.exe brother240tool.exe FluxEngine.cydsn/CortexM3/ARM_GCC_541/Release/FluxEngine.hex fluxengine-installer.exe' | ||||
|         wsl sh -c 'cd fluxengine && zip -9 fluxengine-windows.zip fluxengine.exe fluxengine-gui.exe upgrade-flux-file.exe brother120tool.exe brother240tool.exe FluxEngine.cydsn/CortexM3/ARM_GCC_541/Release/FluxEngine.hex fluxengine-installer.exe' | ||||
|  | ||||
|     - name: date | ||||
|       run: | | ||||
| @@ -57,6 +58,7 @@ jobs: | ||||
|       with: | ||||
|         tag-name: dev | ||||
|         force-branch: false | ||||
|         git-directory: 'fluxengine' | ||||
|       env: | ||||
|         GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||
|  | ||||
| @@ -75,8 +77,8 @@ jobs: | ||||
|       with: | ||||
|         name: Development build ${{ env.RELEASE_DATE }} | ||||
|         files: | | ||||
|           fluxengine.zip | ||||
|           fluxengine-installer.exe | ||||
|           fluxengine/fluxengine.zip | ||||
|           fluxengine/fluxengine-installer.exe | ||||
|         tag_name: dev | ||||
|       env: | ||||
|         GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||
|   | ||||
							
								
								
									
										79
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										79
									
								
								README.md
									
									
									
									
									
								
							| @@ -103,45 +103,46 @@ particular filesystem and can read (and sometimes write, support varies) files | ||||
| directly from disks, flux files or disk images. Some formats have multiple | ||||
| choices because they can store multiple types of file system. | ||||
|  | ||||
| <!-- FORMATSSTART --> | ||||
| <!-- This section is automatically generated. Do not edit. --> | ||||
|  | ||||
| | Profile | Format | Read? | Write? | Filesystem? | | ||||
| |:--------|:-------|:-----:|:------:|:------------| | ||||
| | [`acornadfs`](doc/disk-acornadfs.md) | Acorn ADFS: BBC Micro, Archimedes | 🦖 |  |  | | ||||
| | [`acorndfs`](doc/disk-acorndfs.md) | Acorn DFS: Acorn Atom, BBC Micro series | 🦄 |  | ACORNDFS  | | ||||
| | [`aeslanier`](doc/disk-aeslanier.md) | AES Lanier "No Problem": 616kB 5.25" 77-track SSDD hard sectored | 🦖 |  |  | | ||||
| | [`agat`](doc/disk-agat.md) | Agat: 840kB 5.25" 80-track DS | 🦖 | 🦖 |  | | ||||
| | [`amiga`](doc/disk-amiga.md) | Amiga: 880kB 3.5" DSDD | 🦄 | 🦄 | AMIGAFFS  | | ||||
| | [`ampro`](doc/disk-ampro.md) | Ampro Little Board: CP/M | 🦖 |  | CPMFS  | | ||||
| | [`apple2`](doc/disk-apple2.md) | Apple II: Prodos, Appledos, and CP/M | 🦄 | 🦄 | APPLEDOS CPMFS PRODOS  | | ||||
| | [`atarist`](doc/disk-atarist.md) | Atari ST: Almost PC compatible | 🦄 | 🦄 |  | | ||||
| | [`bk`](doc/disk-bk.md) | BK: 800kB 5.25"/3.5" 80-track 10-sector DSDD | 🦖 | 🦖 |  | | ||||
| | [`brother`](doc/disk-brother.md) | Brother word processors: GCR family | 🦄 | 🦄 | BROTHER120 FATFS  | | ||||
| | [`commodore`](doc/disk-commodore.md) | Commodore: 1541, 1581, 8050 and variations | 🦄 | 🦄 | CBMFS  | | ||||
| | [`eco1`](doc/disk-eco1.md) | VDS Eco1: CP/M; 1210kB 77-track mixed format DSHD | 🦖 |  | CPMFS  | | ||||
| | [`epsonpf10`](doc/disk-epsonpf10.md) | Epson PF-10: CP/M; 3.5" 40-track DSDD | 🦖 |  | CPMFS  | | ||||
| | [`f85`](doc/disk-f85.md) | Durango F85: 461kB 5.25" 77-track SS | 🦖 |  |  | | ||||
| | [`fb100`](doc/disk-fb100.md) | Brother FB-100: 100kB 3.5" 40-track SSSD | 🦖 |  |  | | ||||
| | [`hplif`](doc/disk-hplif.md) | Hewlett-Packard LIF: a variety of disk formats used by HP | 🦄 | 🦄 | LIF  | | ||||
| | [`ibm`](doc/disk-ibm.md) | IBM PC: Generic PC 3.5"/5.25" disks | 🦄 | 🦄 | FATFS  | | ||||
| | [`icl30`](doc/disk-icl30.md) | ICL Model 30: CP/M; 263kB 35-track DSSD | 🦖 |  | CPMFS  | | ||||
| | [`mac`](doc/disk-mac.md) | Macintosh: 400kB/800kB 3.5" GCR | 🦄 | 🦄 | MACHFS  | | ||||
| | [`micropolis`](doc/disk-micropolis.md) | Micropolis: 100tpi MetaFloppy disks | 🦄 | 🦄 |  | | ||||
| | [`ms2000`](doc/disk-ms2000.md) | : MS2000 Microdisk Development System |  |  | MICRODOS  | | ||||
| | [`mx`](doc/disk-mx.md) | DVK MX: Soviet-era PDP-11 clone | 🦖 |  |  | | ||||
| | [`n88basic`](doc/disk-n88basic.md) | N88-BASIC: PC8800/PC98 5.25" 77-track 26-sector DSHD | 🦄 | 🦄 |  | | ||||
| | [`northstar`](doc/disk-northstar.md) | Northstar: 5.25" hard sectored | 🦄 | 🦄 |  | | ||||
| | [`psos`](doc/disk-psos.md) | pSOS: 800kB DSDD with PHILE | 🦄 | 🦄 | PHILE  | | ||||
| | [`rolandd20`](doc/disk-rolandd20.md) | Roland D20: 3.5" electronic synthesiser disks | 🦄 | 🦖 | ROLAND  | | ||||
| | [`rx50`](doc/disk-rx50.md) | Digital RX50: 400kB 5.25" 80-track 10-sector SSDD | 🦖 | 🦖 |  | | ||||
| | [`smaky6`](doc/disk-smaky6.md) | Smaky 6: 308kB 5.25" 77-track 16-sector SSDD, hard sectored | 🦖 |  | SMAKY6  | | ||||
| | [`tids990`](doc/disk-tids990.md) | Texas Instruments DS990: 1126kB 8" DSSD | 🦖 | 🦖 |  | | ||||
| | [`tiki`](doc/disk-tiki.md) | Tiki 100: CP/M |  |  | CPMFS  | | ||||
| | [`victor9k`](doc/disk-victor9k.md) | Victor 9000 / Sirius One: 1224kB 5.25" DSDD GCR | 🦖 | 🦖 |  | | ||||
| | [`zilogmcz`](doc/disk-zilogmcz.md) | Zilog MCZ: 320kB 8" 77-track SSSD hard-sectored | 🦖 |  | ZDOS  | | ||||
| {: .datatable } | ||||
|  | ||||
| <!-- FORMATSSTART --> | ||||
| <!-- This section is automatically generated. Do not edit. --> | ||||
|  | ||||
| | Profile | Format | Read? | Write? | Filesystem? | | ||||
| |:--------|:-------|:-----:|:------:|:------------| | ||||
| | [`acornadfs`](doc/disk-acornadfs.md) | Acorn ADFS: BBC Micro, Archimedes | 🦖 |  |  | | ||||
| | [`acorndfs`](doc/disk-acorndfs.md) | Acorn DFS: Acorn Atom, BBC Micro series | 🦄 |  | ACORNDFS  | | ||||
| | [`aeslanier`](doc/disk-aeslanier.md) | AES Lanier "No Problem": 616kB 5.25" 77-track SSDD hard sectored | 🦖 |  |  | | ||||
| | [`agat`](doc/disk-agat.md) | Agat: 840kB 5.25" 80-track DS | 🦖 | 🦖 |  | | ||||
| | [`amiga`](doc/disk-amiga.md) | Amiga: 880kB 3.5" DSDD | 🦄 | 🦄 | AMIGAFFS  | | ||||
| | [`ampro`](doc/disk-ampro.md) | Ampro Little Board: CP/M | 🦖 |  | CPMFS  | | ||||
| | [`apple2`](doc/disk-apple2.md) | Apple II: Prodos, Appledos, and CP/M | 🦄 | 🦄 | APPLEDOS CPMFS PRODOS  | | ||||
| | [`atarist`](doc/disk-atarist.md) | Atari ST: Almost PC compatible | 🦄 | 🦄 |  | | ||||
| | [`bk`](doc/disk-bk.md) | BK: 800kB 5.25"/3.5" 80-track 10-sector DSDD | 🦖 | 🦖 |  | | ||||
| | [`brother`](doc/disk-brother.md) | Brother word processors: GCR family | 🦄 | 🦄 | BROTHER120 FATFS  | | ||||
| | [`commodore`](doc/disk-commodore.md) | Commodore: 1541, 1581, 8050 and variations | 🦄 | 🦄 | CBMFS  | | ||||
| | [`eco1`](doc/disk-eco1.md) | VDS Eco1: CP/M; 1210kB 77-track mixed format DSHD | 🦖 |  | CPMFS  | | ||||
| | [`epsonpf10`](doc/disk-epsonpf10.md) | Epson PF-10: CP/M; 3.5" 40-track DSDD | 🦖 |  | CPMFS  | | ||||
| | [`f85`](doc/disk-f85.md) | Durango F85: 461kB 5.25" 77-track SS | 🦖 |  |  | | ||||
| | [`fb100`](doc/disk-fb100.md) | Brother FB-100: 100kB 3.5" 40-track SSSD | 🦖 |  |  | | ||||
| | [`hplif`](doc/disk-hplif.md) | Hewlett-Packard LIF: a variety of disk formats used by HP | 🦄 | 🦄 | LIF  | | ||||
| | [`ibm`](doc/disk-ibm.md) | IBM PC: Generic PC 3.5"/5.25" disks | 🦄 | 🦄 | FATFS  | | ||||
| | [`icl30`](doc/disk-icl30.md) | ICL Model 30: CP/M; 263kB 35-track DSSD | 🦖 |  | CPMFS  | | ||||
| | [`mac`](doc/disk-mac.md) | Macintosh: 400kB/800kB 3.5" GCR | 🦄 | 🦄 | MACHFS  | | ||||
| | [`micropolis`](doc/disk-micropolis.md) | Micropolis: 100tpi MetaFloppy disks | 🦄 | 🦄 |  | | ||||
| | [`ms2000`](doc/disk-ms2000.md) | : MS2000 Microdisk Development System |  |  | MICRODOS  | | ||||
| | [`mx`](doc/disk-mx.md) | DVK MX: Soviet-era PDP-11 clone | 🦖 |  |  | | ||||
| | [`n88basic`](doc/disk-n88basic.md) | N88-BASIC: PC8800/PC98 5.25" 77-track 26-sector DSHD | 🦄 | 🦄 |  | | ||||
| | [`northstar`](doc/disk-northstar.md) | Northstar: 5.25" hard sectored | 🦄 | 🦄 |  | | ||||
| | [`psos`](doc/disk-psos.md) | pSOS: 800kB DSDD with PHILE | 🦄 | 🦄 | PHILE  | | ||||
| | [`rolandd20`](doc/disk-rolandd20.md) | Roland D20: 3.5" electronic synthesiser disks | 🦄 | 🦖 | ROLAND  | | ||||
| | [`rx50`](doc/disk-rx50.md) | Digital RX50: 400kB 5.25" 80-track 10-sector SSDD | 🦖 | 🦖 |  | | ||||
| | [`smaky6`](doc/disk-smaky6.md) | Smaky 6: 308kB 5.25" 77-track 16-sector SSDD, hard sectored | 🦖 |  | SMAKY6  | | ||||
| | [`tartu`](doc/disk-tartu.md) | Tartu: The Palivere and variations | 🦄 |  | CPMFS  | | ||||
| | [`tids990`](doc/disk-tids990.md) | Texas Instruments DS990: 1126kB 8" DSSD | 🦖 | 🦖 |  | | ||||
| | [`tiki`](doc/disk-tiki.md) | Tiki 100: CP/M |  |  | CPMFS  | | ||||
| | [`victor9k`](doc/disk-victor9k.md) | Victor 9000 / Sirius One: 1224kB 5.25" DSDD GCR | 🦖 | 🦖 |  | | ||||
| | [`zilogmcz`](doc/disk-zilogmcz.md) | Zilog MCZ: 320kB 8" 77-track SSSD hard-sectored | 🦖 |  | ZDOS  | | ||||
| {: .datatable } | ||||
|  | ||||
| <!-- FORMATSEND --> | ||||
|  | ||||
| ### Notes | ||||
|   | ||||
| @@ -19,6 +19,7 @@ proto( | ||||
|         "./northstar/northstar.proto", | ||||
|         "./rolandd20/rolandd20.proto", | ||||
|         "./smaky6/smaky6.proto", | ||||
|         "./tartu/tartu.proto", | ||||
|         "./tids990/tids990.proto", | ||||
|         "./victor9k/victor9k.proto", | ||||
|         "./zilogmcz/zilogmcz.proto", | ||||
|   | ||||
| @@ -1,6 +1,4 @@ | ||||
| syntax = "proto2"; | ||||
|  | ||||
| import "lib/common.proto"; | ||||
|  | ||||
| message Smaky6DecoderProto {} | ||||
|  | ||||
|   | ||||
							
								
								
									
										84
									
								
								arch/tartu/decoder.cc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								arch/tartu/decoder.cc
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,84 @@ | ||||
| #include "lib/globals.h" | ||||
| #include "lib/decoders/decoders.h" | ||||
| #include "arch/tartu/tartu.h" | ||||
| #include "lib/crc.h" | ||||
| #include "lib/fluxmap.h" | ||||
| #include "lib/decoders/fluxmapreader.h" | ||||
| #include "lib/sector.h" | ||||
| #include <string.h> | ||||
|  | ||||
| constexpr uint64_t HEADER_BITS = 0xaaaaaaaa44895554LL; | ||||
| constexpr uint64_t DATA_BITS =   0xaaaaaaaa44895545LL; | ||||
|  | ||||
| static const FluxPattern HEADER_PATTERN(64, HEADER_BITS); | ||||
| static const FluxPattern DATA_PATTERN(64, DATA_BITS); | ||||
|  | ||||
| const FluxMatchers ANY_RECORD_PATTERN { | ||||
|     &HEADER_PATTERN, | ||||
|     &DATA_PATTERN | ||||
| }; | ||||
|  | ||||
| class TartuDecoder : public Decoder | ||||
| { | ||||
| public: | ||||
|     TartuDecoder(const DecoderProto& config): | ||||
|         Decoder(config), | ||||
|         _config(config.tartu()) | ||||
|     { | ||||
|     } | ||||
|  | ||||
|     void beginTrack() override | ||||
|     { | ||||
|     } | ||||
|  | ||||
|     nanoseconds_t advanceToNextRecord() override | ||||
|     { | ||||
|         return seekToPattern(ANY_RECORD_PATTERN); | ||||
|     } | ||||
|  | ||||
|     void decodeSectorRecord() override | ||||
|     { | ||||
|         if (readRaw64() != HEADER_BITS) | ||||
|             return; | ||||
|  | ||||
|         auto bits = readRawBits(16 * 4); | ||||
|         auto bytes = decodeFmMfm(bits).slice(0, 4); | ||||
|          | ||||
|         ByteReader br(bytes); | ||||
|         uint8_t track = br.read_8(); | ||||
|         _sector->logicalTrack = track >> 1; | ||||
|         _sector->logicalSide = track & 1; | ||||
|         br.skip(1); /* seems always to be 1 */ | ||||
|         _sector->logicalSector = br.read_8(); | ||||
|         uint8_t wantChecksum = br.read_8(); | ||||
|         uint8_t gotChecksum = ~sumBytes(bytes.slice(0, 3)); | ||||
|          | ||||
|         if (wantChecksum == gotChecksum) | ||||
|             _sector->status = Sector::DATA_MISSING; | ||||
|  | ||||
|         _sector->status = Sector::DATA_MISSING; | ||||
|     } | ||||
|  | ||||
|     void decodeDataRecord() override | ||||
|     { | ||||
|         if (readRaw64() != DATA_BITS) | ||||
|             return; | ||||
|      | ||||
|         const auto& bits = readRawBits(129 * 16); | ||||
|         const auto& bytes = decodeFmMfm(bits).slice(0, 129); | ||||
|         _sector->data = bytes.slice(0, 128); | ||||
|  | ||||
|         uint8_t wantChecksum = bytes.reader().seek(128).read_8(); | ||||
|         uint8_t gotChecksum = ~sumBytes(_sector->data); | ||||
|         _sector->status = (wantChecksum == gotChecksum) ? Sector::OK : Sector::BAD_CHECKSUM; | ||||
|     } | ||||
|  | ||||
| private: | ||||
|     const TartuDecoderProto& _config; | ||||
| }; | ||||
|  | ||||
| std::unique_ptr<Decoder> createTartuDecoder(const DecoderProto& config) | ||||
| { | ||||
|     return std::unique_ptr<Decoder>(new TartuDecoder(config)); | ||||
| } | ||||
|  | ||||
							
								
								
									
										114
									
								
								arch/tartu/encoder.cc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								arch/tartu/encoder.cc
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | ||||
| #include "lib/globals.h" | ||||
| #include "lib/decoders/decoders.h" | ||||
| #include "lib/encoders/encoders.h" | ||||
| #include "arch/tartu/tartu.h" | ||||
| #include "lib/crc.h" | ||||
| #include "lib/fluxmap.h" | ||||
| #include "lib/sector.h" | ||||
| #include <string.h> | ||||
|  | ||||
| class TartuEncoder : public Encoder | ||||
| { | ||||
| public: | ||||
|     TartuEncoder(const EncoderProto& config): | ||||
|         Encoder(config), | ||||
|         _config(config.tartu()) | ||||
|     { | ||||
|     } | ||||
|  | ||||
|     std::unique_ptr<Fluxmap> encode(std::shared_ptr<const TrackInfo>& trackInfo, | ||||
|         const std::vector<std::shared_ptr<const Sector>>& sectors, | ||||
|         const Image& image) override | ||||
|     { | ||||
|         _clockRateUs = _config.clock_period_us(); | ||||
|         int bitsPerRevolution = | ||||
|             (_config.target_rotational_period_ms() * 1000.0) / _clockRateUs; | ||||
|  | ||||
|         const auto& sector = *sectors.begin(); | ||||
|         _bits.resize(bitsPerRevolution); | ||||
|         _cursor = 0; | ||||
|  | ||||
|         writeFillerRawBitsUs(_config.gap1_us()); | ||||
|         bool first = true; | ||||
|         for (const auto& sectorData : sectors) | ||||
|         { | ||||
|             if (!first) | ||||
|                 writeFillerRawBitsUs(_config.gap4_us()); | ||||
|             first = false; | ||||
|             writeSector(sectorData); | ||||
|         } | ||||
|  | ||||
|         if (_cursor > _bits.size()) | ||||
|             error("track data overrun"); | ||||
|         writeFillerRawBitsUs(_config.target_rotational_period_ms() * 1000.0); | ||||
|  | ||||
|         std::unique_ptr<Fluxmap> fluxmap(new Fluxmap); | ||||
|         fluxmap->appendBits(_bits, | ||||
|             calculatePhysicalClockPeriod(_clockRateUs * 1e3, | ||||
|                 _config.target_rotational_period_ms() * 1e6)); | ||||
|         return fluxmap; | ||||
|     } | ||||
|  | ||||
| private: | ||||
|     void writeBytes(const Bytes& bytes) | ||||
|     { | ||||
|         encodeMfm(_bits, _cursor, bytes, _lastBit); | ||||
|     } | ||||
|  | ||||
|     void writeRawBits(uint64_t data, int width) | ||||
|     { | ||||
|         _cursor += width; | ||||
|         _lastBit = data & 1; | ||||
|         for (int i = 0; i < width; i++) | ||||
|         { | ||||
|             unsigned pos = _cursor - i - 1; | ||||
|             if (pos < _bits.size()) | ||||
|                 _bits[pos] = data & 1; | ||||
|             data >>= 1; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     void writeFillerRawBitsUs(double us) | ||||
|     { | ||||
|         unsigned count = (us / _clockRateUs) / 2; | ||||
|         for (int i = 0; i < count; i++) | ||||
|             writeRawBits(0b10, 2); | ||||
|     }; | ||||
|  | ||||
|     void writeSector(const std::shared_ptr<const Sector>& sectorData) | ||||
|     { | ||||
|         writeRawBits(_config.header_marker(), 64); | ||||
|         { | ||||
|             Bytes bytes; | ||||
|             ByteWriter bw(bytes); | ||||
|             bw.write_8( | ||||
|                 (sectorData->logicalTrack << 1) | sectorData->logicalSide); | ||||
|             bw.write_8(1); | ||||
|             bw.write_8(sectorData->logicalSector); | ||||
|             bw.write_8(~sumBytes(bytes.slice(0, 3))); | ||||
|             writeBytes(bytes); | ||||
|         } | ||||
|  | ||||
|         writeFillerRawBitsUs(_config.gap3_us()); | ||||
|         writeRawBits(_config.data_marker(), 64); | ||||
|         { | ||||
|             Bytes bytes; | ||||
|             ByteWriter bw(bytes); | ||||
|             bw.append(sectorData->data); | ||||
|             bw.write_8(~sumBytes(bytes.slice(0, sectorData->data.size()))); | ||||
|             writeBytes(bytes); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| private: | ||||
|     const TartuEncoderProto& _config; | ||||
|     double _clockRateUs; | ||||
|     std::vector<bool> _bits; | ||||
|     unsigned _cursor; | ||||
|     bool _lastBit; | ||||
| }; | ||||
|  | ||||
| std::unique_ptr<Encoder> createTartuEncoder(const EncoderProto& config) | ||||
| { | ||||
|     return std::unique_ptr<Encoder>(new TartuEncoder(config)); | ||||
| } | ||||
							
								
								
									
										8
									
								
								arch/tartu/tartu.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								arch/tartu/tartu.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| #ifndef TARTU_H | ||||
| #define TARTU_H | ||||
|  | ||||
| extern std::unique_ptr<Decoder> createTartuDecoder(const DecoderProto& config); | ||||
| extern std::unique_ptr<Encoder> createTartuEncoder(const EncoderProto& config); | ||||
|  | ||||
| #endif | ||||
|  | ||||
							
								
								
									
										27
									
								
								arch/tartu/tartu.proto
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								arch/tartu/tartu.proto
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| syntax = "proto2"; | ||||
|  | ||||
| import "lib/common.proto"; | ||||
|  | ||||
| message TartuDecoderProto {} | ||||
|  | ||||
| message TartuEncoderProto { | ||||
|     optional double clock_period_us = 1 | ||||
|         [ default = 2.0, (help) = "clock rate on the real device (for MFM)" ]; | ||||
|     optional double target_rotational_period_ms = 2 | ||||
|         [ default=200, (help) = "rotational period of target disk" ]; | ||||
|     optional double gap1_us = 3 | ||||
|         [ default = 1200, | ||||
|           (help) = "size of gap 1 (the post-index gap)" ]; | ||||
|     optional double gap3_us = 4 | ||||
|         [ default = 150, | ||||
|           (help) = "size of gap 3 (the pre-data gap)" ]; | ||||
|     optional double gap4_us = 5 | ||||
|         [ default = 180, | ||||
|           (help) = "size of gap 4 (the post-data or format gap)" ]; | ||||
|     optional uint64 header_marker = 6 | ||||
|         [ default = 0xaaaaaaaa44895554, | ||||
|           (help) = "64-bit raw bit pattern of header record marker" ]; | ||||
|     optional uint64 data_marker = 7 | ||||
|         [ default = 0xaaaaaaaa44895545, | ||||
|           (help) = "64-bit raw bit pattern of data record marker" ]; | ||||
| } | ||||
							
								
								
									
										5
									
								
								build.py
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								build.py
									
									
									
									
									
								
							| @@ -143,6 +143,8 @@ cxxlibrary( | ||||
|         "./arch/northstar/encoder.cc", | ||||
|         "./arch/rolandd20/decoder.cc", | ||||
|         "./arch/smaky6/decoder.cc", | ||||
|         "./arch/tartu/decoder.cc", | ||||
|         "./arch/tartu/encoder.cc", | ||||
|         "./arch/tids990/decoder.cc", | ||||
|         "./arch/tids990/encoder.cc", | ||||
|         "./arch/victor9k/decoder.cc", | ||||
| @@ -175,6 +177,7 @@ cxxlibrary( | ||||
|         "arch/micropolis/micropolis.h": "./arch/micropolis/micropolis.h", | ||||
|         "arch/c64/data_gcr.h": "./arch/c64/data_gcr.h", | ||||
|         "arch/c64/c64.h": "./arch/c64/c64.h", | ||||
|         "arch/tartu/tartu.h": "./arch/tartu/tartu.h", | ||||
|         "lib/a2r.h": "./lib/a2r.h", | ||||
|         "lib/bitmap.h": "./lib/bitmap.h", | ||||
|         "lib/bytes.h": "./lib/bytes.h", | ||||
| @@ -278,6 +281,8 @@ else: | ||||
|         ("mac", "scripts/mac800_test.textpb", "--800"), | ||||
|         ("n88basic", "", ""), | ||||
|         ("rx50", "", ""), | ||||
|         ("tartu", "", "--390 40track_drive"), | ||||
|         ("tartu", "", "--780"), | ||||
|         ("tids990", "", ""), | ||||
|         ("victor9k", "", "--612"), | ||||
|         ("victor9k", "", "--1224"), | ||||
|   | ||||
| @@ -11,7 +11,7 @@ from build.ab import ( | ||||
| ) | ||||
| from build.c import cxxlibrary | ||||
| from types import SimpleNamespace | ||||
| import build.pkg | ||||
| from build.pkg import package | ||||
|  | ||||
| emit( | ||||
|     """ | ||||
| @@ -22,6 +22,7 @@ endif | ||||
| """ | ||||
| ) | ||||
|  | ||||
| lib = package(name="protobuf_lib", package="protobuf") | ||||
|  | ||||
| @Rule | ||||
| def proto(self, name, srcs: Targets = None, deps: Targets = None): | ||||
| @@ -67,6 +68,6 @@ def protocc(self, name, srcs: Targets = None, deps: Targets = None): | ||||
|     cxxlibrary( | ||||
|         replaces=self, | ||||
|         srcs=[r], | ||||
|         deps=targetswithtraitsof(deps, "cheaders"), | ||||
|         deps=targetswithtraitsof(deps, "cheaders") + [lib], | ||||
|         hdrs=headers, | ||||
|     ) | ||||
|   | ||||
							
								
								
									
										48
									
								
								doc/disk-tartu.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								doc/disk-tartu.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | ||||
| tartu | ||||
| ==== | ||||
| ## The Palivere and variations | ||||
| <!-- This file is automatically generated. Do not edit. --> | ||||
|  | ||||
| The Tartu Palivere is a 1988 Z80-based computer from Estonia. It is a CP/M | ||||
| machine with 64kB of RAM, running off a 2MHz ꃣ0e30 | ||||
| clone; it operated off punched tape, cassette, external hard drive or floppy, and was notable as being the first ever computer with an Estonian keyboard. | ||||
|  | ||||
| <div style="text-align: center"> | ||||
| <img src="tartu.jpg" alt="The Tartu computer's developer Leo Humal working with one."/> | ||||
| </div> | ||||
|  | ||||
| From a floppy disk perspective, it is interesting because the floppy drive | ||||
| interface is almost entirely handled in software --- necessary at the time as | ||||
| the usual floppy disk interface chip at the time, the ⎲fba5 | ||||
| of the WD1793), was hard to find. Instead, the floppy controller board was | ||||
| implemented entirely using TTL logic. Despite this, the encoding is fairly high | ||||
| density, using MFM and with up to 780kB on a double-sided 80 track disk. | ||||
|  | ||||
| <div style="text-align: center"> | ||||
| <img src="tartu-fdc.jpg" alt="The Tartu FDC with Soviet TTL logic chips."/> | ||||
| </div> | ||||
|  | ||||
| FluxEngine supports reading Tartu disks with CP/M filesystem access. | ||||
|  | ||||
| ## Options | ||||
|  | ||||
|   - Format variants: | ||||
|       - `390`: 390kB 5.25" 40-track DSDD | ||||
|       - `780`: 780kB 5.25" 80-track DSDD | ||||
|  | ||||
| ## Examples | ||||
|  | ||||
| To read: | ||||
|  | ||||
|   - `fluxengine read tartu --390 -s drive:0 -o tartu.img` | ||||
|   - `fluxengine read tartu --780 -s drive:0 -o tartu.img` | ||||
|  | ||||
| To write: | ||||
|  | ||||
|   - `fluxengine write tartu --390 -d drive:0 -i tartu.img` | ||||
|   - `fluxengine write tartu --780 -d drive:0 -i tartu.img` | ||||
|  | ||||
| ## References | ||||
|  | ||||
|   - [The Estonia Museum of Electronics](https://www.elektroonikamuuseum.ee/tartu_arvuti_lugu.html) | ||||
|  | ||||
							
								
								
									
										
											BIN
										
									
								
								doc/tartu-fdc.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								doc/tartu-fdc.jpg
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 165 KiB | 
							
								
								
									
										
											BIN
										
									
								
								doc/tartu.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								doc/tartu.jpg
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 86 KiB | 
| @@ -18,6 +18,7 @@ | ||||
| #include "arch/northstar/northstar.h" | ||||
| #include "arch/rolandd20/rolandd20.h" | ||||
| #include "arch/smaky6/smaky6.h" | ||||
| #include "arch/tartu/tartu.h" | ||||
| #include "arch/tids990/tids990.h" | ||||
| #include "arch/victor9k/victor9k.h" | ||||
| #include "arch/zilogmcz/zilogmcz.h" | ||||
| @@ -51,6 +52,7 @@ std::unique_ptr<Decoder> Decoder::create(const DecoderProto& config) | ||||
|             {DecoderProto::kNorthstar,  createNorthstarDecoder  }, | ||||
|             {DecoderProto::kRolandd20,  createRolandD20Decoder  }, | ||||
|             {DecoderProto::kSmaky6,     createSmaky6Decoder     }, | ||||
|             {DecoderProto::kTartu,      createTartuDecoder      }, | ||||
|             {DecoderProto::kTids990,    createTids990Decoder    }, | ||||
|             {DecoderProto::kVictor9K,   createVictor9kDecoder   }, | ||||
|             {DecoderProto::kZilogmcz,   createZilogMczDecoder   }, | ||||
| @@ -89,7 +91,7 @@ std::shared_ptr<TrackDataFlux> Decoder::decodeToSectors( | ||||
|         Fluxmap::Position recordStart = fmr.tell(); | ||||
|         _sector->clock = advanceToNextRecord(); | ||||
|         if (fmr.eof() || !_sector->clock) | ||||
|             return _trackdata; | ||||
|             break; | ||||
|  | ||||
|         /* Read the sector record. */ | ||||
|  | ||||
| @@ -108,28 +110,26 @@ std::shared_ptr<TrackDataFlux> Decoder::decodeToSectors( | ||||
|         { | ||||
|             /* The data is in a separate record. */ | ||||
|  | ||||
|             for (;;) | ||||
|             _sector->headerStartTime = before.ns(); | ||||
|             _sector->headerEndTime = after.ns(); | ||||
|  | ||||
|             _sector->clock = advanceToNextRecord(); | ||||
|             if (fmr.eof() || !_sector->clock) | ||||
|                 break; | ||||
|  | ||||
|             before = fmr.tell(); | ||||
|             decodeDataRecord(); | ||||
|             after = fmr.tell(); | ||||
|  | ||||
|             if (_sector->status != Sector::DATA_MISSING) | ||||
|             { | ||||
|                 _sector->position = before.bytes; | ||||
|                 _sector->dataStartTime = before.ns(); | ||||
|                 _sector->dataEndTime = after.ns(); | ||||
|                 pushRecord(before, after); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 _sector->headerStartTime = before.ns(); | ||||
|                 _sector->headerEndTime = after.ns(); | ||||
|  | ||||
|                 _sector->clock = advanceToNextRecord(); | ||||
|                 if (fmr.eof() || !_sector->clock) | ||||
|                     break; | ||||
|  | ||||
|                 before = fmr.tell(); | ||||
|                 decodeDataRecord(); | ||||
|                 after = fmr.tell(); | ||||
|  | ||||
|                 if (_sector->status != Sector::DATA_MISSING) | ||||
|                 { | ||||
|                     _sector->position = before.bytes; | ||||
|                     _sector->dataStartTime = before.ns(); | ||||
|                     _sector->dataEndTime = after.ns(); | ||||
|                     pushRecord(before, after); | ||||
|                     break; | ||||
|                 } | ||||
|  | ||||
|                 fmr.skipToEvent(F_BIT_PULSE); | ||||
|                 resetFluxDecoder(); | ||||
|             } | ||||
| @@ -142,6 +142,8 @@ std::shared_ptr<TrackDataFlux> Decoder::decodeToSectors( | ||||
|             _trackdata->sectors.push_back(_sector); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return _trackdata; | ||||
| } | ||||
|  | ||||
| void Decoder::pushRecord( | ||||
|   | ||||
| @@ -15,13 +15,14 @@ import "arch/mx/mx.proto"; | ||||
| import "arch/northstar/northstar.proto"; | ||||
| import "arch/rolandd20/rolandd20.proto"; | ||||
| import "arch/smaky6/smaky6.proto"; | ||||
| import "arch/tartu/tartu.proto"; | ||||
| import "arch/tids990/tids990.proto"; | ||||
| import "arch/victor9k/victor9k.proto"; | ||||
| import "arch/zilogmcz/zilogmcz.proto"; | ||||
| import "lib/fluxsink/fluxsink.proto"; | ||||
| import "lib/common.proto"; | ||||
|  | ||||
| //NEXT: 32 | ||||
| //NEXT: 33 | ||||
| message DecoderProto { | ||||
| 	optional double pulse_debounce_threshold = 1 [default = 0.30, | ||||
| 		(help) = "ignore pulses with intervals shorter than this, in fractions of a clock"]; | ||||
| @@ -50,6 +51,7 @@ message DecoderProto { | ||||
| 		NorthstarDecoderProto northstar = 24; | ||||
| 		RolandD20DecoderProto rolandd20 = 31; | ||||
| 		Smaky6DecoderProto smaky6 = 30; | ||||
| 		TartuDecoderProto tartu = 32; | ||||
| 		Tids990DecoderProto tids990 = 16; | ||||
| 		Victor9kDecoderProto victor9k = 17; | ||||
| 		ZilogMczDecoderProto zilogmcz = 18; | ||||
|   | ||||
| @@ -11,6 +11,7 @@ | ||||
| #include "arch/macintosh/macintosh.h" | ||||
| #include "arch/micropolis/micropolis.h" | ||||
| #include "arch/northstar/northstar.h" | ||||
| #include "arch/tartu/tartu.h" | ||||
| #include "arch/tids990/tids990.h" | ||||
| #include "arch/victor9k/victor9k.h" | ||||
| #include "lib/encoders/encoders.pb.h" | ||||
| @@ -24,8 +25,8 @@ std::unique_ptr<Encoder> Encoder::create(const EncoderProto& config) | ||||
|     static const std::map<int, | ||||
|         std::function<std::unique_ptr<Encoder>(const EncoderProto&)>> | ||||
|         encoders = { | ||||
|             {EncoderProto::kAmiga,      createAmigaEncoder      }, | ||||
|             {EncoderProto::kAgat,       createAgatEncoder       }, | ||||
|             {EncoderProto::kAmiga,      createAmigaEncoder      }, | ||||
|             {EncoderProto::kApple2,     createApple2Encoder     }, | ||||
|             {EncoderProto::kBrother,    createBrotherEncoder    }, | ||||
|             {EncoderProto::kC64,        createCommodore64Encoder}, | ||||
| @@ -33,6 +34,7 @@ std::unique_ptr<Encoder> Encoder::create(const EncoderProto& config) | ||||
|             {EncoderProto::kMacintosh,  createMacintoshEncoder  }, | ||||
|             {EncoderProto::kMicropolis, createMicropolisEncoder }, | ||||
|             {EncoderProto::kNorthstar,  createNorthstarEncoder  }, | ||||
|             {EncoderProto::kTartu,      createTartuEncoder      }, | ||||
|             {EncoderProto::kTids990,    createTids990Encoder    }, | ||||
|             {EncoderProto::kVictor9K,   createVictor9kEncoder   }, | ||||
|     }; | ||||
|   | ||||
| @@ -9,6 +9,7 @@ import "arch/ibm/ibm.proto"; | ||||
| import "arch/macintosh/macintosh.proto"; | ||||
| import "arch/micropolis/micropolis.proto"; | ||||
| import "arch/northstar/northstar.proto"; | ||||
| import "arch/tartu/tartu.proto"; | ||||
| import "arch/tids990/tids990.proto"; | ||||
| import "arch/victor9k/victor9k.proto"; | ||||
|  | ||||
| @@ -27,5 +28,6 @@ message EncoderProto | ||||
|         Victor9kEncoderProto victor9k = 11; | ||||
|         Apple2EncoderProto apple2 = 12; | ||||
|         AgatEncoderProto agat = 13; | ||||
|         TartuEncoderProto tartu = 14; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -42,11 +42,9 @@ public: | ||||
|         commtimeouts.ReadIntervalTimeout = 100; | ||||
|         SetCommTimeouts(_handle, &commtimeouts); | ||||
|  | ||||
|         if (!EscapeCommFunction(_handle, CLRDTR)) | ||||
|             error("Couldn't clear DTR: {}", get_last_error_string()); | ||||
|         Sleep(200); | ||||
|         if (!EscapeCommFunction(_handle, SETDTR)) | ||||
|             error("Couldn't set DTR: {}", get_last_error_string()); | ||||
|         /* Toggle DTR to reset the device. */ | ||||
|  | ||||
|         toggleDtr(); | ||||
|  | ||||
|         PurgeComm(_handle, | ||||
|             PURGE_RXABORT | PURGE_RXCLEAR | PURGE_TXABORT | PURGE_TXCLEAR); | ||||
| @@ -58,6 +56,15 @@ public: | ||||
|     } | ||||
|  | ||||
| public: | ||||
|     void toggleDtr() override | ||||
|     { | ||||
|         if (!EscapeCommFunction(_handle, CLRDTR)) | ||||
|             error("Couldn't clear DTR: {}", get_last_error_string()); | ||||
|         Sleep(200); | ||||
|         if (!EscapeCommFunction(_handle, SETDTR)) | ||||
|             error("Couldn't set DTR: {}", get_last_error_string()); | ||||
|     } | ||||
|  | ||||
|     ssize_t readImpl(uint8_t* buffer, size_t len) override | ||||
|     { | ||||
|         DWORD rlen; | ||||
| @@ -97,6 +104,8 @@ public: | ||||
|             .Parity = NOPARITY, | ||||
|             .StopBits = ONESTOPBIT}; | ||||
|         SetCommState(_handle, &dcb); | ||||
|  | ||||
|         toggleDtr(); | ||||
|     } | ||||
|  | ||||
| private: | ||||
| @@ -157,12 +166,7 @@ public: | ||||
|  | ||||
|         /* Toggle DTR to reset the device. */ | ||||
|  | ||||
|         int flag = TIOCM_DTR; | ||||
|         if (ioctl(_fd, TIOCMBIC, &flag) == -1) | ||||
|             error("cannot clear DTR on serial port: {}", strerror(errno)); | ||||
|         usleep(200000); | ||||
|         if (ioctl(_fd, TIOCMBIS, &flag) == -1) | ||||
|             error("cannot set DTR on serial port: {}", strerror(errno)); | ||||
|         toggleDtr(); | ||||
|  | ||||
|         /* Flush pending input from a generic greaseweazel device */ | ||||
|         tcsetattr(_fd, TCSAFLUSH, &t); | ||||
| @@ -174,6 +178,16 @@ public: | ||||
|     } | ||||
|  | ||||
| public: | ||||
|     void toggleDtr() override | ||||
|     { | ||||
|         int flag = TIOCM_DTR; | ||||
|         if (ioctl(_fd, TIOCMBIC, &flag) == -1) | ||||
|             error("cannot clear DTR on serial port: {}", strerror(errno)); | ||||
|         usleep(200000); | ||||
|         if (ioctl(_fd, TIOCMBIS, &flag) == -1) | ||||
|             error("cannot set DTR on serial port: {}", strerror(errno)); | ||||
|     } | ||||
|  | ||||
|     ssize_t readImpl(uint8_t* buffer, size_t len) override | ||||
|     { | ||||
|         ssize_t rlen = ::read(_fd, buffer, len); | ||||
| @@ -198,6 +212,8 @@ public: | ||||
|         tcgetattr(_fd, &t); | ||||
|         cfsetspeed(&t, baudRate); | ||||
|         tcsetattr(_fd, TCSANOW, &t); | ||||
|  | ||||
|         toggleDtr(); | ||||
|     } | ||||
|  | ||||
| private: | ||||
|   | ||||
| @@ -11,6 +11,7 @@ public: | ||||
|     virtual ssize_t readImpl(uint8_t* buffer, size_t len) = 0; | ||||
|     virtual ssize_t write(const uint8_t* buffer, size_t len) = 0; | ||||
|     virtual void setBaudRate(int baudRate) = 0; | ||||
|     virtual void toggleDtr() = 0; | ||||
|  | ||||
|     void read(uint8_t* buffer, size_t len); | ||||
|     void read(Bytes& bytes); | ||||
|   | ||||
							
								
								
									
										397
									
								
								lib/vfs/cpmfs.cc
									
									
									
									
									
								
							
							
						
						
									
										397
									
								
								lib/vfs/cpmfs.cc
									
									
									
									
									
								
							| @@ -1,19 +1,24 @@ | ||||
| #include "lib/globals.h" | ||||
| #include "lib/vfs/vfs.h" | ||||
| #include "lib/config.pb.h" | ||||
| #include <fmt/format.h> | ||||
| #include <regex> | ||||
|  | ||||
| class CpmFsFilesystem : public Filesystem | ||||
| class CpmFsFilesystem : public Filesystem, public HasBitmap, public HasMount | ||||
| { | ||||
|     class Entry | ||||
|     { | ||||
|     public: | ||||
|         Entry(const Bytes& bytes, int map_entry_size) | ||||
|         Entry(const Bytes& bytes, int map_entry_size, unsigned index): | ||||
|             index(index) | ||||
|         { | ||||
|             if (bytes[0] == 0xe5) | ||||
|                 deleted = true; | ||||
|  | ||||
|             user = bytes[0] & 0x0f; | ||||
|  | ||||
|             { | ||||
|                 std::stringstream ss; | ||||
|                 ss << (char)(user + '0') << ':'; | ||||
|  | ||||
|                 for (int i = 1; i <= 8; i++) | ||||
|                 { | ||||
| @@ -64,13 +69,117 @@ class CpmFsFilesystem : public Filesystem | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Bytes toBytes(int map_entry_size) const | ||||
|         { | ||||
|             Bytes bytes(32); | ||||
|             ByteWriter bw(bytes); | ||||
|  | ||||
|             if (deleted) | ||||
|             { | ||||
|                 for (int i = 0; i < 32; i++) | ||||
|                     bw.write_8(0xe5); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 bw.write_8(user); | ||||
|  | ||||
|                 /* Encode the filename. */ | ||||
|  | ||||
|                 for (int i = 1; i < 12; i++) | ||||
|                     bytes[i] = 0x20; | ||||
|                 for (char c : filename) | ||||
|                 { | ||||
|                     if (islower(c)) | ||||
|                         throw BadPathException(); | ||||
|                     if (c == '.') | ||||
|                     { | ||||
|                         if (bw.pos >= 9) | ||||
|                             throw BadPathException(); | ||||
|                         bw.seek(9); | ||||
|                         continue; | ||||
|                     } | ||||
|                     if ((bw.pos == 9) || (bw.pos == 12)) | ||||
|                         throw BadPathException(); | ||||
|                     bw.write_8(c); | ||||
|                 } | ||||
|  | ||||
|                 /* Set the mode. */ | ||||
|  | ||||
|                 if (mode.find('R') != std::string::npos) | ||||
|                     bytes[9] |= 0x80; | ||||
|                 if (mode.find('S') != std::string::npos) | ||||
|                     bytes[10] |= 0x80; | ||||
|                 if (mode.find('A') != std::string::npos) | ||||
|                     bytes[11] |= 0x80; | ||||
|  | ||||
|                 /* EX, S1, S2, RC */ | ||||
|  | ||||
|                 bw.seek(12); | ||||
|                 bw.write_8(extent & 0x1f); /* EX */ | ||||
|                 bw.write_8(0);             /* S1 */ | ||||
|                 bw.write_8(extent >> 5);   /* S2 */ | ||||
|                 bw.write_8(records);       /* RC */ | ||||
|  | ||||
|                 /* Allocation map. */ | ||||
|  | ||||
|                 switch (map_entry_size) | ||||
|                 { | ||||
|                     case 1: | ||||
|                         for (int i = 0; i < 16; i++) | ||||
|                             bw.write_8(allocation_map[i]); | ||||
|                         break; | ||||
|  | ||||
|                     case 2: | ||||
|                         for (int i = 0; i < 8; i++) | ||||
|                             bw.write_le16(allocation_map[i]); | ||||
|                         break; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return bytes; | ||||
|         } | ||||
|  | ||||
|         void changeFilename(const std::string& filename) | ||||
|         { | ||||
|             static std::regex FORMATTER("(?:(1?[0-9]):)?([^ .]+)\\.?([^ .]*)"); | ||||
|             std::smatch results; | ||||
|             bool matched = std::regex_match(filename, results, FORMATTER); | ||||
|             if (!matched) | ||||
|                 throw BadPathException(); | ||||
|  | ||||
|             std::string user = results[1]; | ||||
|             std::string stem = results[2]; | ||||
|             std::string ext = results[3]; | ||||
|  | ||||
|             if (stem.size() > 8) | ||||
|                 throw BadPathException(); | ||||
|             if (ext.size() > 3) | ||||
|                 throw BadPathException(); | ||||
|  | ||||
|             this->user = std::stoi(user); | ||||
|             if (this->user > 15) | ||||
|                 throw BadPathException(); | ||||
|  | ||||
|             if (ext.empty()) | ||||
|                 this->filename = stem; | ||||
|             else | ||||
|                 this->filename = fmt::format("{}.{}", stem, ext); | ||||
|         } | ||||
|  | ||||
|         std::string combinedFilename() const | ||||
|         { | ||||
|             return fmt::format("{}:{}", user, filename); | ||||
|         } | ||||
|  | ||||
|     public: | ||||
|         unsigned index; | ||||
|         std::string filename; | ||||
|         std::string mode; | ||||
|         unsigned user; | ||||
|         unsigned extent; | ||||
|         unsigned records; | ||||
|         std::vector<unsigned> allocation_map; | ||||
|         bool deleted = false; | ||||
|     }; | ||||
|  | ||||
| public: | ||||
| @@ -83,7 +192,8 @@ public: | ||||
|  | ||||
|     uint32_t capabilities() const override | ||||
|     { | ||||
|         return OP_GETFSDATA | OP_LIST | OP_GETFILE | OP_GETDIRENT; | ||||
|         return OP_GETFSDATA | OP_LIST | OP_GETFILE | OP_PUTFILE | OP_DELETE | | ||||
|                OP_GETDIRENT | OP_CREATE | OP_MOVE | OP_PUTATTRS; | ||||
|     } | ||||
|  | ||||
|     std::map<std::string, std::string> getMetadata() override | ||||
| @@ -94,7 +204,7 @@ public: | ||||
|         for (int d = 0; d < _config.dir_entries(); d++) | ||||
|         { | ||||
|             auto entry = getEntry(d); | ||||
|             if (!entry) | ||||
|             if (entry->deleted) | ||||
|                 continue; | ||||
|  | ||||
|             for (unsigned block : entry->allocation_map) | ||||
| @@ -112,6 +222,17 @@ public: | ||||
|         return attributes; | ||||
|     } | ||||
|  | ||||
|     void create(bool, const std::string&) override | ||||
|     { | ||||
|         auto& start = _config.filesystem_start(); | ||||
|         _filesystemStart = | ||||
|             getOffsetOfSector(start.track(), start.side(), start.sector()); | ||||
|         _sectorSize = getLogicalSectorSize(start.track(), start.side()); | ||||
|  | ||||
|         _directory = Bytes{0xe5} * (_config.dir_entries() * 32); | ||||
|         putCpmBlock(0, _directory); | ||||
|     } | ||||
|  | ||||
|     FilesystemStatus check() override | ||||
|     { | ||||
|         return FS_OK; | ||||
| @@ -127,15 +248,15 @@ public: | ||||
|         for (int d = 0; d < _config.dir_entries(); d++) | ||||
|         { | ||||
|             auto entry = getEntry(d); | ||||
|             if (!entry) | ||||
|             if (entry->deleted) | ||||
|                 continue; | ||||
|  | ||||
|             auto& dirent = map[entry->filename]; | ||||
|             auto& dirent = map[entry->combinedFilename()]; | ||||
|             if (!dirent) | ||||
|             { | ||||
|                 dirent = std::make_unique<Dirent>(); | ||||
|                 dirent->path = {entry->filename}; | ||||
|                 dirent->filename = entry->filename; | ||||
|                 dirent->filename = entry->combinedFilename(); | ||||
|                 dirent->path = {dirent->filename}; | ||||
|                 dirent->mode = entry->mode; | ||||
|                 dirent->length = 0; | ||||
|                 dirent->file_type = TYPE_FILE; | ||||
| @@ -173,6 +294,42 @@ public: | ||||
|         throw FileNotFoundException(); | ||||
|     } | ||||
|  | ||||
|     void putMetadata(const Path& path, | ||||
|         const std::map<std::string, std::string>& metadata) override | ||||
|     { | ||||
|         mount(); | ||||
|         if (path.size() != 1) | ||||
|             throw BadPathException(); | ||||
|  | ||||
|         /* Only updating MODE is supported. */ | ||||
|  | ||||
|         if (metadata.empty()) | ||||
|             return; | ||||
|         if ((metadata.size() != 1) || (metadata.begin()->first != MODE)) | ||||
|             throw UnimplementedFilesystemException(); | ||||
|         auto mode = metadata.begin()->second; | ||||
|  | ||||
|         /* Update all dirents corresponding to this file. */ | ||||
|  | ||||
|         bool found = false; | ||||
|         for (int d = 0; d < _config.dir_entries(); d++) | ||||
|         { | ||||
|             std::unique_ptr<Entry> entry = getEntry(d); | ||||
|             if (entry->deleted) | ||||
|                 continue; | ||||
|             if (path[0] == entry->combinedFilename()) | ||||
|             { | ||||
|                 entry->mode = mode; | ||||
|                 putEntry(entry); | ||||
|                 found = true; | ||||
|             } | ||||
|         } | ||||
|         if (!found) | ||||
|             throw FileNotFoundException(); | ||||
|  | ||||
|         unmount(); | ||||
|     } | ||||
|  | ||||
|     Bytes getFile(const Path& path) override | ||||
|     { | ||||
|         mount(); | ||||
| @@ -190,9 +347,9 @@ public: | ||||
|             for (int d = 0; d < _config.dir_entries(); d++) | ||||
|             { | ||||
|                 entry = getEntry(d); | ||||
|                 if (!entry) | ||||
|                 if (entry->deleted) | ||||
|                     continue; | ||||
|                 if (path[0] != entry->filename) | ||||
|                 if (path[0] != entry->combinedFilename()) | ||||
|                     continue; | ||||
|                 if (entry->extent < logicalExtent) | ||||
|                     continue; | ||||
| @@ -201,7 +358,7 @@ public: | ||||
|                     break; | ||||
|             } | ||||
|  | ||||
|             if (!entry) | ||||
|             if (entry->deleted) | ||||
|             { | ||||
|                 if (logicalExtent == 0) | ||||
|                     throw FileNotFoundException(); | ||||
| @@ -236,8 +393,160 @@ public: | ||||
|         return data; | ||||
|     } | ||||
|  | ||||
| private: | ||||
|     void mount() | ||||
| public: | ||||
|     void putFile(const Path& path, const Bytes& bytes) override | ||||
|     { | ||||
|         mount(); | ||||
|         if (path.size() != 1) | ||||
|             throw BadPathException(); | ||||
|  | ||||
|         /* Test to see if the file already exists. */ | ||||
|  | ||||
|         for (int d = 0; d < _config.dir_entries(); d++) | ||||
|         { | ||||
|             std::unique_ptr<Entry> entry = getEntry(d); | ||||
|             if (entry->deleted) | ||||
|                 continue; | ||||
|             if (path[0] == entry->combinedFilename()) | ||||
|                 throw CannotWriteException(); | ||||
|         } | ||||
|  | ||||
|         /* Write blocks, one at a time. */ | ||||
|  | ||||
|         std::unique_ptr<Entry> entry; | ||||
|         ByteReader br(bytes); | ||||
|         while (!br.eof()) | ||||
|         { | ||||
|             unsigned extent = br.pos / 0x4000; | ||||
|             Bytes block = br.read(_config.block_size()); | ||||
|  | ||||
|             /* Allocate a block and write it. */ | ||||
|  | ||||
|             auto bit = std::find(_bitmap.begin(), _bitmap.end(), false); | ||||
|             if (bit == _bitmap.end()) | ||||
|                 throw DiskFullException(); | ||||
|             *bit = true; | ||||
|             unsigned blocknum = bit - _bitmap.begin(); | ||||
|             putCpmBlock(blocknum, block); | ||||
|  | ||||
|             /* Do we need a new directory entry? */ | ||||
|  | ||||
|             if (!entry || | ||||
|                 entry->allocation_map[std::size(entry->allocation_map) - 1]) | ||||
|             { | ||||
|                 if (entry) | ||||
|                 { | ||||
|                     entry->records = 0x80; | ||||
|                     putEntry(entry); | ||||
|                 } | ||||
|  | ||||
|                 entry.reset(); | ||||
|                 for (int d = 0; d < _config.dir_entries(); d++) | ||||
|                 { | ||||
|                     entry = getEntry(d); | ||||
|                     if (entry->deleted) | ||||
|                         break; | ||||
|                     entry.reset(); | ||||
|                 } | ||||
|  | ||||
|                 if (!entry) | ||||
|                     throw DiskFullException(); | ||||
|                 entry->deleted = false; | ||||
|                 entry->changeFilename(path[0]); | ||||
|                 entry->extent = extent; | ||||
|                 entry->mode = ""; | ||||
|                 std::fill(entry->allocation_map.begin(), | ||||
|                     entry->allocation_map.end(), | ||||
|                     0); | ||||
|             } | ||||
|  | ||||
|             /* Hook up the block in the allocation map. */ | ||||
|  | ||||
|             auto mit = std::find( | ||||
|                 entry->allocation_map.begin(), entry->allocation_map.end(), 0); | ||||
|             *mit = blocknum; | ||||
|         } | ||||
|         if (entry) | ||||
|         { | ||||
|             entry->records = ((bytes.size() & 0x3fff) + 127) / 128; | ||||
|             putEntry(entry); | ||||
|         } | ||||
|  | ||||
|         unmount(); | ||||
|     } | ||||
|  | ||||
|     void moveFile(const Path& oldPath, const Path& newPath) override | ||||
|     { | ||||
|         mount(); | ||||
|         if ((oldPath.size() != 1) || (newPath.size() != 1)) | ||||
|             throw BadPathException(); | ||||
|  | ||||
|         /* Check to make sure that the file exists, and that the new filename | ||||
|          * does not. */ | ||||
|  | ||||
|         bool found = false; | ||||
|         for (int d = 0; d < _config.dir_entries(); d++) | ||||
|         { | ||||
|             auto entry = getEntry(d); | ||||
|             if (entry->deleted) | ||||
|                 continue; | ||||
|  | ||||
|             auto filename = entry->combinedFilename(); | ||||
|             if (filename == oldPath[0]) | ||||
|                 found = true; | ||||
|             if (filename == newPath[0]) | ||||
|                 throw CannotWriteException(); | ||||
|         } | ||||
|         if (!found) | ||||
|             throw FileNotFoundException(); | ||||
|  | ||||
|         /* Now do the rename. */ | ||||
|  | ||||
|         for (int d = 0; d < _config.dir_entries(); d++) | ||||
|         { | ||||
|             auto entry = getEntry(d); | ||||
|             if (entry->deleted) | ||||
|                 continue; | ||||
|  | ||||
|             auto filename = entry->combinedFilename(); | ||||
|             if (filename == oldPath[0]) | ||||
|             { | ||||
|                 entry->changeFilename(newPath[0]); | ||||
|                 putEntry(entry); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         unmount(); | ||||
|     } | ||||
|  | ||||
|     void deleteFile(const Path& path) override | ||||
|     { | ||||
|         mount(); | ||||
|         if (path.size() != 1) | ||||
|             throw BadPathException(); | ||||
|  | ||||
|         /* Remove all dirents for this file. */ | ||||
|  | ||||
|         bool found = false; | ||||
|         for (int d = 0; d < _config.dir_entries(); d++) | ||||
|         { | ||||
|             auto entry = getEntry(d); | ||||
|             if (entry->deleted) | ||||
|                 continue; | ||||
|             if (path[0] != entry->combinedFilename()) | ||||
|                 continue; | ||||
|             entry->deleted = true; | ||||
|             putEntry(entry); | ||||
|             found = true; | ||||
|         } | ||||
|  | ||||
|         if (!found) | ||||
|             throw FileNotFoundException(); | ||||
|         unmount(); | ||||
|     } | ||||
|  | ||||
| public: | ||||
|     void mount() override | ||||
|     { | ||||
|         auto& start = _config.filesystem_start(); | ||||
|         _filesystemStart = | ||||
| @@ -268,26 +577,71 @@ private: | ||||
|         _blocksPerLogicalExtent = 16384 / _config.block_size(); | ||||
|  | ||||
|         _directory = getCpmBlock(0, _dirBlocks); | ||||
|  | ||||
|         /* Create the allocation bitmap. */ | ||||
|  | ||||
|         _bitmap.clear(); | ||||
|         _bitmap.resize(_filesystemBlocks); | ||||
|         for (int d = 0; d < _dirBlocks; d++) | ||||
|             _bitmap[d] = true; | ||||
|         for (int d = 0; d < _config.dir_entries(); d++) | ||||
|         { | ||||
|             std::unique_ptr<Entry> entry = getEntry(d); | ||||
|             if (entry->deleted) | ||||
|                 continue; | ||||
|             for (unsigned block : entry->allocation_map) | ||||
|             { | ||||
|                 if (block >= _filesystemBlocks) | ||||
|                     throw BadFilesystemException(); | ||||
|                 if (block) | ||||
|                     _bitmap[block] = true; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     void unmount() | ||||
|     { | ||||
|         putCpmBlock(0, _directory); | ||||
|     } | ||||
|  | ||||
| private: | ||||
|     std::unique_ptr<Entry> getEntry(unsigned d) | ||||
|     { | ||||
|         auto bytes = _directory.slice(d * 32, 32); | ||||
|         if (bytes[0] == 0xe5) | ||||
|             return nullptr; | ||||
|  | ||||
|         return std::make_unique<Entry>(bytes, _allocationMapSize); | ||||
|         return std::make_unique<Entry>(bytes, _allocationMapSize, d); | ||||
|     } | ||||
|  | ||||
|     Bytes getCpmBlock(uint32_t number, uint32_t count = 1) | ||||
|     void putEntry(std::unique_ptr<Entry>& entry) | ||||
|     { | ||||
|         unsigned sector = number * _blockSectors; | ||||
|         ByteWriter bw(_directory); | ||||
|         bw.seek(entry->index * 32); | ||||
|         bw.append(entry->toBytes(_allocationMapSize)); | ||||
|     } | ||||
|  | ||||
|     unsigned computeSector(uint32_t block) const | ||||
|     { | ||||
|         unsigned sector = block * _blockSectors; | ||||
|         if (_config.has_padding()) | ||||
|             sector += (sector / _config.padding().every()) * | ||||
|                       _config.padding().amount(); | ||||
|         return sector; | ||||
|     } | ||||
|  | ||||
|     Bytes getCpmBlock(uint32_t block, uint32_t count = 1) | ||||
|     { | ||||
|         return getLogicalSector( | ||||
|             sector + _filesystemStart, _blockSectors * count); | ||||
|             computeSector(block) + _filesystemStart, _blockSectors * count); | ||||
|     } | ||||
|  | ||||
|     void putCpmBlock(uint32_t block, const Bytes& bytes) | ||||
|     { | ||||
|         putLogicalSector(computeSector(block) + _filesystemStart, bytes); | ||||
|     } | ||||
|  | ||||
| public: | ||||
|     std::vector<bool> getBitmapForDebugging() override | ||||
|     { | ||||
|         return _bitmap; | ||||
|     } | ||||
|  | ||||
| private: | ||||
| @@ -303,6 +657,7 @@ private: | ||||
|     uint32_t _blocksPerLogicalExtent; | ||||
|     int _allocationMapSize; | ||||
|     Bytes _directory; | ||||
|     std::vector<bool> _bitmap; | ||||
| }; | ||||
|  | ||||
| std::unique_ptr<Filesystem> Filesystem::createCpmFsFilesystem( | ||||
|   | ||||
| @@ -277,4 +277,18 @@ public: | ||||
|     static std::unique_ptr<Filesystem> createFilesystemFromConfig(); | ||||
| }; | ||||
|  | ||||
| /* Used for tests only. */ | ||||
|  | ||||
| class HasBitmap | ||||
| { | ||||
| public: | ||||
|     virtual std::vector<bool> getBitmapForDebugging() = 0; | ||||
| }; | ||||
|  | ||||
| class HasMount | ||||
| { | ||||
| public: | ||||
|     virtual void mount() = 0; | ||||
| }; | ||||
|  | ||||
| #endif | ||||
|   | ||||
| @@ -8,9 +8,9 @@ script="$4" | ||||
| flags="$5" | ||||
| dir="$6" | ||||
|  | ||||
| srcfile=$dir.$format.src.img | ||||
| fluxfile=$dir.$format.$ext | ||||
| destfile=$dir.$format.dest.img | ||||
| srcfile=$dir/src.img | ||||
| fluxfile=$dir/flux.$ext | ||||
| destfile=$dir/dest.img | ||||
|  | ||||
| dd if=/dev/urandom of=$srcfile bs=1048576 count=2 2>&1 | ||||
|  | ||||
| @@ -21,9 +21,11 @@ if [ ! -s $destfile ]; then | ||||
| 	exit 1 | ||||
| fi | ||||
|  | ||||
| truncate $srcfile -r $destfile | ||||
| truncate -r $destfile $srcfile | ||||
| if ! cmp $srcfile $destfile; then | ||||
| 	echo "Comparison failed!" >&2 | ||||
| 	echo "Run this to repeat:" >&2 | ||||
| 	echo "./scripts/encodedecodetest.sh \"$1\" \"$2\" \"$3\" \"$4\" \"$5\" \"$6\"" >&2 | ||||
| 	exit 1 | ||||
| fi | ||||
| exit 0 | ||||
|   | ||||
| @@ -34,6 +34,7 @@ formats = [ | ||||
|     "rx50", | ||||
|     "shugart_drive", | ||||
|     "smaky6", | ||||
|     "tartu", | ||||
|     "tids990", | ||||
|     "tiki", | ||||
|     "victor9k", | ||||
|   | ||||
							
								
								
									
										105
									
								
								src/formats/tartu.textpb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								src/formats/tartu.textpb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | ||||
| shortname: 'Tartu' | ||||
| comment: 'The Palivere and variations' | ||||
| read_support_status: UNICORN | ||||
| write_support_status: DINOSAUR | ||||
|  | ||||
| documentation: | ||||
| <<< | ||||
| The Tartu Palivere is a 1988 Z80-based computer from Estonia. It is a CP/M | ||||
| machine with 64kB of RAM, running off a 2MHz КP580ВМ80А, a Soviet Union 8080 | ||||
| clone; it operated off punched tape, cassette, external hard drive or floppy, and was notable as being the first ever computer with an Estonian keyboard. | ||||
|  | ||||
| <div style="text-align: center"> | ||||
| <img src="tartu.jpg" alt="The Tartu computer's developer Leo Humal working with one."/> | ||||
| </div> | ||||
|  | ||||
| From a floppy disk perspective, it is interesting because the floppy drive | ||||
| interface is almost entirely handled in software --- necessary at the time as | ||||
| the usual floppy disk interface chip at the time, the КR1818VG93 (a Soviet clone | ||||
| of the WD1793), was hard to find. Instead, the floppy controller board was | ||||
| implemented entirely using TTL logic. Despite this, the encoding is fairly high | ||||
| density, using MFM and with up to 780kB on a double-sided 80 track disk. | ||||
|  | ||||
| <div style="text-align: center"> | ||||
| <img src="tartu-fdc.jpg" alt="The Tartu FDC with Soviet TTL logic chips."/> | ||||
| </div> | ||||
|  | ||||
| FluxEngine supports reading and writing Tartu disks with CP/M filesystem access. | ||||
| >>> | ||||
|  | ||||
| documentation: | ||||
| <<< | ||||
| ## References | ||||
|  | ||||
|   - [The Estonia Museum of Electronics](https://www.elektroonikamuuseum.ee/tartu_arvuti_lugu.html) | ||||
| >>> | ||||
|  | ||||
| image_writer { | ||||
| 	filename: "tartu.img" | ||||
| 	type: IMAGETYPE_IMG | ||||
| } | ||||
|  | ||||
| layout { | ||||
| 	layoutdata { | ||||
| 		sector_size: 128 | ||||
| 		physical { | ||||
| 			start_sector: 1 | ||||
| 			count: 39 | ||||
| 		} | ||||
|  | ||||
| 		filesystem { | ||||
| 			start_sector: 1 | ||||
| 			count: 39 | ||||
| 			skew: 3 | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| encoder { | ||||
| 	tartu {} | ||||
| } | ||||
|  | ||||
| decoder { | ||||
| 	tartu {} | ||||
| } | ||||
|  | ||||
| option_group { | ||||
| 	comment: "$formats" | ||||
|  | ||||
| 	option { | ||||
| 		name: "390" | ||||
| 		comment: '390kB 5.25" 40-track DSDD' | ||||
|  | ||||
| 		config { | ||||
| 			layout { | ||||
| 				format_type: FORMATTYPE_40TRACK | ||||
| 				tracks: 40 | ||||
| 				sides: 2 | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	option { | ||||
| 		name: "780" | ||||
| 		comment: '780kB 5.25" 80-track DSDD' | ||||
|  | ||||
| 		config { | ||||
| 			layout { | ||||
| 				format_type: FORMATTYPE_80TRACK | ||||
| 				tracks: 80 | ||||
| 				sides: 2 | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| filesystem { | ||||
| 	type: CPMFS | ||||
| 	cpmfs { | ||||
| 		filesystem_start { | ||||
| 			track: 1 | ||||
| 		} | ||||
| 		block_size: 2048 | ||||
| 		dir_entries: 128 | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										202
									
								
								tests/cpmfs.cc
									
									
									
									
									
								
							
							
						
						
									
										202
									
								
								tests/cpmfs.cc
									
									
									
									
									
								
							| @@ -37,14 +37,50 @@ namespace | ||||
|     }; | ||||
| } | ||||
|  | ||||
| namespace snowhouse | ||||
| { | ||||
|     template <> | ||||
|     struct Stringizer<std::vector<bool>> | ||||
|     { | ||||
|         static std::string ToString(const std::vector<bool>& vector) | ||||
|         { | ||||
|             std::stringstream stream; | ||||
|             stream << '{'; | ||||
|             bool first = true; | ||||
|             for (const auto& item : vector) | ||||
|             { | ||||
|                 if (!first) | ||||
|                     stream << ", "; | ||||
|                 stream << item; | ||||
|                 first = false; | ||||
|             } | ||||
|             stream << '}'; | ||||
|             return stream.str(); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     template <> | ||||
|     struct Stringizer<Bytes> | ||||
|     { | ||||
|         static std::string ToString(const Bytes& bytes) | ||||
|         { | ||||
|             std::stringstream stream; | ||||
|             stream << '\n'; | ||||
|             hexdump(stream, bytes); | ||||
|             return stream.str(); | ||||
|         } | ||||
|     }; | ||||
| } | ||||
|  | ||||
| static Bytes createDirent(const std::string& filename, | ||||
|     int extent, | ||||
|     int records, | ||||
|     const std::initializer_list<int> blocks) | ||||
|     const std::initializer_list<int> blocks, | ||||
|     int user = 0) | ||||
| { | ||||
|     Bytes dirent; | ||||
|     ByteWriter bw(dirent); | ||||
|     bw.write_8(0); | ||||
|     bw.write_8(user); | ||||
|     bw.append(filename); | ||||
|     while (bw.pos != 12) | ||||
|         bw.write_8(' '); | ||||
| @@ -69,6 +105,21 @@ static void setBlock( | ||||
|         sectors->put(block, 0, i)->data = data.slice(i * 256, 256); | ||||
| } | ||||
|  | ||||
| static Bytes getBlock( | ||||
|     const std::shared_ptr<SectorInterface>& sectors, int block, int length) | ||||
| { | ||||
|     Bytes bytes; | ||||
|     ByteWriter bw(bytes); | ||||
|  | ||||
|     for (int i = 0; i < (length + 127) / 128; i++) | ||||
|     { | ||||
|         auto sector = sectors->get(block, 0, i); | ||||
|         bw.append(sector->data); | ||||
|     } | ||||
|  | ||||
|     return bytes; | ||||
| } | ||||
|  | ||||
| static void testPartialExtent() | ||||
| { | ||||
|     auto sectors = std::make_shared<TestSectorInterface>(); | ||||
| @@ -113,6 +164,143 @@ static void testLogicalExtents() | ||||
|     AssertThat(data[0x4000 * 2], Equals(3)); | ||||
| } | ||||
|  | ||||
| static void testBitmap() | ||||
| { | ||||
|     auto sectors = std::make_shared<TestSectorInterface>(); | ||||
|     auto fs = Filesystem::createCpmFsFilesystem( | ||||
|         globalConfig()->filesystem(), sectors); | ||||
|  | ||||
|     setBlock(sectors, | ||||
|         0, | ||||
|         createDirent("FILE", 1, 128, {1, 0, 0, 0, 0, 0, 0, 0, 2}) + | ||||
|             createDirent("FILE", 2, 128, {4}) + (blank_dirent * 62)); | ||||
|  | ||||
|     dynamic_cast<HasMount*>(fs.get())->mount(); | ||||
|     std::vector<bool> bitmap = | ||||
|         dynamic_cast<HasBitmap*>(fs.get())->getBitmapForDebugging(); | ||||
|     AssertThat(bitmap, | ||||
|         Equals(std::vector<bool>{ | ||||
|             1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})); | ||||
| } | ||||
| #if 0 | ||||
|  | ||||
| static void testPutGet() | ||||
| { | ||||
|     auto sectors = std::make_shared<TestSectorInterface>(); | ||||
|     auto fs = Filesystem::createCpmFsFilesystem( | ||||
|         globalConfig()->filesystem(), sectors); | ||||
|     fs->create(true, "volume"); | ||||
|  | ||||
|     fs->putFile(Path("0:FILE1"), Bytes{1, 2, 3, 4}); | ||||
|     fs->putFile(Path("0:FILE2"), Bytes{5, 6, 7, 8}); | ||||
|  | ||||
|     dynamic_cast<HasMount*>(fs.get())->mount(); | ||||
|     std::vector<bool> bitmap = | ||||
|         dynamic_cast<HasBitmap*>(fs.get())->getBitmapForDebugging(); | ||||
|     AssertThat(bitmap, | ||||
|         Equals(std::vector<bool>{ | ||||
|             1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})); | ||||
|  | ||||
|     auto directory = getBlock(sectors, 0, 256).slice(0, 64); | ||||
|     AssertThat(directory, | ||||
|         Equals(createDirent("FILE1", 0, 1, {1}) + | ||||
|                createDirent("FILE2", 0, 1, {2}))); | ||||
|  | ||||
|     auto file1 = getBlock(sectors, 1, 8).slice(0, 8); | ||||
|     AssertThat(file1, Equals(Bytes{1, 2, 3, 4, 0, 0, 0, 0})); | ||||
|  | ||||
|     auto file2 = getBlock(sectors, 2, 8).slice(0, 8); | ||||
|     AssertThat(file2, Equals(Bytes{5, 6, 7, 8, 0, 0, 0, 0})); | ||||
| } | ||||
|  | ||||
| static void testPutBigFile() | ||||
| { | ||||
|     auto sectors = std::make_shared<TestSectorInterface>(); | ||||
|     auto fs = Filesystem::createCpmFsFilesystem( | ||||
|         globalConfig()->filesystem(), sectors); | ||||
|     fs->create(true, "volume"); | ||||
|  | ||||
|     Bytes filedata; | ||||
|     ByteWriter bw(filedata); | ||||
|     while (filedata.size() < 0x9000) | ||||
|         bw.write_le32(bw.pos); | ||||
|  | ||||
|     fs->putFile(Path("0:BIGFILE"), filedata); | ||||
|  | ||||
|     auto directory = getBlock(sectors, 0, 256).slice(0, 64); | ||||
|     AssertThat(directory, | ||||
|         Equals(createDirent("BIGFILE", | ||||
|                    0, | ||||
|                    0x80, | ||||
|                    {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}) + | ||||
|                createDirent("BIGFILE", 2, 0x20, {17, 18}))); | ||||
| } | ||||
|  | ||||
| static void testDelete() | ||||
| { | ||||
|     auto sectors = std::make_shared<TestSectorInterface>(); | ||||
|     auto fs = Filesystem::createCpmFsFilesystem( | ||||
|         globalConfig()->filesystem(), sectors); | ||||
|     fs->create(true, "volume"); | ||||
|  | ||||
|     fs->putFile(Path("0:FILE1"), Bytes{1, 2, 3, 4}); | ||||
|     fs->putFile(Path("0:FILE2"), Bytes{5, 6, 7, 8}); | ||||
|     fs->deleteFile(Path("0:FILE1")); | ||||
|  | ||||
|     auto directory = getBlock(sectors, 0, 256).slice(0, 64); | ||||
|     AssertThat(directory, | ||||
|         Equals((Bytes{0xe5} * 32) + createDirent("FILE2", 0, 1, {2}))); | ||||
| } | ||||
|  | ||||
| static void testMove() | ||||
| { | ||||
|     auto sectors = std::make_shared<TestSectorInterface>(); | ||||
|     auto fs = Filesystem::createCpmFsFilesystem( | ||||
|         globalConfig()->filesystem(), sectors); | ||||
|     fs->create(true, "volume"); | ||||
|  | ||||
|     fs->putFile(Path("0:FILE1"), Bytes{0x55} * 0x9000); | ||||
|     fs->putFile(Path("0:FILE2"), Bytes{5, 6, 7, 8}); | ||||
|  | ||||
|     fs->moveFile(Path("0:FILE1"), Path("1:FILE3")); | ||||
|  | ||||
|     auto directory = getBlock(sectors, 0, 256).slice(0, 32 * 3); | ||||
|     AssertThat(directory, | ||||
|         Equals(createDirent("FILE3", | ||||
|                    0, | ||||
|                    0x80, | ||||
|                    {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, | ||||
|                    1) + | ||||
|                createDirent("FILE3", 2, 0x20, {17, 18}, 1) + | ||||
|                createDirent("FILE2", 0, 1, {19}))); | ||||
| } | ||||
|  | ||||
| static void testPutMetadata() | ||||
| { | ||||
|     auto sectors = std::make_shared<TestSectorInterface>(); | ||||
|     auto fs = Filesystem::createCpmFsFilesystem( | ||||
|         globalConfig()->filesystem(), sectors); | ||||
|     fs->create(true, "volume"); | ||||
|  | ||||
|     fs->putFile(Path("0:FILE1"), Bytes{0x55} * 0x9000); | ||||
|     fs->putFile(Path("0:FILE2"), Bytes{5, 6, 7, 8}); | ||||
|  | ||||
|     fs->putMetadata(Path("0:FILE1"), | ||||
|         std::map<std::string, std::string>{ | ||||
|             {"mode", "SRA"} | ||||
|     }); | ||||
|  | ||||
|     auto directory = getBlock(sectors, 0, 256).slice(0, 32 * 3); | ||||
|     AssertThat(directory, | ||||
|         Equals(createDirent("FILE1   \xa0\xa0\xa0", | ||||
|                    0, | ||||
|                    0x80, | ||||
|                    {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}) + | ||||
|                createDirent("FILE1   \xa0\xa0\xa0", 2, 0x20, {17, 18}) + | ||||
|                createDirent("FILE2", 0, 1, {19}))); | ||||
| } | ||||
|  | ||||
| #endif | ||||
| int main(void) | ||||
| { | ||||
|     try | ||||
| @@ -124,7 +312,7 @@ int main(void) | ||||
|  | ||||
| 			layout { | ||||
| 				format_type: FORMATTYPE_80TRACK | ||||
| 				tracks: 10 | ||||
| 				tracks: 20 | ||||
| 				sides: 1 | ||||
| 				layoutdata { | ||||
| 					sector_size: 256 | ||||
| @@ -148,6 +336,14 @@ int main(void) | ||||
|  | ||||
|         testPartialExtent(); | ||||
|         testLogicalExtents(); | ||||
| #if 0 | ||||
|         testBitmap(); | ||||
|         testPutGet(); | ||||
|         testPutBigFile(); | ||||
|         testDelete(); | ||||
|         testMove(); | ||||
|         testPutMetadata(); | ||||
| #endif | ||||
|     } | ||||
|     catch (const ErrorException& e) | ||||
|     { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user