// license:BSD-3-Clause // copyright-holders:Olivier Galibert // Management of Oric Jasmin floppy images #include "fs_oric_jasmin.h" #include "oric_dsk.h" #include using namespace fs; namespace fs { const oric_jasmin_image ORIC_JASMIN; } // Floppy only, format is 41 tracks, 1/2 heads, 17 sectors. // Filesystem has no subdirectories. // // References are pair of bytes (track + head*41, sector) // // Track 20 sector 1 has the free bitmap and the volume name // // offset 3*(track + head*41): bitmap from track, 24 bits LSB first, // sector 1 in bit 16, sector 17 in bit 0. // 0x01ffff is empty track. Special value // 0x800000 marks a full track. // offset f6-f7: 0x8080 // offset f8-ff: volume name, padded with 0x20 // // // Track 20 sector 2 has the first directory sector. // Directory sector format: // offset 00-01: reference to the sector, (0, 0) for the first one // offset 02-03: reference to the next sector, (0, 0) on the first sector if it's the end, (ff, 00) for the end of another sector // offset 04+ : 14 file entries, 18 bytes each // offset 00-01: reference to the first sector of the inode, (ff, xx) when no entry // offset 02 : U/L for (U)nlocked or (L)ocked file // offset 03-0e: filename.ext, space padding between filename and '.' // offset 0f : S/D for (S)equential (normal) or (D)irect-access files // offset 10-11: number of sectors used by the file, including inode, little-endian // // Inodes: // offset 00-01: reference to the next inode sector, (ff, 00) on the last one // offset 02-03: loading address for the file on the first sector, ffff otherwise // offset 04-05: length of the file in bytes on the first sector, ffff otherwise // offset 06+ : reference to data sectors, (ff, ff) when done namespace { class oric_jasmin_impl : public filesystem_t { public: oric_jasmin_impl(fsblk_t &blockdev); virtual ~oric_jasmin_impl() = default; virtual meta_data volume_metadata() override; virtual err_t volume_metadata_change(const meta_data &info) override; virtual std::pair metadata(const std::vector &path) override; virtual err_t metadata_change(const std::vector &path, const meta_data &meta) override; virtual std::pair> directory_contents(const std::vector &path) override; virtual err_t rename(const std::vector &opath, const std::vector &npath) override; virtual err_t remove(const std::vector &path) override; virtual err_t file_create(const std::vector &path, const meta_data &meta) override; virtual std::pair> file_read(const std::vector &path) override; virtual err_t file_write(const std::vector &path, const std::vector &data) override; virtual err_t format(const meta_data &meta) override; static bool validate_filename(std::string name); private: static u32 cs_to_block(u16 ref); [[maybe_unused]] static u16 block_to_cs(u32 block); bool ref_valid(u16 ref); static std::string read_file_name(const u8 *p); std::vector allocate_blocks(u32 count); void free_blocks(const std::vector &blocks); u32 free_block_count(); static std::string file_name_read(const u8 *p); static std::string file_name_prepare(std::string name); static bool file_is_system(const u8 *entry); meta_data file_metadata(const u8 *entry); std::tuple file_find(std::string name); }; } const char *oric_jasmin_image::name() const { return "oric_jasmin"; } const char *oric_jasmin_image::description() const { return "Oric Jasmin"; } void oric_jasmin_image::enumerate_f(floppy_enumerator &fe, u32 form_factor, const std::vector &variants) const { if(has(form_factor, variants, floppy_image::FF_3, floppy_image::DSDD)) fe.add(FLOPPY_ORIC_JASMIN_FORMAT, 356864, "oric_jasmin_ds", "Oric Jasmin dual-sided"); if(has(form_factor, variants, floppy_image::FF_3, floppy_image::SSDD)) fe.add(FLOPPY_ORIC_JASMIN_FORMAT, 178432, "oric_jasmin_ss", "Oric Jasmin single-sided"); } std::unique_ptr oric_jasmin_image::mount(fsblk_t &blockdev) const { return std::make_unique(blockdev); } bool oric_jasmin_image::can_format() const { return true; } bool oric_jasmin_image::can_read() const { return true; } bool oric_jasmin_image::can_write() const { return true; } bool oric_jasmin_image::has_rsrc() const { return false; } std::vector oric_jasmin_image::volume_meta_description() const { std::vector res; res.emplace_back(meta_description(meta_name::name, "UNTITLED", false, [](const meta_value &m) { return m.as_string().size() <= 8; }, "Volume name, up to 8 characters")); return res; } std::vector oric_jasmin_image::file_meta_description() const { std::vector res; res.emplace_back(meta_description(meta_name::name, "", false, [](const meta_value &m) { return oric_jasmin_impl::validate_filename(m.as_string()); }, "File name, 8.3")); res.emplace_back(meta_description(meta_name::loading_address, 0x501, false, [](const meta_value &m) { return m.as_number() < 0x10000; }, "Loading address of the file")); res.emplace_back(meta_description(meta_name::length, 0, true, nullptr, "Size of the file in bytes")); res.emplace_back(meta_description(meta_name::size_in_blocks, 0, true, nullptr, "Number of blocks used by the file")); res.emplace_back(meta_description(meta_name::locked, false, false, nullptr, "File locked")); res.emplace_back(meta_description(meta_name::sequential, true, false, nullptr, "File sequential")); return res; } oric_jasmin_impl::oric_jasmin_impl(fsblk_t &blockdev) : filesystem_t(blockdev, 256) { } bool oric_jasmin_impl::ref_valid(u16 ref) { u8 track = ref >> 8; u8 sector = ref & 0xff; if(sector < 1 || sector > 17) return false; if(track >= m_blockdev.block_count()/17) return false; return true; } u32 oric_jasmin_impl::cs_to_block(u16 ref) { u8 track = ref >> 8; if(track == 0xff) abort(); u8 sector = ref & 0xff; return track * 17 + sector - 1; } u16 oric_jasmin_impl::block_to_cs(u32 block) { u8 track = block / 17; u8 sector = (block % 17) + 1; return (track << 8) | sector; } bool oric_jasmin_impl::validate_filename(std::string name) { auto pos = name.find('.'); if(pos != std::string::npos) return pos <= 8 && pos > 0 && name.size()-pos-1 <= 3; else return name.size() > 0 && name.size() <= 8; } err_t oric_jasmin_impl::format(const meta_data &meta) { std::string volume_name = meta.get_string(meta_name::name, "UNTITLED"); u32 blocks = m_blockdev.block_count(); m_blockdev.fill(0x6c); u32 bblk = 20*17; auto fmap = m_blockdev.get(bblk); u32 off = 0; for(u32 blk = 0; blk != blocks; blk += 17) { if(blk == bblk) fmap.w24l(off, 0x07fff); else fmap.w24l(off, 0x1ffff); off += 3; } for(u32 blk = blocks; blk != 17*42*2; blk += 17) { fmap.w24l(off, 0x800000); off += 3; } fmap.w8(0xf6, 0x80); fmap.w8(0xf7, 0x80); volume_name.resize(8, ' '); fmap.wstr(0xf8, volume_name); auto bdir = m_blockdev.get(20*17+1); bdir.fill(0xff); bdir.w16l(0, 0x0000); bdir.w16l(2, 0x0000); return ERR_OK; } meta_data oric_jasmin_impl::volume_metadata() { meta_data res; auto bdir = m_blockdev.get(20*17); int len = 8; while(len > 0 && bdir.rodata()[0xf8 + len - 1] == ' ') len--; res.set(meta_name::name, bdir.rstr(0xf8, len)); return res; } err_t oric_jasmin_impl::volume_metadata_change(const meta_data &meta) { if(meta.has(meta_name::name)) { std::string volume_name = meta.get_string(meta_name::name); volume_name.resize(8, ' '); m_blockdev.get(20*17).wstr(0xf8, volume_name); } return ERR_OK; } std::string oric_jasmin_impl::file_name_prepare(std::string fname) { std::string nname; size_t i; for(i = 0; i != 8 && i != fname.size() && fname[i] != '.'; i++) nname += fname[i]; if(nname.size() != 8) nname.insert(nname.end(), 8 - nname.size(), ' '); nname += '.'; while(i != fname.size() && fname[i] != '.') i++; if(i != fname.size()) i++; while(i != fname.size() && nname.size() != 12) nname += fname[i++]; if(nname.size() != 12) nname.insert(nname.end(), 12 - nname.size(), ' '); return nname; } std::string oric_jasmin_impl::file_name_read(const u8 *p) { int main_len; for(main_len = 8; main_len > 0; main_len--) if(p[main_len - 1] != ' ') break; int ext_len; for(ext_len = 4; ext_len > 0; ext_len--) if(p[ext_len + 8 - 1] != ' ') break; std::string name; for(int i=0; i != main_len; i++) name += char(p[i]); for(int i=0; i != ext_len; i++) name += char(p[8 + i]); return name; } bool oric_jasmin_impl::file_is_system(const u8 *entry) { u16 ref = r16b(entry); return ref == 0 && r32b(entry+0xb) == 0x2e535953; } meta_data oric_jasmin_impl::file_metadata(const u8 *entry) { meta_data res; res.set(meta_name::name, file_name_read(entry + 3)); res.set(meta_name::locked, entry[2] == 'L'); res.set(meta_name::sequential, entry[0xf] == 'S'); res.set(meta_name::size_in_blocks, r16l(entry + 0x10)); bool sys = file_is_system(entry); if(sys) res.set(meta_name::length, 0x3e00); else { u16 ref = r16b(entry); auto dblk = m_blockdev.get(cs_to_block(ref)); res.set(meta_name::loading_address, dblk.r16l(2)); res.set(meta_name::length, dblk.r16l(4)); } return res; } std::tuple oric_jasmin_impl::file_find(std::string name) { name = file_name_prepare(name); auto bdir = m_blockdev.get(20*17+1); for(;;) { for(u32 i = 0; i != 14; i ++) { u32 off = 4 + i*18; u16 fref = bdir.r16b(off); if(ref_valid(fref) || file_is_system(bdir.rodata()+off)) { if(memcmp(bdir.rodata() + off + 3, name.data(), 12)) { bool sys = file_is_system(bdir.rodata() + off); return std::make_tuple(bdir, off, sys); } } } u16 ref = bdir.r16b(2); if(!ref || !ref_valid(ref)) return std::make_tuple(bdir, 0U, false); bdir = m_blockdev.get(cs_to_block(ref)); } } std::pair oric_jasmin_impl::metadata(const std::vector &path) { if(path.size() != 1) return std::make_pair(ERR_NOT_FOUND, meta_data()); auto [bdir, off, sys] = file_find(path[0]); std::ignore = sys; if(!off) return std::make_pair(ERR_NOT_FOUND, meta_data()); return std::make_pair(ERR_OK, file_metadata(bdir.rodata() + off)); } err_t oric_jasmin_impl::metadata_change(const std::vector &path, const meta_data &meta) { if(path.size() != 1) return ERR_NOT_FOUND; auto [bdir, off, sys] = file_find(path[0]); if(!off) return ERR_NOT_FOUND; u8 *entry = bdir.data() + off; if(meta.has(meta_name::locked)) w8 (entry+0x02, meta.get_flag(meta_name::locked) ? 'L' : 'U'); if(meta.has(meta_name::name)) wstr(entry+0x03, file_name_prepare(meta.get_string(meta_name::name))); if(meta.has(meta_name::sequential)) w8 (entry+0x0f, meta.get_flag(meta_name::sequential) ? 'D' : 'S'); if(!sys && meta.has(meta_name::loading_address)) m_blockdev.get(cs_to_block(r16b(entry))).w16l(2, meta.get_number(meta_name::loading_address)); return ERR_OK; } std::pair> oric_jasmin_impl::directory_contents(const std::vector &path) { std::pair> res; if(path.size() != 0) { res.first = ERR_NOT_FOUND; return res; } res.first = ERR_OK; auto bdir = m_blockdev.get(20*17+1); for(;;) { for(u32 i = 0; i != 14; i ++) { u32 off = 4 + i*18; u16 fref = bdir.r16b(off); if(ref_valid(fref) || file_is_system(bdir.rodata()+off)) { meta_data meta = file_metadata(bdir.rodata()+off); res.second.emplace_back(dir_entry(dir_entry_type::file, meta)); } } u16 ref = bdir.r16b(2); if(!ref || !ref_valid(ref)) break; bdir = m_blockdev.get(cs_to_block(ref)); } return res; } err_t oric_jasmin_impl::rename(const std::vector &opath, const std::vector &npath) { if(opath.size() != 1 || npath.size() != 1) return ERR_NOT_FOUND; auto [bdir, off, sys] = file_find(opath[0]); std::ignore = sys; if(!off) return ERR_NOT_FOUND; wstr(bdir.data() + off + 0x03, file_name_prepare(npath[0])); return ERR_OK; } err_t oric_jasmin_impl::remove(const std::vector &path) { return ERR_UNSUPPORTED; } err_t oric_jasmin_impl::file_create(const std::vector &path, const meta_data &meta) { if(path.size() != 0) return ERR_NOT_FOUND; // One block of sector list, one block of data u32 nb = 2; // Find the key for the next entry, increase nb if needed auto bdir = m_blockdev.get(20*17+1); u64 id = 0; for(;;) { for(u32 i = 0; i != 14; i ++) { u32 off = 4 + i*18; u16 ref = bdir.r16b(off); if(!ref_valid(ref)) goto found; id++; } u16 ref = bdir.r16b(2); if(!ref || !ref_valid(ref)) { nb ++; break; } bdir = m_blockdev.get(cs_to_block(ref)); } found: auto block = allocate_blocks(nb); if(block.empty()) return ERR_NO_SPACE; auto sblk = m_blockdev.get(cs_to_block(block[0])); sblk.w16b(0, 0xff00); // Next sector sblk.w16l(2, meta.get_number(meta_name::loading_address, 0x500)); sblk.w16l(4, 0); // Length sblk.w16b(6, block[1]); // Data block if(nb == 3) { bdir.w16l(0, block[2]); // Link to the next directory sector bdir = m_blockdev.get(cs_to_block(block[2])); bdir.fill(0xff); bdir.w16l(0, block[2]); // Reference to itself bdir.w16l(2, 0xff00); // No next directory sector } u32 off = 4 + (id % 14) * 18; bdir.w16b(off+0x00, block[0]); // First (and only) sector in the sector list bdir.w8 (off+0x02, meta.get_flag(meta_name::locked, false) ? 'L' : 'U'); bdir.wstr(off+0x03, file_name_prepare(meta.get_string(meta_name::name, ""))); bdir.w8 (off+0x0f, meta.get_flag(meta_name::sequential, true) ? 'S' : 'D'); bdir.w16l(off+0x10, 2); // 2 sectors for an empty file return ERR_OK; } std::pair> oric_jasmin_impl::file_read(const std::vector &path) { std::vector data; if(path.size() != 1) return std::make_pair(ERR_NOT_FOUND, data); auto [bdir, off, sys] = file_find(path[0]); if(!off) return std::make_pair(ERR_NOT_FOUND, data); if(sys) { data.resize(0x3e00); for(u32 i = 0; i != 62; i++) { auto dblk = m_blockdev.get(i); memcpy(data.data() + 256 * i, dblk.rodata(), 256); } } else { const u8 *entry = bdir.rodata() + off; u16 ref = r16b(entry); auto iblk = m_blockdev.get(cs_to_block(ref)); u32 length = iblk.r16l(4); while(ref_valid(ref)) { for(u32 pos = 6; pos != 256 && data.size() < length; pos += 2) { u16 dref = iblk.r16b(pos); if(!ref_valid(dref)) goto done; auto dblk = m_blockdev.get(cs_to_block(dref)); u32 dpos = data.size(); data.resize(dpos + 256); memcpy(data.data() + dpos, dblk.rodata(), 256); if(data.size() >= length) goto done; } ref = iblk.r16b(2); if(!ref_valid(ref)) break; iblk = m_blockdev.get(cs_to_block(ref)); } done: data.resize(length); } return std::make_pair(ERR_OK, data); } err_t oric_jasmin_impl::file_write(const std::vector &path, const std::vector &data) { if(path.size() != 1) return ERR_NOT_FOUND; auto [bdir, off, sys] = file_find(path[0]); if(!off) return ERR_NOT_FOUND; if(sys) { if(data.size() != 0x3e00) return ERR_INVALID; for(u32 i=0; i != 0x3e; i++) m_blockdev.get(i).copy(0, data.data() + i * 256, 256); } else { u8 *entry = bdir.data() + off; u32 cur_ns = r16l(entry + 0x10); // Data sectors first u32 need_ns = (data.size() + 255) / 256; if(need_ns == 0) need_ns = 1; // Add the sector list sectors, 125 entries/sector need_ns += (need_ns + 124)/125; // Enough space? if(cur_ns < need_ns && free_block_count() < need_ns - cur_ns) return ERR_NO_SPACE; u16 load_address = 0; std::vector tofree; u16 iref = r16b(entry); for(u32 i=0; i < cur_ns; i += 125+1) { auto iblk = m_blockdev.get(cs_to_block(iref)); if(!i) load_address = iblk.r16l(2); tofree.push_back(iref); for(u32 j=0; j != 125 && i+j+1 != cur_ns; j++) tofree.push_back(iblk.r16b(6+2*j)); iref = iblk.r16b(2); } free_blocks(tofree); std::vector blocks = allocate_blocks(need_ns); for(u32 i=0; i < need_ns; i += 125+1) { auto iblk = m_blockdev.get(cs_to_block(blocks[i])); iblk.fill(0xff); if(!i) { iblk.w16l(2, load_address); iblk.w16l(4, data.size()); } if(i + 126 < need_ns) iblk.w16b(0, blocks[i+126]); else iblk.w16b(0, 0xff00); for(u32 j=0; j != 125 && i+j+1 != need_ns; j++) { u32 dpos = 256 * (j + i/126*125); u32 size = data.size() - dpos; iblk.w16b(6+j*2, blocks[i+j+1]); auto dblk = m_blockdev.get(cs_to_block(blocks[i+j+1])); if(size >= 256) dblk.copy(0, data.data() + dpos, 256); else { dblk.copy(0, data.data() + dpos, size); dblk.fill(size, 0x55, 256-size); } } } w16l(entry + 0x10, need_ns); w16b(entry + 0x00, blocks[0]); } return ERR_OK; } std::vector oric_jasmin_impl::allocate_blocks(u32 count) { std::vector blocks; if(free_block_count() < count) return blocks; auto fmap = m_blockdev.get(20*17); u32 nf = 0; for(u32 track = 0; track != 2*41 && nf != count; track++) { u32 map = fmap.r24l(track*3); if(map != 0x800000) { for(u32 sect = 1; sect <= 17 && nf != count; sect++) if(map & (0x20000 >> sect)) { blocks.push_back((track << 8) | sect); map &= ~(0x20000 >> sect); nf++; } if(!map) map = 0x800000; fmap.w24l(track*3, map); } } return blocks; } void oric_jasmin_impl::free_blocks(const std::vector &blocks) { auto fmap = m_blockdev.get(20*17); for(u16 ref : blocks) { u32 track = ref >> 8; u32 sect = ref & 0xff; u32 map = fmap.r24l(track*3); if(map == 0x800000) map = 0; map |= 0x20000 >> sect; fmap.w24l(track*3, map); } } u32 oric_jasmin_impl::free_block_count() { auto fmap = m_blockdev.get(20*17); u32 nf = 0; for(u32 track = 0; track != 2*41; track++) { u32 map = fmap.r24l(track*3); if(map != 0x800000) { for(u32 sect = 1; sect <= 17; sect++) if(map & (0x20000 >> sect)) nf++; } } return nf; }