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