1 // license:BSD-3-Clause
2 // copyright-holders:F. Ulivi
3 /*********************************************************************
4
5 hp85_tape.cpp
6
7 HP-85 tape format
8
9 *********************************************************************/
10
11 #include "imgtool.h"
12 #include "formats/imageutl.h"
13 #include "formats/hti_tape.h"
14 #include <iostream>
15
16 // Constants
17 static constexpr unsigned CHARS_PER_FNAME = 6; // Characters in a filename
18 static constexpr unsigned CHARS_PER_EXT = 4; // Characters in (simulated) file extension
19 static constexpr unsigned CHARS_PER_FNAME_EXT = CHARS_PER_FNAME + 1 + CHARS_PER_EXT; // Characters in filename + extension
20 static constexpr unsigned MAX_FILE_NO = 42; // Maximum file #
21 static constexpr unsigned MAX_RECORD_SIZE = 256; // Maximum size of record body
22 static constexpr unsigned MAX_N_RECORDS = 436 * 2 - 5; // Total user-available capacity of tape (in physical records)
23 static constexpr unsigned DIR_RECORDS = 4; // Records reserved to directory
24
25 // Words stored on tape
26 using tape_word_t = hti_format_t::tape_word_t;
27
28 // Tape position, 1 unit = 1 inch / (968 * 1024)
29 using tape_pos_t = hti_format_t::tape_pos_t;
30
31 // File number [0..42]
32 typedef uint8_t file_no_t;
33
34 // Minimum gap size to detect IFGs: 1.25"
35 static constexpr tape_pos_t MIN_IFG_SIZE = 1.25 * hti_format_t::ONE_INCH_POS;
36
37 // Minimum gap size to detect IRGs: 1/32 "
38 static constexpr tape_pos_t MIN_IRG_SIZE = hti_format_t::ONE_INCH_POS / 32;
39
40 // Formatted size of IFGs: 2.5"
41 static constexpr tape_pos_t FMT_IFG_SIZE = 2.5 * hti_format_t::ONE_INCH_POS;
42
43 // Formatted size of IRGs: 1"
44 static constexpr tape_pos_t FMT_IRG_SIZE = hti_format_t::ONE_INCH_POS;
45
46 // Formatted size of records: 2.67"
47 static constexpr tape_pos_t FMT_REC_SIZE = 2.67 * hti_format_t::ONE_INCH_POS;
48
49 // Starting position on tracks: 74" from beginning of tape
50 static constexpr tape_pos_t TRACK_START = 74 * hti_format_t::ONE_INCH_POS;
51
52 // Sync word: 0x0001
53 static constexpr tape_word_t SYNC_WORD = 1;
54
55 // Name of erased (NULL) files
56 static const char *const NULL_FILENAME = "==NULL==";
57
58 // Masks of bits in file type
59 static constexpr uint8_t FT_NEXT_AV_MASK = 0x80; // Next available slot
60 static constexpr uint8_t FT_NULL_FILE_MASK = 0x40; // Erased file
61 static constexpr uint8_t FT_PROG_MASK = 0x20; // PROG (BASIC) file
62 static constexpr uint8_t FT_DATA_MASK = 0x10; // DATA file
63 static constexpr uint8_t FT_BPGM_MASK = 0x08; // BPGM file
64 static constexpr uint8_t FT_WP_MASK = 0x02; // Write protection
65 static constexpr uint8_t FT_HIDDEN_MASK = 0x01; // Hidden file
66
67 /********************************************************************************
68 * Directory entries
69 ********************************************************************************/
70 struct dir_entry_85 {
71 uint8_t filename[ CHARS_PER_FNAME ]; // Filename (left justified, space-padded on the right)
72 file_no_t file_no; // File #
73 uint8_t filetype; // File type
74 uint16_t n_recs; // Physical records
75 uint16_t record_len; // Length of logical records
76 };
77
78 /********************************************************************************
79 * Tape image
80 ********************************************************************************/
81 class tape_image_85 {
82 public:
83 tape_image_85(void);
84
is_dirty(void) const85 bool is_dirty(void) const { return dirty; }
86
87 void format_img(void);
88
89 imgtoolerr_t load_from_file(imgtool::stream *stream);
90 typedef std::vector<uint8_t> sif_file_t;
91 typedef std::unique_ptr<sif_file_t> sif_file_ptr_t;
92 bool load_sif_file(file_no_t file_no , sif_file_t& out);
93 imgtoolerr_t save_to_file(imgtool::stream *stream);
94
95 bool get_dir_entry(unsigned idx , const dir_entry_85*& entry) const;
96 bool find_file(const char *filename , bool ignore_ext , unsigned& idx) const;
97 bool alloc_new_file(dir_entry_85*& entry , sif_file_ptr_t&& file_data);
98 bool delete_dir_entry(unsigned idx);
99 bool finalize_allocation();
100 static void get_filename_and_ext(const dir_entry_85& ent , bool inc_ext , char *out);
101 static void split_filename_and_ext(const char *filename , char *fname , char *ext);
102
103 private:
104 // Tape image
105 hti_format_t image;
106 bool dirty;
107 // Directory
108 std::vector<dir_entry_85> dir;
109 // Content
110 std::vector<sif_file_ptr_t> content;
111 // First file on track 1
112 file_no_t file_track_1;
113 // No. of first record on track 1
114 uint16_t record_track_1;
115
116 bool dec_rec_header(const tape_word_t *hdr , file_no_t& file_no , uint16_t& rec_no , bool& has_body , unsigned& body_len);
117 bool load_whole_tape();
118 static tape_word_t checksum(const tape_word_t *block , unsigned block_len);
119 bool decode_dir(const sif_file_t& file_0);
120 void encode_dir(sif_file_t& file_0) const;
121 void save_words(unsigned track , tape_pos_t& pos , const tape_word_t *block , unsigned block_len);
122 void save_sif_file(unsigned& track , tape_pos_t& pos , file_no_t file_no , const sif_file_t& in);
123 static bool filename_char_check(uint8_t c);
124 static bool filename_check(const uint8_t *filename);
125 };
126
127 /********************************************************************************
128 * Image state
129 ********************************************************************************/
130 typedef struct {
131 imgtool::stream *stream;
132 tape_image_85 *img;
133 } tape_state_t;
134
135 /********************************************************************************
136 * Directory enumeration
137 ********************************************************************************/
138 typedef struct {
139 unsigned dir_idx;
140 } dir_state_t;
141
142 /********************************************************************************
143 * Internal functions
144 ********************************************************************************/
tape_image_85(void)145 tape_image_85::tape_image_85(void)
146 : dirty(false)
147 {
148 image.set_bits_per_word(16);
149 }
150
format_img(void)151 void tape_image_85::format_img(void)
152 {
153 // Create an empty directory
154 dir.clear();
155 content.clear();
156
157 // Allocate space
158 finalize_allocation();
159 }
160
161 namespace {
my_seekproc(void * file,int64_t offset,int whence)162 int my_seekproc(void *file, int64_t offset, int whence)
163 {
164 reinterpret_cast<imgtool::stream *>(file)->seek(offset, whence);
165 return 0;
166 }
167
my_readproc(void * file,void * buffer,size_t length)168 size_t my_readproc(void *file, void *buffer, size_t length)
169 {
170 return reinterpret_cast<imgtool::stream *>(file)->read(buffer, length);
171 }
172
my_writeproc(void * file,const void * buffer,size_t length)173 size_t my_writeproc(void *file, const void *buffer, size_t length)
174 {
175 reinterpret_cast<imgtool::stream *>(file)->write(buffer, length);
176 return length;
177 }
178
my_filesizeproc(void * file)179 uint64_t my_filesizeproc(void *file)
180 {
181 return reinterpret_cast<imgtool::stream *>(file)->size();
182 }
183
184 const struct io_procs my_stream_procs = {
185 nullptr,
186 my_seekproc,
187 my_readproc,
188 my_writeproc,
189 my_filesizeproc
190 };
191 }
192
load_from_file(imgtool::stream * stream)193 imgtoolerr_t tape_image_85::load_from_file(imgtool::stream *stream)
194 {
195 io_generic io;
196 io.file = (void *)stream;
197 io.procs = &my_stream_procs;
198 io.filler = 0;
199
200 if (!image.load_tape(&io)) {
201 return IMGTOOLERR_READERROR;
202 }
203
204 // Prevent track boundary crossing when reading directory
205 file_track_1 = 0;
206
207 // Get directory (file #0)
208 sif_file_t file_0;
209 if (!load_sif_file(0 , file_0)) {
210 return IMGTOOLERR_CORRUPTDIR;
211 }
212
213 if (!decode_dir(file_0)) {
214 return IMGTOOLERR_CORRUPTDIR;
215 }
216
217 dirty = false;
218
219 return IMGTOOLERR_SUCCESS;
220 }
221
load_sif_file(file_no_t file_no,sif_file_t & out)222 bool tape_image_85::load_sif_file(file_no_t file_no , sif_file_t& out)
223 {
224 unsigned track;
225 unsigned gaps_to_go;
226
227 // What track is the file on?
228 if (!file_track_1 || file_no < file_track_1 || (file_no == file_track_1 && record_track_1)) {
229 track = 0;
230 gaps_to_go = file_no + 1;
231 } else {
232 track = 1;
233 gaps_to_go = file_no - file_track_1;
234 if (!record_track_1) {
235 gaps_to_go++;
236 }
237 }
238 tape_pos_t pos = 0;
239
240 hti_format_t::track_iterator_t it;
241 while (gaps_to_go--) {
242 // Search for IFG
243 if (!image.next_data(track , pos , true , false , it)) {
244 return false;
245 }
246 pos = it->first;
247 if (!image.next_gap(track , pos , true , MIN_IFG_SIZE)) {
248 return false;
249 }
250 }
251
252 // Get back to the start of record before the gap
253 image.next_data(track , pos , false , false , it);
254 pos = it->first;
255 bool direction = false;
256
257 uint16_t expected_rec_no = 0;
258
259 out.clear();
260
261 while (true) {
262 image.next_gap(track , pos , direction , MIN_IRG_SIZE);
263 // Read record header
264 if (!image.next_data(track , pos , true , false , it)) {
265 break;
266 }
267 unsigned bit_idx = 15;
268 if (!image.sync_with_record(track , it , bit_idx)) {
269 // Couldn't align
270 return false;
271 }
272
273 // 0 File word
274 // 1 Record word
275 // 2 Length word
276 // 3 Checksum
277 tape_word_t hdr[ 4 ];
278 for (unsigned i = 0; i < 4; i++) {
279 auto res = image.next_word(track , it , bit_idx , hdr[ i ]);
280 if (res != hti_format_t::ADV_CONT_DATA) {
281 return false;
282 }
283 }
284 if (checksum(&hdr[ 0 ] , 3) != hdr[ 3 ]) {
285 return false;
286 }
287
288 file_no_t hdr_file_no;
289 uint16_t hdr_rec_no;
290 bool hdr_has_body;
291 unsigned hdr_body_len;
292
293 if (!dec_rec_header(&hdr[ 0 ] , hdr_file_no , hdr_rec_no , hdr_has_body , hdr_body_len)) {
294 return false;
295 }
296
297 if (hdr_file_no != file_no) {
298 break;
299 }
300 if (!hdr_has_body || !hdr_body_len) {
301 return true;
302 }
303 if (hdr_rec_no != expected_rec_no) {
304 return false;
305 }
306
307 tape_word_t body[ MAX_RECORD_SIZE / 2 + 1 ];
308 unsigned word_no = (hdr_body_len + 1) / 2 + 1;
309 for (unsigned i = 0; i < word_no; i++) {
310 auto res = image.next_word(track , it , bit_idx , body[ i ]);
311 if (res != hti_format_t::ADV_CONT_DATA) {
312 return false;
313 }
314 }
315 if (checksum(&body[ 0 ] , word_no - 1) != body[ word_no - 1 ]) {
316 return false;
317 }
318 for (unsigned i = 0; i < hdr_body_len; i++) {
319 tape_word_t tmp = body[ i / 2 ];
320 out.push_back((uint8_t)(tmp >> 8));
321 i++;
322 if (i < hdr_body_len) {
323 out.push_back((uint8_t)(tmp & 0xff));
324 }
325 }
326
327 // Move to next record (possibly crossing into track 1)
328 expected_rec_no++;
329 if (file_no == file_track_1 && expected_rec_no == record_track_1) {
330 track = 1;
331 pos = 0;
332 } else {
333 pos = it->first;
334 }
335 direction = true;
336
337 if (hdr_body_len < MAX_RECORD_SIZE) {
338 break;
339 }
340 }
341
342 return expected_rec_no != 0;
343 }
344
load_whole_tape()345 bool tape_image_85::load_whole_tape()
346 {
347 content.clear();
348
349 for (const auto& i : dir) {
350 sif_file_ptr_t file;
351 if (!(i.filetype & FT_NULL_FILE_MASK)) {
352 file = std::make_unique<sif_file_t>();
353 if (!load_sif_file(i.file_no, *file)) {
354 return false;
355 }
356 }
357 content.push_back(std::move(file));
358 }
359
360 return true;
361 }
362
dec_rec_header(const tape_word_t * hdr,file_no_t & file_no,uint16_t & rec_no,bool & has_body,unsigned & body_len)363 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)
364 {
365 if ((hdr[ 0 ] & 0x6000) != 0x2000 ||
366 (hdr[ 0 ] & 0x07ff) > MAX_FILE_NO ||
367 (hdr[ 1 ] & 0xf000) != 0x1000) {
368 return false;
369 }
370 file_no = (file_no_t)(hdr[ 0 ] & 0xff);
371 rec_no = (uint16_t)(hdr[ 1 ] & 0xfff);
372 has_body = (hdr[ 0 ] & 0x1800) != 0;
373
374 bool has_file_id = (hdr[ 0 ] & 0x8000) != 0;
375 if (has_file_id != (rec_no == 0)) {
376 return false;
377 }
378
379 if (has_body) {
380 if ((hdr[ 2 ] & 0xff00) != 0xff00) {
381 return false;
382 }
383 body_len = hdr[ 2 ] & 0xff;
384 if (body_len) {
385 body_len++;
386 }
387 } else {
388 body_len = 0;
389 }
390
391 return true;
392 }
393
checksum(const tape_word_t * block,unsigned block_len)394 tape_word_t tape_image_85::checksum(const tape_word_t *block , unsigned block_len)
395 {
396 tape_word_t csum = 0;
397 for (unsigned i = 0; i < block_len; i++) {
398 csum += *block++;
399 }
400 return csum & 0xffff;
401 }
402
save_to_file(imgtool::stream * stream)403 imgtoolerr_t tape_image_85::save_to_file(imgtool::stream *stream)
404 {
405 sif_file_t file_0;
406
407 encode_dir(file_0);
408
409 unsigned track = 0;
410 tape_pos_t pos = TRACK_START;
411
412 image.clear_tape();
413
414 save_sif_file(track , pos , 0 , file_0);
415
416 for (auto i = dir.cbegin(); i != dir.cend(); i++) {
417 file_no_t file_no = i - dir.cbegin();
418 save_sif_file(track , pos , file_no + 1 , *content[ file_no ]);
419 }
420
421 // Empty file at the end
422 file_0.clear();
423 save_sif_file(track , pos , dir.size() + 1 , file_0);
424
425 io_generic io;
426 io.file = (void *)stream;
427 io.procs = &my_stream_procs;
428 io.filler = 0;
429
430 image.save_tape(&io);
431
432 return IMGTOOLERR_SUCCESS;
433 }
434
get_dir_entry(unsigned idx,const dir_entry_85 * & entry) const435 bool tape_image_85::get_dir_entry(unsigned idx , const dir_entry_85*& entry) const
436 {
437 if (idx >= dir.size()) {
438 return false;
439 } else {
440 entry = &dir[ idx ];
441 return true;
442 }
443 }
444
find_file(const char * filename,bool ignore_ext,unsigned & idx) const445 bool tape_image_85::find_file(const char *filename , bool ignore_ext , unsigned& idx) const
446 {
447 if (strcmp(filename , NULL_FILENAME) == 0) {
448 return false;
449 }
450
451 char fname[ CHARS_PER_FNAME_EXT + 1 ];
452 char ext[ CHARS_PER_EXT + 1 ];
453
454 split_filename_and_ext(filename, fname, ext);
455
456 bool has_ext = !ignore_ext && *ext != '\0';
457
458 if (has_ext) {
459 strcat(fname , ".");
460 strcat(fname , ext);
461 }
462
463 for (auto i = dir.cbegin(); i < dir.cend(); i++) {
464 if (i->filetype & FT_NULL_FILE_MASK) {
465 continue;
466 }
467 char full_fname[ CHARS_PER_FNAME_EXT + 1 ];
468
469 get_filename_and_ext(*i, has_ext, full_fname);
470
471 if (strcmp(fname , full_fname) == 0) {
472 idx = i - dir.cbegin();
473 return true;
474 }
475 }
476
477 return false;
478 }
479
alloc_new_file(dir_entry_85 * & entry,sif_file_ptr_t && file_data)480 bool tape_image_85::alloc_new_file(dir_entry_85*& entry , sif_file_ptr_t&& file_data)
481 {
482 if (file_data->size() > MAX_N_RECORDS * MAX_RECORD_SIZE) {
483 // File bigger than tape capacity
484 return false;
485 }
486
487 if (!load_whole_tape()) {
488 return false;
489 }
490
491 dir_entry_85 new_entry;
492 memset(&new_entry , 0 , sizeof(new_entry));
493
494 unsigned idx = MAX_FILE_NO;
495 for (auto i = dir.cbegin(); i != dir.cend(); i++) {
496 if (i->filetype & FT_NULL_FILE_MASK) {
497 idx = i - dir.cbegin();
498 break;
499 }
500 }
501 if (idx >= MAX_FILE_NO) {
502 idx = dir.size();
503 if (idx >= MAX_FILE_NO) {
504 return false;
505 }
506 dir.push_back(new_entry);
507 content.push_back(std::make_unique<sif_file_t>());
508 } else {
509 dir[ idx ] = new_entry;
510 }
511 entry = &dir[ idx ];
512
513 content[ idx ] = std::move(file_data);
514
515 return true;
516 }
517
delete_dir_entry(unsigned idx)518 bool tape_image_85::delete_dir_entry(unsigned idx)
519 {
520 if (idx < dir.size()) {
521 dir[ idx ].filetype = FT_NULL_FILE_MASK;
522 return load_whole_tape();
523 } else {
524 return false;
525 }
526 }
527
finalize_allocation()528 bool tape_image_85::finalize_allocation()
529 {
530 tape_pos_t hole_pos = hti_format_t::next_hole(TRACK_START , true);
531
532 for (unsigned i = 0; i < dir.size(); i++) {
533 if (dir[ i ].filetype & FT_NULL_FILE_MASK) {
534 dir.erase(dir.begin() + i);
535 content.erase(content.begin() + i);
536 i--;
537 }
538 }
539
540 unsigned track = 0;
541 // Position where file #1 starts (that is, after directory)
542 tape_pos_t pos = TRACK_START + FMT_REC_SIZE * DIR_RECORDS + FMT_IRG_SIZE * (DIR_RECORDS - 1) + FMT_IFG_SIZE;
543
544 file_track_1 = 0;
545 record_track_1 = 0;
546
547 for (auto i = dir.begin(); i != dir.end(); i++) {
548 file_no_t file_no = i - dir.begin();
549 unsigned recs = (content[ file_no ]->size() + MAX_RECORD_SIZE - 1) / MAX_RECORD_SIZE;
550 if (!recs) {
551 // Always at least 1 record
552 recs = 1;
553 }
554 i->file_no = file_no + 1;
555 i->n_recs = recs;
556 // Size of file on tape: "recs" records, 1 IFG between record 0 and 1, IRGs between all
557 // other records
558 tape_pos_t file_size = FMT_IFG_SIZE + recs * FMT_REC_SIZE + (recs - 1) * FMT_IRG_SIZE;
559 tape_pos_t rec1_start = pos + FMT_REC_SIZE + FMT_IFG_SIZE;
560 tape_pos_t next_track_pos = 0;
561 if (pos <= hole_pos && hole_pos < rec1_start) {
562 // Hole on record #0
563 file_track_1 = file_no + 1;
564 record_track_1 = 1;
565 next_track_pos = rec1_start;
566 } else if (rec1_start <= hole_pos && hole_pos < (pos + file_size)) {
567 // Hole in this file, records from 1 on
568 file_track_1 = file_no + 1;
569 record_track_1 = (hole_pos - rec1_start) / (FMT_REC_SIZE + FMT_IRG_SIZE) + 2;
570 next_track_pos = rec1_start + (record_track_1 - 1) * (FMT_REC_SIZE + FMT_IRG_SIZE);
571 }
572 if (next_track_pos) {
573 // Move to next track
574 if (++track >= 2) {
575 // Out of space
576 return false;
577 }
578 pos = pos + file_size - next_track_pos + TRACK_START;
579 } else {
580 pos += file_size;
581 }
582 }
583 dirty = true;
584 return true;
585 }
586
587 typedef struct {
588 uint8_t filetype_mask;
589 const char *ext;
590 } file_attr_t;
591
592 static const file_attr_t filetype_table[] = {
593 { FT_PROG_MASK , "PROG" },
594 { FT_DATA_MASK , "DATA" },
595 { FT_BPGM_MASK , "BPGM" }
596 };
597
get_filename_and_ext(const dir_entry_85 & ent,bool inc_ext,char * out)598 void tape_image_85::get_filename_and_ext(const dir_entry_85& ent , bool inc_ext , char *out)
599 {
600 if (ent.filetype & FT_NULL_FILE_MASK) {
601 // Empty directory slot
602 strcpy(out , NULL_FILENAME);
603 } else {
604 const uint8_t *s = &ent.filename[ 0 ];
605 while (*s != '\0' && *s != ' ' && (s - &ent.filename[ 0 ]) < CHARS_PER_FNAME) {
606 *out++ = *s++;
607 }
608 *out = '\0';
609
610 // Decode filetype
611 if (inc_ext) {
612 const char *ext = nullptr;
613 for (const auto& i : filetype_table) {
614 if (ent.filetype & i.filetype_mask) {
615 ext = i.ext;
616 break;
617 }
618 }
619 if (ext != nullptr) {
620 strcat(out , ".");
621 strcat(out , ext);
622 }
623 }
624 }
625 }
626
split_filename_and_ext(const char * filename,char * fname,char * ext)627 void tape_image_85::split_filename_and_ext(const char *filename , char *fname , char *ext)
628 {
629 char *fname_fence = fname + CHARS_PER_FNAME;
630
631 while (fname < fname_fence && *filename != '\0' && *filename != '.') {
632 *fname++ = *filename++;
633 }
634
635 *fname = '\0';
636
637 while (*filename != '\0' && *filename != '.') {
638 filename++;
639 }
640
641 if (*filename == '\0') {
642 *ext = '\0';
643 } else {
644 filename++;
645 strncpy(ext , filename , CHARS_PER_EXT);
646 ext[ CHARS_PER_EXT ] = '\0';
647 }
648 }
649
decode_dir(const sif_file_t & file_0)650 bool tape_image_85::decode_dir(const sif_file_t& file_0)
651 {
652 if (file_0.size() != DIR_RECORDS * MAX_RECORD_SIZE) {
653 return false;
654 }
655 dir.clear();
656
657 // Check DIRSEG (directory segment), i.e. record #0 or #1 of directory
658 // Check FL1TK1 (file & record no. where track 1 begins)
659 if (file_0[ 0xfc ] != 0 ||
660 file_0[ 0x1fc ] != 1 ||
661 file_0[ 0xfd ] != file_0[ 0x1fd ] ||
662 file_0[ 0xfe ] != file_0[ 0x1fe ] ||
663 file_0[ 0xff ] != file_0[ 0x1ff ] ||
664 file_0[ 0xfd ] > MAX_FILE_NO) {
665 return false;
666 }
667
668 // Get FL1TK1
669 file_track_1 = file_0[ 0xfd ];
670 record_track_1 = pick_integer_le(file_0.data() , 0xfe , 2);
671
672 file_no_t file_no = 1;
673
674 // Iterate over all entries of directory
675 for (unsigned i = 0; i < 0x1fc; i += 12) {
676 if (i == 0xfc) {
677 // Skip over 1st copy of DIRSEG/FL1TK1
678 i = 0x100;
679 }
680 const uint8_t *p = file_0.data() + i;
681 dir_entry_85 new_entry;
682
683 // File type
684 new_entry.filetype = p[ 7 ];
685 if (new_entry.filetype & FT_NEXT_AV_MASK) {
686 // Directory ends
687 break;
688 }
689
690 // Filename
691 memcpy(&new_entry.filename[ 0 ] , p , CHARS_PER_FNAME);
692 if (!filename_check(&new_entry.filename[ 0 ])) {
693 return false;
694 }
695
696 // File #
697 // It is also stored at p[ 6 ] but HP85 firmware ignores it
698 new_entry.file_no = file_no++;
699
700 // Physical records
701 new_entry.n_recs = pick_integer_le(p , 8 , 2);
702
703 // Bytes per logical record
704 new_entry.record_len = pick_integer_le(p , 10 , 2);
705 if (new_entry.record_len < 4 || new_entry.record_len >= 32768) {
706 return false;
707 }
708
709 dir.push_back(new_entry);
710 }
711
712 return true;
713 }
714
encode_dir(sif_file_t & file_0) const715 void tape_image_85::encode_dir(sif_file_t& file_0) const
716 {
717 file_0.clear();
718 file_0.resize(DIR_RECORDS * MAX_RECORD_SIZE , 0);
719
720 // Set DIRSEG
721 file_0[ 0xfc ] = 0;
722 file_0[ 0x1fc ] = 1;
723 // Set FL1TK1
724 file_0[ 0xfd ] = file_0[ 0x1fd ] = file_track_1;
725 place_integer_le(file_0.data() , 0xfe , 2 , record_track_1);
726 place_integer_le(file_0.data() , 0x1fe , 2 , record_track_1);
727
728 unsigned i = 0;
729 file_no_t file_no = 1;
730 for (auto entry = dir.cbegin(); entry != dir.cend(); entry++, i += 12, file_no++) {
731 if (i == 0xfc) {
732 // Skip over 1st copy of DIRSEG/FL1TK1
733 i = 0x100;
734 }
735 uint8_t *p_entry = file_0.data() + i;
736 memcpy(&p_entry[ 0 ] , &entry->filename[ 0 ] , CHARS_PER_FNAME);
737 p_entry[ 6 ] = file_no;
738 p_entry[ 7 ] = entry->filetype;
739 place_integer_le(p_entry , 8 , 2 , entry->n_recs);
740 place_integer_le(p_entry , 10 , 2 , entry->record_len);
741 }
742
743 if (file_no <= MAX_FILE_NO) {
744 if (i == 0xfc) {
745 // Skip over 1st copy of DIRSEG/FL1TK1
746 i = 0x100;
747 }
748 file_0[ i + 7 ] = FT_NEXT_AV_MASK;
749 }
750
751 // Two identical copies of directory
752 memcpy(file_0.data() + (DIR_RECORDS / 2) * MAX_RECORD_SIZE , file_0.data() , (DIR_RECORDS / 2) * MAX_RECORD_SIZE);
753 }
754
save_words(unsigned track,tape_pos_t & pos,const tape_word_t * block,unsigned block_len)755 void tape_image_85::save_words(unsigned track , tape_pos_t& pos , const tape_word_t *block , unsigned block_len)
756 {
757 tape_pos_t length;
758 for (unsigned i = 0; i < block_len; i++) {
759 image.write_word(track , pos , *block++ , length);
760 pos += length;
761 }
762 }
763
save_sif_file(unsigned & track,tape_pos_t & pos,file_no_t file_no,const sif_file_t & in)764 void tape_image_85::save_sif_file(unsigned& track , tape_pos_t& pos , file_no_t file_no , const sif_file_t& in)
765 {
766 unsigned rec_no = 0;
767 unsigned bytes_to_go = in.size();
768 sif_file_t::const_iterator in_it = in.cbegin();
769
770 do {
771 if (file_track_1 != 0 && track == 0 &&
772 ((file_no == file_track_1 && rec_no >= record_track_1) || file_no > file_track_1)) {
773 // Switch to track 1
774 track = 1;
775 pos = TRACK_START;
776 }
777 tape_pos_t start_pos = pos;
778
779 unsigned rec_size = std::min(bytes_to_go , MAX_RECORD_SIZE);
780
781 tape_word_t hdr[ 5 ];
782 hdr[ 0 ] = SYNC_WORD;
783 hdr[ 1 ] = (tape_word_t)file_no | 0x2000;
784 if (rec_no == 0) {
785 hdr[ 1 ] |= 0x8000;
786 }
787 if (rec_size) {
788 hdr[ 1 ] |= 0x1800;
789 }
790 hdr[ 2 ] = 0x1000 | (tape_word_t)rec_no;
791 hdr[ 3 ] = 0xff00;
792 if (rec_size) {
793 hdr[ 3 ] |= (rec_size - 1);
794 }
795 hdr[ 4 ] = checksum(&hdr[ 1 ], 3);
796
797 save_words(track, pos, &hdr[ 0 ], 5);
798
799 if (rec_size) {
800 tape_word_t body[ MAX_RECORD_SIZE / 2 + 1 ];
801 unsigned words = 0;
802 while (words < MAX_RECORD_SIZE / 2 && in_it != in.cend()) {
803 tape_word_t w = (tape_word_t)*in_it++ << 8;
804 if (in_it != in.cend()) {
805 w |= *in_it++;
806 }
807 body[ words++ ] = w;
808 }
809 body[ words ] = checksum(&body[ 0 ], words);
810 save_words(track, pos, &body[ 0 ], words + 1);
811 }
812
813 // Pad record up to FMT_REC_SIZE
814 tape_word_t filler = 0xffff;
815 while ((pos - start_pos) < FMT_REC_SIZE) {
816 save_words(track, pos, &filler, 1);
817 }
818
819 if (rec_no == 0) {
820 // IFG
821 pos = start_pos + FMT_REC_SIZE + FMT_IFG_SIZE;
822 } else {
823 // IRG
824 pos = start_pos + FMT_REC_SIZE + FMT_IRG_SIZE;
825 }
826 bytes_to_go -= rec_size;
827 rec_no++;
828 } while (bytes_to_go > 0);
829 }
830
filename_char_check(uint8_t c)831 bool tape_image_85::filename_char_check(uint8_t c)
832 {
833 // Quotation marks are forbidden in file names
834 return 0x20 < c && c < 0x7f && c != '"';
835 }
836
filename_check(const uint8_t * filename)837 bool tape_image_85::filename_check(const uint8_t *filename)
838 {
839 bool ended = false;
840
841 for (unsigned i = 0; i < CHARS_PER_FNAME; i++) {
842 uint8_t c = *filename++;
843
844 if (ended) {
845 if (c != ' ') {
846 return false;
847 }
848 } else if (c == ' ') {
849 ended = true;
850 } else if (!filename_char_check(c)) {
851 return false;
852 }
853 }
854
855 return true;
856 }
857
858 namespace {
get_tape_state(imgtool::image & img)859 tape_state_t& get_tape_state(imgtool::image &img)
860 {
861 tape_state_t *ts = (tape_state_t*)img.extra_bytes();
862
863 return *ts;
864 }
865
get_tape_image(tape_state_t & ts)866 tape_image_85& get_tape_image(tape_state_t& ts)
867 {
868 if (ts.img == nullptr) {
869 ts.img = new tape_image_85;
870 }
871
872 return *(ts.img);
873 }
874 }
875 /********************************************************************************
876 * Imgtool functions
877 ********************************************************************************/
878 namespace {
hp85_tape_open(imgtool::image & image,imgtool::stream::ptr && stream)879 imgtoolerr_t hp85_tape_open(imgtool::image &image, imgtool::stream::ptr &&stream)
880 {
881 tape_state_t& state = get_tape_state(image);
882
883 state.stream = stream.release();
884
885 tape_image_85& tape_image = get_tape_image(state);
886
887 imgtoolerr_t err = tape_image.load_from_file(state.stream);
888 if (err)
889 return err;
890
891 return IMGTOOLERR_SUCCESS;
892 }
893
hp85_tape_create(imgtool::image & image,imgtool::stream::ptr && stream,util::option_resolution * opts)894 imgtoolerr_t hp85_tape_create(imgtool::image &image, imgtool::stream::ptr &&stream, util::option_resolution *opts)
895 {
896 tape_state_t& state = get_tape_state(image);
897
898 state.stream = stream.release();
899
900 tape_image_85& tape_image = get_tape_image(state);
901
902 tape_image.format_img();
903
904 return IMGTOOLERR_SUCCESS;
905 }
906
hp85_tape_close(imgtool::image & image)907 void hp85_tape_close(imgtool::image &image)
908 {
909 tape_state_t& state = get_tape_state(image);
910 tape_image_85& tape_image = get_tape_image(state);
911
912 if (tape_image.is_dirty()) {
913 (void)tape_image.save_to_file(state.stream);
914 }
915
916 delete state.stream;
917
918 // Free tape_image
919 delete &tape_image;
920 }
921
hp85_tape_begin_enum(imgtool::directory & enumeration,const char * path)922 imgtoolerr_t hp85_tape_begin_enum (imgtool::directory &enumeration, const char *path)
923 {
924 dir_state_t *ds = (dir_state_t*)enumeration.extra_bytes();
925
926 ds->dir_idx = 0;
927
928 return IMGTOOLERR_SUCCESS;
929 }
930
hp85_tape_next_enum(imgtool::directory & enumeration,imgtool_dirent & ent)931 imgtoolerr_t hp85_tape_next_enum (imgtool::directory &enumeration, imgtool_dirent &ent)
932 {
933 tape_state_t& state = get_tape_state(enumeration.image());
934 tape_image_85& tape_image = get_tape_image(state);
935 dir_state_t *ds = (dir_state_t*)enumeration.extra_bytes();
936
937 const dir_entry_85 *entry = nullptr;
938
939 if (!tape_image.get_dir_entry(ds->dir_idx, entry)) {
940 ent.eof = 1;
941 } else {
942 ds->dir_idx++;
943
944 unsigned filesize = entry->n_recs * MAX_RECORD_SIZE;
945
946 if (entry->filetype & FT_NULL_FILE_MASK) {
947 ent.filesize = 0;
948 } else {
949 ent.filesize = filesize;
950 }
951
952 tape_image_85::get_filename_and_ext(*entry, true, ent.filename);
953 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' : ' ');
954 }
955
956
957 return IMGTOOLERR_SUCCESS;
958 }
959
hp85_tape_free_space(imgtool::partition & partition,uint64_t * size)960 imgtoolerr_t hp85_tape_free_space(imgtool::partition &partition, uint64_t *size)
961 {
962 tape_state_t& state = get_tape_state(partition.image());
963 tape_image_85& tape_image = get_tape_image(state);
964 unsigned used_recs = 0;
965
966 const dir_entry_85 *entry = nullptr;
967
968 for (unsigned i = 0; i < MAX_FILE_NO; i++) {
969 if (!tape_image.get_dir_entry(i, entry)) {
970 break;
971 }
972 // Ignore erased files
973 if (entry->filetype & FT_NULL_FILE_MASK) {
974 continue;
975 }
976 used_recs += entry->n_recs;
977 }
978
979 if (used_recs >= MAX_N_RECORDS) {
980 *size = 0;
981 } else {
982 *size = (MAX_N_RECORDS - used_recs) * MAX_RECORD_SIZE;
983 }
984
985 return IMGTOOLERR_SUCCESS;
986 }
987
hp85_tape_read_file(imgtool::partition & partition,const char * filename,const char * fork,imgtool::stream & destf)988 imgtoolerr_t hp85_tape_read_file(imgtool::partition &partition, const char *filename, const char *fork, imgtool::stream &destf)
989 {
990 tape_state_t& state = get_tape_state(partition.image());
991 tape_image_85& tape_image = get_tape_image(state);
992
993 unsigned idx;
994
995 if (!tape_image.find_file(filename , false , idx)) {
996 return IMGTOOLERR_FILENOTFOUND;
997 }
998
999 const dir_entry_85 *ent = nullptr;
1000
1001 tape_image.get_dir_entry(idx, ent);
1002
1003 tape_image_85::sif_file_t file_data;
1004
1005 if (!tape_image.load_sif_file(ent->file_no , file_data)) {
1006 return IMGTOOLERR_READERROR;
1007 }
1008
1009 destf.write(file_data.data() , file_data.size());
1010
1011 return IMGTOOLERR_SUCCESS;
1012 }
1013
hp85_tape_write_file(imgtool::partition & partition,const char * filename,const char * fork,imgtool::stream & sourcef,util::option_resolution * opts)1014 imgtoolerr_t hp85_tape_write_file(imgtool::partition &partition, const char *filename, const char *fork, imgtool::stream &sourcef, util::option_resolution *opts)
1015 {
1016 tape_state_t& state = get_tape_state(partition.image());
1017 tape_image_85& tape_image = get_tape_image(state);
1018
1019 unsigned idx;
1020
1021 if (tape_image.find_file(filename , true , idx)) {
1022 // When overwriting a file, delete its old version first
1023 tape_image.delete_dir_entry(idx);
1024 }
1025
1026 unsigned file_size = sourcef.size();
1027
1028 if (!file_size) {
1029 return IMGTOOLERR_SUCCESS;
1030 }
1031 if (file_size > (MAX_N_RECORDS * MAX_RECORD_SIZE)) {
1032 return IMGTOOLERR_NOSPACE;
1033 }
1034
1035 auto p_in_file = std::make_unique<tape_image_85::sif_file_t>();
1036
1037 p_in_file->resize(file_size);
1038
1039 if (sourcef.read(p_in_file->data() , file_size) != file_size) {
1040 return IMGTOOLERR_READERROR;
1041 }
1042
1043 dir_entry_85 *ent = nullptr;
1044
1045 if (!tape_image.alloc_new_file(ent , std::move(p_in_file))) {
1046 return IMGTOOLERR_NOSPACE;
1047 }
1048
1049 char fname[ CHARS_PER_FNAME + 1 ];
1050 char ext[ CHARS_PER_EXT + 1 ];
1051
1052 tape_image_85::split_filename_and_ext(filename, fname, ext);
1053
1054 char *dest = (char *)&ent->filename[ 0 ];
1055 char *dest0 = dest;
1056 char *src = &fname[ 0 ];
1057
1058 while ((dest - dest0) < CHARS_PER_FNAME && *src != '\0') {
1059 *dest++ = *src++;
1060 }
1061 while ((dest - dest0) < CHARS_PER_FNAME) {
1062 *dest++ = ' ';
1063 }
1064
1065 int bpr = opts->lookup_int('B');
1066 if (bpr < 4) {
1067 bpr = MAX_RECORD_SIZE;
1068 } else if (bpr > file_size) {
1069 util::stream_format(std::wcerr, L"BPR value too large, using %u\n", MAX_RECORD_SIZE);
1070 bpr = MAX_RECORD_SIZE;
1071 }
1072 ent->record_len = (uint16_t)bpr;
1073
1074 if (opts->lookup_int('T') == 0) {
1075 // File type defaults to DATA if no extension is given or extension is invalid
1076 ent->filetype = FT_DATA_MASK;
1077 for (const auto& i : filetype_table) {
1078 if (strcmp(i.ext , ext) == 0) {
1079 ent->filetype = i.filetype_mask;
1080 break;
1081 }
1082 }
1083 } else {
1084 ent->filetype = filetype_table[ opts->lookup_int('T') - 1 ].filetype_mask;
1085 }
1086
1087 if (!tape_image.finalize_allocation()) {
1088 return IMGTOOLERR_NOSPACE;
1089 }
1090
1091 return IMGTOOLERR_SUCCESS;
1092 }
1093
hp85_tape_delete_file(imgtool::partition & partition,const char * filename)1094 imgtoolerr_t hp85_tape_delete_file(imgtool::partition &partition, const char *filename)
1095 {
1096 tape_state_t& state = get_tape_state(partition.image());
1097 tape_image_85& tape_image = get_tape_image(state);
1098
1099 unsigned idx;
1100
1101 if (!tape_image.find_file(filename , true , idx)) {
1102 return IMGTOOLERR_FILENOTFOUND;
1103 }
1104
1105 if (!tape_image.delete_dir_entry(idx)) {
1106 return IMGTOOLERR_READERROR;
1107 }
1108 if (!tape_image.finalize_allocation()) {
1109 return IMGTOOLERR_NOSPACE;
1110 }
1111
1112 return IMGTOOLERR_SUCCESS;
1113 }
1114 }
1115 #define HP85_WRITEFILE_OPTSPEC "B[0]-32767;T[0]-3"
1116
1117 OPTION_GUIDE_START(hp85_write_optguide)
1118 OPTION_INT('B' , "bpr" , "Bytes per record")
1119 OPTION_ENUM_START('T' , "ftype" , "File type")
1120 OPTION_ENUM(0 , "auto" , "Automatic (\"DATA\" or by extension)")
1121 OPTION_ENUM(1 , "P" , "PROG")
1122 OPTION_ENUM(2 , "D" , "DATA")
1123 OPTION_ENUM(3 , "B" , "BPGM")
1124 OPTION_ENUM_END
1125 OPTION_GUIDE_END
1126
hp85_tape_get_info(const imgtool_class * imgclass,uint32_t state,union imgtoolinfo * info)1127 void hp85_tape_get_info(const imgtool_class *imgclass, uint32_t state, union imgtoolinfo *info)
1128 {
1129 switch (state) {
1130 case IMGTOOLINFO_STR_NAME:
1131 strcpy(info->s = imgtool_temp_str(), "hp85_tape");
1132 break;
1133
1134 case IMGTOOLINFO_STR_DESCRIPTION:
1135 strcpy(info->s = imgtool_temp_str(), "HP85 tape image");
1136 break;
1137
1138 case IMGTOOLINFO_STR_FILE:
1139 strcpy(info->s = imgtool_temp_str(), __FILE__);
1140 break;
1141
1142 case IMGTOOLINFO_STR_FILE_EXTENSIONS:
1143 strcpy(info->s = imgtool_temp_str(), "hti");
1144 break;
1145
1146 case IMGTOOLINFO_INT_IMAGE_EXTRA_BYTES:
1147 info->i = sizeof(tape_state_t);
1148 break;
1149
1150 case IMGTOOLINFO_INT_DIRECTORY_EXTRA_BYTES:
1151 info->i = sizeof(dir_state_t);
1152 break;
1153
1154 case IMGTOOLINFO_PTR_OPEN:
1155 info->open = hp85_tape_open;
1156 break;
1157
1158 case IMGTOOLINFO_PTR_CREATE:
1159 info->create = hp85_tape_create;
1160 break;
1161
1162 case IMGTOOLINFO_PTR_CLOSE:
1163 info->close = hp85_tape_close;
1164 break;
1165
1166 case IMGTOOLINFO_PTR_BEGIN_ENUM:
1167 info->begin_enum = hp85_tape_begin_enum;
1168 break;
1169
1170 case IMGTOOLINFO_PTR_NEXT_ENUM:
1171 info->next_enum = hp85_tape_next_enum;
1172 break;
1173
1174 case IMGTOOLINFO_PTR_FREE_SPACE:
1175 info->free_space = hp85_tape_free_space;
1176 break;
1177
1178 case IMGTOOLINFO_PTR_READ_FILE:
1179 info->read_file = hp85_tape_read_file;
1180 break;
1181
1182 case IMGTOOLINFO_PTR_WRITE_FILE:
1183 info->write_file = hp85_tape_write_file;
1184 break;
1185
1186 case IMGTOOLINFO_PTR_DELETE_FILE:
1187 info->delete_file = hp85_tape_delete_file;
1188 break;
1189
1190 case IMGTOOLINFO_PTR_WRITEFILE_OPTGUIDE:
1191 info->writefile_optguide = &hp85_write_optguide;
1192 break;
1193
1194 case IMGTOOLINFO_STR_WRITEFILE_OPTSPEC:
1195 strcpy(info->s = imgtool_temp_str(), HP85_WRITEFILE_OPTSPEC);
1196 break;
1197 }
1198 }
1199