From 549a984eab53fcd7fca5edad93a86701ab7250c8 Mon Sep 17 00:00:00 2001 From: David Given Date: Fri, 4 Mar 2022 23:42:29 +0100 Subject: [PATCH] Add support for writing 120kB Brother disk images. Also add some utils tests, because they needed it. --- doc/disk-brother.md | 11 +- lib/globals.h | 2 + lib/utils.cc | 32 ++++- lib/utils.h | 7 +- mkninja.sh | 1 + src/fluxengine.cc | 2 +- src/gui/mainwindow.cc | 2 +- tests/utils.cc | 47 +++++++ tools/brother120tool.cc | 247 ++++++++++++++++++++++++++++++------- tools/brother240tool.cc | 56 +++++---- tools/upgrade-flux-file.cc | 100 ++++++++------- 11 files changed, 382 insertions(+), 125 deletions(-) create mode 100644 tests/utils.cc diff --git a/doc/disk-brother.md b/doc/disk-brother.md index 6cf5eeb1..75c52c2f 100644 --- a/doc/disk-brother.md +++ b/doc/disk-brother.md @@ -111,13 +111,13 @@ with an obvious directory and allocation table. I have reversed engineered a very simple tool for extracting files from it. To show the directory, do: ``` -.obj/brother120tool image.img +brother120tool image.img ``` To extract a file, do: ``` -.obj/brother120tool image.img filename +brother120tool image.img filename ``` Wildcards are supported, so use `'*'` for the filename (remember to quote it) @@ -131,6 +131,13 @@ format](https://mathesoft.eu/brother-wp-1-dokumente/) and has produced a RTF](https://mathesoft.eu/sdm_downloads/wp2rtf/). This will only work on WP-1 files. +To create a disk image (note: this creates a _new_ disk image, overwriting the +previous image), do: + +``` +brother120tool --create image.img filename1 filename2... +``` + Any questions? please [get in touch](https://github.com/davidgiven/fluxengine/issues/new). diff --git a/lib/globals.h b/lib/globals.h index e252361b..92e427c8 100644 --- a/lib/globals.h +++ b/lib/globals.h @@ -34,6 +34,8 @@ extern void hexdumpForSrp16(std::ostream& stream, const Bytes& bytes); struct ErrorException { const std::string message; + + void print() const; }; class Error diff --git a/lib/utils.cc b/lib/utils.cc index 20f19d17..66f90eb2 100644 --- a/lib/utils.cc +++ b/lib/utils.cc @@ -4,6 +4,12 @@ bool emergencyStop = false; static const char* WHITESPACE = " \t\n\r\f\v"; +static const char* SEPARATORS = "/\\"; + +void ErrorException::print() const +{ + std::cerr << message << '\n'; +} bool beginsWith(const std::string& value, const std::string& ending) { @@ -23,20 +29,36 @@ bool endsWith(const std::string& value, const std::string& ending) std::equal(ending.rbegin(), ending.rend(), lowercase.begin()); } -void leftTrimWhitespace(std::string& value) +std::string leftTrimWhitespace(std::string value) { value.erase(0, value.find_first_not_of(WHITESPACE)); + return value; } -void rightTrimWhitespace(std::string& value) +std::string rightTrimWhitespace(std::string value) { value.erase(value.find_last_not_of(WHITESPACE) + 1); + return value; } -void trimWhitespace(std::string& value) +std::string trimWhitespace(const std::string& value) { - leftTrimWhitespace(value); - rightTrimWhitespace(value); + return leftTrimWhitespace(rightTrimWhitespace(value)); +} + +std::string getLeafname(const std::string& value) +{ + constexpr char sep = '/'; + #ifdef _WIN32 + sep = '\\'; + #endif + + size_t i = value.find_last_of(SEPARATORS); + if (i != std::string::npos) { + return value.substr(i+1, value.length() - i); + } + + return value; } void testForEmergencyStop() diff --git a/lib/utils.h b/lib/utils.h index e8fccdb3..5d8bd64c 100644 --- a/lib/utils.h +++ b/lib/utils.h @@ -5,9 +5,10 @@ extern bool beginsWith(const std::string& value, const std::string& beginning); extern bool endsWith(const std::string& value, const std::string& ending); -extern void leftTrimWhitespace(std::string& value); -extern void rightTrimWhitespace(std::string& value); -extern void trimWhitespace(std::string& value); +extern std::string leftTrimWhitespace(std::string value); +extern std::string rightTrimWhitespace(std::string value); +extern std::string trimWhitespace(const std::string& value); +extern std::string getLeafname(const std::string& value); /* If set, any running job will terminate as soon as possible (with an error). */ diff --git a/mkninja.sh b/mkninja.sh index fc7cf135..09498288 100644 --- a/mkninja.sh +++ b/mkninja.sh @@ -640,6 +640,7 @@ runtest fmmfm-test tests/fmmfm.cc runtest greaseweazle-test tests/greaseweazle.cc runtest kryoflux-test tests/kryoflux.cc runtest ldbs-test tests/ldbs.cc +runtest utils-test tests/utils.cc runtest proto-test -I$OBJDIR/proto \ -d $OBJDIR/proto/libconfig.def \ -d $OBJDIR/proto/libtestproto.def \ diff --git a/src/fluxengine.cc b/src/fluxengine.cc index b9ca6061..43b7bac3 100644 --- a/src/fluxengine.cc +++ b/src/fluxengine.cc @@ -148,7 +148,7 @@ int main(int argc, const char* argv[]) } catch (const ErrorException& e) { - std::cerr << e.message << '\n'; + e.print(); exit(1); } } diff --git a/src/gui/mainwindow.cc b/src/gui/mainwindow.cc index 2a8799c1..91659ff7 100644 --- a/src/gui/mainwindow.cc +++ b/src/gui/mainwindow.cc @@ -261,7 +261,7 @@ void MainWindow::ApplyCustomSettings() for (int i = 0; i < additionalSettingsEntry->GetNumberOfLines(); i++) { auto setting = additionalSettingsEntry->GetLineText(i).ToStdString(); - trimWhitespace(setting); + setting = trimWhitespace(setting); if (setting.size() == 0) continue; diff --git a/tests/utils.cc b/tests/utils.cc new file mode 100644 index 00000000..54551690 --- /dev/null +++ b/tests/utils.cc @@ -0,0 +1,47 @@ +#include "globals.h" +#include "utils.h" +#include "snowhouse/snowhouse.h" + +using namespace snowhouse; + +static void testLeftTrim() +{ + AssertThat(leftTrimWhitespace("string"), Equals("string")); + AssertThat(leftTrimWhitespace(" string"), Equals("string")); + AssertThat(leftTrimWhitespace(" string "), Equals("string ")); + AssertThat(leftTrimWhitespace("string "), Equals("string ")); +} + +static void testRightTrim() +{ + AssertThat(rightTrimWhitespace("string"), Equals("string")); + AssertThat(rightTrimWhitespace(" string"), Equals(" string")); + AssertThat(rightTrimWhitespace(" string "), Equals(" string")); + AssertThat(rightTrimWhitespace("string "), Equals("string")); +} + +static void testTrim() +{ + AssertThat(trimWhitespace("string"), Equals("string")); + AssertThat(trimWhitespace(" string"), Equals("string")); + AssertThat(trimWhitespace(" string "), Equals("string")); + AssertThat(trimWhitespace("string "), Equals("string")); +} + +static void testLeafname() +{ + AssertThat(getLeafname(""), Equals("")); + AssertThat(getLeafname("filename"), Equals("filename")); + AssertThat(getLeafname("path/filename"), Equals("filename")); + AssertThat(getLeafname("/path/path/filename"), Equals("filename")); +} + +int main(void) +{ + testLeftTrim(); + testRightTrim(); + testTrim(); + testLeafname(); + return 0; +} + diff --git a/tools/brother120tool.cc b/tools/brother120tool.cc index c1dc392e..b57f6aa6 100644 --- a/tools/brother120tool.cc +++ b/tools/brother120tool.cc @@ -1,10 +1,21 @@ #include "globals.h" +#include "bytes.h" #include "fmt/format.h" +#include "utils.h" #include #include "fnmatch.h" -/* Theoretical maximum number of sectors. */ -static const int SECTOR_COUNT = 640; +/* Number of sectors on a 120kB disk. */ +static constexpr int SECTOR_COUNT = 468; + +/* Start sector for data (after the directory */ +static constexpr int DATA_START_SECTOR = 14; + +/* Size of a sector */ +static constexpr int SECTOR_SIZE = 256; + +/* Number of dirents in a directory. */ +static constexpr int DIRECTORY_SIZE = 128; struct Dirent { @@ -14,19 +25,14 @@ struct Dirent int sectorCount; }; -static std::ifstream inputFile; +static std::fstream file; static std::map> directory; static std::map allocationTable; -static std::string& rtrim(std::string& s, const char* t = " \t\n\r\f\v") -{ - s.erase(s.find_last_not_of(t) + 1); - return s; -} - void syntax() { std::cout << "Syntax: brother120tool []\n" + " brother120tool --create \n" "If you specify a filename, it's extracted into the current directory.\n" "Wildcards are allowed. If you don't, the directory is listed instead.\n"; exit(0); @@ -34,26 +40,64 @@ void syntax() void readDirectory() { - for (int i=0; i<0x80; i++) + for (int i=0; i dirent(new Dirent); - dirent->filename = filename; - dirent->type = buffer[8]; - dirent->startSector = buffer[9]*256 + buffer[10]; - dirent->sectorCount = buffer[11]; + dirent->filename = filename; + dirent->type = br.read_8(); + dirent->startSector = br.read_be16(); + dirent->sectorCount = br.read_8(); directory[filename] = std::move(dirent); } } +void writeDirectory() +{ + Bytes buffer(2048); + ByteWriter bw(buffer); + + int count = 0; + for (const auto& it : directory) + { + const auto& dirent = it.second; + + if (count == DIRECTORY_SIZE) + Error() << "too many files on disk"; + + bw.append(dirent->filename); + for (int i=dirent->filename.size(); i<8; i++) + bw.write_8(' '); + + bw.write_8(dirent->type); + bw.write_be16(dirent->startSector); + bw.write_8(dirent->sectorCount); + bw.write_be32(0); /* unknown */ + count++; + } + + static const Bytes padding(15); + while (count < DIRECTORY_SIZE) + { + bw.write_8(0xf0); + bw.append(padding); + count++; + } + + file.seekp(0, std::ifstream::beg); + buffer.writeTo(file); +} + static bool isValidFile(const Dirent& dirent) { return (dirent.filename[0] & 0x80) == 0; @@ -61,17 +105,47 @@ static bool isValidFile(const Dirent& dirent) void readAllocationTable() { - for (int sector=14; sector!=SECTOR_COUNT; sector++) + for (int sector=1; sector!=SECTOR_COUNT; sector++) { - inputFile.seekg((sector-1)*2 + 0x800, std::ifstream::beg); - uint8_t buffer[2]; - inputFile.read((char*) buffer, sizeof(buffer)); + file.seekg((sector-1)*2 + 0x800, std::ifstream::beg); + Bytes buffer(2); + file.read((char*) &buffer[0], buffer.size()); - uint16_t nextSector = (buffer[0]*256) + buffer[1]; + uint16_t nextSector = buffer.reader().read_be16(); allocationTable[sector] = nextSector; } } +void writeAllocationTable() +{ + Bytes buffer(SECTOR_COUNT*2); + ByteWriter bw(buffer); + + for (int sector=1; sector<(DATA_START_SECTOR-2); sector++) + bw.write_le16(sector+1); + bw.write_le16(0xffff); + bw.write_le16(0xffff); + for (int sector=DATA_START_SECTOR; sector!=SECTOR_COUNT; sector++) + bw.write_be16(allocationTable[sector]); + + file.seekp(0x800, std::ifstream::beg); + buffer.writeTo(file); +} + +uint16_t allocateSector() +{ + for (int sector=DATA_START_SECTOR; sector!=SECTOR_COUNT; sector++) + { + if (allocationTable[sector] == 0) + { + allocationTable[sector] = 0xffff; + return sector; + } + } + Error() << "unable to allocate sector --- disk full"; + return 0; +} + void checkConsistency() { /* Verify that we more-or-less understand the format by fscking the disk. */ @@ -121,6 +195,54 @@ void listDirectory() } } +void insertFile(const std::string& filename) +{ + auto leafname = getLeafname(filename); + if (leafname.size() > 8) + Error() << "filename too long"; + std::cout << fmt::format("Inserting '{}'\n", leafname); + + std::ifstream inputFile(filename, std::ios::in | std::ios::binary); + if (!inputFile) + Error() << fmt::format("unable to open input file: {}", strerror(errno)); + + if (directory.find(leafname) != directory.end()) + Error() << fmt::format("duplicate filename: {}", leafname); + + auto dirent = std::make_unique(); + dirent->filename = leafname; + dirent->type = 0; + dirent->startSector = 0xffff; + dirent->sectorCount = 0; + + uint16_t lastSector = 0xffff; + while (!inputFile.eof()) + { + char buffer[SECTOR_SIZE]; + inputFile.read(buffer, sizeof(buffer)); + if (inputFile.gcount() == 0) + break; + if (inputFile.bad()) + Error() << fmt::format("I/O error on read: {}", strerror(errno)); + + uint16_t thisSector = allocateSector(); + if (lastSector == 0xffff) + dirent->startSector = thisSector; + else + allocationTable[lastSector] = thisSector; + dirent->sectorCount++; + + file.seekp((thisSector-1) * 0x100, std::ifstream::beg); + file.write(buffer, sizeof(buffer)); + if (file.bad()) + Error() << fmt::format("I/O error on write: {}", strerror(errno)); + + lastSector = thisSector; + } + + directory[leafname] = std::move(dirent); +} + void extractFile(const std::string& pattern) { for (const auto& i : directory) @@ -143,8 +265,8 @@ void extractFile(const std::string& pattern) while ((sector != 0) && (sector != 0xffff)) { uint8_t buffer[256]; - inputFile.seekg((sector-1) * 0x100, std::ifstream::beg); - if (!inputFile.read((char*) buffer, sizeof(buffer))) + file.seekg((sector-1) * 0x100, std::ifstream::beg); + if (!file.read((char*) buffer, sizeof(buffer))) Error() << fmt::format("I/O error on read: {}", strerror(errno)); if (!outputFile.write((const char*) buffer, sizeof(buffer))) Error() << fmt::format("I/O error on write: {}", strerror(errno)); @@ -154,27 +276,66 @@ void extractFile(const std::string& pattern) } } -int main(int argc, const char* argv[]) +static void doCreate(int argc, const char* argv[]) { - if (argc < 2) - syntax(); - - inputFile.open(argv[1], std::ios::in | std::ios::binary); - if (!inputFile.is_open()) + if (argc < 3) + syntax(); + + file.open(argv[1], std::ios::out | std::ios::binary | std::ios::trunc); + if (!file.is_open()) + Error() << fmt::format("cannot open output file '{}'", argv[1]); + + file.seekp(SECTOR_COUNT*SECTOR_SIZE - 1, std::ifstream::beg); + file.put(0); + + for (int i=2; i 1) && (strcmp(argv[1], "--create") == 0)) + doCreate(argc-1, argv+1); + else + doExtract(argc, argv); + } + catch (const ErrorException& e) + { + e.print(); + exit(1); + } return 0; } diff --git a/tools/brother240tool.cc b/tools/brother240tool.cc index 68ef1e8e..cf365523 100644 --- a/tools/brother240tool.cc +++ b/tools/brother240tool.cc @@ -26,31 +26,39 @@ void putbyte(uint32_t offset, uint8_t value) int main(int argc, const char* argv[]) { - if (argc < 2) - syntax(); - - inputFile.open(argv[1], std::ios::in | std::ios::out | std::ios::binary); - if (!inputFile.is_open()) - Error() << fmt::format("cannot open input file '{}'", argv[1]); + try + { + if (argc < 2) + syntax(); + + inputFile.open(argv[1], std::ios::in | std::ios::out | std::ios::binary); + if (!inputFile.is_open()) + Error() << fmt::format("cannot open input file '{}'", argv[1]); - uint8_t b1 = getbyte(0x015); - uint8_t b2 = getbyte(0x100); - if ((b1 == 0x58) && (b2 == 0x58)) - { - std::cerr << "Flipping from Brother to DOS.\n"; - putbyte(0x015, 0xf0); - putbyte(0x100, 0xf0); - } - else if ((b1 == 0xf0) && (b2 == 0xf0)) - { - std::cerr << "Flipping from DOS to Brother.\n"; - putbyte(0x015, 0x58); - putbyte(0x100, 0x58); - } - else - Error() << "Unknown image format."; + uint8_t b1 = getbyte(0x015); + uint8_t b2 = getbyte(0x100); + if ((b1 == 0x58) && (b2 == 0x58)) + { + std::cerr << "Flipping from Brother to DOS.\n"; + putbyte(0x015, 0xf0); + putbyte(0x100, 0xf0); + } + else if ((b1 == 0xf0) && (b2 == 0xf0)) + { + std::cerr << "Flipping from DOS to Brother.\n"; + putbyte(0x015, 0x58); + putbyte(0x100, 0x58); + } + else + Error() << "Unknown image format."; - inputFile.close(); - return 0; + inputFile.close(); + return 0; + } + catch (const ErrorException& e) + { + e.print(); + exit(1); + } } diff --git a/tools/upgrade-flux-file.cc b/tools/upgrade-flux-file.cc index 6351eab8..e5afb950 100644 --- a/tools/upgrade-flux-file.cc +++ b/tools/upgrade-flux-file.cc @@ -255,59 +255,67 @@ static void translateFluxVersion2(Fluxmap& fluxmap, const Bytes& bytes) int main(int argc, const char* argv[]) { - if ((argc != 2) || (strcmp(argv[1], "--help") == 0)) - syntax(); + try + { + if ((argc != 2) || (strcmp(argv[1], "--help") == 0)) + syntax(); - std::string filename = argv[1]; - if (!isSqlite(filename)) - { - std::cout << "File is up to date.\n"; - exit(0); - } + std::string filename = argv[1]; + if (!isSqlite(filename)) + { + std::cout << "File is up to date.\n"; + exit(0); + } - std::string outFilename = filename + ".out.flux"; - auto db = sqlOpen(filename, SQLITE_OPEN_READONLY); - int version = sqlGetVersion(db); + std::string outFilename = filename + ".out.flux"; + auto db = sqlOpen(filename, SQLITE_OPEN_READONLY); + int version = sqlGetVersion(db); - { - auto fluxsink = FluxSink::createFl2FluxSink(outFilename); - for (const auto& locations : sqlFindFlux(db)) - { - unsigned cylinder = locations.first; - unsigned head = locations.second; - Bytes bytes = sqlReadFluxBytes(db, cylinder, head); - Fluxmap fluxmap; - switch (version) - { - case FLUX_VERSION_2: - translateFluxVersion2(fluxmap, bytes); - break; + { + auto fluxsink = FluxSink::createFl2FluxSink(outFilename); + for (const auto& locations : sqlFindFlux(db)) + { + unsigned cylinder = locations.first; + unsigned head = locations.second; + Bytes bytes = sqlReadFluxBytes(db, cylinder, head); + Fluxmap fluxmap; + switch (version) + { + case FLUX_VERSION_2: + translateFluxVersion2(fluxmap, bytes); + break; - case FLUX_VERSION_3: - fluxmap.appendBytes(bytes); - break; + case FLUX_VERSION_3: + fluxmap.appendBytes(bytes); + break; - default: - Error() << fmt::format( - "you cannot upgrade version {} files (please file a " - "bug)", - version); - } - fluxsink->writeFlux(cylinder, head, fluxmap); - std::cout << '.' << std::flush; - } + default: + Error() << fmt::format( + "you cannot upgrade version {} files (please file a " + "bug)", + version); + } + fluxsink->writeFlux(cylinder, head, fluxmap); + std::cout << '.' << std::flush; + } - std::cout << "Writing output file...\n"; - } + std::cout << "Writing output file...\n"; + } - sqlite3_close(db); + sqlite3_close(db); - if (remove(filename.c_str()) != 0) - Error() << fmt::format( - "couldn't remove input file: {}", strerror(errno)); + if (remove(filename.c_str()) != 0) + Error() << fmt::format( + "couldn't remove input file: {}", strerror(errno)); - if (rename(outFilename.c_str(), filename.c_str()) != 0) - Error() << fmt::format( - "couldn't replace input file: {}", strerror(errno)); - return 0; + if (rename(outFilename.c_str(), filename.c_str()) != 0) + Error() << fmt::format( + "couldn't replace input file: {}", strerror(errno)); + return 0; + } + catch (const ErrorException& e) + { + e.print(); + exit(1); + } }