/* BaseDiskImage.java (c) 2010-2015 Edward Swartz All rights reserved. This program and the accompanying materials are made available under the terms of the Eclipse Public License v1.0 which accompanies this distribution, and is available at http://www.eclipse.org/legal/epl-v10.html */ package v9t9.engine.files.image; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; import java.util.ArrayList; import java.util.List; import v9t9.common.client.ISettingsHandler; import v9t9.common.cpu.ICpu; import v9t9.common.files.Catalog; import v9t9.common.files.CatalogEntry; import v9t9.common.files.DiskImageFDR; import v9t9.common.files.EmulatedDiskImageFile; import v9t9.common.files.FDR; import v9t9.common.files.IDiskHeader; import v9t9.common.files.IDiskImage; import v9t9.common.files.IEmulatedFile; import v9t9.common.files.IdMarker; import v9t9.common.files.IndexSector; import v9t9.common.files.VIB; import v9t9.engine.Dumper; import v9t9.engine.dsr.realdisk.ICRCAlgorithm; import ejs.base.properties.IPersistable; import ejs.base.settings.ISettingSection; /** * @author ejs * */ public abstract class BaseDiskImage implements IPersistable, IDiskImage { static class DSKheader implements IDiskHeader { private String spec; /** tracks per side */ private int tracks; /** 1 or 2 */ private int sides; /** # tracks per side */ private int secsPerTrack; /** bytes per track */ private int tracksize; /** offset for track 0 data */ private int track0offs; /** if true, tracks go in opposite order on side 2 */ private boolean invertedSide2; private boolean side2DirectionKnown; /* (non-Javadoc) * @see v9t9.common.files.IDiskHeader#getPath() */ @Override public String getPath() { return spec; } @Override public void setPath(String spec) { this.spec = spec; } @Override public String toString() { return "DSKheader [spec="+spec+", tracks=" + tracks + ", sides=" + sides + ", secsPerTrack=" + secsPerTrack + ", tracksize=" + tracksize + ", track0offs=" + track0offs + "]"; } @Override public void setSides(int sides) { this.sides = sides; } @Override public void setTracks(int tracks) { this.tracks = tracks; } @Override public void setSecsPerTrack(int secsPerTrack) { this.secsPerTrack = secsPerTrack; } @Override public void setTrackSize(int tracksize) { this.tracksize = tracksize; } @Override public void setTrack0Offset(int track0offs) { this.track0offs = track0offs; } @Override public int getTracks() { return tracks; } /* (non-Javadoc) * @see v9t9.engine.files.image.IDiskHeader#getSides() */ @Override public int getSides() { return sides; } /* (non-Javadoc) * @see v9t9.engine.files.image.IDiskHeader#getTrackSize() */ @Override public int getTrackSize() { return tracksize; } /* (non-Javadoc) * @see v9t9.engine.files.image.IDiskHeader#getTrack0Offset() */ @Override public int getTrack0Offset() { return track0offs; } /* (non-Javadoc) * @see v9t9.common.files.IDiskHeader#getSecsPerTrack() */ @Override public int getSecsPerTrack() { return secsPerTrack; } public long getImageSize() { return (tracksize & 0xffff) * (tracks & 0xff) * sides; } protected int getTrackOffset(int track) { return track0offs + (tracksize & 0xffff) * track; } public int getTrackOffset(int track, int side) { int effTrack = side == 0 ? track : (invertedSide2 ? (tracks * 2 - track - 1) : (tracks + track)); return getTrackOffset(effTrack); } public void setInvertedSide2(boolean invertedSide2) { this.invertedSide2 = invertedSide2; } public boolean isInvertedSide2() { return invertedSide2; } /** * @param b */ public void setSide2DirectionKnown(boolean b) { side2DirectionKnown = b; } /** * @return the side2DirectionKnown */ public boolean isSide2DirectionKnown() { return side2DirectionKnown; } }; private String name; protected File spec; private RandomAccessFile handle; protected boolean trackFetched; protected boolean trackChanged; protected byte trackBuffer[] = new byte[RealDiskConsts.DSKbuffersize]; protected DSKheader hdr = new DSKheader(); protected boolean readonly; protected int track; protected int side; protected Dumper dumper; protected IDiskFormat fmFormat; protected IDiskFormat mfmFormat; protected IDiskFormat trackFormat = new FMFormat(dumper); protected List<IdMarker> trackMarkers; private Catalog catalog; /** * @param name * @param settings * */ public BaseDiskImage( String name, File spec, ISettingsHandler settings) { this.name = name; this.spec = spec; dumper = new Dumper(settings, RealDiskSettings.diskImageDebug, ICpu.settingDumpFullInstructions); fmFormat = new FMFormat(dumper); mfmFormat = new MFMFormat(dumper); } /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return getClass().getSimpleName() + ": " + spec; } /* (non-Javadoc) * @see v9t9.common.files.IDiskImage#isDiskImageOpen() */ @Override public boolean isDiskImageOpen() { return handle != null; } /** * @param file */ public void setFile(File file) { try { //flush(); closeDiskImage(); } catch (IOException e) { } this.spec = file; } /** * */ public void closeDiskImage() throws IOException { commitTrack(); reset(); } /** * @throws IOException * */ protected void reset() throws IOException { if (handle != null) { handle.close(); handle = null; } trackFetched = false; trackChanged = false; trackMarkers = null; catalog = null; } public void growImageForContent() throws IOException { long sz, len; if (spec == null || handle == null) return; len = spec.length(); sz = hdr.getImageSize(); /* never shrink */ if (sz > len) { handle.setLength(sz); } } protected abstract short getDefaultTrackSize(); /** * */ public void openDiskImage() throws IOException { openDiskImage(false); } /** * @throws IOException * @throws FileNotFoundException */ public void openDiskImage(boolean readOnly) throws IOException { if (getHandle() != null) closeDiskImage(); if (spec.isFile() && spec.exists()) { hdr.setPath(spec.getPath()); if (readOnly) { setHandle(new RandomAccessFile(spec, "r")); readonly = true; } else { try { setHandle(new RandomAccessFile(spec, "rw")); readonly = false; } catch (IOException e) { setHandle(new RandomAccessFile(spec, "r")); readonly = true; } } } else { readonly = readOnly; if (!readonly) { createDiskImage(); closeDiskImage(); return; } else { throw new FileNotFoundException("no disk image file exists: " + spec.toString()); } } /* get disk info */ try { readImageHeader(); } catch (IOException e) { try { createDiskImage(); } catch (IOException e2) { closeDiskImage(); throw e2; } readImageHeader(); } trackFetched = false; trackChanged = false; dumper.info("Opened {0} disk ''{1}'' {2},\n#tracks={3}, tracksize={4}, sides={5}", getDiskType(), spec, name, hdr.getTracks(), hdr.getTrackSize(), hdr.getSides()); } public void createDiskImage() throws IOException { dumper.info("Creating new {2} disk image at {0} ({1})", name, spec, getDiskType()); /* defaults */ hdr.setPath(spec.getPath()); hdr.setTracks(40); hdr.setSides(1); hdr.setSecsPerTrack(9); hdr.setTrackSize(getDefaultTrackSize()); hdr.setTrack0Offset(getHeaderSize()); /* create file */ handle = null; handle = new RandomAccessFile(spec, "rw"); writeImageHeader(); } /** * */ public byte[] readCurrentTrackData() throws IOException { if (handle == null) return trackBuffer; if (!trackFetched) { long diskoffs = getTrackDiskOffset(); dumper.info("Reading {0} bytes of data on track {1}, trackoffset = {2}, offset = >{3}", hdr.getTrackSize(), track, diskoffs, Long.toHexString(diskoffs)); // TODO: need to encapsulate as track data when reading from sector disk handle.seek(diskoffs); try { handle.read(trackBuffer, 0, hdr.getTrackSize()); } catch (IndexOutOfBoundsException e) { throw (IOException) new IOException().initCause(e); } trackFetched = true; trackChanged = false; trackFormat = null; trackMarkers = null; ensureFormatAndTrackMarkers(); } return trackBuffer; } /** * */ private void ensureFormatAndTrackMarkers() { if (trackFormat == null || trackMarkers == null || trackMarkers.isEmpty()) { fetchFormatAndTrackMarkers(); } } protected void updateDiskSize() throws IOException { if (handle == null) return; if (!readonly && (track >= hdr.getTracks() || side >= hdr.getSides())) { // grow the disk if (track >= hdr.getTracks()) hdr.setTracks(track); if (side >= hdr.getSides()) hdr.setSides(side); writeImageHeader(); } // refresh trackFetched = false; trackChanged = false; trackMarkers = null; } /** * @return */ public long getTrackDiskOffset() { return hdr.getTrackOffset(track, side); } /** * @param rwBuffer * @param i * @param buflen */ public void readTrackData(byte[] rwBuffer, int i, int buflen) throws IOException { readCurrentTrackData(); buflen = Math.min(hdr.getTrackSize(), buflen); System.arraycopy(trackBuffer, 0, rwBuffer, i, buflen); } /** */ public void commitTrack() throws IOException { if (!trackFetched || !trackChanged) return; if (handle == null) throw new IOException("lost disk before committing track"); int size = getTrackSize(); long diskoffs = getTrackDiskOffset(); dumper.info("Writing {0} bytes of data on track {1}, offset = >{2}", size, track, Long.toHexString(diskoffs)); handle.seek(diskoffs); handle.write(trackBuffer, 0, size); boolean newHeader = false; if (side > 0 && hdr.getSides() == 1) { hdr.setSides(2); newHeader = true; } if (track > hdr.getTracks()) { hdr.setTracks(track); newHeader = true; } if (trackMarkers.size() > hdr.getSecsPerTrack()) { int cnt = trackMarkers.size(); if (cnt <= 9) hdr.setSecsPerTrack(9); else hdr.setSecsPerTrack(18); hdr.setTrackSize(hdr.getSecsPerTrack() * (256 + 100)); // TODO: scan for actual markers newHeader = true; } if (newHeader) { writeImageHeader(); } handle.getFD().sync(); trackFetched = true; trackChanged = false; } /** * @return */ public int getTrackSize() { return hdr.tracksize; } /** * @param addNewSection */ public void saveState(ISettingSection section) { section.put("FilePath", spec.getAbsolutePath()); } public void loadState(ISettingSection section) { spec = RealDiskSettings.getDefaultDiskImage(name); if (section == null) return; String path = section.get("FilePath"); if (path != null) spec = new File(path); } /** * */ public void closeIfMissing() { if (!spec.exists()) { try { reset(); } catch (IOException e) { } } } @Override public void setSide(int side) throws IOException { if (side != this.side) { commitTrack(); this.side = side; if (side == 1 && !hdr.isSide2DirectionKnown()) { discoverSide2TrackOrdering(); } updateDiskSize(); } } /** * Set the {@link IDiskHeader#setInvertedSide2(boolean)} flag */ abstract protected void discoverSide2TrackOrdering(); /* (non-Javadoc) * @see v9t9.common.files.IDiskImage#getSide() */ @Override public int getSide() { return side; } // for utilities, not realtime usage public void readSectorData(IdMarker currentMarker, byte[] rwBuffer, int start, int buflen) { if (currentMarker != null) { try { ICRCAlgorithm crcAlg = trackFormat.getCRCAlgorithm(); crcAlg.reset(); crcAlg.feed(currentMarker.dataCode); for (int offs = 0; offs < buflen; offs++) { byte b = trackBuffer[currentMarker.dataoffset + 1 + offs]; crcAlg.feed(b); rwBuffer[start + offs] = b; } //System.arraycopy(trackBuffer, currentMarker.dataoffset + 1, rwBuffer, start, buflen); } catch (ArrayIndexOutOfBoundsException e) { System.err.println("Possible bogus sector size: " + buflen); } RealDiskUtils.dumpBuffer(dumper, rwBuffer, 0, 256); } } /** Fetch ID markers into markers and establish format */ protected abstract void fetchFormatAndTrackMarkers(); public IdMarker readSector(int sector, byte[] rwBuffer, int start, int buflen) throws IOException { if (hdr.getSecsPerTrack() == 0) throw new IOException("unformatted disk"); int secsPerTrack = hdr.getSecsPerTrack(); boolean gotSector0 = false; if (sector != 0) { // try again if the sector 0 information differs byte[] sec0 = new byte[256]; readSector(0, sec0, 0, 256); VIB vib = VIB.createVIB(sec0, 0); if (vib.isFormatted() && vib.getSecsPerTrack() > 0) { secsPerTrack = vib.getSecsPerTrack(); gotSector0 = true; } } int track; byte side; byte tracksec; track = (sector / secsPerTrack); side = (byte) (track >= 40 ? 1 : 0); // ensure we have the inverted flag set setSide(side); track %= 40; if (side > 0 && hdr.isInvertedSide2()) track = 39 - track; setTrack(track); readCurrentTrackData(); tracksec = (byte) (sector % secsPerTrack); for (IdMarker marker : getTrackMarkers()) { if (marker.sectorid == tracksec && marker.trackid == track) { readSectorData(marker, rwBuffer, start, buflen); return marker; } } if (!gotSector0 && secsPerTrack == 18 && getTrackMarkers().size() > 0 && getTrackMarkers().size() < 18) { // recalc -- maybe we were wrong secsPerTrack = 9; track = (sector / secsPerTrack); side = (byte) (track >= 40 ? 1 : 0); track %= 40; if (side > 0) track = 39 - track; setTrack(track); setSide(side); readCurrentTrackData(); tracksec = (byte) (sector % secsPerTrack); for (IdMarker marker : getTrackMarkers()) { if (marker.sectorid == tracksec && marker.trackid == track) { readSectorData(marker, rwBuffer, start, buflen); return marker; } } } throw new MissingSectorException(sector, getTrackDiskOffset(), spec+": sector " + tracksec + " not found on track " + track + ", side " + side, null); } /** * @return * @throws IOException */ public Catalog readCatalog(String devname) throws IOException { List<CatalogEntry> entries = new ArrayList<CatalogEntry>(); byte[] asec = new byte[256]; byte[] fdrSec = new byte[256]; readSector(0, asec, 0, 256); VIB vib = VIB.createVIB(asec, 0); String volume = vib.getVolumeName(); int total = vib.getTotalSecs(); int used = vib.getSecsUsed(); readSector(1, asec, 0, 256); for (int ent = 0; ent < 256; ent+=2) { int sec = ((asec[ent] << 8) | (asec[ent+1] & 0xff)) & 0xffff; if (sec == 0) break; readSector(sec, fdrSec, 0, 256); DiskImageFDR fdr = DiskImageFDR.createFDR(fdrSec, 0); entries.add(new CatalogEntry(sec, fdr.getFileName(), new EmulatedDiskImageFile(this, sec, fdr, fdr.getFileName()))); } return new Catalog(this, devname, volume, total, used, entries); } /** * @param handle the handle to set */ public void setHandle(RandomAccessFile handle) { this.handle = handle; } /** * @return the handle */ public RandomAccessFile getHandle() { return handle; } /** * @param name the name to set */ public void setName(String name) { this.name = name; } /** * @return the name */ public String getName() { return name; } /* (non-Javadoc) * @see v9t9.common.files.IEmulatedDisk#isValid() */ @Override public boolean isValid() { boolean wasOpen = isDiskImageOpen(); if (!wasOpen) { try { openDiskImage(true); if (hdr.getSides() <= 2 && hdr.getTrack0Offset() <= 256 && (hdr.getTracks() == 40 || hdr.getTracks() == 80) && hdr.getTrackSize() < RealDiskConsts.DSKbuffersize) { return true; } return false; } catch (IOException e) { e.printStackTrace(); return false; } finally { if (!wasOpen) { try { closeDiskImage(); } catch (IOException e) { } } } } return true; } /* (non-Javadoc) * @see v9t9.common.files.IDiskImage#isFormatted() */ @Override public boolean isFormatted() { boolean wasOpen = isDiskImageOpen(); if (!wasOpen) { try { openDiskImage(true); byte[] sec0 = new byte[256]; readSector(0, sec0, 0, 256); VIB vib = VIB.createVIB(sec0, 0); return vib.isFormatted(); } catch (FileNotFoundException e) { return false; } catch (IOException e) { e.printStackTrace(); return false; } finally { if (!wasOpen) { try { closeDiskImage(); } catch (IOException e) { } } } } return true; } /* (non-Javadoc) * @see v9t9.common.files.IDiskImage#getHeader() */ @Override public IDiskHeader getHeader() { return hdr; } /** * @return */ public List<IdMarker> getTrackMarkers() { ensureFormatAndTrackMarkers(); return trackMarkers; } /** * @return */ public boolean isReadOnly() { return readonly; } /** * Write data written for the track; may be larger than allowed track size * @param rwBuffer * @param buflen * @param start * @param fdc */ @Override public void writeTrackData(byte[] rwBuffer, int start, int buflen) { buflen = Math.min(RealDiskConsts.DSKbuffersize, buflen); if (buflen == 0) buflen = Math.min(rwBuffer.length, RealDiskConsts.DSKbuffersize); if (mfmFormat.doesFormatMatch(rwBuffer, buflen)) trackFormat = mfmFormat; else trackFormat = fmFormat; // fetch and update markers trackMarkers = trackFormat.fetchIdMarkers(rwBuffer, buflen, true); System.arraycopy(rwBuffer, start, trackBuffer, 0, buflen); trackFetched = true; trackChanged = true; // dump contents RealDiskUtils.dumpBuffer(dumper, rwBuffer, start, buflen); } /* (non-Javadoc) * @see v9t9.common.files.IEmulatedDisk#readCatalog() */ @Override public Catalog readCatalog() throws IOException { if (catalog == null) { boolean wasOpen = isDiskImageOpen(); if (!wasOpen) openDiskImage(true); String devname = "DSK" + name.charAt(name.length() - 1); Catalog catalog = readCatalog(devname); if (!wasOpen) closeDiskImage(); this.catalog = catalog; } return catalog; } /* (non-Javadoc) * @see v9t9.common.files.IEmulatedDisk#getFile(java.lang.String) */ @Override public IEmulatedFile getFile(String name) throws IOException { Catalog catalog = readCatalog(); CatalogEntry ent = catalog.findEntry(name); if (ent == null) throw new FileNotFoundException(name); return ent.getFile(); } /* (non-Javadoc) * @see v9t9.common.files.IEmulatedDisk#getPath() */ @Override public String getPath() { return spec.getPath(); } /* (non-Javadoc) * @see v9t9.common.files.IEmulatedDisk#createFile(java.lang.String, v9t9.common.files.FDR) */ @Override public IEmulatedFile createFile(final String fileName, final FDR fdr) throws IOException { final Catalog catalog = readCatalog(); for (CatalogEntry entry : catalog.getEntries()) { if (entry.getFile().getFileName().equalsIgnoreCase(fileName)) { // existing file deleteFile(entry.getFile()); break; } } final int indexSector = allocateSector(2); if (indexSector < 0) throw new IOException("no sectors free"); updateSector(1, new SectorUpdater() { @Override public boolean updateSector(byte[] sec1) throws IOException { IndexSector index = IndexSector.create(sec1); // free sectors for FDR index.add(indexSector, fileName, catalog); System.arraycopy(index.toBytes(), 0, sec1, 0, sec1.length); return true; } }); final DiskImageFDR diskfdr = new DiskImageFDR(); diskfdr.copyFrom(fdr); diskfdr.setSectorsUsed(0); // get sectors for storage diskfdr.allocateSectors(this, fdr.getSectorsUsed()); // dump FDR to sector updateSector(indexSector, new SectorUpdater() { @Override public boolean updateSector(byte[] fdrSec) throws IOException { byte[] out = diskfdr.toBytes(); System.arraycopy(out, 0, fdrSec, 0, out.length); return true; } }); EmulatedDiskImageFile file = new EmulatedDiskImageFile(this, indexSector, diskfdr, diskfdr.getFileName()); commitTrack(); return file; } /* (non-Javadoc) * @see v9t9.common.files.IDiskImage#updateSector(int, v9t9.common.files.IDiskImage.SectorUpdater) */ @Override public void updateSector(int num, SectorUpdater sectorUpdater) throws IOException { byte[] sec = new byte[256]; IdMarker marker = readSector(num, sec, 0, 256); if (sectorUpdater.updateSector(sec)) { writeSectorData(sec, num, 256, marker); commitTrack(); } } /** * @return */ public int allocateSector(final int start) throws IOException { final int[] num = { -1 }; updateSector(0, new SectorUpdater() { @Override public boolean updateSector(byte[] content) throws IOException { VIB vib = VIB.createVIB(content, 0); num[0] = vib.allocateSector(start); System.arraycopy(vib.toBytes(), 0, content, 0, 256); return true; } }); return num[0]; } /** * @param file */ public void deleteFile(IEmulatedFile file) throws IOException { if (false == file instanceof EmulatedDiskImageFile) throw new IOException("unexpected file type: " + file.getClass()); final EmulatedDiskImageFile dfile = (EmulatedDiskImageFile) file; final int[] contents = dfile.getFDR().getContentSectors(); updateSector(0, new SectorUpdater() { @Override public boolean updateSector(byte[] sec0) throws IOException { VIB vib = VIB.createVIB(sec0, 0); // free sectors for content for (int csec : contents) { if (csec > 0) vib.deallocateSector(csec); } // free index sector if (dfile.getIndexSector() > 0) { vib.deallocateSector(dfile.getIndexSector()); } System.arraycopy(vib.toBytes(), 0, sec0, 0, sec0.length); return true; } }); updateSector(1, new SectorUpdater() { @Override public boolean updateSector(byte[] sec1) throws IOException { IndexSector index = IndexSector.create(sec1); // free sectors for FDR index.remove(dfile.getIndexSector()); System.arraycopy(index.toBytes(), 0, sec1, 0, sec1.length); return true; } }); } /* (non-Javadoc) * @see v9t9.common.files.IDiskImage#getTrack() */ @Override public int getTrack() { return track; } /* (non-Javadoc) * @see v9t9.common.files.IDiskImage#setTrack(int) */ @Override public void setTrack(int track) throws IOException { if (this.track != track) { commitTrack(); this.track = track; updateDiskSize(); } } }