diff options
author | 2017-10-03 14:14:15 +0200 | |
---|---|---|
committer | 2017-10-03 23:14:15 +1100 | |
commit | 70d6a5addd6cac35df51cb9de182bffbe56af8b3 (patch) | |
tree | 782dd6368446f235ffe87ed63a3dae79b3ffe62e | |
parent | a7eaed12766365a59655b7fc1abda7a3ca782958 (diff) |
Support for HP85 tapes added to imgtool (#2688)
-rw-r--r-- | scripts/src/tools.lua | 3 | ||||
-rw-r--r-- | src/tools/imgtool/main.cpp | 2 | ||||
-rw-r--r-- | src/tools/imgtool/modules.cpp | 1 | ||||
-rw-r--r-- | src/tools/imgtool/modules/hp85_tape.cpp | 1196 |
4 files changed, 1200 insertions, 2 deletions
diff --git a/scripts/src/tools.lua b/scripts/src/tools.lua index 083554cd76c..ba9ea8a1274 100644 --- a/scripts/src/tools.lua +++ b/scripts/src/tools.lua @@ -752,7 +752,8 @@ files { MAME_DIR .. "src/tools/imgtool/modules/psion.cpp", MAME_DIR .. "src/tools/imgtool/modules/bml3.cpp", MAME_DIR .. "src/tools/imgtool/modules/hp48.cpp", - MAME_DIR .. "src/tools/imgtool/modules/hp9845_tape.cpp", + MAME_DIR .. "src/tools/imgtool/modules/hp9845_tape.cpp", + MAME_DIR .. "src/tools/imgtool/modules/hp85_tape.cpp", } configuration { "mingw*" or "vs*" } diff --git a/src/tools/imgtool/main.cpp b/src/tools/imgtool/main.cpp index 516c4f49e4e..ee3f603dc6f 100644 --- a/src/tools/imgtool/main.cpp +++ b/src/tools/imgtool/main.cpp @@ -412,7 +412,7 @@ static int cmd_getall(const struct command *c, int argc, char *argv[]) imgtool_dirent ent; filter_getinfoproc filter; int unnamedargs; - const char *path = nullptr; + const char *path = ""; int arg; int partition_index = 0; diff --git a/src/tools/imgtool/modules.cpp b/src/tools/imgtool/modules.cpp index 7cc32959fa5..ec2762d34fc 100644 --- a/src/tools/imgtool/modules.cpp +++ b/src/tools/imgtool/modules.cpp @@ -102,5 +102,6 @@ MODULE(psion) MODULE(bml3) MODULE(hp48) MODULE(hp9845_tape) +MODULE(hp85_tape) #endif /* MODULES_RECURSIVE */ diff --git a/src/tools/imgtool/modules/hp85_tape.cpp b/src/tools/imgtool/modules/hp85_tape.cpp new file mode 100644 index 00000000000..599a2a43a92 --- /dev/null +++ b/src/tools/imgtool/modules/hp85_tape.cpp @@ -0,0 +1,1196 @@ +// license:BSD-3-Clause +// copyright-holders:F. Ulivi +/********************************************************************* + + hp85_tape.cpp + + HP-85 tape format + +*********************************************************************/ + +#include "imgtool.h" +#include "formats/imageutl.h" +#include "formats/hti_tape.h" +#include <iostream> + +// Constants +static constexpr unsigned CHARS_PER_FNAME = 6; // Characters in a filename +static constexpr unsigned CHARS_PER_EXT = 4; // Characters in (simulated) file extension +static constexpr unsigned CHARS_PER_FNAME_EXT = CHARS_PER_FNAME + 1 + CHARS_PER_EXT; // Characters in filename + extension +static constexpr unsigned MAX_FILE_NO = 42; // Maximum file # +static constexpr unsigned MAX_RECORD_SIZE = 256; // Maximum size of record body +static constexpr unsigned MAX_N_RECORDS = 436 * 2 - 5; // Total user-available capacity of tape (in physical records) +static constexpr unsigned DIR_RECORDS = 4; // Records reserved to directory + +// Words stored on tape +using tape_word_t = hti_format_t::tape_word_t; + +// Tape position, 1 unit = 1 inch / (968 * 1024) +using tape_pos_t = hti_format_t::tape_pos_t; + +// File number [0..42] +typedef uint8_t file_no_t; + +// Minimum gap size to detect IFGs: 1.25" +static constexpr tape_pos_t MIN_IFG_SIZE = 1.25 * hti_format_t::ONE_INCH_POS; + +// Minimum gap size to detect IRGs: 1/32 " +static constexpr tape_pos_t MIN_IRG_SIZE = hti_format_t::ONE_INCH_POS / 32; + +// Formatted size of IFGs: 2.5" +static constexpr tape_pos_t FMT_IFG_SIZE = 2.5 * hti_format_t::ONE_INCH_POS; + +// Formatted size of IRGs: 1" +static constexpr tape_pos_t FMT_IRG_SIZE = hti_format_t::ONE_INCH_POS; + +// Formatted size of records: 2.85" +static constexpr tape_pos_t FMT_REC_SIZE = 2.85 * hti_format_t::ONE_INCH_POS; + +// Starting position on tracks: 74" from beginning of tape +static constexpr tape_pos_t TRACK_START = 74 * hti_format_t::ONE_INCH_POS; + +// Sync word: 0x0001 +static constexpr tape_word_t SYNC_WORD = 1; + +// Name of erased (NULL) files +static const char *const NULL_FILENAME = "==NULL=="; + +// Masks of bits in file type +static constexpr uint8_t FT_NEXT_AV_MASK = 0x80; // Next available slot +static constexpr uint8_t FT_NULL_FILE_MASK = 0x40; // Erased file +static constexpr uint8_t FT_PROG_MASK = 0x20; // PROG (BASIC) file +static constexpr uint8_t FT_DATA_MASK = 0x10; // DATA file +static constexpr uint8_t FT_BPGM_MASK = 0x08; // BPGM file +static constexpr uint8_t FT_WP_MASK = 0x02; // Write protection +static constexpr uint8_t FT_HIDDEN_MASK = 0x01; // Hidden file + +/******************************************************************************** + * Directory entries + ********************************************************************************/ +struct dir_entry_85 { + uint8_t filename[ CHARS_PER_FNAME ]; // Filename (left justified, space-padded on the right) + file_no_t file_no; // File # + uint8_t filetype; // File type + uint16_t n_recs; // Physical records + uint16_t record_len; // Length of logical records +}; + +/******************************************************************************** + * Tape image + ********************************************************************************/ +class tape_image_85 { +public: + tape_image_85(void); + + bool is_dirty(void) const { return dirty; } + + void format_img(void); + + imgtoolerr_t load_from_file(imgtool::stream *stream); + typedef std::vector<uint8_t> sif_file_t; + typedef std::unique_ptr<sif_file_t> sif_file_ptr_t; + bool load_sif_file(file_no_t file_no , sif_file_t& out); + imgtoolerr_t save_to_file(imgtool::stream *stream); + + bool get_dir_entry(unsigned idx , const dir_entry_85*& entry) const; + bool find_file(const char *filename , bool ignore_ext , unsigned& idx) const; + bool alloc_new_file(dir_entry_85*& entry , sif_file_ptr_t&& file_data); + bool delete_dir_entry(unsigned idx); + bool finalize_allocation(); + static void get_filename_and_ext(const dir_entry_85& ent , bool inc_ext , char *out); + static void split_filename_and_ext(const char *filename , char *fname , char *ext); + +private: + // Tape image + hti_format_t image; + bool dirty; + // Directory + std::vector<dir_entry_85> dir; + // Content + std::vector<sif_file_ptr_t> content; + // First file on track 1 + file_no_t file_track_1; + // No. of first record on track 1 + uint16_t record_track_1; + + bool dec_rec_header(const tape_word_t *hdr , file_no_t& file_no , uint16_t& rec_no , bool& has_body , unsigned& body_len); + bool load_whole_tape(); + static tape_word_t checksum(const tape_word_t *block , unsigned block_len); + bool decode_dir(const sif_file_t& file_0); + void encode_dir(sif_file_t& file_0) const; + void save_words(unsigned track , tape_pos_t& pos , const tape_word_t *block , unsigned block_len); + void save_sif_file(unsigned& track , tape_pos_t& pos , file_no_t file_no , const sif_file_t& in); + static bool filename_char_check(uint8_t c); + static bool filename_check(const uint8_t *filename); +}; + +/******************************************************************************** + * Image state + ********************************************************************************/ +typedef struct { + imgtool::stream *stream; + tape_image_85 *img; +} tape_state_t; + +/******************************************************************************** + * Directory enumeration + ********************************************************************************/ +typedef struct { + unsigned dir_idx; +} dir_state_t; + +/******************************************************************************** + * Internal functions + ********************************************************************************/ +tape_image_85::tape_image_85(void) + : dirty(false) +{ +} + +void tape_image_85::format_img(void) +{ + // Create an empty directory + dir.clear(); + content.clear(); + + // Allocate space + finalize_allocation(); +} + +namespace { + int my_seekproc(void *file, int64_t offset, int whence) + { + reinterpret_cast<imgtool::stream *>(file)->seek(offset, whence); + return 0; + } + + size_t my_readproc(void *file, void *buffer, size_t length) + { + return reinterpret_cast<imgtool::stream *>(file)->read(buffer, length); + } + + size_t my_writeproc(void *file, const void *buffer, size_t length) + { + reinterpret_cast<imgtool::stream *>(file)->write(buffer, length); + return length; + } + + uint64_t my_filesizeproc(void *file) + { + return reinterpret_cast<imgtool::stream *>(file)->size(); + } + + const struct io_procs my_stream_procs = { + nullptr, + my_seekproc, + my_readproc, + my_writeproc, + my_filesizeproc + }; +} + +imgtoolerr_t tape_image_85::load_from_file(imgtool::stream *stream) +{ + io_generic io; + io.file = (void *)stream; + io.procs = &my_stream_procs; + io.filler = 0; + + if (!image.load_tape(&io)) { + return IMGTOOLERR_READERROR; + } + + // Prevent track boundary crossing when reading directory + file_track_1 = 0; + + // Get directory (file #0) + sif_file_t file_0; + if (!load_sif_file(0 , file_0)) { + return IMGTOOLERR_CORRUPTDIR; + } + + if (!decode_dir(file_0)) { + return IMGTOOLERR_CORRUPTDIR; + } + + dirty = false; + + return IMGTOOLERR_SUCCESS; +} + +bool tape_image_85::load_sif_file(file_no_t file_no , sif_file_t& out) +{ + unsigned track; + unsigned gaps_to_go; + + // What track is the file on? + if (!file_track_1 || file_no < file_track_1 || (file_no == file_track_1 && record_track_1)) { + track = 0; + gaps_to_go = file_no + 1; + } else { + track = 1; + gaps_to_go = file_no - file_track_1; + if (!record_track_1) { + gaps_to_go++; + } + } + tape_pos_t pos = 0; + + hti_format_t::track_iterator_t it; + while (gaps_to_go--) { + // Search for IFG + if (!image.next_data(track , pos , true , false , it)) { + return false; + } + pos = it->first; + if (!image.next_gap(track , pos , true , MIN_IFG_SIZE)) { + return false; + } + } + + // Get back to the start of record before the gap + image.next_data(track , pos , false , false , it); + pos = it->first; + bool direction = false; + + uint16_t expected_rec_no = 0; + + out.clear(); + + while (true) { + image.next_gap(track , pos , direction , MIN_IRG_SIZE); + // Read record header + if (!image.next_data(track , pos , true , false , it)) { + break; + } + // 0 Sync word + // 1 File word + // 2 Record word + // 3 Length word + // 4 Checksum + tape_word_t hdr[ 5 ]; + auto res = hti_format_t::ADV_CONT_DATA; + for (unsigned i = 0; i < 5; i++) { + if (res != hti_format_t::ADV_CONT_DATA) { + return false; + } + hdr[ i ] = it->second; + res = image.adv_it(track , true , it); + } + if (hdr[ 0 ] != SYNC_WORD || + checksum(&hdr[ 1 ] , 3) != hdr[ 4 ]) { + return false; + } + + file_no_t hdr_file_no; + uint16_t hdr_rec_no; + bool hdr_has_body; + unsigned hdr_body_len; + + if (!dec_rec_header(&hdr[ 1 ] , hdr_file_no , hdr_rec_no , hdr_has_body , hdr_body_len)) { + return false; + } + + if (hdr_file_no != file_no) { + break; + } + if (!hdr_has_body || !hdr_body_len) { + return true; + } + if (hdr_rec_no != expected_rec_no) { + return false; + } + + tape_word_t body[ MAX_RECORD_SIZE / 2 + 1 ]; + unsigned word_no = (hdr_body_len + 1) / 2 + 1; + for (unsigned i = 0; i < word_no; i++) { + if (res != hti_format_t::ADV_CONT_DATA) { + return false; + } + body[ i ] = it->second; + res = image.adv_it(track , true , it); + } + if (checksum(&body[ 0 ] , word_no - 1) != body[ word_no - 1 ]) { + return false; + } + for (unsigned i = 0; i < hdr_body_len; i++) { + tape_word_t tmp = body[ i / 2 ]; + out.push_back((uint8_t)(tmp >> 8)); + i++; + if (i < hdr_body_len) { + out.push_back((uint8_t)(tmp & 0xff)); + } + } + + // Move to next record (possibly crossing into track 1) + expected_rec_no++; + if (file_no == file_track_1 && expected_rec_no == record_track_1) { + track = 1; + pos = 0; + } else { + pos = it->first; + } + direction = true; + + if (hdr_body_len < MAX_RECORD_SIZE) { + break; + } + } + + return expected_rec_no != 0; +} + +bool tape_image_85::load_whole_tape() +{ + content.clear(); + + for (const auto& i : dir) { + sif_file_ptr_t file; + if (!(i.filetype & FT_NULL_FILE_MASK)) { + file = std::make_unique<sif_file_t>(); + if (!load_sif_file(i.file_no, *file)) { + return false; + } + } + content.push_back(std::move(file)); + } + + return true; +} + +bool tape_image_85::dec_rec_header(const tape_word_t *hdr , file_no_t& file_no , uint16_t& rec_no , bool& has_body , unsigned& body_len) +{ + if ((hdr[ 0 ] & 0x6000) != 0x2000 || + (hdr[ 0 ] & 0x07ff) > MAX_FILE_NO || + (hdr[ 1 ] & 0xf000) != 0x1000) { + return false; + } + file_no = (file_no_t)(hdr[ 0 ] & 0xff); + rec_no = (uint16_t)(hdr[ 1 ] & 0xfff); + has_body = (hdr[ 0 ] & 0x1800) != 0; + + bool has_file_id = (hdr[ 0 ] & 0x8000) != 0; + if (has_file_id != (rec_no == 0)) { + return false; + } + + if (has_body) { + if ((hdr[ 2 ] & 0xff00) != 0xff00) { + return false; + } + body_len = hdr[ 2 ] & 0xff; + if (body_len) { + body_len++; + } + } else { + body_len = 0; + } + + return true; +} + +tape_word_t tape_image_85::checksum(const tape_word_t *block , unsigned block_len) +{ + tape_word_t csum = 0; + for (unsigned i = 0; i < block_len; i++) { + csum += *block++; + } + return csum & 0xffff; +} + +imgtoolerr_t tape_image_85::save_to_file(imgtool::stream *stream) +{ + sif_file_t file_0; + + encode_dir(file_0); + + unsigned track = 0; + tape_pos_t pos = TRACK_START; + + image.clear_tape(); + + save_sif_file(track , pos , 0 , file_0); + + for (auto i = dir.cbegin(); i != dir.cend(); i++) { + file_no_t file_no = i - dir.cbegin(); + save_sif_file(track , pos , file_no + 1 , *content[ file_no ]); + } + + // Empty file at the end + file_0.clear(); + save_sif_file(track , pos , dir.size() + 1 , file_0); + + io_generic io; + io.file = (void *)stream; + io.procs = &my_stream_procs; + io.filler = 0; + + image.save_tape(&io); + + return IMGTOOLERR_SUCCESS; +} + +bool tape_image_85::get_dir_entry(unsigned idx , const dir_entry_85*& entry) const +{ + if (idx >= dir.size()) { + return false; + } else { + entry = &dir[ idx ]; + return true; + } +} + +bool tape_image_85::find_file(const char *filename , bool ignore_ext , unsigned& idx) const +{ + if (strcmp(filename , NULL_FILENAME) == 0) { + return false; + } + + char fname[ CHARS_PER_FNAME_EXT + 1 ]; + char ext[ CHARS_PER_EXT + 1 ]; + + split_filename_and_ext(filename, fname, ext); + + bool has_ext = !ignore_ext && *ext != '\0'; + + if (has_ext) { + strcat(fname , "."); + strcat(fname , ext); + } + + for (auto i = dir.cbegin(); i < dir.cend(); i++) { + if (i->filetype & FT_NULL_FILE_MASK) { + continue; + } + char full_fname[ CHARS_PER_FNAME_EXT + 1 ]; + + get_filename_and_ext(*i, has_ext, full_fname); + + if (strcmp(fname , full_fname) == 0) { + idx = i - dir.cbegin(); + return true; + } + } + + return false; +} + +bool tape_image_85::alloc_new_file(dir_entry_85*& entry , sif_file_ptr_t&& file_data) +{ + if (file_data->size() > MAX_N_RECORDS * MAX_RECORD_SIZE) { + // File bigger than tape capacity + return false; + } + + if (!load_whole_tape()) { + return false; + } + + dir_entry_85 new_entry; + memset(&new_entry , 0 , sizeof(new_entry)); + + unsigned idx = MAX_FILE_NO; + for (auto i = dir.cbegin(); i != dir.cend(); i++) { + if (i->filetype & FT_NULL_FILE_MASK) { + idx = i - dir.cbegin(); + break; + } + } + if (idx >= MAX_FILE_NO) { + idx = dir.size(); + if (idx >= MAX_FILE_NO) { + return false; + } + dir.push_back(new_entry); + content.push_back(std::make_unique<sif_file_t>()); + } else { + dir[ idx ] = new_entry; + } + entry = &dir[ idx ]; + + content[ idx ] = std::move(file_data); + + return true; +} + +bool tape_image_85::delete_dir_entry(unsigned idx) +{ + if (idx < dir.size()) { + dir[ idx ].filetype = FT_NULL_FILE_MASK; + return load_whole_tape(); + } else { + return false; + } +} + +bool tape_image_85::finalize_allocation() +{ + tape_pos_t hole_pos = hti_format_t::next_hole(TRACK_START , true); + + for (unsigned i = 0; i < dir.size(); i++) { + if (dir[ i ].filetype & FT_NULL_FILE_MASK) { + dir.erase(dir.begin() + i); + content.erase(content.begin() + i); + i--; + } + } + + unsigned track = 0; + // Position where file #1 starts (that is, after directory) + tape_pos_t pos = TRACK_START + FMT_REC_SIZE * DIR_RECORDS + FMT_IRG_SIZE * (DIR_RECORDS - 1) + FMT_IFG_SIZE; + + file_track_1 = 0; + record_track_1 = 0; + + for (auto i = dir.begin(); i != dir.end(); i++) { + file_no_t file_no = i - dir.begin(); + unsigned recs = (content[ file_no ]->size() + MAX_RECORD_SIZE - 1) / MAX_RECORD_SIZE; + if (!recs) { + // Always at least 1 record + recs = 1; + } + i->file_no = file_no + 1; + i->n_recs = recs; + // Size of file on tape: "recs" records, 1 IFG between record 0 and 1, IRGs between all + // other records + tape_pos_t file_size = FMT_IFG_SIZE + recs * FMT_REC_SIZE + (recs - 1) * FMT_IRG_SIZE; + tape_pos_t rec1_start = pos + FMT_REC_SIZE + FMT_IFG_SIZE; + tape_pos_t next_track_pos = 0; + if (pos <= hole_pos && hole_pos < rec1_start) { + // Hole on record #0 + file_track_1 = file_no + 1; + record_track_1 = 1; + next_track_pos = rec1_start; + } else if (rec1_start <= hole_pos && hole_pos < (pos + file_size)) { + // Hole in this file, records from 1 on + file_track_1 = file_no + 1; + record_track_1 = (hole_pos - rec1_start) / (FMT_REC_SIZE + FMT_IRG_SIZE) + 2; + next_track_pos = rec1_start + (record_track_1 - 1) * (FMT_REC_SIZE + FMT_IRG_SIZE); + } + if (next_track_pos) { + // Move to next track + if (++track >= 2) { + // Out of space + return false; + } + pos = pos + file_size - next_track_pos + TRACK_START; + } else { + pos += file_size; + } + } + dirty = true; + return true; +} + +typedef struct { + uint8_t filetype_mask; + const char *ext; +} file_attr_t; + +static const file_attr_t filetype_table[] = { + { FT_PROG_MASK , "PROG" }, + { FT_DATA_MASK , "DATA" }, + { FT_BPGM_MASK , "BPGM" } +}; + +void tape_image_85::get_filename_and_ext(const dir_entry_85& ent , bool inc_ext , char *out) +{ + if (ent.filetype & FT_NULL_FILE_MASK) { + // Empty directory slot + strcpy(out , NULL_FILENAME); + } else { + const uint8_t *s = &ent.filename[ 0 ]; + while (*s != '\0' && *s != ' ' && (s - &ent.filename[ 0 ]) < CHARS_PER_FNAME) { + *out++ = *s++; + } + *out = '\0'; + + // Decode filetype + if (inc_ext) { + const char *ext = nullptr; + for (const auto& i : filetype_table) { + if (ent.filetype & i.filetype_mask) { + ext = i.ext; + break; + } + } + if (ext != nullptr) { + strcat(out , "."); + strcat(out , ext); + } + } + } +} + +void tape_image_85::split_filename_and_ext(const char *filename , char *fname , char *ext) +{ + char *fname_fence = fname + CHARS_PER_FNAME; + + while (fname < fname_fence && *filename != '\0' && *filename != '.') { + *fname++ = *filename++; + } + + *fname = '\0'; + + while (*filename != '\0' && *filename != '.') { + filename++; + } + + if (*filename == '\0') { + *ext = '\0'; + } else { + filename++; + strncpy(ext , filename , CHARS_PER_EXT); + ext[ CHARS_PER_EXT ] = '\0'; + } +} + +bool tape_image_85::decode_dir(const sif_file_t& file_0) +{ + if (file_0.size() != DIR_RECORDS * MAX_RECORD_SIZE) { + return false; + } + dir.clear(); + + // Check DIRSEG (directory segment), i.e. record #0 or #1 of directory + // Check FL1TK1 (file & record no. where track 1 begins) + if (file_0[ 0xfc ] != 0 || + file_0[ 0x1fc ] != 1 || + file_0[ 0xfd ] != file_0[ 0x1fd ] || + file_0[ 0xfe ] != file_0[ 0x1fe ] || + file_0[ 0xff ] != file_0[ 0x1ff ] || + file_0[ 0xfd ] > MAX_FILE_NO) { + return false; + } + + // Get FL1TK1 + file_track_1 = file_0[ 0xfd ]; + record_track_1 = pick_integer_le(file_0.data() , 0xfe , 2); + + file_no_t file_no = 1; + + // Iterate over all entries of directory + for (unsigned i = 0; i < 0x1fc; i += 12) { + if (i == 0xfc) { + // Skip over 1st copy of DIRSEG/FL1TK1 + i = 0x100; + } + const uint8_t *p = file_0.data() + i; + dir_entry_85 new_entry; + + // File type + new_entry.filetype = p[ 7 ]; + if (new_entry.filetype & FT_NEXT_AV_MASK) { + // Directory ends + break; + } + + // Filename + memcpy(&new_entry.filename[ 0 ] , p , CHARS_PER_FNAME); + if (!filename_check(&new_entry.filename[ 0 ])) { + return false; + } + + // File # + // It is also stored at p[ 6 ] but HP85 firmware ignores it + new_entry.file_no = file_no++; + + // Physical records + new_entry.n_recs = pick_integer_le(p , 8 , 2); + + // Bytes per logical record + new_entry.record_len = pick_integer_le(p , 10 , 2); + if (new_entry.record_len < 4 || new_entry.record_len >= 32768) { + return false; + } + + dir.push_back(new_entry); + } + + return true; +} + +void tape_image_85::encode_dir(sif_file_t& file_0) const +{ + file_0.clear(); + file_0.resize(DIR_RECORDS * MAX_RECORD_SIZE , 0); + + // Set DIRSEG + file_0[ 0xfc ] = 0; + file_0[ 0x1fc ] = 1; + // Set FL1TK1 + file_0[ 0xfd ] = file_0[ 0x1fd ] = file_track_1; + place_integer_le(file_0.data() , 0xfe , 2 , record_track_1); + place_integer_le(file_0.data() , 0x1fe , 2 , record_track_1); + + unsigned i = 0; + file_no_t file_no = 1; + for (auto entry = dir.cbegin(); entry != dir.cend(); entry++, i += 12, file_no++) { + if (i == 0xfc) { + // Skip over 1st copy of DIRSEG/FL1TK1 + i = 0x100; + } + uint8_t *p_entry = file_0.data() + i; + memcpy(&p_entry[ 0 ] , &entry->filename[ 0 ] , CHARS_PER_FNAME); + p_entry[ 6 ] = file_no; + p_entry[ 7 ] = entry->filetype; + place_integer_le(p_entry , 8 , 2 , entry->n_recs); + place_integer_le(p_entry , 10 , 2 , entry->record_len); + } + + if (file_no <= MAX_FILE_NO) { + if (i == 0xfc) { + // Skip over 1st copy of DIRSEG/FL1TK1 + i = 0x100; + } + file_0[ i + 7 ] = FT_NEXT_AV_MASK; + } + + // Two identical copies of directory + memcpy(file_0.data() + (DIR_RECORDS / 2) * MAX_RECORD_SIZE , file_0.data() , (DIR_RECORDS / 2) * MAX_RECORD_SIZE); +} + +void tape_image_85::save_words(unsigned track , tape_pos_t& pos , const tape_word_t *block , unsigned block_len) +{ + tape_pos_t length; + for (unsigned i = 0; i < block_len; i++) { + image.write_word(track , pos , *block++ , length); + pos += length; + } +} + +void tape_image_85::save_sif_file(unsigned& track , tape_pos_t& pos , file_no_t file_no , const sif_file_t& in) +{ + unsigned rec_no = 0; + unsigned bytes_to_go = in.size(); + sif_file_t::const_iterator in_it = in.cbegin(); + + do { + if (file_track_1 != 0 && track == 0 && + ((file_no == file_track_1 && rec_no >= record_track_1) || file_no > file_track_1)) { + // Switch to track 1 + track = 1; + pos = TRACK_START; + } + tape_pos_t start_pos = pos; + + unsigned rec_size = std::min(bytes_to_go , MAX_RECORD_SIZE); + + tape_word_t hdr[ 5 ]; + hdr[ 0 ] = SYNC_WORD; + hdr[ 1 ] = (tape_word_t)file_no | 0x2000; + if (rec_no == 0) { + hdr[ 1 ] |= 0x8000; + } + if (rec_size) { + hdr[ 1 ] |= 0x1800; + } + hdr[ 2 ] = 0x1000 | (tape_word_t)rec_no; + hdr[ 3 ] = 0xff00; + if (rec_size) { + hdr[ 3 ] |= (rec_size - 1); + } + hdr[ 4 ] = checksum(&hdr[ 1 ], 3); + + save_words(track, pos, &hdr[ 0 ], 5); + + if (rec_size) { + tape_word_t body[ MAX_RECORD_SIZE / 2 + 1 ]; + unsigned words = 0; + while (words < MAX_RECORD_SIZE / 2 && in_it != in.cend()) { + tape_word_t w = (tape_word_t)*in_it++ << 8; + if (in_it != in.cend()) { + w |= *in_it++; + } + body[ words++ ] = w; + } + body[ words ] = checksum(&body[ 0 ], words); + save_words(track, pos, &body[ 0 ], words + 1); + } + + // Pad record up to FMT_REC_SIZE + tape_word_t filler = 0xffff; + while ((pos - start_pos) < FMT_REC_SIZE) { + save_words(track, pos, &filler, 1); + } + + if (rec_no == 0) { + // IFG + pos = start_pos + FMT_REC_SIZE + FMT_IFG_SIZE; + } else { + // IRG + pos = start_pos + FMT_REC_SIZE + FMT_IRG_SIZE; + } + bytes_to_go -= rec_size; + rec_no++; + } while (bytes_to_go > 0); +} + +bool tape_image_85::filename_char_check(uint8_t c) +{ + // Quotation marks are forbidden in file names + return 0x20 < c && c < 0x7f && c != '"'; +} + +bool tape_image_85::filename_check(const uint8_t *filename) +{ + bool ended = false; + + for (unsigned i = 0; i < CHARS_PER_FNAME; i++) { + uint8_t c = *filename++; + + if (ended) { + if (c != ' ') { + return false; + } + } else if (c == ' ') { + ended = true; + } else if (!filename_char_check(c)) { + return false; + } + } + + return true; +} + +namespace { + tape_state_t& get_tape_state(imgtool::image &img) + { + tape_state_t *ts = (tape_state_t*)img.extra_bytes(); + + return *ts; + } + + tape_image_85& get_tape_image(tape_state_t& ts) + { + if (ts.img == nullptr) { + ts.img = global_alloc(tape_image_85); + } + + return *(ts.img); + } +} +/******************************************************************************** + * Imgtool functions + ********************************************************************************/ +namespace { + imgtoolerr_t hp85_tape_open(imgtool::image &image, imgtool::stream::ptr &&stream) + { + tape_state_t& state = get_tape_state(image); + + state.stream = stream.release(); + + tape_image_85& tape_image = get_tape_image(state); + + imgtoolerr_t err = tape_image.load_from_file(state.stream); + if (err) + return err; + + return IMGTOOLERR_SUCCESS; + } + + imgtoolerr_t hp85_tape_create(imgtool::image &image, imgtool::stream::ptr &&stream, util::option_resolution *opts) + { + tape_state_t& state = get_tape_state(image); + + state.stream = stream.release(); + + tape_image_85& tape_image = get_tape_image(state); + + tape_image.format_img(); + + return IMGTOOLERR_SUCCESS; + } + + void hp85_tape_close(imgtool::image &image) + { + tape_state_t& state = get_tape_state(image); + tape_image_85& tape_image = get_tape_image(state); + + if (tape_image.is_dirty()) { + (void)tape_image.save_to_file(state.stream); + } + + delete state.stream; + + // Free tape_image + global_free(&tape_image); + } + + imgtoolerr_t hp85_tape_begin_enum (imgtool::directory &enumeration, const char *path) + { + dir_state_t *ds = (dir_state_t*)enumeration.extra_bytes(); + + ds->dir_idx = 0; + + return IMGTOOLERR_SUCCESS; + } + + imgtoolerr_t hp85_tape_next_enum (imgtool::directory &enumeration, imgtool_dirent &ent) + { + tape_state_t& state = get_tape_state(enumeration.image()); + tape_image_85& tape_image = get_tape_image(state); + dir_state_t *ds = (dir_state_t*)enumeration.extra_bytes(); + + const dir_entry_85 *entry = nullptr; + + if (!tape_image.get_dir_entry(ds->dir_idx, entry)) { + ent.eof = 1; + } else { + ds->dir_idx++; + + unsigned filesize = entry->n_recs * MAX_RECORD_SIZE; + + if (entry->filetype & FT_NULL_FILE_MASK) { + ent.filesize = 0; + } else { + ent.filesize = filesize; + } + + tape_image_85::get_filename_and_ext(*entry, true, ent.filename); + snprintf(ent.attr , sizeof(ent.attr) , "%u %u %u %c%c" , entry->record_len , filesize / entry->record_len , entry->file_no , (entry->filetype & FT_WP_MASK) ? 'W' : ' ' , (entry->filetype & FT_HIDDEN_MASK) ? 'H' : ' '); + } + + + return IMGTOOLERR_SUCCESS; + } + + imgtoolerr_t hp85_tape_free_space(imgtool::partition &partition, uint64_t *size) + { + tape_state_t& state = get_tape_state(partition.image()); + tape_image_85& tape_image = get_tape_image(state); + unsigned used_recs = 0; + + const dir_entry_85 *entry = nullptr; + + for (unsigned i = 0; i < MAX_FILE_NO; i++) { + if (!tape_image.get_dir_entry(i, entry)) { + break; + } + // Ignore erased files + if (entry->filetype & FT_NULL_FILE_MASK) { + continue; + } + used_recs += entry->n_recs; + } + + if (used_recs >= MAX_N_RECORDS) { + *size = 0; + } else { + *size = (MAX_N_RECORDS - used_recs) * MAX_RECORD_SIZE; + } + + return IMGTOOLERR_SUCCESS; + } + + imgtoolerr_t hp85_tape_read_file(imgtool::partition &partition, const char *filename, const char *fork, imgtool::stream &destf) + { + tape_state_t& state = get_tape_state(partition.image()); + tape_image_85& tape_image = get_tape_image(state); + + unsigned idx; + + if (!tape_image.find_file(filename , false , idx)) { + return IMGTOOLERR_FILENOTFOUND; + } + + const dir_entry_85 *ent = nullptr; + + tape_image.get_dir_entry(idx, ent); + + tape_image_85::sif_file_t file_data; + + if (!tape_image.load_sif_file(ent->file_no , file_data)) { + return IMGTOOLERR_READERROR; + } + + destf.write(file_data.data() , file_data.size()); + + return IMGTOOLERR_SUCCESS; + } + + imgtoolerr_t hp85_tape_write_file(imgtool::partition &partition, const char *filename, const char *fork, imgtool::stream &sourcef, util::option_resolution *opts) + { + tape_state_t& state = get_tape_state(partition.image()); + tape_image_85& tape_image = get_tape_image(state); + + unsigned idx; + + if (tape_image.find_file(filename , true , idx)) { + // When overwriting a file, delete its old version first + tape_image.delete_dir_entry(idx); + } + + unsigned file_size = sourcef.size(); + + if (!file_size) { + return IMGTOOLERR_SUCCESS; + } + if (file_size > (MAX_N_RECORDS * MAX_RECORD_SIZE)) { + return IMGTOOLERR_NOSPACE; + } + + auto p_in_file = std::make_unique<tape_image_85::sif_file_t>(); + + p_in_file->resize(file_size); + + if (sourcef.read(p_in_file->data() , file_size) != file_size) { + return IMGTOOLERR_READERROR; + } + + dir_entry_85 *ent = nullptr; + + if (!tape_image.alloc_new_file(ent , std::move(p_in_file))) { + return IMGTOOLERR_NOSPACE; + } + + char fname[ CHARS_PER_FNAME + 1 ]; + char ext[ CHARS_PER_EXT + 1 ]; + + tape_image_85::split_filename_and_ext(filename, fname, ext); + + char *dest = (char *)&ent->filename[ 0 ]; + char *dest0 = dest; + char *src = &fname[ 0 ]; + + while ((dest - dest0) < CHARS_PER_FNAME && *src != '\0') { + *dest++ = *src++; + } + while ((dest - dest0) < CHARS_PER_FNAME) { + *dest++ = ' '; + } + + int bpr = opts->lookup_int('B'); + if (bpr < 4) { + bpr = MAX_RECORD_SIZE; + } else if (bpr > file_size) { + util::stream_format(std::wcerr, L"BPR value too large, using %u\n", MAX_RECORD_SIZE); + bpr = MAX_RECORD_SIZE; + } + ent->record_len = (uint16_t)bpr; + + if (opts->lookup_int('T') == 0) { + // File type defaults to DATA if no extension is given or extension is invalid + ent->filetype = FT_DATA_MASK; + for (const auto& i : filetype_table) { + if (strcmp(i.ext , ext) == 0) { + ent->filetype = i.filetype_mask; + break; + } + } + } else { + ent->filetype = filetype_table[ opts->lookup_int('T') - 1 ].filetype_mask; + } + + if (!tape_image.finalize_allocation()) { + return IMGTOOLERR_NOSPACE; + } + + return IMGTOOLERR_SUCCESS; + } + + imgtoolerr_t hp85_tape_delete_file(imgtool::partition &partition, const char *filename) + { + tape_state_t& state = get_tape_state(partition.image()); + tape_image_85& tape_image = get_tape_image(state); + + unsigned idx; + + if (!tape_image.find_file(filename , true , idx)) { + return IMGTOOLERR_FILENOTFOUND; + } + + if (!tape_image.delete_dir_entry(idx)) { + return IMGTOOLERR_READERROR; + } + if (!tape_image.finalize_allocation()) { + return IMGTOOLERR_NOSPACE; + } + + return IMGTOOLERR_SUCCESS; + } +} +#define HP85_WRITEFILE_OPTSPEC "B[0]-32767;T[0]-3" + +OPTION_GUIDE_START(hp85_write_optguide) + OPTION_INT('B' , "bpr" , "Bytes per record") + OPTION_ENUM_START('T' , "ftype" , "File type") + OPTION_ENUM(0 , "auto" , "Automatic (\"DATA\" or by extension)") + OPTION_ENUM(1 , "P" , "PROG") + OPTION_ENUM(2 , "D" , "DATA") + OPTION_ENUM(3 , "B" , "BPGM") + OPTION_ENUM_END +OPTION_GUIDE_END + +void hp85_tape_get_info(const imgtool_class *imgclass, uint32_t state, union imgtoolinfo *info) +{ + switch (state) { + case IMGTOOLINFO_STR_NAME: + strcpy(info->s = imgtool_temp_str(), "hp85_tape"); + break; + + case IMGTOOLINFO_STR_DESCRIPTION: + strcpy(info->s = imgtool_temp_str(), "HP85 tape image"); + break; + + case IMGTOOLINFO_STR_FILE: + strcpy(info->s = imgtool_temp_str(), __FILE__); + break; + + case IMGTOOLINFO_STR_FILE_EXTENSIONS: + strcpy(info->s = imgtool_temp_str(), "hti"); + break; + + case IMGTOOLINFO_INT_IMAGE_EXTRA_BYTES: + info->i = sizeof(tape_state_t); + break; + + case IMGTOOLINFO_INT_DIRECTORY_EXTRA_BYTES: + info->i = sizeof(dir_state_t); + break; + + case IMGTOOLINFO_PTR_OPEN: + info->open = hp85_tape_open; + break; + + case IMGTOOLINFO_PTR_CREATE: + info->create = hp85_tape_create; + break; + + case IMGTOOLINFO_PTR_CLOSE: + info->close = hp85_tape_close; + break; + + case IMGTOOLINFO_PTR_BEGIN_ENUM: + info->begin_enum = hp85_tape_begin_enum; + break; + + case IMGTOOLINFO_PTR_NEXT_ENUM: + info->next_enum = hp85_tape_next_enum; + break; + + case IMGTOOLINFO_PTR_FREE_SPACE: + info->free_space = hp85_tape_free_space; + break; + + case IMGTOOLINFO_PTR_READ_FILE: + info->read_file = hp85_tape_read_file; + break; + + case IMGTOOLINFO_PTR_WRITE_FILE: + info->write_file = hp85_tape_write_file; + break; + + case IMGTOOLINFO_PTR_DELETE_FILE: + info->delete_file = hp85_tape_delete_file; + break; + + case IMGTOOLINFO_PTR_WRITEFILE_OPTGUIDE: + info->writefile_optguide = &hp85_write_optguide; + break; + + case IMGTOOLINFO_STR_WRITEFILE_OPTSPEC: + strcpy(info->s = imgtool_temp_str(), HP85_WRITEFILE_OPTSPEC); + break; + } +} |