From c953204c60f913c35e0aa5ba7c71777d252f352e Mon Sep 17 00:00:00 2001 From: Denis Molony Date: Mon, 1 Jun 2015 19:35:51 +1000 Subject: [PATCH] Initial commit --- resources/buildNumber | 3 + src/com/bytezone/diskbrowser/DateTime.java | 42 ++ .../diskbrowser/FileFormatException.java | 17 + .../bytezone/diskbrowser/HexFormatter.java | 439 ++++++++++++ src/com/bytezone/diskbrowser/LZW.java | 113 +++ src/com/bytezone/diskbrowser/LZW1.java | 88 +++ src/com/bytezone/diskbrowser/LZW2.java | 112 +++ src/com/bytezone/diskbrowser/NuFX.java | 268 +++++++ src/com/bytezone/diskbrowser/Thread.java | 136 ++++ .../diskbrowser/applefile/AbstractFile.java | 95 +++ .../applefile/AppleFileSource.java | 32 + .../applefile/ApplesoftConstants.java | 29 + .../applefile/AssemblerConstants.java | 67 ++ .../applefile/AssemblerProgram.java | 268 +++++++ .../applefile/AssemblerStatement.java | 363 ++++++++++ .../diskbrowser/applefile/BasicProgram.java | 658 ++++++++++++++++++ .../diskbrowser/applefile/BootSector.java | 48 ++ .../diskbrowser/applefile/Charset.java | 31 + .../diskbrowser/applefile/Command.java | 180 +++++ .../applefile/DefaultAppleFile.java | 32 + .../applefile/ErrorMessageFile.java | 25 + .../diskbrowser/applefile/HiResImage.java | 262 +++++++ .../diskbrowser/applefile/IconFile.java | 201 ++++++ .../applefile/IntegerBasicProgram.java | 240 +++++++ .../diskbrowser/applefile/LodeRunner.java | 93 +++ .../diskbrowser/applefile/MerlinSource.java | 95 +++ .../diskbrowser/applefile/PascalCode.java | 69 ++ .../applefile/PascalCodeStatement.java | 329 +++++++++ .../applefile/PascalConstants.java | 118 ++++ .../diskbrowser/applefile/PascalInfo.java | 29 + .../applefile/PascalProcedure.java | 175 +++++ .../diskbrowser/applefile/PascalSegment.java | 134 ++++ .../diskbrowser/applefile/PascalText.java | 50 ++ .../diskbrowser/applefile/ShapeTable.java | 150 ++++ .../diskbrowser/applefile/SimpleText.java | 46 ++ .../diskbrowser/applefile/SimpleText2.java | 149 ++++ .../applefile/StoredVariables.java | 242 +++++++ .../diskbrowser/applefile/TextBuffer.java | 43 ++ .../diskbrowser/applefile/TextFile.java | 159 +++++ .../diskbrowser/applefile/VisicalcFile.java | 57 ++ .../applefile/VisicalcSpreadsheet.java | 564 +++++++++++++++ .../diskbrowser/applefile/WizardryTitle.java | 51 ++ .../diskbrowser/applefile/equates.txt | 158 +++++ .../appleworks/AppleworksADBFile.java | 185 +++++ .../appleworks/AppleworksSSFile.java | 308 ++++++++ .../appleworks/AppleworksWPFile.java | 187 +++++ .../bytezone/diskbrowser/appleworks/Cell.java | 31 + .../diskbrowser/appleworks/CellAddress.java | 21 + .../diskbrowser/appleworks/CellConstant.java | 40 ++ .../diskbrowser/appleworks/CellFormat.java | 50 ++ .../diskbrowser/appleworks/CellFormula.java | 59 ++ .../diskbrowser/appleworks/CellLabel.java | 30 + .../diskbrowser/appleworks/CellValue.java | 36 + .../diskbrowser/appleworks/LabelReport.java | 16 + .../diskbrowser/appleworks/Record.java | 178 +++++ .../diskbrowser/appleworks/Report.java | 182 +++++ .../diskbrowser/appleworks/TableReport.java | 211 ++++++ .../catalog/AbstractCatalogCreator.java | 13 + .../catalog/AbstractDiskCreator.java | 35 + .../diskbrowser/catalog/CatalogLister.java | 12 + .../diskbrowser/catalog/DiskLister.java | 19 + .../catalog/DocumentCreatorFactory.java | 52 ++ .../catalog/TextCatalogCreator.java | 90 +++ .../diskbrowser/catalog/TextDiskCreator.java | 61 ++ src/com/bytezone/diskbrowser/cpm/CPMDisk.java | 66 ++ .../disk/AbstractFormattedDisk.java | 411 +++++++++++ .../diskbrowser/disk/AbstractSector.java | 123 ++++ .../bytezone/diskbrowser/disk/AppleDisk.java | 441 ++++++++++++ .../diskbrowser/disk/AppleDiskAddress.java | 65 ++ .../bytezone/diskbrowser/disk/DataDisk.java | 52 ++ .../disk/DefaultAppleFileSource.java | 88 +++ .../diskbrowser/disk/DefaultDataSource.java | 49 ++ .../diskbrowser/disk/DefaultSector.java | 26 + src/com/bytezone/diskbrowser/disk/Disk.java | 60 ++ .../diskbrowser/disk/DiskAddress.java | 12 + .../diskbrowser/disk/DiskFactory.java | 362 ++++++++++ .../diskbrowser/disk/DualDosDisk.java | 274 ++++++++ .../diskbrowser/disk/FormattedDisk.java | 79 +++ .../bytezone/diskbrowser/disk/SectorList.java | 48 ++ .../diskbrowser/disk/SectorListConverter.java | 65 ++ .../bytezone/diskbrowser/disk/SectorType.java | 21 + .../diskbrowser/dos/AbstractCatalogEntry.java | 277 ++++++++ .../diskbrowser/dos/CatalogEntry.java | 135 ++++ .../diskbrowser/dos/DeletedCatalogEntry.java | 105 +++ .../diskbrowser/dos/DosCatalogSector.java | 105 +++ src/com/bytezone/diskbrowser/dos/DosDisk.java | 407 +++++++++++ .../diskbrowser/dos/DosTSListSector.java | 78 +++ .../diskbrowser/dos/DosVTOCSector.java | 146 ++++ .../bytezone/diskbrowser/gui/AboutAction.java | 60 ++ .../bytezone/diskbrowser/gui/AbstractTab.java | 160 +++++ .../diskbrowser/gui/AppleDiskTab.java | 163 +++++ .../diskbrowser/gui/CatalogPanel.java | 448 ++++++++++++ .../diskbrowser/gui/CloseTabAction.java | 31 + .../diskbrowser/gui/CreateDatabaseAction.java | 23 + .../bytezone/diskbrowser/gui/DataPanel.java | 335 +++++++++ .../bytezone/diskbrowser/gui/DataSource.java | 18 + .../diskbrowser/gui/DiskAndFileSelector.java | 138 ++++ .../bytezone/diskbrowser/gui/DiskBrowser.java | 196 ++++++ .../bytezone/diskbrowser/gui/DiskDetails.java | 41 ++ .../diskbrowser/gui/DiskLayoutImage.java | 273 ++++++++ .../diskbrowser/gui/DiskLayoutPanel.java | 245 +++++++ .../diskbrowser/gui/DiskLayoutSelection.java | 125 ++++ .../diskbrowser/gui/DiskLegendPanel.java | 96 +++ .../diskbrowser/gui/DiskSelectedEvent.java | 40 ++ .../gui/DiskSelectionListener.java | 8 + .../diskbrowser/gui/DuplicateAction.java | 314 +++++++++ .../diskbrowser/gui/ExecuteDiskAction.java | 38 + .../gui/FileNodeSelectedEvent.java | 33 + .../gui/FileNodeSelectionListener.java | 8 + .../diskbrowser/gui/FileSelectedEvent.java | 40 ++ .../gui/FileSelectionListener.java | 8 + .../diskbrowser/gui/FileSystemTab.java | 241 +++++++ .../diskbrowser/gui/HideCatalogAction.java | 47 ++ .../diskbrowser/gui/HideLayoutAction.java | 47 ++ .../diskbrowser/gui/InterleaveAction.java | 34 + .../diskbrowser/gui/LineWrapAction.java | 29 + .../bytezone/diskbrowser/gui/MenuHandler.java | 253 +++++++ .../gui/NoDisksFoundException.java | 6 + .../diskbrowser/gui/OpenFileAction.java | 48 ++ .../diskbrowser/gui/PreferencesAction.java | 39 ++ .../diskbrowser/gui/PreferencesDialog.java | 203 ++++++ .../bytezone/diskbrowser/gui/PrintAction.java | 52 ++ .../diskbrowser/gui/PrintDocument.java | 129 ++++ .../bytezone/diskbrowser/gui/RedoHandler.java | 235 +++++++ .../diskbrowser/gui/RefreshTreeAction.java | 34 + .../diskbrowser/gui/RootDirectoryAction.java | 53 ++ .../bytezone/diskbrowser/gui/ScrollRuler.java | 129 ++++ .../diskbrowser/gui/SectorSelectedEvent.java | 50 ++ .../gui/SectorSelectionListener.java | 8 + .../gui/ShowFreeSectorsAction.java | 31 + src/com/bytezone/diskbrowser/gui/Tab.java | 20 + .../bytezone/diskbrowser/gui/TreeBuilder.java | 352 ++++++++++ .../diskbrowser/icons/Symbol-Left-32.png | Bin 0 -> 3770 bytes .../diskbrowser/icons/Symbol-Right-32.png | Bin 0 -> 3854 bytes .../diskbrowser/icons/arrow_refresh.png | Bin 0 -> 674 bytes .../diskbrowser/icons/arrow_refresh_32.png | Bin 0 -> 1664 bytes src/com/bytezone/diskbrowser/icons/disk.jpg | Bin 0 -> 724 bytes src/com/bytezone/diskbrowser/icons/disk.png | Bin 0 -> 730 bytes src/com/bytezone/diskbrowser/icons/disk2.png | Bin 0 -> 226 bytes .../diskbrowser/icons/folder_explore_16.png | Bin 0 -> 815 bytes .../diskbrowser/icons/folder_explore_32.png | Bin 0 -> 1823 bytes .../diskbrowser/icons/font_add_32.png | Bin 0 -> 1805 bytes .../diskbrowser/icons/font_delete_32.png | Bin 0 -> 1816 bytes .../diskbrowser/icons/information_16.png | Bin 0 -> 764 bytes .../diskbrowser/icons/information_32.png | Bin 0 -> 2112 bytes .../bytezone/diskbrowser/icons/printer_16.png | Bin 0 -> 697 bytes .../bytezone/diskbrowser/icons/printer_32.png | Bin 0 -> 1143 bytes .../diskbrowser/icons/save_delete_16.png | Bin 0 -> 715 bytes .../diskbrowser/icons/save_delete_32.png | Bin 0 -> 1869 bytes .../diskbrowser/icons/script_gear_32.png | Bin 0 -> 1602 bytes .../diskbrowser/infocom/Abbreviations.java | 74 ++ .../diskbrowser/infocom/AttributeManager.java | 85 +++ .../diskbrowser/infocom/CodeManager.java | 167 +++++ .../diskbrowser/infocom/Dictionary.java | 241 +++++++ .../bytezone/diskbrowser/infocom/Globals.java | 44 ++ .../bytezone/diskbrowser/infocom/Grammar.java | 331 +++++++++ .../bytezone/diskbrowser/infocom/Header.java | 136 ++++ .../diskbrowser/infocom/InfocomDisk.java | 265 +++++++ .../diskbrowser/infocom/Instruction.java | 493 +++++++++++++ .../diskbrowser/infocom/ObjectAnalyser.java | 217 ++++++ .../diskbrowser/infocom/ObjectManager.java | 96 +++ .../diskbrowser/infocom/PropertyManager.java | 92 +++ .../diskbrowser/infocom/PropertyTester.java | 65 ++ .../bytezone/diskbrowser/infocom/Routine.java | 153 ++++ .../diskbrowser/infocom/StringManager.java | 70 ++ .../bytezone/diskbrowser/infocom/ZObject.java | 203 ++++++ .../bytezone/diskbrowser/infocom/ZString.java | 156 +++++ .../pascal/PascalCatalogSector.java | 75 ++ .../diskbrowser/pascal/PascalDisk.java | 497 +++++++++++++ .../diskbrowser/prodos/CatalogEntry.java | 49 ++ .../diskbrowser/prodos/DirectoryHeader.java | 19 + .../diskbrowser/prodos/FileEntry.java | 560 +++++++++++++++ .../prodos/ProdosBitMapSector.java | 72 ++ .../prodos/ProdosCatalogSector.java | 196 ++++++ .../diskbrowser/prodos/ProdosConstants.java | 195 ++++++ .../diskbrowser/prodos/ProdosDirectory.java | 126 ++++ .../diskbrowser/prodos/ProdosDisk.java | 247 +++++++ .../prodos/ProdosExtendedKeySector.java | 45 ++ .../diskbrowser/prodos/ProdosIndexSector.java | 35 + .../prodos/SubDirectoryHeader.java | 43 ++ .../prodos/VolumeDirectoryHeader.java | 124 ++++ .../diskbrowser/wizardry/AbstractImage.java | 11 + .../diskbrowser/wizardry/Character.java | 331 +++++++++ .../diskbrowser/wizardry/CodedMessage.java | 27 + .../bytezone/diskbrowser/wizardry/Dice.java | 27 + .../diskbrowser/wizardry/ExperienceLevel.java | 44 ++ .../bytezone/diskbrowser/wizardry/Header.java | 227 ++++++ .../bytezone/diskbrowser/wizardry/Image.java | 59 ++ .../diskbrowser/wizardry/ImageV2.java | 32 + .../bytezone/diskbrowser/wizardry/Item.java | 141 ++++ .../diskbrowser/wizardry/MazeAddress.java | 21 + .../diskbrowser/wizardry/MazeCell.java | 322 +++++++++ .../diskbrowser/wizardry/MazeLevel.java | 216 ++++++ .../diskbrowser/wizardry/Message.java | 71 ++ .../diskbrowser/wizardry/Monster.java | 268 +++++++ .../diskbrowser/wizardry/PlainMessage.java | 18 + .../bytezone/diskbrowser/wizardry/Reward.java | 141 ++++ .../bytezone/diskbrowser/wizardry/Spell.java | 369 ++++++++++ .../wizardry/WizardryScenarioDisk.java | 599 ++++++++++++++++ 199 files changed, 24747 insertions(+) create mode 100755 resources/buildNumber create mode 100644 src/com/bytezone/diskbrowser/DateTime.java create mode 100644 src/com/bytezone/diskbrowser/FileFormatException.java create mode 100755 src/com/bytezone/diskbrowser/HexFormatter.java create mode 100644 src/com/bytezone/diskbrowser/LZW.java create mode 100644 src/com/bytezone/diskbrowser/LZW1.java create mode 100644 src/com/bytezone/diskbrowser/LZW2.java create mode 100644 src/com/bytezone/diskbrowser/NuFX.java create mode 100644 src/com/bytezone/diskbrowser/Thread.java create mode 100755 src/com/bytezone/diskbrowser/applefile/AbstractFile.java create mode 100755 src/com/bytezone/diskbrowser/applefile/AppleFileSource.java create mode 100755 src/com/bytezone/diskbrowser/applefile/ApplesoftConstants.java create mode 100755 src/com/bytezone/diskbrowser/applefile/AssemblerConstants.java create mode 100755 src/com/bytezone/diskbrowser/applefile/AssemblerProgram.java create mode 100755 src/com/bytezone/diskbrowser/applefile/AssemblerStatement.java create mode 100644 src/com/bytezone/diskbrowser/applefile/BasicProgram.java create mode 100644 src/com/bytezone/diskbrowser/applefile/BootSector.java create mode 100755 src/com/bytezone/diskbrowser/applefile/Charset.java create mode 100644 src/com/bytezone/diskbrowser/applefile/Command.java create mode 100755 src/com/bytezone/diskbrowser/applefile/DefaultAppleFile.java create mode 100644 src/com/bytezone/diskbrowser/applefile/ErrorMessageFile.java create mode 100755 src/com/bytezone/diskbrowser/applefile/HiResImage.java create mode 100644 src/com/bytezone/diskbrowser/applefile/IconFile.java create mode 100755 src/com/bytezone/diskbrowser/applefile/IntegerBasicProgram.java create mode 100644 src/com/bytezone/diskbrowser/applefile/LodeRunner.java create mode 100644 src/com/bytezone/diskbrowser/applefile/MerlinSource.java create mode 100755 src/com/bytezone/diskbrowser/applefile/PascalCode.java create mode 100755 src/com/bytezone/diskbrowser/applefile/PascalCodeStatement.java create mode 100755 src/com/bytezone/diskbrowser/applefile/PascalConstants.java create mode 100644 src/com/bytezone/diskbrowser/applefile/PascalInfo.java create mode 100755 src/com/bytezone/diskbrowser/applefile/PascalProcedure.java create mode 100755 src/com/bytezone/diskbrowser/applefile/PascalSegment.java create mode 100755 src/com/bytezone/diskbrowser/applefile/PascalText.java create mode 100755 src/com/bytezone/diskbrowser/applefile/ShapeTable.java create mode 100755 src/com/bytezone/diskbrowser/applefile/SimpleText.java create mode 100755 src/com/bytezone/diskbrowser/applefile/SimpleText2.java create mode 100755 src/com/bytezone/diskbrowser/applefile/StoredVariables.java create mode 100644 src/com/bytezone/diskbrowser/applefile/TextBuffer.java create mode 100755 src/com/bytezone/diskbrowser/applefile/TextFile.java create mode 100644 src/com/bytezone/diskbrowser/applefile/VisicalcFile.java create mode 100644 src/com/bytezone/diskbrowser/applefile/VisicalcSpreadsheet.java create mode 100755 src/com/bytezone/diskbrowser/applefile/WizardryTitle.java create mode 100644 src/com/bytezone/diskbrowser/applefile/equates.txt create mode 100644 src/com/bytezone/diskbrowser/appleworks/AppleworksADBFile.java create mode 100644 src/com/bytezone/diskbrowser/appleworks/AppleworksSSFile.java create mode 100755 src/com/bytezone/diskbrowser/appleworks/AppleworksWPFile.java create mode 100644 src/com/bytezone/diskbrowser/appleworks/Cell.java create mode 100644 src/com/bytezone/diskbrowser/appleworks/CellAddress.java create mode 100644 src/com/bytezone/diskbrowser/appleworks/CellConstant.java create mode 100644 src/com/bytezone/diskbrowser/appleworks/CellFormat.java create mode 100644 src/com/bytezone/diskbrowser/appleworks/CellFormula.java create mode 100644 src/com/bytezone/diskbrowser/appleworks/CellLabel.java create mode 100644 src/com/bytezone/diskbrowser/appleworks/CellValue.java create mode 100644 src/com/bytezone/diskbrowser/appleworks/LabelReport.java create mode 100644 src/com/bytezone/diskbrowser/appleworks/Record.java create mode 100644 src/com/bytezone/diskbrowser/appleworks/Report.java create mode 100644 src/com/bytezone/diskbrowser/appleworks/TableReport.java create mode 100755 src/com/bytezone/diskbrowser/catalog/AbstractCatalogCreator.java create mode 100755 src/com/bytezone/diskbrowser/catalog/AbstractDiskCreator.java create mode 100755 src/com/bytezone/diskbrowser/catalog/CatalogLister.java create mode 100755 src/com/bytezone/diskbrowser/catalog/DiskLister.java create mode 100755 src/com/bytezone/diskbrowser/catalog/DocumentCreatorFactory.java create mode 100755 src/com/bytezone/diskbrowser/catalog/TextCatalogCreator.java create mode 100755 src/com/bytezone/diskbrowser/catalog/TextDiskCreator.java create mode 100644 src/com/bytezone/diskbrowser/cpm/CPMDisk.java create mode 100755 src/com/bytezone/diskbrowser/disk/AbstractFormattedDisk.java create mode 100755 src/com/bytezone/diskbrowser/disk/AbstractSector.java create mode 100755 src/com/bytezone/diskbrowser/disk/AppleDisk.java create mode 100755 src/com/bytezone/diskbrowser/disk/AppleDiskAddress.java create mode 100755 src/com/bytezone/diskbrowser/disk/DataDisk.java create mode 100755 src/com/bytezone/diskbrowser/disk/DefaultAppleFileSource.java create mode 100755 src/com/bytezone/diskbrowser/disk/DefaultDataSource.java create mode 100755 src/com/bytezone/diskbrowser/disk/DefaultSector.java create mode 100755 src/com/bytezone/diskbrowser/disk/Disk.java create mode 100755 src/com/bytezone/diskbrowser/disk/DiskAddress.java create mode 100755 src/com/bytezone/diskbrowser/disk/DiskFactory.java create mode 100755 src/com/bytezone/diskbrowser/disk/DualDosDisk.java create mode 100755 src/com/bytezone/diskbrowser/disk/FormattedDisk.java create mode 100755 src/com/bytezone/diskbrowser/disk/SectorList.java create mode 100644 src/com/bytezone/diskbrowser/disk/SectorListConverter.java create mode 100755 src/com/bytezone/diskbrowser/disk/SectorType.java create mode 100644 src/com/bytezone/diskbrowser/dos/AbstractCatalogEntry.java create mode 100644 src/com/bytezone/diskbrowser/dos/CatalogEntry.java create mode 100644 src/com/bytezone/diskbrowser/dos/DeletedCatalogEntry.java create mode 100755 src/com/bytezone/diskbrowser/dos/DosCatalogSector.java create mode 100755 src/com/bytezone/diskbrowser/dos/DosDisk.java create mode 100755 src/com/bytezone/diskbrowser/dos/DosTSListSector.java create mode 100755 src/com/bytezone/diskbrowser/dos/DosVTOCSector.java create mode 100755 src/com/bytezone/diskbrowser/gui/AboutAction.java create mode 100755 src/com/bytezone/diskbrowser/gui/AbstractTab.java create mode 100755 src/com/bytezone/diskbrowser/gui/AppleDiskTab.java create mode 100755 src/com/bytezone/diskbrowser/gui/CatalogPanel.java create mode 100644 src/com/bytezone/diskbrowser/gui/CloseTabAction.java create mode 100644 src/com/bytezone/diskbrowser/gui/CreateDatabaseAction.java create mode 100755 src/com/bytezone/diskbrowser/gui/DataPanel.java create mode 100755 src/com/bytezone/diskbrowser/gui/DataSource.java create mode 100755 src/com/bytezone/diskbrowser/gui/DiskAndFileSelector.java create mode 100755 src/com/bytezone/diskbrowser/gui/DiskBrowser.java create mode 100644 src/com/bytezone/diskbrowser/gui/DiskDetails.java create mode 100644 src/com/bytezone/diskbrowser/gui/DiskLayoutImage.java create mode 100644 src/com/bytezone/diskbrowser/gui/DiskLayoutPanel.java create mode 100755 src/com/bytezone/diskbrowser/gui/DiskLayoutSelection.java create mode 100644 src/com/bytezone/diskbrowser/gui/DiskLegendPanel.java create mode 100755 src/com/bytezone/diskbrowser/gui/DiskSelectedEvent.java create mode 100755 src/com/bytezone/diskbrowser/gui/DiskSelectionListener.java create mode 100644 src/com/bytezone/diskbrowser/gui/DuplicateAction.java create mode 100755 src/com/bytezone/diskbrowser/gui/ExecuteDiskAction.java create mode 100644 src/com/bytezone/diskbrowser/gui/FileNodeSelectedEvent.java create mode 100644 src/com/bytezone/diskbrowser/gui/FileNodeSelectionListener.java create mode 100755 src/com/bytezone/diskbrowser/gui/FileSelectedEvent.java create mode 100755 src/com/bytezone/diskbrowser/gui/FileSelectionListener.java create mode 100755 src/com/bytezone/diskbrowser/gui/FileSystemTab.java create mode 100755 src/com/bytezone/diskbrowser/gui/HideCatalogAction.java create mode 100755 src/com/bytezone/diskbrowser/gui/HideLayoutAction.java create mode 100755 src/com/bytezone/diskbrowser/gui/InterleaveAction.java create mode 100755 src/com/bytezone/diskbrowser/gui/LineWrapAction.java create mode 100755 src/com/bytezone/diskbrowser/gui/MenuHandler.java create mode 100644 src/com/bytezone/diskbrowser/gui/NoDisksFoundException.java create mode 100755 src/com/bytezone/diskbrowser/gui/OpenFileAction.java create mode 100644 src/com/bytezone/diskbrowser/gui/PreferencesAction.java create mode 100644 src/com/bytezone/diskbrowser/gui/PreferencesDialog.java create mode 100755 src/com/bytezone/diskbrowser/gui/PrintAction.java create mode 100755 src/com/bytezone/diskbrowser/gui/PrintDocument.java create mode 100644 src/com/bytezone/diskbrowser/gui/RedoHandler.java create mode 100755 src/com/bytezone/diskbrowser/gui/RefreshTreeAction.java create mode 100755 src/com/bytezone/diskbrowser/gui/RootDirectoryAction.java create mode 100644 src/com/bytezone/diskbrowser/gui/ScrollRuler.java create mode 100755 src/com/bytezone/diskbrowser/gui/SectorSelectedEvent.java create mode 100755 src/com/bytezone/diskbrowser/gui/SectorSelectionListener.java create mode 100755 src/com/bytezone/diskbrowser/gui/ShowFreeSectorsAction.java create mode 100755 src/com/bytezone/diskbrowser/gui/Tab.java create mode 100755 src/com/bytezone/diskbrowser/gui/TreeBuilder.java create mode 100644 src/com/bytezone/diskbrowser/icons/Symbol-Left-32.png create mode 100644 src/com/bytezone/diskbrowser/icons/Symbol-Right-32.png create mode 100644 src/com/bytezone/diskbrowser/icons/arrow_refresh.png create mode 100644 src/com/bytezone/diskbrowser/icons/arrow_refresh_32.png create mode 100755 src/com/bytezone/diskbrowser/icons/disk.jpg create mode 100644 src/com/bytezone/diskbrowser/icons/disk.png create mode 100755 src/com/bytezone/diskbrowser/icons/disk2.png create mode 100644 src/com/bytezone/diskbrowser/icons/folder_explore_16.png create mode 100644 src/com/bytezone/diskbrowser/icons/folder_explore_32.png create mode 100644 src/com/bytezone/diskbrowser/icons/font_add_32.png create mode 100644 src/com/bytezone/diskbrowser/icons/font_delete_32.png create mode 100644 src/com/bytezone/diskbrowser/icons/information_16.png create mode 100644 src/com/bytezone/diskbrowser/icons/information_32.png create mode 100644 src/com/bytezone/diskbrowser/icons/printer_16.png create mode 100644 src/com/bytezone/diskbrowser/icons/printer_32.png create mode 100644 src/com/bytezone/diskbrowser/icons/save_delete_16.png create mode 100644 src/com/bytezone/diskbrowser/icons/save_delete_32.png create mode 100644 src/com/bytezone/diskbrowser/icons/script_gear_32.png create mode 100755 src/com/bytezone/diskbrowser/infocom/Abbreviations.java create mode 100644 src/com/bytezone/diskbrowser/infocom/AttributeManager.java create mode 100644 src/com/bytezone/diskbrowser/infocom/CodeManager.java create mode 100755 src/com/bytezone/diskbrowser/infocom/Dictionary.java create mode 100644 src/com/bytezone/diskbrowser/infocom/Globals.java create mode 100644 src/com/bytezone/diskbrowser/infocom/Grammar.java create mode 100755 src/com/bytezone/diskbrowser/infocom/Header.java create mode 100755 src/com/bytezone/diskbrowser/infocom/InfocomDisk.java create mode 100755 src/com/bytezone/diskbrowser/infocom/Instruction.java create mode 100644 src/com/bytezone/diskbrowser/infocom/ObjectAnalyser.java create mode 100755 src/com/bytezone/diskbrowser/infocom/ObjectManager.java create mode 100644 src/com/bytezone/diskbrowser/infocom/PropertyManager.java create mode 100644 src/com/bytezone/diskbrowser/infocom/PropertyTester.java create mode 100644 src/com/bytezone/diskbrowser/infocom/Routine.java create mode 100644 src/com/bytezone/diskbrowser/infocom/StringManager.java create mode 100755 src/com/bytezone/diskbrowser/infocom/ZObject.java create mode 100755 src/com/bytezone/diskbrowser/infocom/ZString.java create mode 100644 src/com/bytezone/diskbrowser/pascal/PascalCatalogSector.java create mode 100755 src/com/bytezone/diskbrowser/pascal/PascalDisk.java create mode 100755 src/com/bytezone/diskbrowser/prodos/CatalogEntry.java create mode 100755 src/com/bytezone/diskbrowser/prodos/DirectoryHeader.java create mode 100755 src/com/bytezone/diskbrowser/prodos/FileEntry.java create mode 100755 src/com/bytezone/diskbrowser/prodos/ProdosBitMapSector.java create mode 100755 src/com/bytezone/diskbrowser/prodos/ProdosCatalogSector.java create mode 100755 src/com/bytezone/diskbrowser/prodos/ProdosConstants.java create mode 100755 src/com/bytezone/diskbrowser/prodos/ProdosDirectory.java create mode 100755 src/com/bytezone/diskbrowser/prodos/ProdosDisk.java create mode 100755 src/com/bytezone/diskbrowser/prodos/ProdosExtendedKeySector.java create mode 100755 src/com/bytezone/diskbrowser/prodos/ProdosIndexSector.java create mode 100755 src/com/bytezone/diskbrowser/prodos/SubDirectoryHeader.java create mode 100755 src/com/bytezone/diskbrowser/prodos/VolumeDirectoryHeader.java create mode 100755 src/com/bytezone/diskbrowser/wizardry/AbstractImage.java create mode 100755 src/com/bytezone/diskbrowser/wizardry/Character.java create mode 100755 src/com/bytezone/diskbrowser/wizardry/CodedMessage.java create mode 100755 src/com/bytezone/diskbrowser/wizardry/Dice.java create mode 100755 src/com/bytezone/diskbrowser/wizardry/ExperienceLevel.java create mode 100755 src/com/bytezone/diskbrowser/wizardry/Header.java create mode 100755 src/com/bytezone/diskbrowser/wizardry/Image.java create mode 100755 src/com/bytezone/diskbrowser/wizardry/ImageV2.java create mode 100755 src/com/bytezone/diskbrowser/wizardry/Item.java create mode 100755 src/com/bytezone/diskbrowser/wizardry/MazeAddress.java create mode 100755 src/com/bytezone/diskbrowser/wizardry/MazeCell.java create mode 100755 src/com/bytezone/diskbrowser/wizardry/MazeLevel.java create mode 100755 src/com/bytezone/diskbrowser/wizardry/Message.java create mode 100755 src/com/bytezone/diskbrowser/wizardry/Monster.java create mode 100755 src/com/bytezone/diskbrowser/wizardry/PlainMessage.java create mode 100755 src/com/bytezone/diskbrowser/wizardry/Reward.java create mode 100755 src/com/bytezone/diskbrowser/wizardry/Spell.java create mode 100755 src/com/bytezone/diskbrowser/wizardry/WizardryScenarioDisk.java diff --git a/resources/buildNumber b/resources/buildNumber new file mode 100755 index 0000000..02c5bd2 --- /dev/null +++ b/resources/buildNumber @@ -0,0 +1,3 @@ +#Build Number for ANT. Do not edit! +#Mon Jun 01 19:29:14 AEST 2015 +build.number=627 diff --git a/src/com/bytezone/diskbrowser/DateTime.java b/src/com/bytezone/diskbrowser/DateTime.java new file mode 100644 index 0000000..7705a0c --- /dev/null +++ b/src/com/bytezone/diskbrowser/DateTime.java @@ -0,0 +1,42 @@ +package com.bytezone.diskbrowser; + +class DateTime +{ + private static String[] months = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", + "Sep", "Oct", "Nov", "Dec" }; + private static String[] days = { "", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", + "Friday", "Saturday" }; + + private final int second; + private final int minute; + private final int hour; + private final int year; + private final int day; + private final int month; + private final int weekDay; + + public DateTime (byte[] buffer, int ptr) + { + second = buffer[ptr] & 0xFF; + minute = buffer[++ptr] & 0xFF; + hour = buffer[++ptr] & 0xFF; + year = buffer[++ptr] & 0xFF; + day = buffer[++ptr] & 0xFF; + month = buffer[++ptr] & 0xFF; + ++ptr; // empty + weekDay = buffer[++ptr] & 0xFF; + } + + public String format () + { + return String.format ("%02d:%02d:%02d %s %d %s %d", hour, minute, second, days[weekDay], + day, months[month], year); + } + + @Override + public String toString () + { + return "DateTime [second=" + second + ", minute=" + minute + ", hour=" + hour + ", year=" + + year + ", day=" + day + ", month=" + month + ", weekDay=" + weekDay + "]"; + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/FileFormatException.java b/src/com/bytezone/diskbrowser/FileFormatException.java new file mode 100644 index 0000000..59b6a56 --- /dev/null +++ b/src/com/bytezone/diskbrowser/FileFormatException.java @@ -0,0 +1,17 @@ +package com.bytezone.diskbrowser; + +public class FileFormatException extends RuntimeException +{ + String message; + + public FileFormatException (String string) + { + this.message = string; + } + + @Override + public String toString () + { + return message; + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/HexFormatter.java b/src/com/bytezone/diskbrowser/HexFormatter.java new file mode 100755 index 0000000..7986fdf --- /dev/null +++ b/src/com/bytezone/diskbrowser/HexFormatter.java @@ -0,0 +1,439 @@ +package com.bytezone.diskbrowser; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.util.GregorianCalendar; + +public class HexFormatter +{ + private static String[] hex = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", + "C", "D", "E", "F" }; + private static MathContext mathContext = new MathContext (9); + + public static String format (byte[] buffer) + { + return format (buffer, 0, buffer.length); + } + + public static String format (byte[] buffer, int offset, int length) + { + return format (buffer, offset, length, true, 0); + } + + public static String format (byte[] buffer, int offset, int length, int startingAddress) + { + return format (buffer, offset, length, true, startingAddress); + } + + public static String formatNoHeader (byte[] buffer, int offset, int length) + { + return format (buffer, offset, length, false, 0); + } + + public static String formatNoHeader (byte[] buffer, int offset, int length, + int startingAddress) + { + return format (buffer, offset, length, false, startingAddress); + } + + public static String format (byte[] buffer, int offset, int length, boolean header, + int startingAddress) + { + StringBuffer line = new StringBuffer (); + int[] freq = new int[256]; + + if (header) + { + line.append (" "); + for (int i = 0; i < 16; i++) + line.append (" " + hex[i]); + if (offset == 0) + line.append ("\n"); + } + + for (int i = offset; i < offset + length; i += 16) + { + if (line.length () > 0 && i > 0) + line.append ("\n"); + + // print offset + for (int temp = i + startingAddress, max = 65536; max > 0; max /= 16) + { + if (temp >= max) + { + line.append (hex[temp / max]); + temp %= max; + } + else + line.append ("0"); + } + + // print hex values + line.append (" : "); + StringBuffer trans = new StringBuffer (); + StringBuffer hexLine = new StringBuffer (); + + for (int j = i; (j < i + 16) && (j < offset + length); j++) + { + int c = buffer[j] & 0xFF; + freq[c]++; + hexLine.append (String.format ("%02X ", c)); + + if (c > 127) + { + if (c < 160) + c -= 64; + else + c -= 128; + } + if (c < 32 || c == 127) // non-printable + trans.append ("."); + else + // standard ascii + trans.append ((char) c); + } + while (hexLine.length () < 48) + hexLine.append (" "); + + line.append (hexLine.toString () + ": " + trans.toString ()); + } + + if (false) + { + line.append ("\n\n"); + int totalBits = 0; + for (int i = 0; i < freq.length; i++) + if (freq[i] > 0) + { + totalBits += (Integer.bitCount (i) * freq[i]); + line.append (String.format ("%02X %3d %d%n", i, freq[i], Integer.bitCount (i))); + } + line.append (String.format ("%nTotal bits : %d%n", totalBits)); + } + return line.toString (); + } + + public static String sanitiseString (byte[] buffer, int offset, int length) + { + StringBuilder trans = new StringBuilder (); + for (int j = offset; j < offset + length; j++) + { + int c = buffer[j] & 0xFF; + + if (c > 127) + { + if (c < 160) + c -= 64; + else + c -= 128; + } + + if (c < 32 || c == 127) // non-printable + trans.append ("."); + else + trans.append ((char) c); // standard ascii + } + return trans.toString (); + } + + public static String getString (byte[] buffer) + { + return getString (buffer, 0, buffer.length); + } + + public static String getString (byte[] buffer, int offset, int length) + { + StringBuffer text = new StringBuffer (); + + for (int i = offset; i < offset + length; i++) + { + int c = intValue (buffer[i]); + if (c > 127) + { + if (c < 160) + c -= 64; + else + c -= 128; + } + if (c == 13) + text.append ("\n"); + else if (c < 32) // non-printable + text.append ("."); + else + // standard ascii + text.append ((char) c); + } + return text.toString (); + } + + public static String getString2 (byte[] buffer, int offset, int length) + { + StringBuffer text = new StringBuffer (); + + for (int i = offset; i < offset + length; i++) + { + int c = intValue (buffer[i]); + if (c == 136 && text.length () > 0) + { + System.out.println (text.toString ()); + text.deleteCharAt (text.length () - 1); + System.out.println ("deleted"); + continue; + } + if (c > 127) + { + if (c < 160) + c -= 64; + else + c -= 128; + } + if (c < 32) // non-printable + text.append ((char) (c + 64)); + else + // standard ascii + text.append ((char) c); + } + return text.toString (); + } + + public static String getHexString (byte[] buffer, int offset, int length) + { + return getHexString (buffer, offset, length, true); + } + + public static String getHexString (byte[] buffer) + { + return getHexString (buffer, 0, buffer.length); + } + + public static String getHexString (byte[] buffer, int offset, int length, boolean space) + { + StringBuilder hex = new StringBuilder (); + for (int i = 0; i < length; i++) + { + hex.append (String.format ("%02X", buffer[offset + i])); + if (space) + hex.append (' '); + } + if (length > 0 && space) + hex.deleteCharAt (hex.length () - 1); + return hex.toString (); + } + + public static String getHexStringReversed (byte[] buffer, int offset, int length, + boolean space) + { + StringBuilder hex = new StringBuilder (); + for (int i = length - 1; i >= 0; i--) + { + hex.append (String.format ("%02X", buffer[offset + i])); + if (space) + hex.append (' '); + } + if (length > 0 && space) + hex.deleteCharAt (hex.length () - 1); + return hex.toString (); + } + + public static char byteValue (byte b) + { + int c = intValue (b); + if (c > 127) + c -= 128; + if (c > 95) + c -= 64; + if (c < 32) // non-printable + return '.'; + return (char) c; // standard ascii + + } + + public static String format4 (int value) + { + if (value < 0) + return "***err**"; + StringBuffer text = new StringBuffer (); + for (int i = 0, weight = 4096; i < 4; i++) + { + int digit = value / weight; + if (digit < 0 || digit > 15) + return "***err**"; + text.append (hex[digit]); + value %= weight; + weight /= 16; + } + return text.toString (); + } + + public static String format3 (int value) + { + return format4 (value).substring (1); + } + + public static String format2 (int value) + { + if (value < 0) + value += 256; + String text = hex[value / 16] + hex[value % 16]; + return text; + } + + public static String format1 (int value) + { + String text = hex[value]; + return text; + } + + public static int intValue (byte b1) + { + // int i1 = b1; + // if (i1 < 0) + // i1 += 256; + + // return i1; + return b1 & 0xFF; + } + + public static int intValue (byte b1, byte b2) + { + return intValue (b1) + intValue (b2) * 256; + } + + public static int intValue (byte b1, byte b2, byte b3) + { + return intValue (b1) + intValue (b2) * 256 + intValue (b3) * 65536; + } + + public static int getLong (byte[] buffer, int ptr) + { + int val = 0; + for (int i = 3; i >= 0; i--) + { + val <<= 8; + val += buffer[ptr + i] & 0xFF; + } + return val; + } + + public static double getSANEDouble (byte[] buffer, int offset) + { + long bits = 0; + for (int i = 7; i >= 0; i--) + { + bits <<= 8; + bits |= buffer[offset + i] & 0xFF; + } + + return Double.longBitsToDouble (bits); + } + + public static int getWord (byte[] buffer, int ptr) + { + int val = 0; + for (int i = 1; i >= 0; i--) + { + val <<= 8; + val += buffer[ptr + i] & 0xFF; + } + return val; + } + + public static int getSignedWord (byte b1, byte b2) + { + return (b2 << 8) | (b1 & 0xFF); + } + + // public static int getSignedWord (byte[] buffer, int ptr) + // { + // short val = buffer[ptr] << 8; + // return buffer[ptr] * 256 + buffer[ptr + 1]; + // } + + public static double floatValueOld (byte[] buffer, int offset) + { + double val = 0; + + int exponent = HexFormatter.intValue (buffer[offset]) - 0x80; + + int mantissa = + (buffer[offset + 1] & 0x7F) * 0x1000000 + intValue (buffer[offset + 2]) * 0x10000 + + intValue (buffer[offset + 3]) * 0x100 + intValue (buffer[offset + 4]); + + int weight1 = 1; + long weight2 = 2147483648L; + double multiplier = 0; + + for (int i = 0; i < 32; i++) + { + if ((mantissa & weight2) > 0) + multiplier += (1.0 / weight1); + weight2 /= 2; + weight1 *= 2; + } + val = Math.pow (2, exponent - 1) * (multiplier + 1); + + return val; + } + + public static double floatValue (byte[] buffer, int ptr) + { + int exponent = buffer[ptr] & 0x7F; // biased 128 + if (exponent == 0) + return 0.0; + + int mantissa = + (buffer[ptr + 1] & 0x7F) << 24 | (buffer[ptr + 2] & 0xFF) << 16 + | (buffer[ptr + 3] & 0xFF) << 8 | (buffer[ptr + 4] & 0xFF); + boolean negative = (buffer[ptr + 1] & 0x80) > 0; + double value = 0.5; + for (int i = 2, weight = 0x40000000; i <= 32; i++, weight >>>= 1) + if ((mantissa & weight) > 0) + value += Math.pow (0.5, i); + value *= Math.pow (2, exponent); + BigDecimal bd = new BigDecimal (value); + double rounded = bd.round (mathContext).doubleValue (); + return negative ? rounded * -1 : rounded; + } + + public static GregorianCalendar getAppleDate (byte[] buffer, int offset) + { + int date = HexFormatter.intValue (buffer[offset], buffer[offset + 1]); + if (date > 0) + { + int year = (date & 0xFE00) >> 9; + int month = (date & 0x01E0) >> 5; + int day = date & 0x001F; + int hour = HexFormatter.intValue (buffer[offset + 3]) & 0x1F; + int minute = HexFormatter.intValue (buffer[offset + 2]) & 0x3F; + if (year < 70) + year += 2000; + else + year += 1900; + return new GregorianCalendar (year, month - 1, day, hour, minute); + } + return null; + } + + public static GregorianCalendar getPascalDate (byte[] buffer, int offset) + { + int year = intValue (buffer[offset + 1]); + int day = (buffer[offset] & 0xF0) >> 4; + int month = buffer[offset] & 0x0F; + if (day == 0 || month == 0) + return null; + if (year % 2 > 0) + day += 16; + year /= 2; + if (year < 70) + year += 2000; + else + year += 1900; + return new GregorianCalendar (year, month - 1, day); + } + + public static String getPascalString (byte[] buffer, int offset) + { + int length = HexFormatter.intValue (buffer[offset]); + return HexFormatter.getString (buffer, offset + 1, length); + } +} diff --git a/src/com/bytezone/diskbrowser/LZW.java b/src/com/bytezone/diskbrowser/LZW.java new file mode 100644 index 0000000..b65d1dd --- /dev/null +++ b/src/com/bytezone/diskbrowser/LZW.java @@ -0,0 +1,113 @@ +package com.bytezone.diskbrowser; + +import java.util.ArrayList; +import java.util.List; + +class LZW +{ + static protected final String[] st = new String[0x1000]; + static protected final int TRACK_LENGTH = 0x1000; + + protected final List chunks = new ArrayList (); + protected int volume; + protected byte runLengthChar; + protected int crc; + protected int crcBase; + + private int buffer; // one character buffer + private int bitsLeft; // unused bits left in buffer + + private int ptr; + private int startPtr; + protected byte[] bytes; + + static + { + for (int i = 0; i < 256; i++) + st[i] = "" + (char) i; + } + + public void setBuffer (byte[] buffer, int ptr) + { + bytes = buffer; + startPtr = this.ptr = ptr; + bitsLeft = 0; + } + + public int bytesRead () + { + return ptr - startPtr; + } + + private void fillBuffer () + { + buffer = bytes[ptr++] & 0xFF; + bitsLeft = 8; + } + + private boolean readBoolean () + { + if (bitsLeft == 0) + fillBuffer (); + + bitsLeft--; + boolean bit = ((buffer << bitsLeft) & 0x80) == 0x80; + return bit; + } + + protected int readInt (int width) + { + if (width < 8 || width > 12) + throw new RuntimeException ("Illegal value of r = " + width); + + int x = 0; + for (int i = 0, weight = 1; i < width; i++, weight <<= 1) + if (readBoolean ()) + x |= weight; + + return x; + } + + protected byte[] undoRLE (byte[] inBuffer, int inPtr, int length) + { + byte[] outBuffer = new byte[TRACK_LENGTH]; + int outPtr = 0; + int max = inPtr + length; + + while (inPtr < max) + { + byte b = inBuffer[inPtr++]; + if (b == runLengthChar) + { + b = inBuffer[inPtr++]; + int rpt = inBuffer[inPtr++] & 0xFF; + while (rpt-- >= 0) + outBuffer[outPtr++] = b; + } + else + outBuffer[outPtr++] = b; + } + + assert outPtr == TRACK_LENGTH; + return outBuffer; + } + + public byte[] getData () + { + byte[] buffer = new byte[chunks.size () * TRACK_LENGTH]; + int trackNumber = 0; + + for (byte[] track : chunks) + System.arraycopy (track, 0, buffer, trackNumber++ * TRACK_LENGTH, TRACK_LENGTH); + + if (crc != NuFX.getCRC (buffer, crcBase)) + System.out.println ("\n*** LZW CRC mismatch ***"); + + return buffer; + } + + protected int width (int maximumValue) + { + return 32 - Integer.numberOfLeadingZeros (maximumValue); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/LZW1.java b/src/com/bytezone/diskbrowser/LZW1.java new file mode 100644 index 0000000..aa8e1d5 --- /dev/null +++ b/src/com/bytezone/diskbrowser/LZW1.java @@ -0,0 +1,88 @@ +package com.bytezone.diskbrowser; + +import java.util.Objects; + +import com.bytezone.common.Utility; + +class LZW1 extends LZW +{ + public LZW1 (byte[] buffer) + { + bytes = Objects.requireNonNull (buffer); + + crc = Utility.getWord (buffer, 0); + crcBase = 0; + + volume = buffer[2] & 0xFF; + runLengthChar = (byte) (buffer[3] & 0xFF); + int ptr = 4; + + while (ptr < buffer.length - 1) // what is in the last byte? + { + int rleLength = Utility.getWord (buffer, ptr); + int lzwPerformed = buffer[ptr + 2] & 0xFF; + ptr += 3; + + if (lzwPerformed != 0) + { + setBuffer (buffer, ptr); // prepare to read n-bit integers + byte[] lzwBuffer = undoLZW (rleLength); + + if (rleLength == TRACK_LENGTH) // no run length encoding + chunks.add (lzwBuffer); + else + chunks.add (undoRLE (lzwBuffer, 0, lzwBuffer.length)); + + ptr += bytesRead (); // since the setBuffer() + } + else + { + if (rleLength == TRACK_LENGTH) // no run length encoding + { + byte[] originalBuffer = new byte[TRACK_LENGTH]; + System.arraycopy (buffer, ptr, originalBuffer, 0, originalBuffer.length); + chunks.add (originalBuffer); + } + else + chunks.add (undoRLE (buffer, ptr, rleLength)); + + ptr += rleLength; + } + } + } + + protected byte[] undoLZW (int rleLength) + { + byte[] lzwBuffer = new byte[rleLength]; // must fill this array from input + int ptr = 0; + int nextEntry = 0x100; // always start with a fresh table + String prev = ""; + + while (ptr < rleLength) + { + int codeWord = readInt (width (nextEntry + 1)); + String s = (nextEntry == codeWord) ? prev + prev.charAt (0) : st[codeWord]; + + if (nextEntry < st.length) + st[nextEntry++] = prev + s.charAt (0); + + for (int i = 0; i < s.length (); i++) + lzwBuffer[ptr++] = (byte) s.charAt (i); + + prev = s; + } + return lzwBuffer; + } + + @Override + public String toString () + { + StringBuilder text = new StringBuilder (); + + text.append (String.format (" crc ............... %,d (%04X)%n", crc, crc)); + text.append (String.format (" volume ............ %,d%n", volume)); + text.append (String.format (" RLE char .......... $%02X", runLengthChar)); + + return text.toString (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/LZW2.java b/src/com/bytezone/diskbrowser/LZW2.java new file mode 100644 index 0000000..5c0e3e5 --- /dev/null +++ b/src/com/bytezone/diskbrowser/LZW2.java @@ -0,0 +1,112 @@ +package com.bytezone.diskbrowser; + +import java.util.Objects; + +import com.bytezone.common.Utility; + +class LZW2 extends LZW +{ + private int nextEntry = 0x100; + private String prev = ""; + private int codeWord; + + public LZW2 (byte[] buffer, int crc) + { + bytes = Objects.requireNonNull (buffer); + + this.crc = crc; + crcBase = 0xFFFF; + codeWord = 0; + + volume = buffer[0] & 0xFF; + runLengthChar = (byte) (buffer[1] & 0xFF); + int ptr = 2; + + while (ptr < buffer.length - 1) // what is in the last byte? + { + int rleLength = Utility.getWord (buffer, ptr); + boolean lzwPerformed = (rleLength & 0x8000) != 0; + ptr += 2; + + if (lzwPerformed) + { + rleLength &= 0x0FFF; // remove the LZW flag + if (rleLength == 0) + rleLength = TRACK_LENGTH; + + int chunkLength = Utility.getWord (buffer, ptr); + ptr += 2; + + setBuffer (buffer, ptr); // prepare to read n-bit integers + byte[] lzwBuffer = undoLZW (rleLength); + + assert (chunkLength - 4) == bytesRead (); + + if (rleLength == TRACK_LENGTH) // no run length encoding + chunks.add (lzwBuffer); + else + chunks.add (undoRLE (lzwBuffer, 0, lzwBuffer.length)); + + ptr += bytesRead (); // since the setBuffer() + } + else + { + nextEntry = 0x100; + if (rleLength == 0) + rleLength = TRACK_LENGTH; + + if (rleLength == TRACK_LENGTH) // no run length encoding + { + byte[] originalBuffer = new byte[TRACK_LENGTH]; + System.arraycopy (buffer, ptr, originalBuffer, 0, originalBuffer.length); + chunks.add (originalBuffer); + } + else + chunks.add (undoRLE (buffer, ptr, rleLength)); + + ptr += rleLength; + } + } + } + + protected byte[] undoLZW (int rleLength) + { + byte[] lzwBuffer = new byte[rleLength]; // must fill this array from buffer + int ptr = 0; + + while (ptr < rleLength) + { + codeWord = readInt (width (nextEntry + 1)); + + if (codeWord == 0x100) // clear the table + { + nextEntry = 0x100; + codeWord = readInt (9); + prev = ""; + } + + String s = (nextEntry == codeWord) ? prev + prev.charAt (0) : st[codeWord]; + + if (nextEntry < st.length) + st[nextEntry++] = prev + s.charAt (0); + + for (int i = 0; i < s.length (); i++) + lzwBuffer[ptr++] = (byte) s.charAt (i); + + prev = s; + } + + return lzwBuffer; + } + + @Override + public String toString () + { + StringBuilder text = new StringBuilder (); + + text.append (String.format (" volume ............ %,d%n", volume)); + text.append (String.format (" RLE char .......... $%02X", runLengthChar)); + + return text.toString (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/NuFX.java b/src/com/bytezone/diskbrowser/NuFX.java new file mode 100644 index 0000000..cfc6aa8 --- /dev/null +++ b/src/com/bytezone/diskbrowser/NuFX.java @@ -0,0 +1,268 @@ +package com.bytezone.diskbrowser; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import com.bytezone.common.Utility; + +public class NuFX +{ + private static String[] fileSystems = {// + "", "ProDOS/SOS", "DOS 3.3", "DOS 3.2", "Apple II Pascal", "Macintosh HFS", + "Macintosh MFS", "Lisa File System", "Apple CP/M", "", "MS-DOS", "High Sierra", + "ISO 9660", "AppleShare" }; + private final Header header; + private final byte[] buffer; + private final boolean debug = false; + + private final List records = new ArrayList (); + private final List threads = new ArrayList (); + + public NuFX (Path path) throws FileFormatException, IOException + { + buffer = Files.readAllBytes (path); + header = new Header (buffer); + + int dataPtr = 48; + if (header.bin2) + dataPtr += 128; + + if (debug) + System.out.printf ("%s%n%n", header); + + for (int rec = 0; rec < header.totalRecords; rec++) + { + Record record = new Record (dataPtr); + records.add (record); + + if (debug) + System.out.printf ("Record: %d%n%n%s%n%n", rec, record); + + dataPtr += record.attributes + record.fileNameLength; + int threadsPtr = dataPtr; + dataPtr += record.totThreads * 16; + + for (int i = 0; i < record.totThreads; i++) + { + Thread thread = new Thread (buffer, threadsPtr + i * 16, dataPtr); + threads.add (thread); + dataPtr += thread.getCompressedEOF (); + + if (debug) + System.out.printf ("Thread: %d%n%n%s%n%n", i, thread); + } + } + } + + public byte[] getBuffer () + { + for (Thread thread : threads) + if (thread.hasDisk ()) + return thread.getData (); + return null; + } + + @Override + public String toString () + { + for (Thread thread : threads) + if (thread.hasDisk ()) + return thread.toString (); + return "no disk"; + } + + protected static int getCRC (final byte[] buffer, int base) + { + int crc = base; + for (int j = 0; j < buffer.length; j++) + { + crc = ((crc >>> 8) | (crc << 8)) & 0xFFFF; + crc ^= (buffer[j] & 0xFF); + crc ^= ((crc & 0xFF) >> 4); + crc ^= (crc << 12) & 0xFFFF; + crc ^= ((crc & 0xFF) << 5) & 0xFFFF; + } + + crc &= 0xFFFF; + return crc; + } + + class Header + { + private final int totalRecords; + private final int version; + private final int eof; + private final int crc; + private final DateTime created; + private final DateTime modified; + boolean bin2; + + public Header (byte[] buffer) throws FileFormatException + { + int ptr = 0; + + while (true) + { + if (isNuFile (buffer, ptr)) + break; + + if (isBin2 (buffer, ptr)) + { + ptr += 128; + bin2 = true; + continue; + } + + throw new FileFormatException ("NuFile not found"); + } + + crc = Utility.getWord (buffer, ptr + 6); + totalRecords = Utility.getLong (buffer, ptr + 8); + created = new DateTime (buffer, ptr + 12); + modified = new DateTime (buffer, ptr + 20); + version = Utility.getWord (buffer, ptr + 28); + eof = Utility.getLong (buffer, ptr + 38); + + byte[] crcBuffer = new byte[40]; + System.arraycopy (buffer, ptr + 8, crcBuffer, 0, crcBuffer.length); + if (crc != getCRC (crcBuffer, 0)) + { + System.out.println ("***** Master CRC mismatch *****"); + throw new FileFormatException ("Master CRC failed"); + } + } + + private boolean isNuFile (byte[] buffer, int ptr) + { + if (buffer[ptr] == 0x4E && buffer[ptr + 1] == (byte) 0xF5 && buffer[ptr + 2] == 0x46 + && buffer[ptr + 3] == (byte) 0xE9 && buffer[ptr + 4] == 0x6C + && buffer[ptr + 5] == (byte) 0xE5) + return true; + return false; + } + + private boolean isBin2 (byte[] buffer, int ptr) + { + if (buffer[ptr] == 0x0A && buffer[ptr + 1] == 0x47 && buffer[ptr + 2] == 0x4C + && buffer[ptr + 18] == (byte) 0x02) + return true; + return false; + } + + @Override + public String toString () + { + StringBuilder text = new StringBuilder (); + + text.append (String.format ("Master CRC ..... %,d (%04X)%n", crc, crc)); + text.append (String.format ("Records ........ %,d%n", totalRecords)); + text.append (String.format ("Created ........ %s%n", created.format ())); + text.append (String.format ("Modified ....... %s%n", modified.format ())); + text.append (String.format ("Version ........ %,d%n", version)); + text.append (String.format ("Master EOF ..... %,d", eof)); + + return text.toString (); + } + } + + class Record + { + private final int totThreads; + private final int crc; + private final char separator; + private final int fileSystemID; + private final int attributes; + private final int version; + private final int access; + private final int fileType; + private final int auxType; + private final int storType; + private final DateTime created; + private final DateTime modified; + private final DateTime archived; + private final int optionSize; + private final int fileNameLength; + private final String fileName; + + public Record (int dataPtr) throws FileFormatException + { + // check for NuFX + if (!isNuFX (buffer, dataPtr)) + throw new FileFormatException ("NuFX not found"); + + crc = Utility.getWord (buffer, dataPtr + 4); + attributes = Utility.getWord (buffer, dataPtr + 6); + version = Utility.getWord (buffer, dataPtr + 8); + totThreads = Utility.getLong (buffer, dataPtr + 10); + fileSystemID = Utility.getWord (buffer, dataPtr + 14); + separator = (char) (buffer[dataPtr + 16] & 0x00FF); + access = Utility.getLong (buffer, dataPtr + 18); + fileType = Utility.getLong (buffer, dataPtr + 22); + auxType = Utility.getLong (buffer, dataPtr + 26); + storType = Utility.getWord (buffer, dataPtr + 30); + created = new DateTime (buffer, dataPtr + 32); + modified = new DateTime (buffer, dataPtr + 40); + archived = new DateTime (buffer, dataPtr + 48); + optionSize = Utility.getWord (buffer, dataPtr + 56); + fileNameLength = Utility.getWord (buffer, dataPtr + attributes - 2); + + int len = attributes + fileNameLength - 6; + byte[] crcBuffer = new byte[len + totThreads * 16]; + System.arraycopy (buffer, dataPtr + 6, crcBuffer, 0, crcBuffer.length); + + if (crc != getCRC (crcBuffer, 0)) + { + System.out.println ("***** Header CRC mismatch *****"); + throw new FileFormatException ("Header CRC failed"); + } + + if (fileNameLength > 0) + { + int start = dataPtr + attributes; + int end = start + fileNameLength; + for (int i = start; i < end; i++) + buffer[i] &= 0x7F; + fileName = new String (buffer, start, fileNameLength); + } + else + fileName = ""; + } + + private boolean isNuFX (byte[] buffer, int ptr) + { + if (buffer[ptr] == 0x4E && buffer[ptr + 1] == (byte) 0xF5 && buffer[ptr + 2] == 0x46 + && buffer[ptr + 3] == (byte) 0xD8) + return true; + return false; + } + + @Override + public String toString () + { + StringBuilder text = new StringBuilder (); + + text.append (String.format ("Header CRC ..... %,d (%04X)%n", crc, crc)); + text.append (String.format ("Attributes ..... %d%n", attributes)); + text.append (String.format ("Version ........ %d%n", version)); + text.append (String.format ("Threads ........ %d%n", totThreads)); + text.append (String.format ("File sys id .... %d (%s)%n", fileSystemID, + fileSystems[fileSystemID])); + text.append (String.format ("Separator ...... %s%n", separator)); + text.append (String.format ("Access ......... %,d%n", access)); + text.append (String.format ("File type ...... %,d%n", fileType)); + text.append (String.format ("Aux type ....... %,d%n", auxType)); + text.append (String.format ("Stor type ...... %,d%n", storType)); + text.append (String.format ("Created ........ %s%n", created.format ())); + text.append (String.format ("Modified ....... %s%n", modified.format ())); + text.append (String.format ("Archived ....... %s%n", archived.format ())); + text.append (String.format ("Option size .... %,d%n", optionSize)); + text.append (String.format ("Filename len ... %,d%n", fileNameLength)); + text.append (String.format ("Filename ....... %s", fileName)); + + return text.toString (); + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/Thread.java b/src/com/bytezone/diskbrowser/Thread.java new file mode 100644 index 0000000..2788038 --- /dev/null +++ b/src/com/bytezone/diskbrowser/Thread.java @@ -0,0 +1,136 @@ +package com.bytezone.diskbrowser; + +import com.bytezone.common.Utility; + +class Thread +{ + private static String[] threadClassText = { "Message", "Control", "Data", "Filename" }; + private static String[] formatText = { // + "Uncompressed", "Huffman squeeze", "LZW/1", "LZW/2", "Unix 12-bit Compress", + "Unix 16-bit Compress" }; + private static String[][] threadKindText = {// + { "ASCII text", "predefined EOF", "IIgs icon" }, + { "create directory", "undefined", "undefined" }, + { "data fork", "disk image", "resource fork" }, + { "filename", "undefined", "undefined" } }; + + private final ThreadHeader header; + private final byte[] data; + private String filename; + private String message; + private LZW lzw; + + public Thread (byte[] buffer, int offset, int dataOffset) + { + header = new ThreadHeader (buffer, offset); + + data = new byte[header.compressedEOF]; + System.arraycopy (buffer, dataOffset, data, 0, data.length); + + switch (header.threadClass) + { + case 0: + if (header.threadKind == 1) + message = new String (data, 0, header.uncompressedEOF); + break; + + case 1: + break; + + case 2: + if (header.threadKind == 1) + { + if (header.format == 2) + lzw = new LZW1 (data); + else if (header.format == 3) + lzw = new LZW2 (data, header.crc); + else if (header.format == 1) + { + // Huffman Squeeze + System.out.println ("Huffman Squeeze format - not written yet"); + } + } + break; + + case 3: + if (header.threadKind == 0) + filename = new String (data, 0, header.uncompressedEOF); + break; + + default: + System.out.println ("Unknown threadClass: " + header.threadClass); + } + } + + public byte[] getData () + { + return hasDisk () ? lzw.getData () : null; + } + + int getCompressedEOF () + { + return header.compressedEOF; + } + + public boolean hasDisk () + { + return lzw != null; + } + + @Override + public String toString () + { + StringBuilder text = new StringBuilder (header.toString ()); + + if (filename != null) + text.append ("\n filename .......... " + filename); + else if (message != null) + text.append ("\n message ........... " + message); + else if (lzw != null) + { + text.append ("\n"); + text.append (lzw); + } + + return text.toString (); + } + + class ThreadHeader + { + private final int threadClass; + private final int format; + private final int threadKind; + private final int crc; + private final int uncompressedEOF; + private final int compressedEOF; + + public ThreadHeader (byte[] buffer, int offset) + { + threadClass = Utility.getWord (buffer, offset); + format = Utility.getWord (buffer, offset + 2); + threadKind = Utility.getWord (buffer, offset + 4); + crc = Utility.getWord (buffer, offset + 6); + uncompressedEOF = Utility.getLong (buffer, offset + 8); + compressedEOF = Utility.getLong (buffer, offset + 12); + // System.out.println (Utility.toHex (buffer, offset, 16)); + } + + @Override + public String toString () + { + StringBuilder text = new StringBuilder (); + + text.append (String.format (" threadClass ....... %d %s%n", threadClass, + threadClassText[threadClass])); + text.append (String + .format (" format ............ %d %s%n", format, formatText[format])); + text.append (String.format (" kind .............. %d %s%n", threadKind, + threadKindText[threadClass][threadKind])); + text.append (String.format (" crc ............... %,d%n", crc)); + text.append (String.format (" uncompressedEOF ... %,d%n", uncompressedEOF)); + text.append (String.format (" compressedEOF ..... %,d (%08X)", compressedEOF, + compressedEOF)); + return text.toString (); + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/applefile/AbstractFile.java b/src/com/bytezone/diskbrowser/applefile/AbstractFile.java new file mode 100755 index 0000000..ff002b7 --- /dev/null +++ b/src/com/bytezone/diskbrowser/applefile/AbstractFile.java @@ -0,0 +1,95 @@ +package com.bytezone.diskbrowser.applefile; + +import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.JComponent; +import javax.swing.JPanel; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.gui.DataSource; + +public abstract class AbstractFile implements DataSource +{ + public String name; + public byte[] buffer; + AssemblerProgram assembler; + protected BufferedImage image; + protected List hexBlocks = new ArrayList (); + + public AbstractFile (String name, byte[] buffer) + { + this.name = name; + this.buffer = buffer; + } + + @Override + public String getText () // Override this to get a tailored text representation + { + return "Name : " + name + "\n\nNo text description"; + } + + @Override + public String getAssembler () + { + if (buffer == null) + return "No buffer"; + if (assembler == null) + this.assembler = new AssemblerProgram (name, buffer, 0); + return assembler.getText (); + } + + @Override + public String getHexDump () + { + if (hexBlocks.size () > 0) + { + StringBuilder text = new StringBuilder (); + + for (HexBlock hb : hexBlocks) + { + if (hb.title != null) + text.append (hb.title + "\n\n"); + text.append (HexFormatter.format (buffer, hb.ptr, hb.size) + "\n\n"); + } + text.deleteCharAt (text.length () - 1); + text.deleteCharAt (text.length () - 1); + + return text.toString (); + } + if (buffer == null || buffer.length == 0) + return "No buffer"; + if (buffer.length <= 99999) + return HexFormatter.format (buffer, 0, buffer.length); + return HexFormatter.format (buffer, 0, 99999); + } + + @Override + public BufferedImage getImage () + { + return image; + } + + @Override + public JComponent getComponent () + { + System.out.println ("In AbstractFile.getComponent()"); + JPanel panel = new JPanel (); + return panel; + } + + protected class HexBlock + { + public int ptr; + public int size; + public String title; + + public HexBlock (int ptr, int size, String title) + { + this.ptr = ptr; + this.size = size; + this.title = title; + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/applefile/AppleFileSource.java b/src/com/bytezone/diskbrowser/applefile/AppleFileSource.java new file mode 100755 index 0000000..fa2a5ba --- /dev/null +++ b/src/com/bytezone/diskbrowser/applefile/AppleFileSource.java @@ -0,0 +1,32 @@ +package com.bytezone.diskbrowser.applefile; + +import java.util.List; + +import com.bytezone.diskbrowser.disk.DiskAddress; +import com.bytezone.diskbrowser.disk.FormattedDisk; +import com.bytezone.diskbrowser.gui.DataSource; + +public interface AppleFileSource +{ + /* + * Returns a name that uniquely identifies this object within the disk. + */ + public String getUniqueName (); + + /* + * DataSource is implemented by AbstractSector and AbstractFile, and provides + * routines to display the data in various formats (text, hex, assembler and + * image). + */ + public DataSource getDataSource (); + + /* + * Returns a list of sectors used by this object. + */ + public List getSectors (); + + /* + * Returns the actual FormattedDisk that owns this object. + */ + public FormattedDisk getFormattedDisk (); +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/applefile/ApplesoftConstants.java b/src/com/bytezone/diskbrowser/applefile/ApplesoftConstants.java new file mode 100755 index 0000000..b84005f --- /dev/null +++ b/src/com/bytezone/diskbrowser/applefile/ApplesoftConstants.java @@ -0,0 +1,29 @@ +package com.bytezone.diskbrowser.applefile; + +public interface ApplesoftConstants +{ + String[] tokens = { "END", "FOR ", "NEXT ", "DATA ", "INPUT ", "DEL", "DIM ", "READ ", "GR", + "TEXT", "PR#", "IN#", "CALL ", "PLOT", "HLIN ", "VLIN ", "HGR2", "HGR", + "HCOLOR=", "HPLOT ", "DRAW ", "XDRAW ", "HTAB ", "HOME", "ROT=", "SCALE=", + "SHLOAD", "TRACE", "NOTRACE", "NORMAL", "INVERSE", "FLASH", "COLOR=", "POP", + "VTAB ", "HIMEM:", "LOMEM:", "ONERR ", "RESUME", "RECALL", "STORE", "SPEED=", + "LET ", "GOTO ", "RUN", "IF ", "RESTORE", "& ", "GOSUB ", "RETURN", "REM ", + "STOP", "ON ", "WAIT", "LOAD", "SAVE", "DEF", "POKE ", "PRINT ", "CONT", + "LIST", "CLEAR", "GET ", "NEW", "TAB(", "TO ", "FN", "SPC(", "THEN ", "AT ", + "NOT ", "STEP ", "+ ", "- ", "* ", "/ ", "^ ", "AND ", "OR ", "> ", "= ", + "< ", "SGN", "INT", "ABS", "USR", "FRE", "SCRN(", "PDL", "POS ", "SQR", "RND", + "LOG", "EXP", "COS", "SIN", "TAN", "ATN", "PEEK", "LEN", "STR$", "VAL", "ASC", + "CHR$", "LEFT$", "RIGHT$", "MID$" }; + + int[] tokenAddresses = { 0xD870, 0xD766, 0xDCF9, 0xD995, 0xDBB2, 0xF331, 0xDFD9, 0xDBE2, 0xF390, + 0xF399, 0xF1E5, 0xF1DE, 0xF1D5, 0xF225, 0xF232, 0xF241, 0xF3D8, 0xF3E2, + 0xF6E9, 0xF6FE, 0xF769, 0xF76F, 0xF7E7, 0xFC58, 0xF721, 0xF727, 0xF775, + 0xF26D, 0xF26F, 0xF273, 0xF277, 0xF280, 0xF24F, 0xD96B, 0xF256, 0xF286, + 0xF2A6, 0xF2CB, 0xF318, 0xF3BC, 0xF39F, 0xF262, 0xDA46, 0xD93E, 0xD912, + 0xD9C9, 0xD849, 0x03F5, 0xD921, 0xD96B, 0xD9DC, 0xD86E, 0xD9EC, 0xE784, + 0xD8C9, 0xD8B0, 0xE313, 0xE77B, 0xFDAD5, 0xD896, 0xD6A5, 0xD66A, 0xDBA0, + 0xD649, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xEB90, + 0xEC23, 0xEBAF, 0x000A, 0xE2DE, 0xD412, 0xDFCD, 0xE2FF, 0xEE8D, 0xEFAE, + 0xE941, 0xEF09, 0xEFEA, 0xEFF1, 0xF03A, 0xF09E, 0xE764, 0xE6D6, 0xE3C5, + 0xE707, 0xE6E5, 0xE646, 0xE65A, 0xE686, 0xE691 }; +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/applefile/AssemblerConstants.java b/src/com/bytezone/diskbrowser/applefile/AssemblerConstants.java new file mode 100755 index 0000000..5b23c83 --- /dev/null +++ b/src/com/bytezone/diskbrowser/applefile/AssemblerConstants.java @@ -0,0 +1,67 @@ +package com.bytezone.diskbrowser.applefile; + +public interface AssemblerConstants +{ + // 1A = INC A, 3A = DEC A + String[] mnemonics = { "BRK", "ORA", "???", "???", "TSB", "ORA", "ASL", "???", // 00 + "PHP", "ORA", "ASL", "???", "TSB", "ORA", "ASL", "???", // 08 + "BPL", "ORA", "ORA", "???", "TRB", "ORA", "ASL", "???", // 10 + "CLC", "ORA", "INC", "???", "TRB", "ORA", "ASL", "???", // 18 + "JSR", "AND", "???", "???", "BIT", "AND", "ROL", "???", // 20 + "PLP", "AND", "ROL", "???", "BIT", "AND", "ROL", "???", // 28 + "BMI", "AND", "AND", "???", "BIT", "AND", "ROL", "???", // 30 + "SEC", "AND", "DEC", "???", "BIT", "AND", "ROL", "???", // 38 + "RTI", "EOR", "???", "???", "???", "EOR", "LSR", "???", // 40 + "PHA", "EOR", "LSR", "???", "JMP", "EOR", "LSR", "???", // 48 + "BVC", "EOR", "EOR", "???", "???", "EOR", "LSR", "???", // 50 + "CLI", "EOR", "PHY", "???", "???", "EOR", "LSR", "???", // 58 + "RTS", "ADC", "???", "???", "STZ", "ADC", "ROR", "???", // 60 + "PLA", "ADC", "ROR", "???", "JMP", "ADC", "ROR", "???", // 68 + "BVS", "ADC", "ADC", "???", "STZ", "ADC", "ROR", "???", // 70 + "SEI", "ADC", "PLY", "???", "JMP", "ADC", "ROR", "???", // 78 + "BRA", "STA", "???", "???", "STY", "STA", "STX", "???", // 80 + "DEY", "BIT", "TXA", "???", "STY", "STA", "STX", "???", // 88 + "BCC", "STA", "STA", "???", "STY", "STA", "STX", "???", // 90 + "TYA", "STA", "TXS", "???", "STZ", "STA", "STZ", "???", // 98 + "LDY", "LDA", "LDX", "???", "LDY", "LDA", "LDX", "???", // A0 + "TAY", "LDA", "TAX", "???", "LDY", "LDA", "LDX", "???", // A8 + "BCS", "LDA", "LDA", "???", "LDY", "LDA", "LDX", "???", // B0 + "CLV", "LDA", "TSX", "???", "LDY", "LDA", "LDX", "???", // B8 + "CPY", "CMP", "???", "???", "CPY", "CMP", "DEC", "???", // C0 + "INY", "CMP", "DEX", "???", "CPY", "CMP", "DEC", "???", // C8 + "BNE", "CMP", "CMP", "???", "???", "CMP", "DEC", "???", // D0 + "CLD", "CMP", "PHX", "???", "???", "CMP", "DEC", "???", // D8 + "CPX", "SBC", "???", "???", "CPX", "SBC", "INC", "???", // E0 + "INX", "SBC", "NOP", "???", "CPX", "SBC", "INC", "???", // E8 + "BEQ", "SBC", "SBC", "???", "???", "SBC", "INC", "???", // F0 + "SED", "SBC", "PLX", "???", "???", "SBC", "INC", "???" }; // F8 + + byte[] sizes2 = { 1, 2, 0, 0, 2, 2, 2, 0, 1, 2, 1, 0, 3, 3, 3, 0, // 00 - 0F + 2, 2, 2, 0, 2, 2, 2, 0, 1, 3, 1, 0, 3, 3, 3, 0, // 10 - 1F + 3, 2, 0, 0, 2, 2, 2, 0, 1, 2, 1, 0, 3, 3, 3, 0, // 20 - 2F + 2, 2, 2, 0, 2, 2, 2, 0, 1, 3, 1, 0, 3, 3, 3, 0, // 30 - 3F + 1, 2, 0, 0, 0, 2, 2, 0, 1, 2, 1, 0, 3, 3, 3, 0, // 40 - 4F + 2, 2, 2, 0, 0, 2, 2, 0, 1, 3, 1, 0, 0, 3, 3, 0, // 50 - 5F + 1, 2, 0, 0, 2, 2, 2, 0, 1, 2, 1, 0, 3, 3, 3, 0, // 60 - 6F + 2, 2, 2, 0, 2, 2, 2, 0, 1, 3, 1, 0, 3, 3, 3, 0, // 70 - 7F + 2, 2, 0, 0, 2, 2, 2, 0, 1, 2, 1, 0, 3, 3, 3, 0, // 80 - 8F + 2, 2, 2, 0, 2, 2, 2, 0, 1, 3, 1, 0, 3, 3, 3, 0, // 90 - 9F + 2, 2, 2, 0, 2, 2, 2, 0, 1, 2, 1, 0, 3, 3, 3, 0, // A0 - AF + 2, 2, 2, 0, 2, 2, 2, 0, 1, 3, 1, 0, 3, 3, 3, 0, // B0 - BF + 2, 2, 0, 0, 2, 2, 2, 0, 1, 2, 1, 0, 3, 3, 3, 0, // C0 - CF + 2, 2, 2, 0, 0, 2, 2, 0, 1, 3, 1, 0, 0, 3, 3, 0, // D0 - DF + 2, 2, 0, 0, 2, 2, 2, 0, 1, 2, 1, 0, 3, 3, 3, 0, // E0 - EF + 2, 2, 2, 0, 0, 2, 2, 0, 1, 3, 1, 0, 0, 3, 3, 0 }; // F0 - FF + + byte[] sizes = { 1, 1, 2, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2 }; + + String[] mode = + { "Implied", "Accumulator", "Immediate", "Absolute", "Absolute, X", "Absolute, Y", + "(Absolute, X)", "(Absolute)", "Zero page", "Zero page, X", "Zero page, Y", + "(Zero page, X)", "(Zero page), Y", "(Zero page)", "Relative" }; + + byte[] chip65c02 = + { 0x04, 0x0C, 0x12, 0x14, 0x1A, 0x1C, 0x32, 0x34, 0x3A, 0x3C, 0x52, 0x5A, 0x64, 0x72, + 0x74, 0x7A, 0x7C, (byte) 0x80, (byte) 0x89, (byte) 0x92, (byte) 0x9C, + (byte) 0x9E, (byte) 0xB2, (byte) 0xD2, (byte) 0xDA, (byte) 0xF2, (byte) 0xFA, }; +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/applefile/AssemblerProgram.java b/src/com/bytezone/diskbrowser/applefile/AssemblerProgram.java new file mode 100755 index 0000000..4a329f4 --- /dev/null +++ b/src/com/bytezone/diskbrowser/applefile/AssemblerProgram.java @@ -0,0 +1,268 @@ +package com.bytezone.diskbrowser.applefile; + +import java.io.BufferedReader; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.bytezone.diskbrowser.gui.DiskBrowser; + +public class AssemblerProgram extends AbstractFile +{ + private final int loadAddress; + private int executeOffset; + private static Map equates; + + public AssemblerProgram (String name, byte[] buffer, int address) + { + super (name, buffer); + this.loadAddress = address; + + if (equates == null) + getEquates (); + } + + public AssemblerProgram (String name, byte[] buffer, int address, int executeOffset) + { + this (name, buffer, address); + this.executeOffset = executeOffset; + } + + @Override + public String getText () + { + StringBuilder pgm = new StringBuilder (); + + pgm.append (String.format ("Name : %s%n", name)); + pgm.append (String.format ("Length : $%04X (%,d)%n", buffer.length, buffer.length)); + pgm.append (String.format ("Load at : $%04X (%,d)%n", loadAddress, loadAddress)); + + if (executeOffset > 0) + pgm.append (String.format ("Entry : $%04X%n", (loadAddress + executeOffset))); + pgm.append (String.format ("%n")); + + return pgm.append (getStringBuilder ()).toString (); + } + + public StringBuilder getStringBuilder () + { + if (true) + return getStringBuilder2 (); + + StringBuilder pgm = new StringBuilder (); + + int ptr = executeOffset; + int address = loadAddress + executeOffset; + + // if the assembly doesn't start at the beginning, just dump the bytes that are skipped + for (int i = 0; i < executeOffset; i++) + pgm.append (String.format ("%04X: %02X%n", (loadAddress + i), buffer[i])); + + while (ptr < buffer.length) + { + StringBuilder line = new StringBuilder (); + + AssemblerStatement cmd = new AssemblerStatement (buffer[ptr]); + + if (cmd.size == 2 && ptr < buffer.length - 1) + cmd.addData (buffer[ptr + 1]); + else if (cmd.size == 3 && ptr < buffer.length - 2) + cmd.addData (buffer[ptr + 1], buffer[ptr + 2]); + else + cmd.size = 1; + + line.append (String.format ("%04X: ", address)); + for (int i = 0; i < cmd.size; i++) + line.append (String.format ("%02X ", buffer[ptr + i])); + while (line.length () < 20) + line.append (" "); + line.append (cmd.mnemonic + " " + cmd.operand); + if (cmd.offset != 0) + { + int branch = address + cmd.offset + 2; + line.append (String.format ("$%04X", branch < 0 ? branch += 0xFFFF : branch)); + } + + if (cmd.target > 0 + && (cmd.target < loadAddress - 1 || cmd.target > (loadAddress + buffer.length))) + { + while (line.length () < 40) + line.append (" "); + + String text = equates.get (cmd.target); + if (text != null) + line.append ("; " + text); + else + for (int i = 0, max = ApplesoftConstants.tokenAddresses.length; i < max; i++) + if (cmd.target == ApplesoftConstants.tokenAddresses[i]) + { + line.append ("; Applesoft - " + ApplesoftConstants.tokens[i]); + break; + } + } + pgm.append (line.toString () + "\n"); + address += cmd.size; + ptr += cmd.size; + } + + if (pgm.length () > 0) + pgm.deleteCharAt (pgm.length () - 1); + + return pgm; + } + + public StringBuilder getStringBuilder2 () + { + StringBuilder pgm = new StringBuilder (); + List lines = getLines (); + + // if the assembly doesn't start at the beginning, just dump the bytes that are skipped + for (int i = 0; i < executeOffset; i++) + pgm.append (String.format (" %04X: %02X%n", (loadAddress + i), buffer[i])); + + for (AssemblerStatement cmd : lines) + { + StringBuilder line = new StringBuilder (); + + line.append (String.format ("%3.3s %04X: %02X ", getArrow (cmd), cmd.address, cmd.value)); + + if (cmd.size > 1) + line.append (String.format ("%02X ", cmd.operand1)); + if (cmd.size > 2) + line.append (String.format ("%02X ", cmd.operand2)); + + while (line.length () < 23) + line.append (" "); + + line.append (cmd.mnemonic + " " + cmd.operand); + if (cmd.offset != 0) + { + int branch = cmd.address + cmd.offset + 2; + line.append (String.format ("$%04X", branch < 0 ? branch += 0xFFFF : branch)); + } + + if (cmd.target > 0 + && (cmd.target < loadAddress - 1 || cmd.target > (loadAddress + buffer.length))) + { + while (line.length () < 40) + line.append (" "); + + String text = equates.get (cmd.target); + if (text != null) + line.append ("; " + text); + else + for (int i = 0, max = ApplesoftConstants.tokenAddresses.length; i < max; i++) + if (cmd.target == ApplesoftConstants.tokenAddresses[i]) + { + line.append ("; Applesoft - " + ApplesoftConstants.tokens[i]); + break; + } + } + pgm.append (line.toString () + "\n"); + } + + if (pgm.length () > 0) + pgm.deleteCharAt (pgm.length () - 1); + + return pgm; + } + + private List getLines () + { + List lines = new ArrayList (); + Map linesMap = new HashMap (); + List targets = new ArrayList (); + + int ptr = executeOffset; + int address = loadAddress + executeOffset; + + while (ptr < buffer.length) + { + AssemblerStatement cmd = new AssemblerStatement (buffer[ptr]); + lines.add (cmd); + linesMap.put (address, cmd); + cmd.address = address; + + if (cmd.size == 2 && ptr < buffer.length - 1) + cmd.addData (buffer[ptr + 1]); + else if (cmd.size == 3 && ptr < buffer.length - 2) + cmd.addData (buffer[ptr + 1], buffer[ptr + 2]); + else + cmd.size = 1; + + if (cmd.target >= loadAddress && cmd.target < (loadAddress + buffer.length) + && (cmd.value == 0x4C || cmd.value == 0x6C || cmd.value == 0x20)) + targets.add (cmd.target); + if (cmd.offset != 0) + targets.add (cmd.address + cmd.offset + 2); + + address += cmd.size; + ptr += cmd.size; + } + + for (Integer target : targets) + { + AssemblerStatement cmd = linesMap.get (target); + if (cmd != null) + cmd.isTarget = true; + } + + return lines; + } + + private String getArrow (AssemblerStatement cmd) + { + String arrow = ""; + if (cmd.value == 0x4C || cmd.value == 0x6C || cmd.value == 0x60 || cmd.offset != 0) + arrow = "<--"; + if (cmd.value == 0x20 && // JSR + cmd.target >= loadAddress && cmd.target < (loadAddress + buffer.length)) + arrow = "<--"; + if (cmd.isTarget) + if (arrow.isEmpty ()) + arrow = "-->"; + else + arrow = "<->"; + return arrow; + } + + @Override + public String getAssembler () + { + return getStringBuilder ().toString (); + } + + private void getEquates () + { + equates = new HashMap (); + DataInputStream inputEquates = + new DataInputStream (DiskBrowser.class.getClassLoader () + .getResourceAsStream ("com/bytezone/diskbrowser/applefile/equates.txt")); + BufferedReader in = new BufferedReader (new InputStreamReader (inputEquates)); + + String line; + try + { + while ((line = in.readLine ()) != null) + { + if (!line.isEmpty () && !line.startsWith ("*")) + { + int address = Integer.parseInt (line.substring (0, 4), 16); + if (equates.containsKey (address)) + System.out.println ("Duplicate equate entry : " + address); + else + equates.put (address, line.substring (6)); + } + } + in.close (); + } + catch (IOException e) + { + e.printStackTrace (); + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/applefile/AssemblerStatement.java b/src/com/bytezone/diskbrowser/applefile/AssemblerStatement.java new file mode 100755 index 0000000..71b98bf --- /dev/null +++ b/src/com/bytezone/diskbrowser/applefile/AssemblerStatement.java @@ -0,0 +1,363 @@ +package com.bytezone.diskbrowser.applefile; + +import java.util.Arrays; +import java.util.Comparator; + +import com.bytezone.diskbrowser.HexFormatter; + +public class AssemblerStatement +{ + public byte value; + public String mnemonic; + public String operand; + public int size; + public int mode; + public int opcode; + public int target; + public int offset; + public String comment; + public int address; + public boolean isTarget; + public byte operand1, operand2; + + public static void print () + { + AssemblerStatement[] statements = new AssemblerStatement[256]; + System.out.println (); + for (int i = 0; i < 256; i++) + { + if (i % 16 == 0 && i > 0) + System.out.println (); + AssemblerStatement as = new AssemblerStatement ((byte) i); + statements[i] = as; + if (as.size == 1) + as.addData (); + else if (as.size == 2) + as.addData ((byte) 1); + else if (as.size == 3) + as.addData ((byte) 1, (byte) 1); + if ((i / 8) % 2 == 0) + System.out.printf ("%02X %15.15s ", i, as); + } + + Arrays.sort (statements, new Comparator () + { + @Override + public int compare (AssemblerStatement o1, AssemblerStatement o2) + { + if (o1.mnemonic.equals (o2.mnemonic)) + return o1.mode == o2.mode ? 0 : o1.mode < o2.mode ? -1 : 1; + return o1.mnemonic.compareTo (o2.mnemonic); + } + }); + + System.out.println (); + String lastMnemonic = ""; + for (AssemblerStatement as : statements) + if (as.size > 0) + { + if (!as.mnemonic.equals (lastMnemonic)) + { + lastMnemonic = as.mnemonic; + System.out.println (); + } + System.out.printf ("%3s %-15s %s%n", as.mnemonic, AssemblerConstants.mode[as.mode], as); + } + } + + public AssemblerStatement (byte opcode) + { + this.value = opcode; + this.opcode = HexFormatter.intValue (opcode); + this.mnemonic = AssemblerConstants.mnemonics[this.opcode]; + this.size = AssemblerConstants.sizes2[this.opcode]; + this.operand = ""; + } + + public void addData () + { + switch (this.opcode) + { + case 0x00: // BRK + case 0x08: // PHP + case 0x18: // CLC + case 0x28: // PLP + case 0x38: // SEC + case 0x40: // RTI + case 0x48: // PHA + case 0x58: // CLI + case 0x5A: // NOP + case 0x60: // RTS + case 0x68: // PLA + case 0x78: // SEI + case 0x7A: // NOP + case 0x88: // DEY + case 0x8A: // TXA + case 0x98: // TYA + case 0x9A: // TXS + case 0xA8: // TAY + case 0xAA: // TAX + case 0xB8: // CLV + case 0xBA: // TSX + case 0xC8: // INY + case 0xCA: // DEX + case 0xD8: // CLD + case 0xDA: // NOP + case 0xE8: // INX + case 0xEA: // NOP + case 0xF8: // SED + case 0xFA: // NOP + mode = 0; // Implied + break; + + case 0x0A: // ASL + case 0x1A: // NOP + case 0x2A: // ROL + case 0x3A: // NOP + case 0x4A: // LSR + case 0x6A: // ROR + mode = 1; // Accumulator + break; + + default: + System.out.println ("Not found (0) : " + opcode); + } + } + + public void addData (byte b) + { + operand1 = b; + String address = "$" + HexFormatter.format2 (b); +// if (this.mnemonic.equals ("JSR")) +// this.target = HexFormatter.intValue (b); + + switch (this.opcode) + { + case 0x09: // ORA + case 0x29: // AND + case 0x49: // EOR + case 0x69: // ADC + case 0x89: // NOP - 65c02 + case 0xA0: // LDY + case 0xA2: // LDX + case 0xA9: // LDA + case 0xC0: // CPY + case 0xC9: // CMP + case 0xE0: // CPX + case 0xE9: // SBC + operand = "#" + address; + mode = 2; // Immediate + break; + + case 0x04: // NOP - 65c02 + case 0x05: // ORA + case 0x06: // ASL + case 0x14: // NOP - 65c02 + case 0x24: // BIT + case 0x25: // AND + case 0x26: // ROL + case 0x45: // EOR + case 0x46: // LSR + case 0x64: // NOP - 65c02 + case 0x65: // ADC + case 0x66: // ROR + case 0x84: // STY + case 0x85: // STA + case 0x86: // STX + case 0xA4: // LDY + case 0xA5: // LDA + case 0xA6: // LDX + case 0xC4: // CPY + case 0xC5: // CMP + case 0xC6: // DEC + case 0xE4: // CPX + case 0xE5: // SBC + case 0xE6: // INC + target = HexFormatter.intValue (b); + operand = address; + mode = 8; // Zero page + break; + + case 0x15: // ORA + case 0x16: // ASL + case 0x34: // NOP - 65c02 + case 0x35: // AND + case 0x36: // ROL + case 0x55: // EOR + case 0x56: // LSR + case 0x74: // NOP - 65c02 + case 0x75: // ADC + case 0x76: // ROR + case 0x94: // STY + case 0x95: // STA + case 0xB4: // LDY + case 0xB5: // LDA + case 0xD5: // CMP + case 0xD6: // DEC + case 0xF5: // SBC + case 0xF6: // INC + operand = address + ",X"; + mode = 9; // Zero page, X + break; + + case 0x96: // STX + case 0xB6: // LDX + operand = address + ",Y"; + mode = 10; // Zero page, Y + break; + + case 0x01: // ORA + case 0x21: // AND + case 0x41: // EOR + case 0x61: // ADC + case 0x81: // STA + case 0xA1: // LDA + case 0xC1: // CMP + case 0xE1: // SEC + operand = "(" + address + ",X)"; + mode = 11; // (Indirect, X) + break; + + case 0x11: // ORA + case 0x31: // AND + case 0x51: // EOR + case 0x71: // ADC + case 0x91: // STA + case 0xB1: // LDA + case 0xD1: // CMP + case 0xF1: // SBC + operand = "(" + address + "),Y"; + mode = 12; // (Indirect), Y + break; + + case 0x12: // NOP + case 0x32: // NOP + case 0x52: // NOP + case 0x72: // NOP + case 0x92: // NOP + case 0xB2: // NOP + case 0xD2: // NOP + case 0xF2: // NOP + operand = "(" + address + ")"; // all 65c02 + mode = 13; // (zero page) + break; + + case 0x10: // BPL + case 0x30: // BMI + case 0x50: // BVC + case 0x70: // BVS + case 0x80: // NOP - 65c02 + case 0x90: // BCC + case 0xB0: // BCS + case 0xD0: // BNE + case 0xF0: // BEQ + offset = b; + mode = 14; // relative + this.target = HexFormatter.intValue (b); + break; + + default: + System.out.println ("Not found (1) : " + opcode); + } + } + + public void addData (byte b1, byte b2) + { + operand1 = b1; + operand2 = b2; + String address = "$" + HexFormatter.format2 (b2) + HexFormatter.format2 (b1); +// if (this.mnemonic.equals ("JSR") || this.mnemonic.equals ("JMP") +// || this.mnemonic.equals ("BIT") || this.mnemonic.equals ("STA") +// || this.mnemonic.equals ("LDA")) +// this.target = HexFormatter.intValue (b1, b2); + + switch (this.opcode) + { + case 0x0C: // NOP - 65c02 + case 0x0D: // ORA + case 0x0E: // ASL + case 0x1C: // NOP - 65c02 + case 0x20: // JSR + case 0x2C: // BIT + case 0x2D: // AND + case 0x2E: // ROL + case 0x4C: // JMP + case 0x4D: // EOR + case 0x4E: // LSR + case 0x6D: // ADC + case 0x6E: // ROR + case 0x8C: // STY + case 0x8D: // STA + case 0x8E: // STX + case 0x9C: // NOP - 65c02 + case 0xAC: // LDY + case 0xAD: // LDA + case 0xAE: // LDX + case 0xCC: // CPY + case 0xCD: // CMP + case 0xCE: // DEC + case 0xEC: // CPX + case 0xED: // SBC + case 0xEE: // INC + operand = address; + mode = 3; // absolute + this.target = HexFormatter.intValue (b1, b2); + break; + + case 0x1D: // ORA + case 0x1E: // ASL + case 0x3C: // NOP - 65c02 + case 0x3D: // AND + case 0x3E: // ROL + case 0x5D: // EOR + case 0x5E: // LSR + case 0x7D: // ADC + case 0x7E: // ROR + case 0x9D: // STA + case 0x9E: // NOP - 65c02 + case 0xBC: // LDY + case 0xBD: // LDA + case 0xDD: // CMP + case 0xDE: // DEC + case 0xFD: // SBC + case 0xFE: // INC + operand = address + ",X"; + mode = 4; // absolute, X + break; + + case 0x19: // ORA + case 0x39: // AND + case 0x59: // EOR + case 0x79: // ADC + case 0x99: // STA + case 0xB9: // LDA + case 0xBE: // LDX + case 0xD9: // CMP + case 0xF9: // SBC + operand = address + ",Y"; + mode = 5; // absolute, Y + break; + + case 0x7C: // NOP - 65c02 + operand = "(" + address + ",X)"; + mode = 6; // (absolute, X) + break; + + case 0x6C: // JMP + operand = "(" + address + ")"; + mode = 7; // (absolute) + break; + + default: + System.out.println ("Not found (2) : " + opcode); + } + } + + @Override + public String toString () + { + if (offset == 0) + return String.format ("%d %3s %-10s %02X", size, mnemonic, operand, value); + return String.format ("%d %3s %-10s %02X", size, mnemonic, operand + "+" + offset, value); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/applefile/BasicProgram.java b/src/com/bytezone/diskbrowser/applefile/BasicProgram.java new file mode 100644 index 0000000..ddafad6 --- /dev/null +++ b/src/com/bytezone/diskbrowser/applefile/BasicProgram.java @@ -0,0 +1,658 @@ +package com.bytezone.diskbrowser.applefile; + +import java.util.*; + +import com.bytezone.diskbrowser.HexFormatter; + +public class BasicProgram extends AbstractFile +{ + private static final byte ASCII_QUOTE = 0x22; + private static final byte ASCII_COLON = 0x3A; + private static final byte ASCII_SEMI_COLON = 0x3B; + + private static final byte TOKEN_FOR = (byte) 0x81; + private static final byte TOKEN_NEXT = (byte) 0x82; + private static final byte TOKEN_LET = (byte) 0xAA; + private static final byte TOKEN_GOTO = (byte) 0xAB; + private static final byte TOKEN_IF = (byte) 0xAD; + private static final byte TOKEN_GOSUB = (byte) 0xB0; + private static final byte TOKEN_REM = (byte) 0xB2; + private static final byte TOKEN_PRINT = (byte) 0xBA; + private static final byte TOKEN_THEN = (byte) 0xC4; + private static final byte TOKEN_EQUALS = (byte) 0xD0; + + private final List sourceLines = new ArrayList (); + private final int endPtr; + private final Set gotoLines = new HashSet (); + private final Set gosubLines = new HashSet (); + + boolean splitRem = false; // should be a user preference + boolean alignAssign = true; // should be a user preference + boolean showTargets = true; // should be a user preference + boolean showHeader = true; // should be a user preference + boolean onlyShowTargetLineNumbers = false; // should be a user preference + int wrapPrintAt = 40; + int wrapRemAt = 60; + + public BasicProgram (String name, byte[] buffer) + { + super (name, buffer); + + int ptr = 0; + int prevOffset = 0; + + int max = buffer.length - 4; // need at least 4 bytes to make a SourceLine + while (ptr < max) + { + int offset = HexFormatter.intValue (buffer[ptr], buffer[ptr + 1]); + if (offset <= prevOffset) + break; + + SourceLine line = new SourceLine (ptr); + sourceLines.add (line); + ptr += line.length; + prevOffset = offset; + } + endPtr = ptr; + } + + @Override + public String getText () + { + StringBuilder fullText = new StringBuilder (); + Stack loopVariables = new Stack (); + if (showHeader) + addHeader (fullText); + int alignPos = 0; + StringBuilder text; + int baseOffset = showTargets ? 12 : 8; + + for (SourceLine line : sourceLines) + { + text = new StringBuilder (getBase (line) + " "); + + int indent = loopVariables.size (); // each full line starts at the loop indent + int ifIndent = 0; // IF statement(s) limit back indentation by NEXT + + for (SubLine subline : line.sublines) + { + // Allow empty statements (caused by a single colon) + if (subline.isEmpty ()) + continue; + + // A REM statement might conceal an assembler routine - see P.CREATE on Diags2E.DSK + if (subline.is (TOKEN_REM) && subline.containsToken ()) + { + int address = subline.getAddress () + 1; // skip the REM token + fullText.append (text + + String.format ("REM - Inline assembler @ $%02X (%d)%n", address, address)); + String padding = " ".substring (0, text.length () + 2); + for (String asm : subline.getAssembler ()) + fullText.append (padding + asm + "\n"); + continue; + } + + // Reduce the indent by each NEXT, but only as far as the IF indent allows + if (subline.is (TOKEN_NEXT)) + { + popLoopVariables (loopVariables, subline); + indent = Math.max (ifIndent, loopVariables.size ()); + } + + // Are we joining REM lines with the previous subline? + if (!splitRem && subline.isJoinableRem ()) + { + // Join this REM statement to the previous line, so no indenting + fullText.deleteCharAt (fullText.length () - 1); // remove newline + fullText.append (" "); + } + // ... otherwise do all the indenting and showing of targets etc. + else + { + // Prepare target indicators for subsequent sublines (ie no line number) + if (showTargets && !subline.isFirst ()) + if (subline.is (TOKEN_GOSUB)) + text.append ("<<--"); + else if (subline.is (TOKEN_GOTO) || subline.isImpliedGoto ()) + text.append (" <--"); + + // Align assign statements if required + if (alignAssign) + alignPos = alignEqualsPosition (subline, alignPos); + + int column = indent * 2 + baseOffset; + while (text.length () < column) + text.append (" "); + } + + // Add the current text, then reset it + int pos = subline.is (TOKEN_REM) ? 0 : alignPos; + String lineText = subline.getAlignedText (pos); + + // if (subline.is (TOKEN_REM) && lineText.length () > wrapRemAt + 4) + // { + // System.out.println (subline.getAlignedText (pos)); + // String copy = lineText.substring (4); + // text.append ("REM "); + // int inset = text.length (); + // System.out.println (inset); + // List remarks = splitRemark (copy, wrapRemAt); + // for (String remark : remarks) + // text.append (" ".substring (0, inset) + remark); + // } + // else + text.append (lineText); + + // Check for a wrapable PRINT statement (see FROM MACHINE LANGUAGE TO BASIC on DOSToolkit2eB.dsk) + if (subline.is (TOKEN_PRINT) && wrapPrintAt > 0 && countChars (text, ASCII_QUOTE) == 2 + && countChars (text, ASCII_SEMI_COLON) == 0) + { + int first = text.indexOf ("\""); + int last = text.indexOf ("\"", first + 1); + if ((last - first) > wrapPrintAt) + { + int ptr = first + wrapPrintAt; + do + { + fullText.append (text.substring (0, ptr) + + "\n ".substring (0, first + 1)); + text.delete (0, ptr); + ptr = wrapPrintAt; + } while (text.length () > wrapPrintAt); + } + } + + fullText.append (text + "\n"); + text.setLength (0); + + // Calculate indent changes that take effect after the current subline + if (subline.is (TOKEN_IF)) + ifIndent = ++indent; + else if (subline.is (TOKEN_FOR)) + { + loopVariables.push (subline.forVariable); + ++indent; + } + } + + // Reset the alignment value if we just left an IF - the indentation will be different now. + if (ifIndent > 0) + alignPos = 0; + } + + fullText.deleteCharAt (fullText.length () - 1); // remove last newline + return fullText.toString (); + } + + private List splitRemark (String remark, int wrapLength) + { + List remarks = new ArrayList (); + while (remark.length () > wrapLength) + { + int max = Math.min (wrapLength, remark.length () - 1); + while (max > 0 && remark.charAt (max) != ' ') + --max; + System.out.println (remark.substring (0, max)); + remarks.add (remark.substring (0, max) + "\n"); + if (max == 0) + break; + remark = remark.substring (max + 1); + } + remarks.add (remark); + System.out.println (remark); + return remarks; + } + + private int countChars (StringBuilder text, byte ch) + { + int total = 0; + for (int i = 0; i < text.length (); i++) + if (text.charAt (i) == ch) + total++; + return total; + } + + private String getBase (SourceLine line) + { + if (!showTargets) + return String.format (" %5d", line.lineNumber); + + String lineNumberText = String.format ("%5d", line.lineNumber); + SubLine subline = line.sublines.get (0); + String c1 = " ", c2 = " "; + if (subline.is (TOKEN_GOSUB)) + c1 = "<<"; + if (subline.is (TOKEN_GOTO)) + c1 = " <"; + if (gotoLines.contains (line.lineNumber)) + c2 = "> "; + if (gosubLines.contains (line.lineNumber)) + c2 = ">>"; + if (c1.equals (" ") && !c2.equals (" ")) + c1 = "--"; + if (!c1.equals (" ") && c2.equals (" ")) + c2 = "--"; + if (onlyShowTargetLineNumbers && !c2.startsWith (">")) + lineNumberText = ""; + return String.format ("%s%s %s", c1, c2, lineNumberText); + } + + // Decide whether the current subline needs to be aligned on its equals sign. If so, + // and the column hasn't been calculated, read ahead to find the highest position. + private int alignEqualsPosition (SubLine subline, int currentAlignPosition) + { + if (subline.assignEqualPos > 0) // does the line have an equals sign? + { + if (currentAlignPosition == 0) + currentAlignPosition = findHighest (subline); // examine following sublines for alignment + return currentAlignPosition; + } + return 0; // reset it + } + + // The IF processing is so that any assignment that is being aligned doesn't continue + // to the next full line (because the indentation has changed). + private int findHighest (SubLine startSubline) + { + boolean started = false; + int highestAssign = startSubline.assignEqualPos; + fast: for (SourceLine line : sourceLines) + { + boolean inIf = false; + for (SubLine subline : line.sublines) + { + if (started) + { + // Stop when we come to a line without an equals sign (except for non-split REMs). + // Lines that start with a REM always break. + if (subline.assignEqualPos == 0 + // && (splitRem || !subline.is (TOKEN_REM) || subline.isFirst ())) + && (splitRem || !subline.isJoinableRem ())) + break fast; // of champions + + if (subline.assignEqualPos > highestAssign) + highestAssign = subline.assignEqualPos; + } + else if (subline == startSubline) + started = true; + else if (subline.is (TOKEN_IF)) + inIf = true; + } + if (started && inIf) + break; + } + return highestAssign; + } + + @Override + public String getHexDump () + { + if (buffer.length < 2) + return super.getHexDump (); + + StringBuilder pgm = new StringBuilder (); + if (showHeader) + addHeader (pgm); + + int ptr = 0; + int offset = HexFormatter.intValue (buffer[0], buffer[1]); + int programLoadAddress = offset - getLineLength (0); + + while (ptr <= endPtr) // stop at the same place as the source listing + { + int length = getLineLength (ptr); + if (length == 0) + { + pgm.append (HexFormatter.formatNoHeader (buffer, ptr, 2, programLoadAddress)); + ptr += 2; + break; + } + + if (ptr + length < buffer.length) + pgm.append (HexFormatter.formatNoHeader (buffer, ptr, length, programLoadAddress) + + "\n\n"); + ptr += length; + } + + if (ptr < buffer.length) + { + int length = buffer.length - ptr; + pgm.append ("\n\n"); + pgm.append (HexFormatter.formatNoHeader (buffer, ptr, length, programLoadAddress)); + } + + return pgm.toString (); + } + + private void addHeader (StringBuilder pgm) + { + pgm.append ("Name : " + name + "\n"); + pgm.append ("Length : $" + HexFormatter.format4 (buffer.length)); + pgm.append (" (" + buffer.length + ")\n"); + + int programLoadAddress = getLoadAddress (); + pgm.append ("Load at : $" + HexFormatter.format4 (programLoadAddress)); + pgm.append (" (" + programLoadAddress + ")\n\n"); + } + + private int getLoadAddress () + { + int programLoadAddress = 0; + if (buffer.length > 1) + { + int offset = HexFormatter.intValue (buffer[0], buffer[1]); + programLoadAddress = offset - getLineLength (0); + } + return programLoadAddress; + } + + private int getLineLength (int ptr) + { + int offset = HexFormatter.intValue (buffer[ptr], buffer[ptr + 1]); + if (offset == 0) + return 0; + ptr += 4; + int length = 5; + + while (ptr < buffer.length && buffer[ptr++] != 0) + length++; + + return length; + } + + private void popLoopVariables (Stack loopVariables, SubLine subline) + { + if (subline.nextVariables.length == 0) // naked NEXT + { + if (loopVariables.size () > 0) + loopVariables.pop (); + } + else + for (String variable : subline.nextVariables) + // e.g. NEXT X,Y,Z + while (loopVariables.size () > 0) + if (sameVariable (variable, loopVariables.pop ())) + break; + } + + private boolean sameVariable (String v1, String v2) + { + if (v1.equals (v2)) + return true; + if (v1.length () >= 2 && v2.length () >= 2 && v1.charAt (0) == v2.charAt (0) + && v1.charAt (1) == v2.charAt (1)) + return true; + return false; + } + + private class SourceLine + { + List sublines = new ArrayList (); + int lineNumber; + int linePtr; + int length; + + public SourceLine (int ptr) + { + linePtr = ptr; + lineNumber = HexFormatter.intValue (buffer[ptr + 2], buffer[ptr + 3]); + + int startPtr = ptr += 4; + boolean inString = false; // can toggle + boolean inRemark = false; // can only go false -> true + byte b; + + while ((b = buffer[ptr++]) != 0) + { + switch (b) + { + // break IF statements into two sublines (allows for easier line indenting) + case TOKEN_IF: + if (!inString && !inRemark) + { + // skip to THEN or GOTO - if not found then it's an error + while (buffer[ptr] != TOKEN_THEN && buffer[ptr] != TOKEN_GOTO + && buffer[ptr] != 0) + ptr++; + + // keep THEN with the IF + if (buffer[ptr] == TOKEN_THEN) + ++ptr; + + // create subline from the condition (and THEN if it exists) + sublines.add (new SubLine (this, startPtr, ptr - startPtr)); + startPtr = ptr; + } + break; + + // end of subline, so add it, advance startPtr and continue + case ASCII_COLON: + if (!inString && !inRemark) + { + sublines.add (new SubLine (this, startPtr, ptr - startPtr)); + startPtr = ptr; + } + break; + + case TOKEN_REM: + if (!inString && !inRemark) + inRemark = true; + break; + + case ASCII_QUOTE: + if (!inRemark) + inString = !inString; + break; + } + } + + // add whatever is left + sublines.add (new SubLine (this, startPtr, ptr - startPtr)); + this.length = ptr - linePtr; + } + } + + private class SubLine + { + SourceLine parent; + int startPtr; + int length; + String[] nextVariables; + String forVariable = ""; + int targetLine = -1; + + // used for aligning the equals sign + int assignEqualPos; + + public SubLine (SourceLine parent, int startPtr, int length) + { + this.parent = parent; + this.startPtr = startPtr; + this.length = length; + + byte b = buffer[startPtr]; + if ((b & 0x80) > 0) // token + { + switch (b) + { + case TOKEN_FOR: + int p = startPtr + 1; + while (buffer[p] != TOKEN_EQUALS) + forVariable += (char) buffer[p++]; + break; + + case TOKEN_NEXT: + if (length == 2) // no variables + nextVariables = new String[0]; + else + { + String varList = new String (buffer, startPtr + 1, length - 2); + nextVariables = varList.split (","); + } + break; + + case TOKEN_LET: + recordEqualsPosition (); + break; + + case TOKEN_GOTO: + String target = new String (buffer, startPtr + 1, length - 2); + try + { + gotoLines.add (Integer.parseInt (target)); + } + catch (NumberFormatException e) + { + System.out.println ("Error parsing : GOTO " + target + " in " + + parent.lineNumber); + } + break; + + case TOKEN_GOSUB: + String target2 = new String (buffer, startPtr + 1, length - 2); + try + { + gosubLines.add (Integer.parseInt (target2)); + } + catch (NumberFormatException e) + { + System.out.println ("Error parsing : GOSUB " + target2 + " in " + + parent.lineNumber); + } + break; + } + } + else + { + if (b >= 48 && b <= 57) // numeric, so must be a line number + { + String target = new String (buffer, startPtr, length - 1); + try + { + targetLine = Integer.parseInt (target); + gotoLines.add (targetLine); + } + catch (NumberFormatException e) + { + System.out.println (target); + System.out.println (HexFormatter.format (buffer, startPtr, length - 1)); + System.out.println (e.toString ()); + } + } + else if (alignAssign) + recordEqualsPosition (); + } + } + + private boolean isImpliedGoto () + { + byte b = buffer[startPtr]; + if ((b & 0x80) > 0) // token + return false; + return (b >= 48 && b <= 57); + } + + // Record the position of the equals sign so it can be aligned with adjacent lines. + private void recordEqualsPosition () + { + int p = startPtr + 1; + int max = startPtr + length; + while (buffer[p] != TOKEN_EQUALS && p < max) + p++; + if (buffer[p] == TOKEN_EQUALS) + assignEqualPos = toString ().indexOf ('='); // use expanded line + } + + private boolean isJoinableRem () + { + return is (TOKEN_REM) && !isFirst (); + } + + public boolean isFirst () + { + return (parent.linePtr + 4) == startPtr; + } + + public boolean is (byte token) + { + return buffer[startPtr] == token; + } + + public boolean isEmpty () + { + return length == 1 && buffer[startPtr] == 0; + } + + public boolean containsToken () + { + // ignore first byte, check the rest for tokens + for (int p = startPtr + 1, max = startPtr + length; p < max; p++) + if ((buffer[p] & 0x80) > 0) + return true; + return false; + } + + public int getAddress () + { + return getLoadAddress () + startPtr; + } + + public String getAlignedText (int alignPosition) + { + StringBuilder line = toStringBuilder (); + + while (alignPosition-- > assignEqualPos) + line.insert (assignEqualPos, ' '); + + return line.toString (); + } + + // A REM statement might conceal an assembler routine + public String[] getAssembler () + { + byte[] buffer2 = new byte[length - 1]; + System.arraycopy (buffer, startPtr + 1, buffer2, 0, buffer2.length); + AssemblerProgram program = + new AssemblerProgram ("REM assembler", buffer2, getAddress () + 1); + return program.getAssembler ().split ("\n"); + } + + @Override + public String toString () + { + return toStringBuilder ().toString (); + } + + public StringBuilder toStringBuilder () + { + StringBuilder line = new StringBuilder (); + + // All sublines end with 0 or : except IF lines that are split into two + int max = startPtr + length - 1; + if (buffer[max] == 0) + --max; + + for (int p = startPtr; p <= max; p++) + { + byte b = buffer[p]; + if ((b & 0x80) > 0) // token + { + if (line.length () > 0 && line.charAt (line.length () - 1) != ' ') + line.append (' '); + int val = b & 0x7F; + if (val < ApplesoftConstants.tokens.length) + line.append (ApplesoftConstants.tokens[b & 0x7F]); + // else + // System.out.println ("Bad value : " + val + " " + line.toString () + " " + // + parent.lineNumber); + } + else if (b < 32) // CTRL character + line.append ("^" + (char) (b + 64)); // would be better in inverse text + else + line.append ((char) b); + } + + return line; + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/applefile/BootSector.java b/src/com/bytezone/diskbrowser/applefile/BootSector.java new file mode 100644 index 0000000..5deca09 --- /dev/null +++ b/src/com/bytezone/diskbrowser/applefile/BootSector.java @@ -0,0 +1,48 @@ +package com.bytezone.diskbrowser.applefile; + +import com.bytezone.diskbrowser.disk.AbstractSector; +import com.bytezone.diskbrowser.disk.Disk; + +public class BootSector extends AbstractSector +{ + AssemblerProgram assembler; + String name; // DOS or Prodos + + public BootSector (Disk disk, byte[] buffer, String name) + { + super (disk, buffer); + this.name = name; + } + + @Override + public String createText () + { + StringBuilder text = new StringBuilder (); + + if (assembler == null) + { + // The first byte in the buffer is the number of sectors to read in (minus 1) + int sectors = buffer[0] & 0xFF; + // System.out.printf ("Sectors to read : %d%n", (sectors + 1)); + if (sectors > 0) + { + int bufferSize = buffer.length * (sectors + 1); + byte[] newBuffer = new byte[bufferSize]; + System.arraycopy (buffer, 0, newBuffer, 0, buffer.length); + + for (int i = 0; i < sectors; i++) + { + byte[] buf = disk.readSector (i + 1); + // System.out.printf ("%d %d %d%n", buf.length, buffer.length, newBuffer.length); + System.arraycopy (buf, 0, newBuffer, i * buf.length, buf.length); + } + buffer = newBuffer; + } + assembler = new AssemblerProgram (name + " Boot Loader", buffer, 0x800, 1); + } + + text.append (assembler.getText ()); + + return text.toString (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/applefile/Charset.java b/src/com/bytezone/diskbrowser/applefile/Charset.java new file mode 100755 index 0000000..f66b115 --- /dev/null +++ b/src/com/bytezone/diskbrowser/applefile/Charset.java @@ -0,0 +1,31 @@ +package com.bytezone.diskbrowser.applefile; + +import com.bytezone.diskbrowser.HexFormatter; + +public class Charset extends AbstractFile +{ + public Charset (String name, byte[] buffer) + { + super (name, buffer); + } + + public String getText () + { + StringBuilder text = new StringBuilder (); + for (int i = 0; i < buffer.length; i += 8) + { + for (int line = 7; line >= 0; line--) + { + int value = HexFormatter.intValue (buffer[i + line]); + for (int bit = 0; bit < 8; bit++) + { + text.append ((value & 0x01) == 1 ? "X" : "."); + value >>= 1; + } + text.append ("\n"); + } + text.append ("\n"); + } + return text.toString (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/applefile/Command.java b/src/com/bytezone/diskbrowser/applefile/Command.java new file mode 100644 index 0000000..9e79126 --- /dev/null +++ b/src/com/bytezone/diskbrowser/applefile/Command.java @@ -0,0 +1,180 @@ +package com.bytezone.diskbrowser.applefile; + +import java.util.ArrayList; +import java.util.List; + +// Brendan Robert's code from JACE + +public class Command +{ + public static enum TOKEN + { + END ((byte) 0x080, "END"), + FOR ((byte) 0x081, "FOR"), + NEXT ((byte) 0x082, "NEXT"), + DATA ((byte) 0x083, "DATA"), + INPUT ((byte) 0x084, "INPUT"), + DEL ((byte) 0x085, "DEL"), + DIM ((byte) 0x086, "DIM"), + READ ((byte) 0x087, "READ"), + GR ((byte) 0x088, "GR"), + TEXT ((byte) 0x089, "TEXT"), + PR ((byte) 0x08A, "PR#"), + IN ((byte) 0x08B, "IN#"), + CALL ((byte) 0x08C, "CALL"), + PLOT ((byte) 0x08D, "PLOT"), + HLIN ((byte) 0x08E, "HLIN"), + VLIN ((byte) 0x08F, "VLIN"), + HGR2 ((byte) 0x090, "HGR2"), + HGR ((byte) 0x091, "HGR"), + HCOLOR ((byte) 0x092, "HCOLOR="), + HPLOT ((byte) 0x093, "HPLOT"), + DRAW ((byte) 0x094, "DRAW"), + XDRAW ((byte) 0x095, "XDRAW"), + HTAB ((byte) 0x096, "HTAB"), + HOME ((byte) 0x097, "HOME"), + ROT ((byte) 0x098, "ROT="), + SCALE ((byte) 0x099, "SCALE="), + SHLOAD ((byte) 0x09A, "SHLOAD"), + TRACE ((byte) 0x09B, "TRACE"), + NOTRACE ((byte) 0x09C, "NOTRACE"), + NORMAL ((byte) 0x09D, "NORMAL"), + INVERSE ((byte) 0x09E, "INVERSE"), + FLASH ((byte) 0x09F, "FLASH"), + COLOR ((byte) 0x0A0, "COLOR="), + POP ((byte) 0x0A1, "POP"), + VTAB ((byte) 0x0A2, "VTAB"), + HIMEM ((byte) 0x0A3, "HIMEM:"), + LOMEM ((byte) 0x0A4, "LOMEM:"), + ONERR ((byte) 0x0A5, "ONERR"), + RESUME ((byte) 0x0A6, "RESUME"), + RECALL ((byte) 0x0A7, "RECALL"), + STORE ((byte) 0x0A8, "STORE"), + SPEED ((byte) 0x0A9, "SPEED="), + LET ((byte) 0x0AA, "LET"), + GOTO ((byte) 0x0AB, "GOTO"), + RUN ((byte) 0x0AC, "RUN"), + IF ((byte) 0x0AD, "IF"), + RESTORE ((byte) 0x0AE, "RESTORE"), + AMPERSAND ((byte) 0x0AF, "&"), + GOSUB ((byte) 0x0B0, "GOSUB"), + RETURN ((byte) 0x0B1, "RETURN"), + REM ((byte) 0x0B2, "REM"), + STOP ((byte) 0x0B3, "STOP"), + ONGOTO ((byte) 0x0B4, "ON"), + WAIT ((byte) 0x0B5, "WAIT"), + LOAD ((byte) 0x0B6, "LOAD"), + SAVE ((byte) 0x0B7, "SAVE"), + DEF ((byte) 0x0B8, "DEF"), + POKE ((byte) 0x0B9, "POKE"), + PRINT ((byte) 0x0BA, "PRINT"), + CONT ((byte) 0x0BB, "CONT"), + LIST ((byte) 0x0BC, "LIST"), + CLEAR ((byte) 0x0BD, "CLEAR"), + GET ((byte) 0x0BE, "GET"), + NEW ((byte) 0x0BF, "NEW"), + TAB ((byte) 0x0C0, "TAB("), + TO ((byte) 0x0C1, "TO"), + FN ((byte) 0x0C2, "FN"), + SPC ((byte) 0x0c3, "SPC"), + THEN ((byte) 0x0c4, "THEN"), + AT ((byte) 0x0c5, "AT"), + NOT ((byte) 0x0c6, "NOT"), + STEP ((byte) 0x0c7, "STEP"), + PLUS ((byte) 0x0c8, "+"), + MINUS ((byte) 0x0c9, "-"), + MULTIPLY ((byte) 0x0Ca, "*"), + DIVIDE ((byte) 0x0Cb, "/"), + POWER ((byte) 0x0Cc, "^"), + AND ((byte) 0x0Cd, "AND"), + OR ((byte) 0x0Ce, "OR"), + GREATER ((byte) 0x0CF, ">"), + EQUAL ((byte) 0x0d0, "="), + LESS ((byte) 0x0d1, "<"), + SGN ((byte) 0x0D2, "SGN"), + INT ((byte) 0x0D3, "INT"), + ABS ((byte) 0x0D4, "ABS"), + USR ((byte) 0x0D5, "USR"), + FRE ((byte) 0x0D6, "FRE"), + SCREEN ((byte) 0x0D7, "SCRN("), + PDL ((byte) 0x0D8, "PDL"), + POS ((byte) 0x0D9, "POS"), + SQR ((byte) 0x0DA, "SQR"), + RND ((byte) 0x0DB, "RND"), + LOG ((byte) 0x0DC, "LOG"), + EXP ((byte) 0x0DD, "EXP"), + COS ((byte) 0x0DE, "COS"), + SIN ((byte) 0x0DF, "SIN"), + TAN ((byte) 0x0E0, "TAN"), + ATN ((byte) 0x0E1, "ATN"), + PEEK ((byte) 0x0E2, "PEEK"), + LEN ((byte) 0x0E3, "LEN"), + STR ((byte) 0x0E4, "STR$"), + VAL ((byte) 0x0E5, "VAL"), + ASC ((byte) 0x0E6, "ASC"), + CHR ((byte) 0x0E7, "CHR$"), + LEFT ((byte) 0x0E8, "LEFT$"), + RIGHT ((byte) 0x0E9, "RIGHT$"), + MID ((byte) 0x0EA, "MID$"); + private final String str; + private final byte b; + + TOKEN (byte b, String str) + { + this.b = b; + this.str = str; + } + + @Override + public String toString () + { + return str; + } + + public static TOKEN fromByte (byte b) + { + for (TOKEN t : values ()) + if (t.b == b) + return t; + return null; + } + } + + public static class ByteOrToken + { + byte b; + TOKEN t; + boolean isToken = false; + + public ByteOrToken (byte b) + { + TOKEN t = TOKEN.fromByte (b); + if (t != null) + { + isToken = true; + this.t = t; + } + else + { + isToken = false; + this.b = b; + } + } + + @Override + public String toString () + { + return isToken ? " " + t.toString () + " " : String.valueOf ((char) b); + } + } + List parts = new ArrayList (); + + @Override + public String toString () + { + String out = ""; + for (ByteOrToken p : parts) + out += p.toString (); + return out; + } +} diff --git a/src/com/bytezone/diskbrowser/applefile/DefaultAppleFile.java b/src/com/bytezone/diskbrowser/applefile/DefaultAppleFile.java new file mode 100755 index 0000000..0d8464d --- /dev/null +++ b/src/com/bytezone/diskbrowser/applefile/DefaultAppleFile.java @@ -0,0 +1,32 @@ +package com.bytezone.diskbrowser.applefile; + +public class DefaultAppleFile extends AbstractFile +{ + String text; + + public DefaultAppleFile (String name, byte[] buffer) + { + super (name, buffer); + } + + public DefaultAppleFile (String name, byte[] buffer, String text) + { + super (name, buffer); + this.text = "Name : " + name + "\n\n" + text; + } + + public void setText (String text) + { + this.text = text; + } + + @Override + public String getText () + { + if (text != null) + return text; + if (buffer == null) + return "Invalid file : " + name; + return super.getText (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/applefile/ErrorMessageFile.java b/src/com/bytezone/diskbrowser/applefile/ErrorMessageFile.java new file mode 100644 index 0000000..70630ed --- /dev/null +++ b/src/com/bytezone/diskbrowser/applefile/ErrorMessageFile.java @@ -0,0 +1,25 @@ +package com.bytezone.diskbrowser.applefile; + +public class ErrorMessageFile extends AbstractFile +{ + String text; + + public ErrorMessageFile (String name, byte[] buffer, Exception e) + { + super (name, buffer); + + StringBuilder text = new StringBuilder (); + text.append ("Oops! : " + e.toString () + "\n\n"); + for (StackTraceElement ste : e.getStackTrace ()) + text.append (ste + "\n"); + if (text.length () > 0) + text.deleteCharAt (text.length () - 1); + this.text = text.toString (); + } + + @Override + public String getText () + { + return text; + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/applefile/HiResImage.java b/src/com/bytezone/diskbrowser/applefile/HiResImage.java new file mode 100755 index 0000000..5708a16 --- /dev/null +++ b/src/com/bytezone/diskbrowser/applefile/HiResImage.java @@ -0,0 +1,262 @@ +package com.bytezone.diskbrowser.applefile; + +import java.awt.image.BufferedImage; +import java.awt.image.DataBuffer; +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import javax.imageio.ImageIO; + +import com.bytezone.diskbrowser.HexFormatter; + +public class HiResImage extends AbstractFile +{ + int fileType; + int auxType; + byte[] unpackedBuffer; + + public HiResImage (String name, byte[] buffer) + { + super (name, buffer); + if (name.equals ("FLY LOGO") || name.equals ("BIGBAT.PAC")) // && reportedLength == 0x14FA) + { + this.buffer = unscrunch (buffer); + } + if (isGif (buffer)) + makeGif (); + else + makeScreen1 (buffer); + } + + public HiResImage (String name, byte[] buffer, int fileType, int auxType) + { + super (name, buffer); + this.fileType = fileType; + this.auxType = auxType; + + if (fileType == 192 && auxType == 1) + { + unpackedBuffer = unpackBytes (buffer); + makeScreen2 (unpackedBuffer); + } + if (fileType == 192 && auxType == 2) + { + System.out.println ("yippee - Preferred picture format - " + name); + } + } + + private void makeScreen1 (byte[] buffer) + { + int rows = buffer.length <= 8192 ? 192 : 384; + image = new BufferedImage (280, rows, BufferedImage.TYPE_BYTE_GRAY); + + DataBuffer db = image.getRaster ().getDataBuffer (); + + int element = 0; + byte[] mask = { 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 }; + + for (int z = 0; z < rows / 192; z++) + for (int i = 0; i < 3; i++) + for (int j = 0; j < 8; j++) + for (int k = 0; k < 8; k++) + { + int base = i * 0x28 + j * 0x80 + k * 0x400 + z * 0x2000; + int max = Math.min (base + 40, buffer.length); + for (int ptr = base; ptr < max; ptr++) + { + byte val = buffer[ptr]; + if (val == 0) // no pixels set + { + element += 7; + continue; + } + for (int bit = 6; bit >= 0; bit--) + { + if ((val & mask[bit]) > 0) + db.setElem (element, 255); + element++; + } + } + } + } + + private void makeScreen2 (byte[] buffer) + { + // System.out.println (HexFormatter.format (buffer, 32000, 640)); + // for (int table = 0; table < 200; table++) + // { + // System.out.println (HexFormatter.format (buffer, ptr, 32)); + // for (int color = 0; color < 16; color++) + // { + // int red = buffer[ptr++] & 0x0F; + // int green = (buffer[ptr] & 0xF0) >> 4; + // int blue = buffer[ptr++] & 0x0F; + // Color c = new Color (red, green, blue); + // } + // } + + image = new BufferedImage (320, 200, BufferedImage.TYPE_BYTE_GRAY); + DataBuffer db = image.getRaster ().getDataBuffer (); + + int element = 0; + int ptr = 0; + for (int row = 0; row < 200; row++) + for (int col = 0; col < 160; col++) + { + int pix1 = (buffer[ptr] & 0xF0) >> 4; + int pix2 = buffer[ptr] & 0x0F; + if (pix1 > 0) + db.setElem (element, 255); + if (pix2 > 0) + db.setElem (element + 1, 255); + element += 2; + ptr++; + } + } + + // Beagle Bros routine to expand a hi-res screen + private byte[] unscrunch (byte[] src) + { + // byte[] dst = new byte[src.length < 0x2000 ? 0x2000 : 0x4000]; + byte[] dst = new byte[0x2000]; + int p1 = 0; + int p2 = 0; + // while (p1 < dst.length && p2 < src.length) + while (p1 < dst.length) + { + // System.out.printf ("%04X %04X%n", p1, p2); + byte b = src[p2++]; + if ((b == (byte) 0x80) || (b == (byte) 0xFF)) + { + b &= 0x7F; + int rpt = src[p2++]; + for (int i = 0; i < rpt; i++) + dst[p1++] = b; + } + else + dst[p1++] = b; + } + return dst; + } + + private void makeGif () + { + try + { + image = ImageIO.read (new ByteArrayInputStream (buffer)); + } + catch (IOException e) + { + e.printStackTrace (); + } + } + + private byte[] unpackBytes (byte[] buffer) + { + // routine found here - http://kpreid.livejournal.com/4319.html + + byte[] newBuf = new byte[32768]; + byte[] fourBuf = new byte[4]; + + // System.out.println (HexFormatter.format (buffer)); + + int ptr = 0, newPtr = 0; + while (ptr < buffer.length) + { + int type = (buffer[ptr] & 0xC0) >> 6; + int count = (buffer[ptr++] & 0x3F) + 1; + + switch (type) + { + case 0: + while (count-- != 0) + newBuf[newPtr++] = buffer[ptr++]; + break; + + case 1: + byte b = buffer[ptr++]; + while (count-- != 0) + newBuf[newPtr++] = b; + break; + + case 2: + for (int i = 0; i < 4; i++) + fourBuf[i] = buffer[ptr++]; + while (count-- != 0) + for (int i = 0; i < 4; i++) + newBuf[newPtr++] = fourBuf[i]; + break; + + case 3: + b = buffer[ptr++]; + count *= 4; + while (count-- != 0) + newBuf[newPtr++] = b; + break; + } + } + + return newBuf; + } + + @Override + public String getText () + { + String auxText = ""; + StringBuilder text = new StringBuilder ("Image File : " + name); + text.append (String.format ("%nFile type : $%02X", fileType)); + + switch (fileType) + { + case 8: + if (auxType < 0x4000) + auxText = "Graphics File"; + else if (auxType == 0x4000) + auxText = "Packed Hi-Res File"; + else if (auxType == 0x4001) + auxText = "Packed Double Hi-Res File"; + break; + + case 192: + if (auxType == 1) + { + if (unpackedBuffer == null) + unpackedBuffer = unpackBytes (buffer); + auxText = "Packed Super Hi-Res Image"; + } + else if (auxType == 2) + auxText = "Super Hi-Res Image"; + else if (auxType == 3) + auxText = "Packed QuickDraw II PICT File"; + break; + + case 193: + if (auxType == 0) + auxText = "Super Hi-res Screen Image"; + else if (auxType == 1) + auxText = "QuickDraw PICT File"; + else if (auxType == 2) + auxText = "Super Hi-Res 3200 color image"; + } + + if (!auxText.isEmpty ()) + text.append (String.format ("%nAux type : $%04X %s", auxType, auxText)); + + text.append (String.format ("%nFile size : %,d", buffer.length)); + if (unpackedBuffer != null) + { + text.append (String.format ("%nUnpacked : %,d%n%n", unpackedBuffer.length)); + text.append (HexFormatter.format (unpackedBuffer)); + } + + return text.toString (); + } + + public static boolean isGif (byte[] buffer) + { + if (buffer.length < 6) + return false; + String text = new String (buffer, 0, 6); + return text.equals ("GIF89a") || text.equals ("GIF87a"); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/applefile/IconFile.java b/src/com/bytezone/diskbrowser/applefile/IconFile.java new file mode 100644 index 0000000..4a79aa4 --- /dev/null +++ b/src/com/bytezone/diskbrowser/applefile/IconFile.java @@ -0,0 +1,201 @@ +package com.bytezone.diskbrowser.applefile; + +import java.util.ArrayList; +import java.util.List; + +import com.bytezone.diskbrowser.HexFormatter; + +public class IconFile extends AbstractFile +{ + private final int iBlkNext; + private final int iBlkID; + private final int iBlkPath; + private final String iBlkName; + private final List icons = new ArrayList (); + + public IconFile (String name, byte[] buffer) + { + super (name, buffer); + + iBlkNext = HexFormatter.getLong (buffer, 0); + iBlkID = HexFormatter.getWord (buffer, 4); + iBlkPath = HexFormatter.getLong (buffer, 6); + iBlkName = HexFormatter.getHexString (buffer, 10, 16); + + int ptr = 26; + while (true) + { + int dataLen = HexFormatter.getWord (buffer, ptr); + if (dataLen == 0) + break; + icons.add (new Icon (buffer, ptr)); + ptr += dataLen; + } + } + + @Override + public String getText () + { + StringBuilder text = new StringBuilder ("Name : " + name + "\n\n"); + + text.append (String.format ("Next Icon file .. %d%n", iBlkNext)); + text.append (String.format ("Block ID ........ %d%n", iBlkID)); + text.append (String.format ("Block path ...... %d%n", iBlkPath)); + text.append (String.format ("Block name ...... %s%n", iBlkName)); + + text.append ("\n"); + for (Icon icon : icons) + { + text.append (icon); + text.append ("\n\n"); + } + + return text.toString (); + } + + class Icon + { + byte[] buffer; + int iDataLen; + String pathName; + String dataName; + int iDataType; + int iDataAux; + Image largeImage; + Image smallImage; + + public Icon (byte[] fullBuffer, int ptr) + { + iDataLen = HexFormatter.getWord (fullBuffer, ptr); + + buffer = new byte[iDataLen]; + System.arraycopy (fullBuffer, ptr, buffer, 0, buffer.length); + + int len = buffer[2] & 0xFF; + pathName = new String (buffer, 3, len); + + len = buffer[66] & 0xFF; + dataName = new String (buffer, 67, len); + + iDataType = HexFormatter.getWord (buffer, 82); + iDataAux = HexFormatter.getWord (buffer, 84); + + largeImage = new Image (buffer, 86); + smallImage = new Image (buffer, 86 + largeImage.size ()); + } + + @Override + public String toString () + { + StringBuilder text = new StringBuilder (); + text.append (String.format ("Data length .. %04X%n", iDataLen)); + text.append (String.format ("Path name .... %s%n", pathName)); + text.append (String.format ("Data name .... %s%n", dataName)); + text.append ("\n"); + text.append (largeImage); + text.append ("\n"); + text.append ("\n"); + text.append (smallImage); + return text.toString (); + } + } + + class Image + { + int iconType; + int iconSize; + int iconHeight; + int iconWidth; + byte[] main; + byte[] mask; + + public Image (byte[] buffer, int ptr) + { + iconType = HexFormatter.getWord (buffer, ptr); + iconSize = HexFormatter.getWord (buffer, ptr + 2); + iconHeight = HexFormatter.getWord (buffer, ptr + 4); + iconWidth = HexFormatter.getWord (buffer, ptr + 6); + + main = new byte[iconSize]; + mask = new byte[iconSize]; + + System.arraycopy (buffer, ptr + 8, main, 0, iconSize); + System.arraycopy (buffer, ptr + 8 + iconSize, mask, 0, iconSize); + } + + public int size () + { + return 8 + iconSize * 2; + } + + @Override + public String toString () + { + StringBuilder text = new StringBuilder (); + + text.append (String.format ("Icon type .... %04X%n", iconType)); + text.append (String.format ("Icon size .... %d%n", iconSize)); + text.append (String.format ("Icon height .. %d%n", iconHeight)); + text.append (String.format ("Icon width ... %d%n%n", iconWidth)); + appendIcon (text, main); + text.append ("\n\n"); + appendIcon (text, mask); + + return text.toString (); + } + + /* + Offset Color RGB Mini-Palette + + 0 Black 000 0 + 1 Blue 00F 1 + 2 Yellow FF0 2 + 3 White FFF 3 + 4 Black 000 0 + 5 Red D00 1 + 6 Green 0E0 2 + 7 White FFF 3 + + 8 Black 000 0 + 9 Blue 00F 1 + 10 Yellow FF0 2 + 11 White FFF 3 + 12 Black 000 0 + 13 Red D00 1 + 14 Green 0E0 2 + 15 White FFF 3 + + The displayMode word bits are defined as: + + Bit 0 selectedIconBit 1 = invert image before copying + Bit 1 openIconBit 1 = copy light-gray pattern instead of image + Bit 2 offLineBit 1 = AND light-gray pattern to image being copied + Bits 3-7 reserved. + Bits 8-11 foreground color to apply to black part of black & white icons + Bits 12-15 background color to apply to white part of black & white icons + + Bits 0-2 can occur at once and are tested in the order 1-2-0. + + "Color is only applied to the black and white icons if bits 15-8 are not all 0. + Colored pixels in an icon are inverted by black pixels becoming white and any + other color of pixel becoming black." + */ + + private void appendIcon (StringBuilder text, byte[] buffer) + { + int rowBytes = 1 + (iconWidth - 1) / 2; + for (int i = 0; i < main.length; i += rowBytes) + { + for (int ptr = i, max = i + rowBytes; ptr < max; ptr++) + { + int left = (byte) ((buffer[ptr] & 0xF0) >> 4); + int right = (byte) (buffer[ptr] & 0x0F); + text.append (String.format ("%X %X ", left, right)); + } + text.append ("\n"); + } + if (text.length () > 0) + text.deleteCharAt (text.length () - 1); + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/applefile/IntegerBasicProgram.java b/src/com/bytezone/diskbrowser/applefile/IntegerBasicProgram.java new file mode 100755 index 0000000..70829b8 --- /dev/null +++ b/src/com/bytezone/diskbrowser/applefile/IntegerBasicProgram.java @@ -0,0 +1,240 @@ +package com.bytezone.diskbrowser.applefile; + +import com.bytezone.diskbrowser.HexFormatter; + +public class IntegerBasicProgram extends AbstractFile +{ + private static String[] tokens = + { "?", "?", "?", " : ", "?", "?", "?", "?", "?", "?", "?", "?", "CLR", "?", "?", "?", + "HIMEM: ", "LOMEM: ", " + ", " - ", " * ", " / ", " = ", " # ", " >= ", " > ", + " <= ", " <> ", " < ", " AND ", " OR ", " MOD ", "^", "+", "(", ",", " THEN ", + " THEN ", ",", ",", "\"", "\"", "(", "!", "!", "(", "PEEK ", "RND ", "SGN", + "ABS", "PDL", "RNDX", "(", "+", "-", "NOT ", "(", "=", "#", "LEN(", "ASC(", + "SCRN(", ",", "(", "$", "$", "(", ", ", ",", ";", ";", ";", ",", ",", ",", + "TEXT", "GR ", "CALL ", "DIM ", "DIM ", "TAB ", "END", "INPUT ", "INPUT ", + "INPUT ", "FOR ", " = ", " TO ", " STEP ", "NEXT ", ",", "RETURN", "GOSUB ", + "REM ", "LET ", "GOTO ", "IF ", "PRINT ", "PRINT ", "PRINT", "POKE ", ",", + "COLOR=", "PLOT", ",", "HLIN", ",", " AT ", "VLIN ", ",", " AT ", "VTAB ", " = ", + " = ", ")", ")", "LIST ", ",", "LIST ", "POP ", "NODSP ", "NODSP ", "NOTRACE ", + "DSP ", "DSP ", "TRACE ", "PR#", "IN#", }; + + public IntegerBasicProgram (String name, byte[] buffer) + { + super (name, buffer); + } + + public String getText () + { + StringBuilder pgm = new StringBuilder (); + pgm.append ("Name : " + name + "\n"); + pgm.append ("Length : $" + HexFormatter.format4 (buffer.length) + " (" + buffer.length + + ")\n\n"); + int ptr = 0; + boolean looksLikeAssembler = checkForAssembler (); // this can probably go + boolean looksLikeSCAssembler = checkForSCAssembler (); + + while (ptr < buffer.length) + { + int lineLength = HexFormatter.intValue (buffer[ptr]); + /* + * It appears that lines ending in 00 are S-C Assembler programs, and + * lines ending in 01 are Integer Basic programs. + */ + int p2 = ptr + lineLength - 1; + if (p2 < 0 || (buffer[p2] != 1 && buffer[p2] != 0)) + { + pgm.append ("\nPossible assembler code follows\n"); + break; + } + if (lineLength <= 0) + break; + + if (looksLikeSCAssembler) + appendSCAssembler (pgm, ptr, lineLength); + else if (looksLikeAssembler) + appendAssembler (pgm, ptr, lineLength); + else + appendInteger (pgm, ptr, lineLength); + + pgm.append ("\n"); + ptr += lineLength; + } + + if (ptr < buffer.length) + { + int address = HexFormatter.intValue (buffer[ptr + 2], buffer[ptr + 3]); + int remainingBytes = buffer.length - ptr - 5; + byte[] newBuffer = new byte[remainingBytes]; + System.arraycopy (buffer, ptr + 4, newBuffer, 0, remainingBytes); + AssemblerProgram ap = new AssemblerProgram ("embedded", newBuffer, address); + pgm.append ("\n" + ap.getText () + "\n"); + } + + pgm.deleteCharAt (pgm.length () - 1); + return pgm.toString (); + } + + private void appendAssembler (StringBuilder pgm, int ptr, int lineLength) + { + for (int i = ptr + 3; i < ptr + lineLength - 1; i++) + { + if ((buffer[i] & 0x80) == 0x80) + { + int spaces = buffer[i] & 0x0F; + for (int j = 0; j < spaces; j++) + pgm.append (' '); + continue; + } + int b = HexFormatter.intValue (buffer[i]); + pgm.append ((char) b); + } + } + + private boolean checkForAssembler () + { + int ptr = 0; + + while (ptr < buffer.length) + { + int lineLength = HexFormatter.intValue (buffer[ptr]); + int p2 = ptr + lineLength - 1; + if (p2 < 0 || (buffer[p2] != 1 && buffer[p2] != 0)) + break; + if (lineLength <= 0) // in case of looping bug + break; + // check for comments + if (buffer[ptr + 3] == 0x3B || buffer[ptr + 3] == 0x2A) + return true; + ptr += lineLength; + } + return false; + } + + private boolean checkForSCAssembler () + { + int lineLength = HexFormatter.intValue (buffer[0]); + if (lineLength <= 0) + return false; + return buffer[lineLength - 1] == 0; + } + + private void appendSCAssembler (StringBuilder pgm, int ptr, int lineLength) + { + int lineNumber = + HexFormatter.intValue (buffer[ptr + 2]) * 256 + + HexFormatter.intValue (buffer[ptr + 1]); + pgm.append (String.format ("%4d: ", lineNumber)); + int p2 = ptr + 3; + while (buffer[p2] != 0) + { + if (buffer[p2] == (byte) 0xC0) + { + int repeat = buffer[p2 + 1]; + for (int i = 0; i < repeat; i++) + pgm.append ((char) buffer[p2 + 2]); + p2 += 2; + } + else if ((buffer[p2] & 0x80) != 0) + { + int spaces = buffer[p2] & 0x7F; + for (int i = 0; i < spaces; i++) + pgm.append (' '); + } + else + pgm.append ((char) buffer[p2]); + p2++; + } + } + + private void appendInteger (StringBuilder pgm, int ptr, int lineLength) + { + int lineNumber = HexFormatter.intValue (buffer[ptr + 1], buffer[ptr + 2]); + + boolean inString = false; + boolean inRemark = false; + + String lineText = String.format ("%5d ", lineNumber); + int lineTab = lineText.length (); + pgm.append (lineText); + + for (int p = ptr + 3; p < ptr + lineLength - 1; p++) + { + int b = HexFormatter.intValue (buffer[p]); + + if (b == 0x03 // token for colon (:) + && !inString && !inRemark && buffer[p + 1] != 1) // not end of + // line + { + pgm.append (":\n" + " ".substring (0, lineTab)); + continue; + } + + if (b >= 0xB0 && b <= 0xB9 // numeric literal + && (buffer[p - 1] & 0x80) == 0 // not a variable name + && !inString && !inRemark) + { + pgm.append (HexFormatter.intValue (buffer[p + 1], buffer[p + 2])); + p += 2; + continue; + } + + if (b >= 128) + { + b -= 128; + if (b >= 32) + pgm.append ((char) b); + else + pgm.append (""); + } + else if (!tokens[b].equals ("?")) + { + pgm.append (tokens[b]); + if ((b == 40 || b == 41) && !inRemark) // double quotes + inString = !inString; + if (b == 0x5D) + inRemark = true; + } + else + pgm.append (" ." + HexFormatter.format2 (b) + ". "); + } + } + + public String getHexDump () + { + if (false) + return super.getHexDump (); + + StringBuffer pgm = new StringBuffer (); + + pgm.append ("Name : " + name + "\n"); + pgm.append ("Length : $" + HexFormatter.format4 (buffer.length) + " (" + buffer.length + + ")\n\n"); + + int ptr = 0; + + while (ptr < buffer.length) + { + int lineLength = HexFormatter.intValue (buffer[ptr]); + int p2 = ptr + lineLength - 1; + if (p2 < 0 || buffer[p2] > 1) + { + System.out.println ("invalid line"); + break; + } + pgm.append (HexFormatter.formatNoHeader (buffer, ptr, lineLength)); + pgm.append ("\n"); + if (lineLength <= 0) + { + System.out.println ("looping"); + break; + } + ptr += lineLength; + pgm.append ("\n"); + } + + if (pgm.length () > 0) + pgm.deleteCharAt (pgm.length () - 1); + + return pgm.toString (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/applefile/LodeRunner.java b/src/com/bytezone/diskbrowser/applefile/LodeRunner.java new file mode 100644 index 0000000..7cd8148 --- /dev/null +++ b/src/com/bytezone/diskbrowser/applefile/LodeRunner.java @@ -0,0 +1,93 @@ +package com.bytezone.diskbrowser.applefile; + +import com.bytezone.diskbrowser.HexFormatter; + +public class LodeRunner extends AbstractFile +{ + public LodeRunner (String name, byte[] buffer) + { + super (name, buffer); + } + + @Override + public String getText () + { + StringBuilder text = new StringBuilder (); + text.append ("Lode Runner Level\n\n"); + + for (int level = 0; level < 150; level++) + { + int ptr = level * 256 + 226; + String levelName = ""; + if (buffer[ptr] != 0 && buffer[ptr] != (byte) 0xFF) + levelName = HexFormatter.sanitiseString (buffer, ptr, 15); + text.append (String.format ("Level %d %s%n%n", level + 1, levelName)); + + ptr = 0; + for (int i = level * 256, max = i + 224; i < max; i++) + { + String val = String.format ("%02X", buffer[i]); + text = addPosition (text, val.charAt (0)); + text.append (' '); + text = addPosition (text, val.charAt (1)); + text.append (' '); + if (++ptr % 14 == 0) + text.append ("\n"); + } + text.append ("\n\n"); + } + + return text.toString (); + } + + private StringBuilder addPosition (StringBuilder text, char c) + { + switch (c) + { + case '0': + text.append (' '); // space + break; + + case '1': + text.append ('-'); // diggable floor + break; + + case '2': + text.append ('='); // undiggable floor + break; + + case '3': + text.append ('+'); // ladder + break; + + case '4': + text.append ('^'); // hand over hand bar + break; + + case '5': + text.append ('~'); // trap door + break; + + case '6': + text.append ('#'); // hidden ladder + break; + + case '7': + text.append ('$'); // gold + break; + + case '8': + text.append ('*'); // enemy + break; + + case '9': + text.append ('x'); // player + break; + + default: + text.append (c); + } + + return text; + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/applefile/MerlinSource.java b/src/com/bytezone/diskbrowser/applefile/MerlinSource.java new file mode 100644 index 0000000..51f900d --- /dev/null +++ b/src/com/bytezone/diskbrowser/applefile/MerlinSource.java @@ -0,0 +1,95 @@ +package com.bytezone.diskbrowser.applefile; + +public class MerlinSource extends AbstractFile +{ + int ptr; + private static int[] tabs = { 12, 19, 35 }; + private static int TAB_POS = tabs[2]; + private final int recordLength; + private final int eof; + + // Source : Prodos text file + public MerlinSource (String name, byte[] buffer, int recordLength, int eof) + { + super (name, buffer); + this.eof = eof; + this.recordLength = recordLength; + } + + // Source : Dos binary file + public MerlinSource (String name, byte[] buffer) + { + super (name, buffer); + this.eof = 0; + this.recordLength = 0; + } + + @Override + public String getText () + { + StringBuilder text = new StringBuilder (); + + text.append ("Name : " + name + "\n"); + if (recordLength > 0) // a prodos text file + { + text.append (String.format ("Record length : %,8d%n", recordLength)); + text.append (String.format ("End of file : %,8d%n", eof)); + } + else + text.append (String.format ("End of file : %,8d%n", buffer.length)); + text.append ("\n"); + + ptr = 0; + while (ptr < buffer.length && buffer[ptr] != 0) + text.append (getLine () + "\n"); + if (text.length () > 0) + text.deleteCharAt (text.length () - 1); + return text.toString (); + } + + private String getLine () + { + StringBuilder line = new StringBuilder (); + boolean comment = false; + boolean string = false; + while (ptr < buffer.length) + { + int val = buffer[ptr++] & 0x7F; + if (val == 0x0D) + break; + if (val == '*' && line.length () == 0) + comment = true; + if (val == '"') + string = !string; + if (val == ';' && !comment) + { + comment = true; + while (line.length () < TAB_POS) + line.append (' '); + } + if (val == ' ' && !comment && !string) + { + line = tab (line); + if (line.length () >= tabs[2]) + comment = true; + } + else + line.append ((char) val); + } + return line.toString (); + } + + private StringBuilder tab (StringBuilder text) + { + int nextTab = 0; + for (int tab : tabs) + if (text.length () < tab) + { + nextTab = tab; + break; + } + while (text.length () < nextTab) + text.append (' '); + return text; + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/applefile/PascalCode.java b/src/com/bytezone/diskbrowser/applefile/PascalCode.java new file mode 100755 index 0000000..a4ba3b8 --- /dev/null +++ b/src/com/bytezone/diskbrowser/applefile/PascalCode.java @@ -0,0 +1,69 @@ +package com.bytezone.diskbrowser.applefile; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import com.bytezone.diskbrowser.HexFormatter; + +public class PascalCode extends AbstractFile implements PascalConstants, Iterable +{ + List segments = new ArrayList (16); + String codeName; + String comment; + + public static void print () + { + for (int i = 0; i < 216; i++) + System.out.printf ("%3d %d %3s %s%n", i + 128, PascalConstants.mnemonicSize[i], + PascalConstants.mnemonics[i], PascalConstants.descriptions[i]); + } + + public PascalCode (String name, byte[] buffer) + { + super (name, buffer); + int nonameCounter = 0; + + // Build segment list (up to 16 segments) + for (int i = 0; i < 16; i++) + { + codeName = HexFormatter.getString (buffer, 0x40 + i * 8, 8).trim (); + int size = HexFormatter.intValue (buffer[i * 4 + 2], buffer[i * 4 + 3]); + if (size > 0) + { + if (codeName.length () == 0) + codeName = ""; + segments.add (new PascalSegment (codeName, buffer, i)); + } + } + comment = HexFormatter.getPascalString (buffer, 0x1B0); + } + + public String getText () + { + StringBuilder text = new StringBuilder (getHeader ()); + + text.append ("Segment Dictionary\n==================\n\n"); + + text.append ("Slot Addr Len Len Name Kind" + + " Text Seg# Mtyp Vers I/S\n"); + text.append ("---- ---- ----- ----- -------- ---------------" + + " ---- ---- ---- ---- ---\n"); + + for (PascalSegment segment : segments) + text.append (segment.toText () + "\n"); + text.append ("\nComment : " + comment + "\n\n"); + + return text.toString (); + } + + private String getHeader () + { + return "Name : " + name + "\n\n"; + } + + public Iterator iterator () + { + return segments.iterator (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/applefile/PascalCodeStatement.java b/src/com/bytezone/diskbrowser/applefile/PascalCodeStatement.java new file mode 100755 index 0000000..156fef8 --- /dev/null +++ b/src/com/bytezone/diskbrowser/applefile/PascalCodeStatement.java @@ -0,0 +1,329 @@ +package com.bytezone.diskbrowser.applefile; + +import java.util.ArrayList; +import java.util.List; + +import com.bytezone.diskbrowser.HexFormatter; + +public class PascalCodeStatement implements PascalConstants +{ + private static final String[] compValue = + { "invalid", "", "REAL", "", "STR", "", "BOOL", "", "POWR", "", "BYT", "", "WORD" }; + + int length; + int val; + int p1, p2, p3; + String mnemonic; + String extras = ""; + String description; + String text; + int ptr; // temp + byte[] buffer; + boolean jumpTarget; + List jumps = new ArrayList (); + + public PascalCodeStatement (byte[] buffer, int ptr, int procPtr) + { + this.ptr = ptr; + this.buffer = buffer; + length = 1; + val = buffer[ptr] & 0xFF; + if (val <= 127) + { + mnemonic = "SLDC"; + extras = "#" + val; + description = "Short load constant - push #" + val; + } + else if (val >= 248) + { + mnemonic = "SIND"; + extras = "#" + (val - 248); + description = "Short index load - push word *ToS + #" + (val - 248); + } + else if (val >= 232) + { + mnemonic = "SLDO"; + extras = "#" + (val - 231); + description = "Short load global - push BASE + #" + (val - 231); + } + else if (val >= 216) + { + mnemonic = "SLDL"; + extras = "#" + (val - 215); + description = "Short load local - push MP + #" + (val - 215); + } + else + { + mnemonic = mnemonics[val - 128]; + description = descriptions[val - 128]; + + length = mnemonicSize[val - 128]; + if (length != 1) + { + switch (val) + { + // W1, W2, W3, - word aligned case jump + case 172: //XJP + int padding = (ptr % 2) == 0 ? 1 : 0; + p1 = getWord (buffer, ptr + padding + 1); + p2 = getWord (buffer, ptr + padding + 3); + p3 = getWord (buffer, ptr + padding + 5); + length = (p2 - p1 + 1) * 2 + 7 + padding; + setParameters (p1, p2, String.format ("%04X", p3)); + int v = p1; + int min = ptr + padding + 7; + int max = min + (p2 - p1) * 2; + for (int i = min; i <= max; i += 2) + { + jumps.add (new Jump (i, i - HexFormatter.intValue (buffer[i], buffer[i + 1]), v++)); + } + break; + + // UB, - word aligned + case 179: //LDC + p1 = buffer[ptr + 1] & 0xFF; + padding = ptr % 2 == 0 ? 0 : 1; + length = p1 * 2 + padding + 2; + setParameters (p1); + break; + + // UB, + case 166: // LSA + case 208: // LPA + p1 = buffer[ptr + 1] & 0xFF; + length = p1 + 2; + if (val == 166) + { + text = HexFormatter.getPascalString (buffer, ptr + 1); + description += ": " + text; + } + break; + + // W + case 199: // LDCI + p1 = getWord (buffer, ptr + 1); + setParameters (p1); + break; + + // B + case 162: // INC + case 163: // IND + case 164: // IXA + case 165: // LAO + case 168: // MOV + case 169: // LDO + case 171: // SRO + case 198: // LLA + case 202: // LDL + case 204: // STL + case 213: // BPT + length = getLengthOfB (buffer[ptr + 1]) + 1; + p1 = getValueOfB (buffer, ptr + 1, length - 1); + setParameters (p1); + break; + + // DB, B or UB, B + case 157: // LDE + case 167: // LAE + case 178: // LDA + case 182: // LOD + case 184: // STR + case 209: // STE + length = getLengthOfB (buffer[ptr + 2]) + 2; + p1 = buffer[ptr + 1] & 0xFF; + p2 = getValueOfB (buffer, ptr + 2, length - 2); + setParameters (p1, p2); + break; + + // UB1, UB2 + case 192: // IXP + case 205: // CXP + p1 = buffer[ptr + 1] & 0xFF; + p2 = buffer[ptr + 2] & 0xFF; + setParameters (p1, p2); + break; + + // SB or DB + case 161: // FJP + case 173: // RNP + case 185: // UJP + case 193: // RBP + case 211: // EFJ + case 212: // NFJ + p1 = buffer[ptr + 1]; + if (val == 173 || val == 193) // return from procedure + setParameters (p1); + else if (p1 < 0) + { + // look up jump table entry + int address = procPtr + p1; + int ptr2 = address - ((buffer[address + 1] & 0xFF) * 256 + (buffer[address] & 0xFF)); + extras = String.format ("$%04X", ptr2); + jumps.add (new Jump (ptr, ptr2)); + } + else + { + int address = ptr + length + p1; + extras = String.format ("$%04X", address); + jumps.add (new Jump (ptr, address)); + } + break; + + // UB + case 160: // AOJ + case 170: // SAS + case 174: // CIP + case 188: // LDM + case 189: // STM + case 194: // CBP + case 206: // CLP + case 207: // CGP + p1 = buffer[ptr + 1] & 0xFF; + setParameters (p1); + break; + + // CSP + case 158: + p1 = buffer[ptr + 1]; + description = "Call standard procedure - " + CSP[p1]; + break; + + // Non-integer comparisons + case 175: + case 176: + case 177: + case 180: + case 181: + case 183: + p1 = buffer[ptr + 1]; // 2/4/6/8/10/12 + if (p1 < 0 || p1 >= compValue.length) + { + System.out.printf ("%d %d %d%n", val, p1, ptr); + mnemonic += "******************************"; + break; + } + mnemonic += compValue[p1]; + if (p1 == 10 || p1 == 12) + { + length = getLengthOfB (buffer[ptr + 2]) + 2; + p2 = getValueOfB (buffer, ptr + 2, length - 2); + setParameters (p2); + } + break; + + default: + System.out.println ("Forgot : " + val); + } + } + } + } + + private int getWord (byte[] buffer, int ptr) + { + return (buffer[ptr + 1] & 0xFF) * 256 + (buffer[ptr] & 0xFF); + } + + private int getLengthOfB (byte b) + { + return (b & 0x80) == 0x80 ? 2 : 1; + } + + private int getValueOfB (byte[] buffer, int ptr, int length) + { + if (length == 2) + return (buffer[ptr] & 0x7F) * 256 + (buffer[ptr + 1] & 0xFF); + return buffer[ptr] & 0xFF; + } + + private void setParameters (int p1) + { + description = description.replaceFirst (":1", p1 + ""); + extras = "#" + p1; + } + + private void setParameters (int p1, int p2) + { + setParameters (p1); + extras += ", #" + p2; + description = description.replaceFirst (":2", p2 + ""); + } + + private void setParameters (int p1, int p2, String p3) + { + setParameters (p1, p2); + description = description.replaceFirst (":3", p3); + } + + public String toString () + { + String hex = getHex (buffer, ptr, length > 4 ? 4 : length); + StringBuilder text = new StringBuilder (); + text.append (String.format ("%2s%05X: %-11s %-6s %-8s %s%n", jumpTarget ? "->" : "", ptr, + hex, mnemonic, extras, description)); + if (length > 4) + { + int bytesLeft = length - 4; + int jmp = 0; + int p = ptr + 4; + while (bytesLeft > 0) + { + String line = getHex (buffer, p, (bytesLeft > 4) ? 4 : bytesLeft); + text.append (" " + line); + if (jumps.size () > 0) + { + if (jmp < jumps.size ()) + text.append (" " + jumps.get (jmp++)); + if (jmp < jumps.size ()) + text.append (" " + jumps.get (jmp++)); + } + text.append ("\n"); + bytesLeft -= 4; + p += 4; + } + } + return text.toString (); + } + + private String getHex (byte[] buffer, int offset, int length) + { + if ((offset + length) >= buffer.length) + { + System.out.println ("too many"); + return "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; + } + StringBuilder text = new StringBuilder (); + for (int i = 0; i < length; i++) + text.append (String.format ("%02X ", buffer[offset + i])); + if (text.length () > 0) + text.deleteCharAt (text.length () - 1); + return text.toString (); + } + + class Jump + { + int addressFrom; + int addressTo; + boolean caseJump; + int caseValue; + + public Jump (int addressFrom, int addressTo) + { + this.addressFrom = addressFrom; + this.addressTo = addressTo; + } + + public Jump (int addressFrom, int addressTo, int value) + { + this (addressFrom, addressTo); + this.caseValue = value; + this.caseJump = true; + } + + public String toString () + { + if (caseJump) + return String.format ("%3d: %04X", caseValue, addressTo); + return String.format ("%04X", addressTo); + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/applefile/PascalConstants.java b/src/com/bytezone/diskbrowser/applefile/PascalConstants.java new file mode 100755 index 0000000..02433b6 --- /dev/null +++ b/src/com/bytezone/diskbrowser/applefile/PascalConstants.java @@ -0,0 +1,118 @@ +package com.bytezone.diskbrowser.applefile; + +public interface PascalConstants +{ + static String[] mnemonics = + { "ABI", "ABR", "ADI", "ADR", "LAND", "DIF", "DVI", "DVR", "CHK", "FLO", "FLT", + "INN", "INT", "LOR", "MODI", "MPI", "MPR", "NGI", "NGR", "LNOT", "SRS", "SBI", + "SBR", "SGS", "SQI", "SQR", "STO", "IXS", "UNI", "LDE", "CSP", "LDCN", "ADJ", + "FJP", "INC", "IND", "IXA", "LAO", "LSA", "LAE", "MOV", "LDO", "SAS", "SRO", "XJP", + "RNP", "CIP", "EQU", "GEQ", "GRT", "LDA", "LDC", "LEQ", "LES", "LOD", "NEQ", "STR", + "UJP", "LDP", "STP", "LDM", "STM", "LDB", "STB", "IXP", "RBP", "CBP", "EQUI", + "GEQI", "GRTI", "LLA", "LDCI", "LEQI", "LESI", "LDL", "NEQI", "STL", "CXP", "CLP", + "CGP", "LPA", "STE", "???", "EFJ", "NFJ", "BPT", "XIT", "NOP" }; + + static int[] mnemonicSize = + // + // 128 - 155 + // 156 - 183 + // 184 - 211 + // 212 - 239 + // 240 - 255 + + { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 3, 2, 1, 2, 2, 2, 2, 2, 2, 0, 3, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 3, 0, 2, 2, 3, 2, + 3, 2, 1, 1, 2, 2, 1, 1, 3, 2, 2, 1, 1, 1, 2, 3, 1, 1, 2, 1, 2, 3, 2, 2, 0, 3, 1, 2, + 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }; + + static String[] descriptions = + { + "Absolute value of integer - push ABS(ToS)", + "Absolute value of real - push abs((real)ToS)", + "Add integers (tos + tos-1)", + "Add reals - push ToS + ToS-1", + "Logical AND", + "Set difference - push difference of sets ToS-1 and ToS", + "Divide integers - push ToS-1 / ToS", + "Divide reals - push ToS-1 / ToS", + "Check subrange bounds - assert ToS-1 <= ToS-2 <= ToS, pop ToS, pop ToS-1", + "Float next-to-ToS - push integer ToS-1 after converting to a real", + "Float ToS - push integer ToS after converting to a float", + "Set Membership - if int ToS-1 is in set ToS, push true, else push false", + "Set Intersection - push TOS AND TOS-1", + "Logical OR", + "Modulo integers - push ToS-1 % ToS", + "Multiply TOS by TOS-1", + "Multiply reals - push ToS-1 * ToS", + "Negate Integer - push two's complement of ToS", + "Negate real - push -((real)ToS)", + "Logical Not - push one's complement of ToS", + "Build a subrange set", + "Subtract Integers push ToS-1 - ToS", + "Subtract reals - push ToS-1 - ToS", + "Build a singleton set", + "Square integer - push ToS ^ 2", + "Square real - push ToS ^ 2", + "Store indirect word - store ToS into word pointed to by ToS-1", + "Index string array - push &(*ToS-1 + ToS)", + "Set union - push union of sets ToS OR ToS-1", + "Load extended word - push word at segment :1+:2", + "Call Standard Procedure #:1 - ", + "Load Constant NIL", + "Adjust set", + "Jump if ToS false", + "Increment field ptr - push ToS+:1", + "Static index and load word", + "Compute word pointer from ToS-1 + ToS * :1 words", + "Load Global - push (BASE+:1)", + "Load constant string address", + "Load extended address - push address of word at segment :1+:2", + "Move words - transfer :1 words from *ToS to *ToS-1", + "Load Global Word - push BASE+:1", + "String Assign", + "Store TOS into BASE+:1", + "Case Jump - :1::2, Error: :3", + "Return from non-base procedure (pass :1 words)", + "Call intermediate procedure #:1", + "ToS-1 == ToS", + "ToS-1 >= ToS", + "ToS-1 > ToS", + "Load Intermediate Address - push :1th activation record +:2 bytes", + "Load multi-word constant - :1 words", + "ToS-1 <= ToS", + "ToS-1 < ToS", + "Load Intermediate Word - push :1th activation record +:2 bytes", + "ToS-1 <> ToS", + "Store intermediate word - store TOS into :2, traverse :1", + "Unconditional jump", + "Load Packed Field - push *ToS", + "Store into packed field", + "Load multiple words - push block of unsigned bytes at *ToS", + "Store multiple words - store block of UB at ToS to *ToS-1", + "Load Byte - index the byte pointer ToS-1 by integer index ToS and push that byte", + "Store Byte - index the byte pointer ToS-2 by integer index ToS-1 and move ToS to that location", + "Index packed array - do complicated stuff with :1 and :2", + "Return from base procedure (pass :1 words)", + "Call Base Procedure :1 at lex level -1 or 0", "Compare Integer : ToS-1 = ToS", + "Compare Integer : TOS-1 >= TOS", "Compare Integer : TOS-1 > ToS", + "Load Local Address - push MP+:1", "Load Word - push #:1", + "Compare Integer : TOS-1 <= TOS", "Compare Integer : TOS-1 < ToS", + "Load Local Word - push MP+:1", "Compare Integer : TOS-1 <> TOS", + "Store Local Word - store ToS into MP+:1", + "Call external procedure #:2 in segment #:1", "Call local procedure #:1", + "Call global procedure #:1", "Load a packed array - use :1 and :2", + "Store extended word - store ToS into word at segment :1+:2", "210 ", + "Equal false jump - jump :1 if ToS-1 <> ToS", + "Not equal false jump - jump :1 if ToS-1 == ToS", + "Breakpoint - not used (does NOP)", "Exit OS - cold boot", "No-op" }; + + static String[] CSP = + { "000", "NEW", "MVL", "MVR", "EXIT", "", "", "IDS", "TRS", "TIM", "FLC", "SCN", "", + "", "", "", "", "", "", "", "", "021", "TNC", "RND", "", "", "", "", "", "", "", + "MRK", "RLS", "33", "34", "POT", "36", "37", "38", "39", "40" }; + + static String[] SegmentKind = + { "Linked", "HostSeg", "SegProc", "UnitSeg", "SeprtSeg", "UnlinkedIntrins", + "LinkedIntrins", "DataSeg" }; +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/applefile/PascalInfo.java b/src/com/bytezone/diskbrowser/applefile/PascalInfo.java new file mode 100644 index 0000000..c584801 --- /dev/null +++ b/src/com/bytezone/diskbrowser/applefile/PascalInfo.java @@ -0,0 +1,29 @@ +package com.bytezone.diskbrowser.applefile; + +public class PascalInfo extends AbstractFile +{ + + public PascalInfo (String name, byte[] buffer) + { + super (name, buffer); + } + + @Override + public String getText () + { + StringBuilder text = new StringBuilder (getHeader ()); + + for (int i = 0; i < buffer.length; i++) + if (buffer[i] == 0x0D) + text.append ("\n"); + else + text.append ((char) buffer[i]); + + return text.toString (); + } + + private String getHeader () + { + return "Name : " + name + "\n\n"; + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/applefile/PascalProcedure.java b/src/com/bytezone/diskbrowser/applefile/PascalProcedure.java new file mode 100755 index 0000000..dd18bdc --- /dev/null +++ b/src/com/bytezone/diskbrowser/applefile/PascalProcedure.java @@ -0,0 +1,175 @@ +package com.bytezone.diskbrowser.applefile; + +import java.util.ArrayList; +import java.util.List; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.applefile.PascalCodeStatement.Jump; + +public class PascalProcedure +{ + // all procedures have these fields + byte[] buffer; + int procOffset; + int offset; + int slot; + boolean valid; + + // only valid procedures have these fields + int procedureNo; + int procLevel; + int codeStart; + int codeEnd; + int parmSize; + int dataSize; + List statements = new ArrayList (); + AssemblerProgram assembler; + int jumpTable = -8; + + public PascalProcedure (byte[] buffer, int slot) + { + this.buffer = buffer; + this.slot = slot; + int p = buffer.length - 2 - slot * 2; + offset = HexFormatter.intValue (buffer[p], buffer[p + 1]); + procOffset = p - offset; + valid = procOffset > 0; + + if (valid) + { + procedureNo = buffer[procOffset] & 0xFF; + procLevel = buffer[procOffset + 1]; + codeStart = HexFormatter.intValue (buffer[procOffset - 2], buffer[procOffset - 1]); + codeEnd = HexFormatter.intValue (buffer[procOffset - 4], buffer[procOffset - 3]); + parmSize = HexFormatter.intValue (buffer[procOffset - 6], buffer[procOffset - 5]); + dataSize = HexFormatter.intValue (buffer[procOffset - 8], buffer[procOffset - 7]); + } + } + + private void decode () + { + if (statements.size () > 0 || assembler != null) + return; + int ptr = procOffset - codeStart - 2; + int max = procOffset + jumpTable; + + if (codeEnd == 0) + { + int len = codeStart + jumpTable + 2; + if (len > 0) + { + byte[] asmBuf = new byte[len]; + System.arraycopy (buffer, ptr, asmBuf, 0, len); + assembler = new AssemblerProgram ("Proc", asmBuf, ptr); + } + return; + } + + while (ptr < max) + { + PascalCodeStatement cs = new PascalCodeStatement (buffer, ptr, procOffset); + if (cs.length <= 0) + { + System.out.println ("error - length <= 0 : " + cs); + break; + } + statements.add (cs); + if (cs.val == 185 || cs.val == 161) + if (cs.p1 < jumpTable) + { + jumpTable = cs.p1; + max = procOffset + jumpTable; + } + ptr += cs.length; + } + + // Tidy up left-over bytes at the end + if (statements.size () > 1) + { + PascalCodeStatement lastStatement = statements.get (statements.size () - 1); + PascalCodeStatement secondLastStatement = statements.get (statements.size () - 2); + if (lastStatement.val == 0 + && (secondLastStatement.val == 0xD6 || secondLastStatement.val == 0xC1 || secondLastStatement.val == 0xAD)) + statements.remove (statements.size () - 1); + } + + // Mark statements that are jump targets + int actualEnd = procOffset - codeEnd - 4; + for (PascalCodeStatement cs : statements) + { + if (cs.ptr == actualEnd) + { + cs.jumpTarget = true; + continue; + } + for (Jump cj : cs.jumps) + for (PascalCodeStatement cs2 : statements) + if (cs2.ptr == cj.addressTo) + { + cs2.jumpTarget = true; + break; + } + } + } + + public List extractStrings () + { + decode (); + List strings = new ArrayList (); + for (PascalCodeStatement cs : statements) + if (cs.val == 166) + strings.add (cs); + return strings; + } + + public String toString () + { + if (!valid) + return ""; + decode (); + + StringBuilder text = new StringBuilder ("\nProcedure Header\n================\n\n"); + + if (false) + text.append (HexFormatter.format (buffer, procOffset + jumpTable, 2 - jumpTable) + "\n\n"); + + text.append (String.format ("Level.......%5d %02X%n", procLevel, procLevel & 0xFF)); + text.append (String.format ("Proc no.....%5d %02X%n", procedureNo, procedureNo)); + text.append (String.format ("Code entry..%5d %04X (%04X - %04X = %04X)%n", codeStart, + codeStart, (procOffset - 2), codeStart, (procOffset - codeStart - 2))); + text.append (String.format ("Code exit...%5d %04X", codeEnd, codeEnd)); + if (codeEnd > 0) + text.append (String.format (" (%04X - %04X = %04X)%n", (procOffset - 4), codeEnd, + (procOffset - codeEnd - 4))); + else + text.append (String.format ("%n")); + text.append (String.format ("Parm size...%5d %04X%n", parmSize, parmSize)); + text.append (String.format ("Data size...%5d %04X%n%n", dataSize, dataSize)); + + text.append ("Procedure Code\n==============\n\n"); + + int ptr = procOffset - codeStart - 2; + if (false) + text.append (HexFormatter.format (buffer, ptr, codeStart + jumpTable + 2) + "\n\n"); + + if (codeEnd == 0) + text.append (assembler.getAssembler () + "\n"); + else + { + for (PascalCodeStatement cs : statements) + text.append (cs); + + if (jumpTable < -8 && false) + { + text.append ("\nJump table:\n"); + for (int i = procOffset + jumpTable; i < procOffset - 8; i += 2) + { + ptr = i - ((buffer[i + 1] & 0xFF) * 256 + (buffer[i] & 0xFF)); + text.append (String.format ("%05X : %02X %02X --> %04X%n", i, buffer[i], buffer[i + 1], + ptr)); + } + } + } + return text.toString (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/applefile/PascalSegment.java b/src/com/bytezone/diskbrowser/applefile/PascalSegment.java new file mode 100755 index 0000000..fe99913 --- /dev/null +++ b/src/com/bytezone/diskbrowser/applefile/PascalSegment.java @@ -0,0 +1,134 @@ +package com.bytezone.diskbrowser.applefile; + +import java.util.ArrayList; +import java.util.List; + +import com.bytezone.diskbrowser.FileFormatException; +import com.bytezone.diskbrowser.HexFormatter; + +public class PascalSegment extends AbstractFile implements PascalConstants +{ + int segmentNoHeader; + int segmentNoBody; + public final int blockNo; + public final int size; + List procedures; + int segKind; + int textAddress; + int machineType; + int version; + int intrinsSegs1; + int intrinsSegs2; + int slot; + int totalProcedures; + + public PascalSegment (String name, byte[] fullBuffer, int seq) + { + super (name, fullBuffer); // sets this.buffer to the full buffer temporarily + this.slot = seq; + this.blockNo = HexFormatter.intValue (fullBuffer[seq * 4], fullBuffer[seq * 4 + 1]); + this.size = HexFormatter.intValue (fullBuffer[seq * 4 + 2], fullBuffer[seq * 4 + 3]); + this.segmentNoHeader = fullBuffer[0x100 + seq * 2]; + segKind = + HexFormatter.intValue (fullBuffer[0xC0 + seq * 2], fullBuffer[0xC0 + seq * 2 + 1]); + textAddress = + HexFormatter.intValue (fullBuffer[0xE0 + seq * 2], fullBuffer[0xE0 + seq * 2 + 1]); + int flags = fullBuffer[0x101 + seq * 2] & 0xFF; + machineType = flags & 0x0F; + version = (flags & 0xD0) >> 5; + intrinsSegs1 = + HexFormatter.intValue (fullBuffer[0x120 + seq * 4], fullBuffer[0x120 + seq * 4 + 1]); + intrinsSegs2 = + HexFormatter.intValue (fullBuffer[0x120 + seq * 4 + 2], + fullBuffer[0x120 + seq * 4 + 3]); + + int offset = blockNo * 512; + if (offset < fullBuffer.length) + { + buffer = new byte[size]; // replaces this.buffer with the segment buffer only + System.arraycopy (fullBuffer, blockNo * 512, buffer, 0, size); + totalProcedures = buffer[size - 1] & 0xFF; + segmentNoBody = buffer[size - 2] & 0xFF; + if (segmentNoBody != segmentNoHeader) + System.out.println ("Segment number mismatch : " + segmentNoBody + " / " + + segmentNoHeader); + } + else + { + System.out.println ("Error in blocksize for pascal disk"); + throw new FileFormatException ("Error in PascalSegment"); + } + } + + private void buildProcedureList () + { + procedures = new ArrayList (totalProcedures); + + for (int i = 1; i <= totalProcedures; i++) + procedures.add (new PascalProcedure (buffer, i)); + } + + public String toText () + { + return String + .format (" %2d %02X %04X %,6d %-8s %-15s %3d %3d %d %d %d %d", + slot, blockNo, size, size, name, SegmentKind[segKind], textAddress, + segmentNoHeader, machineType, version, intrinsSegs1, intrinsSegs2); + } + + @Override + public String getText () + { + if (procedures == null) + buildProcedureList (); + + StringBuilder text = new StringBuilder (); + String title = "Segment - " + name; + text.append (title + "\n" + + "===============================".substring (0, title.length ()) + "\n\n"); + String warning = + segmentNoBody == segmentNoHeader ? "" : " (" + segmentNoHeader + " in header)"; + text.append (String.format ("Address........ %02X%n", blockNo)); + text.append (String.format ("Length......... %04X%n", buffer.length)); + text.append (String.format ("Machine type... %d%n", machineType)); + text.append (String.format ("Version........ %d%n", version)); + text.append (String.format ("Segment........ %d%s%n", segmentNoBody, warning)); + text.append (String.format ("Total procs.... %d%n", procedures.size ())); + + text.append ("\nProcedure Dictionary\n====================\n\n"); + + int len = procedures.size () * 2 + 2; + if (false) + text.append (HexFormatter.format (buffer, buffer.length - len, len) + "\n\n"); + + text.append ("Proc Offset Lvl Entry Exit Parm Data Proc header\n"); + text.append ("---- ------ --- ----- ---- ---- ---- --------------------\n"); + for (PascalProcedure procedure : procedures) + { + if (procedure.valid) + { + int address = size - procedure.slot * 2 - 2; + text.append (String + .format (" %2d %04X %3d %04X %04X %04X %04X (%04X - %04X = %04X)%n", + procedure.procedureNo, procedure.offset, procedure.procLevel, + procedure.codeStart, procedure.codeEnd, procedure.parmSize, + procedure.dataSize, address, procedure.offset, procedure.procOffset)); + } + else + text.append (String.format (" %2d %04X%n", procedure.slot, procedure.offset)); + } + + text.append ("\nStrings\n=======\n"); + for (PascalProcedure pp : procedures) + { + List strings = pp.extractStrings (); + for (PascalCodeStatement cs : strings) + text.append (String.format (" %2d %04X %s%n", pp.procedureNo, cs.ptr, cs.text)); + } + + for (PascalProcedure procedure : procedures) + if (procedure.valid) + text.append (procedure); + return text.toString (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/applefile/PascalText.java b/src/com/bytezone/diskbrowser/applefile/PascalText.java new file mode 100755 index 0000000..121d44f --- /dev/null +++ b/src/com/bytezone/diskbrowser/applefile/PascalText.java @@ -0,0 +1,50 @@ +package com.bytezone.diskbrowser.applefile; + +public class PascalText extends AbstractFile +{ + public PascalText (String name, byte[] buffer) + { + super (name, buffer); + } + + @Override + public String getText () + { + StringBuilder text = new StringBuilder (getHeader ()); + + int ptr = 0x400; + while (ptr < buffer.length) + { + if (buffer[ptr] == 0x00) + { + ++ptr; + continue; + } + if (buffer[ptr] == 0x10) + { + int tab = buffer[ptr + 1] - 0x20; + while (tab-- > 0) + text.append (" "); + ptr += 2; + } + String line = getLine (ptr); + text.append (line + "\n"); + ptr += line.length () + 1; + } + + return text.toString (); + } + + private String getHeader () + { + return "Name : " + name + "\n\n"; + } + + private String getLine (int ptr) + { + StringBuilder line = new StringBuilder (); + while (buffer[ptr] != 0x0D) + line.append ((char) buffer[ptr++]); + return line.toString (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/applefile/ShapeTable.java b/src/com/bytezone/diskbrowser/applefile/ShapeTable.java new file mode 100755 index 0000000..3307c74 --- /dev/null +++ b/src/com/bytezone/diskbrowser/applefile/ShapeTable.java @@ -0,0 +1,150 @@ +package com.bytezone.diskbrowser.applefile; + +import com.bytezone.diskbrowser.HexFormatter; + +public class ShapeTable extends AbstractFile +{ + private static final int SIZE = 400; + + public ShapeTable (String name, byte[] buffer) + { + super (name, buffer); + } + + @Override + public String getText () + { + StringBuffer text = new StringBuffer (); + int totalShapes = buffer[0] & 0xFF; + int startPos = SIZE / 2; + + for (int i = 0; i < totalShapes; i++) + { + int offset = HexFormatter.intValue (buffer[i * 2 + 2], buffer[i * 2 + 3]); + int[][] grid = new int[SIZE][SIZE]; + int row = startPos; + int col = row; + if (i > 0) + text.append ("\n"); + text.append ("Shape " + i + " :\n"); + + while (buffer[offset] != 0) + { + int value = buffer[offset++] & 0xFF; + int v1 = value >> 6; + int v2 = (value & 0x38) >> 3; + int v3 = value & 0x07; +// System.out.printf ("%02X %02X %02X %02X%n", value, v1, v2, v3); + + if (v3 >= 4) + { + grid[row][col] = 1; + grid[0][col]++; + grid[row][0]++; + } + if (v3 == 0 || v3 == 4) + row--; + else if (v3 == 1 || v3 == 5) + col++; + else if (v3 == 2 || v3 == 6) + row++; + else + col--; + + if (v2 >= 4) + { + grid[row][col] = 1; + grid[0][col]++; + grid[row][0]++; + } + if (v2 == 0 && v1 != 0) + row--; + else if (v2 == 4) + row--; + else if (v2 == 1 || v2 == 5) + col++; + else if (v2 == 2 || v2 == 6) + row++; + else if (v2 == 3 || v2 == 7) + col--; + + if (v1 == 1) + col++; + else if (v1 == 2) + row++; + else if (v1 == 3) + col--; + + } + text.append ("\n"); + + int minRow = startPos, maxRow = startPos; + int minCol = startPos, maxCol = startPos; + for (row = 1; row < grid.length; row++) + { + if (grid[row][0] > 0) + { + if (row < minRow) + minRow = row; + if (row > maxRow) + maxRow = row; + } + } + + for (col = 1; col < grid[0].length; col++) + { + if (grid[0][col] > 0) + { + if (col < minCol) + minCol = col; + if (col > maxCol) + maxCol = col; + } + } + + for (row = minRow; row <= maxRow; row++) + { + for (col = minCol; col <= maxCol; col++) + { + if (col == startPos && row == startPos) + text.append (grid[row][col] > 0 ? " @" : " ."); + else if (grid[row][col] == 0) + text.append (" "); + else + text.append (" X"); + } + text.append ("\n"); + } + } + + text.deleteCharAt (text.length () - 1); + return text.toString (); + } + + public static boolean isShapeTable (byte[] buffer) + { + if (buffer.length == 0 || buffer[buffer.length - 1] != 0) + return false; + int totalShapes = buffer[0] & 0xFF; + if (totalShapes == 0) + return false; + + int lastOffset = 0; + for (int i = 0; i < totalShapes; i++) + { + // check index table entry is inside the file + int ptr = i * 2 + 2; + if (ptr >= buffer.length - 1) + return false; + + // check index points inside the file + int offset = HexFormatter.intValue (buffer[ptr], buffer[ptr + 1]); + if (offset == 0 || offset < lastOffset || offset >= buffer.length) + return false; + + lastOffset = offset; + } + + return true; + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/applefile/SimpleText.java b/src/com/bytezone/diskbrowser/applefile/SimpleText.java new file mode 100755 index 0000000..5143df1 --- /dev/null +++ b/src/com/bytezone/diskbrowser/applefile/SimpleText.java @@ -0,0 +1,46 @@ +package com.bytezone.diskbrowser.applefile; + +public class SimpleText extends AbstractFile +{ + + public SimpleText (String name, byte[] buffer) + { + super (name, buffer); + } + + @Override + public String getText () + { + StringBuilder text = new StringBuilder (); + + text.append ("Name : " + name + "\n"); + text.append (String.format ("End of file : %,8d%n%n", buffer.length)); + + int ptr = 0; + while (ptr < buffer.length) + { + String line = getLine (ptr); + text.append (line + "\n"); + ptr += line.length () + 1; + if (ptr < buffer.length && buffer[ptr] == 0x0A) + ptr++; + } + return text.toString (); + } + + private String getLine (int ptr) + { + StringBuilder line = new StringBuilder (); + while (ptr < buffer.length && buffer[ptr] != 0x0D) + line.append ((char) buffer[ptr++]); + return line.toString (); + } + + public static boolean isHTML (byte[] buffer) + { + String text = new String (buffer, 0, buffer.length); + if (text.indexOf ("HTML") > 0 || text.indexOf ("html") > 0) + return true; + return false; + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/applefile/SimpleText2.java b/src/com/bytezone/diskbrowser/applefile/SimpleText2.java new file mode 100755 index 0000000..8a1065b --- /dev/null +++ b/src/com/bytezone/diskbrowser/applefile/SimpleText2.java @@ -0,0 +1,149 @@ +package com.bytezone.diskbrowser.applefile; + +import java.util.ArrayList; +import java.util.List; + +import com.bytezone.diskbrowser.HexFormatter; + +public class SimpleText2 extends AbstractFile +{ + List lineStarts = new ArrayList (); + int loadAddress; + boolean showByte = false; + + public SimpleText2 (String name, byte[] buffer, int loadAddress) + { + super (name, buffer); + this.loadAddress = loadAddress; + + // store a pointer to each new line + int ptr = 0; + while (buffer[ptr] != -1) + { + int length = buffer[ptr] & 0xFF; + lineStarts.add (ptr); + ptr += length + 1; + } + } + + public String getText () + { + StringBuilder text = new StringBuilder (); + + text.append ("Name : " + name + "\n"); + text.append (String.format ("Length : $%04X (%d)%n", buffer.length, buffer.length)); + text.append (String.format ("Load at : $%04X%n%n", loadAddress)); + + for (Integer i : lineStarts) + text.append (String.format ("%05X %s%n", i, getLine (i))); + return text.toString (); + } + + public String getHexDump () + { + StringBuilder text = new StringBuilder (); + + for (Integer i : lineStarts) + text.append (HexFormatter.formatNoHeader (buffer, i, (buffer[i] & 0xFF) + 1) + "\n"); + text.append (HexFormatter.formatNoHeader (buffer, buffer.length - 2, 2) + "\n"); + + return text.toString (); + } + + // convert buffer to text, ignore line-break at the end + private String getLine (int ptr) + { + StringBuilder line = new StringBuilder (); + int length = buffer[ptr] & 0xFF; + while (--length > 0) + { + int val = buffer[++ptr] & 0xFF; + if (val == 0xBB) + { + while (line.length () < 35) + line.append (' '); + line.append (';'); + } + else if (val >= 0x80) + { + while (line.length () < 10) + line.append (' '); + if (val == 0xDC) + line.append (String.format ("EQU", val)); + else if (val == 0xD0) + line.append (String.format ("STA", val)); + else if (val == 0xD2) + line.append (String.format ("STY", val)); + else if (val == 0xD4) + line.append (String.format ("LSR", val)); + else if (val == 0xD5) + line.append (String.format ("ROR", val)); + else if (val == 0xD7) + line.append (String.format ("ASL", val)); + else if (val == 0xD9) + line.append (String.format ("EQ ", val)); + else if (val == 0xDB) + line.append (String.format ("TGT", val)); + else if (val == 0xDA) + line.append (String.format ("ORG", val)); + else if (val == 0xB1) + line.append (String.format ("TYA", val)); + else if (val == 0xC1) + line.append (String.format ("AND", val)); + else if (val == 0xC4) + line.append (String.format ("CMP", val)); + else if (val == 0xC8) + line.append (String.format ("EOR", val)); + else if (val == 0xCA) + line.append (String.format ("JMP", val)); + else if (val == 0xCB) + line.append (String.format ("JSR", val)); + else if (val == 0xCD) + line.append (String.format ("LDA", val)); + else if (val == 0xCE) + line.append (String.format ("LDX", val)); + else if (val == 0xCF) + line.append (String.format ("LDY", val)); + else if (val == 0xA1) + line.append (String.format ("PHA", val)); + else if (val == 0xA2) + line.append (String.format ("PLA", val)); + else if (val == 0xA5) + line.append (String.format ("RTS", val)); + else if (val == 0xA9) + line.append (String.format ("SEC", val)); + else if (val == 0xAD) + line.append (String.format ("TAY", val)); + else if (val == 0x82) + line.append (String.format ("BMI", val)); + else if (val == 0x84) + line.append (String.format ("BCS", val)); + else if (val == 0x85) + line.append (String.format ("BPL", val)); + else if (val == 0x86) + line.append (String.format ("BNE", val)); + else if (val == 0x87) + line.append (String.format ("BEQ", val)); + else if (val == 0x99) + line.append (String.format ("CLC", val)); + else if (val == 0x9C) + line.append (String.format ("DEX", val)); + else if (val == 0x9F) + line.append (String.format ("INY", val)); + else + line.append (String.format (".%02X.", val)); + + line.append (' '); + ++ptr; + if (buffer[ptr] < 0x20 && showByte) + { + val = buffer[ptr] & 0xFF; + line.append (String.format (".%02X. ", val)); + } + } + else + line.append ((char) val); + } + return line.toString (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/applefile/StoredVariables.java b/src/com/bytezone/diskbrowser/applefile/StoredVariables.java new file mode 100755 index 0000000..0a85416 --- /dev/null +++ b/src/com/bytezone/diskbrowser/applefile/StoredVariables.java @@ -0,0 +1,242 @@ +package com.bytezone.diskbrowser.applefile; + +import com.bytezone.diskbrowser.HexFormatter; + +public class StoredVariables extends AbstractFile +{ + public StoredVariables (String name, byte[] buffer) + { + super (name, buffer); + } + + public String getText () + { + StringBuilder text = new StringBuilder (); + String strValue = null; + int intValue = 0; + // double doubleValue = 0.0; + int strPtr = buffer.length; + + text.append ("File length : " + HexFormatter.format4 (buffer.length)); + int totalLength = HexFormatter.intValue (buffer[0], buffer[1]); + text.append ("\nTotal length : " + HexFormatter.format4 (totalLength)); + + int varLength = HexFormatter.intValue (buffer[2], buffer[3]); + text.append ("\nVar length : " + HexFormatter.format4 (varLength)); + text.append ("\n\n"); + + // list simple variables + + int ptr = 5; + while (ptr < varLength + 5) + { + String variableName = getVariableName (buffer[ptr], buffer[ptr + 1]); + text.append (variableName); + + char suffix = variableName.charAt (variableName.length () - 1); + if (suffix == '$') + { + int strLength = HexFormatter.intValue (buffer[ptr + 2]); + strPtr -= strLength; + strValue = HexFormatter.getString (buffer, strPtr, strLength); + text.append (" = " + strValue); + } + else if (suffix == '%') + { + intValue = HexFormatter.intValue (buffer[ptr + 3], buffer[ptr + 2]); + if ((buffer[ptr + 2] & 0x80) > 0) + intValue -= 65536; + text.append (" = " + intValue); + } + else + { + if (hasValue (ptr + 2)) + { + String value = HexFormatter.floatValue (buffer, ptr + 2) + ""; + if (value.endsWith (".0")) + text.append (" = " + value.substring (0, value.length () - 2)); + else + text.append (" = " + value); + } + } + + text.append ("\n"); + ptr += 7; + } + listArrays (text, ptr, totalLength, strPtr); + + return text.toString (); + } + + private String getVariableName (byte b1, byte b2) + { + char c1, c2, suffix; + + if ((b1 & 0x80) > 0) // integer + { + c1 = (char) (b1 & 0x7F); + c2 = (char) (b2 & 0x7F); + suffix = '%'; + } + else if ((b2 & 0x80) > 0) // string + { + c1 = (char) b1; + c2 = (char) (b2 & 0x7F); + suffix = '$'; + } + else + { + c1 = (char) b1; + c2 = (char) b2; + suffix = ' '; + } + + StringBuffer variableName = new StringBuffer (); + variableName.append (c1); + if (c2 > 32) + variableName.append (c2); + if (suffix != ' ') + variableName.append (suffix); + + return variableName.toString (); + } + + private String getDimensionText (int[] values) + { + StringBuilder text = new StringBuilder ("("); + for (int i = 0; i < values.length; i++) + { + text.append (values[i]); + if (i < values.length - 1) + text.append (','); + } + return text.append (')').toString (); + } + + private void listArrays (StringBuilder text, int ptr, int totalLength, int strPtr) + { + while (ptr < totalLength + 5) + { + String variableName = getVariableName (buffer[ptr], buffer[ptr + 1]); + text.append ("\n"); + int offset = HexFormatter.intValue (buffer[ptr + 2], buffer[ptr + 3]); + int dimensions = HexFormatter.intValue (buffer[ptr + 4]); + int[] dimensionSizes = new int[dimensions]; + int totalElements = 0; + for (int i = 0; i < dimensions; i++) + { + int p = i * 2 + 5 + ptr; + int elements = HexFormatter.intValue (buffer[p + 1], buffer[p]); + dimensionSizes[dimensions - i - 1] = elements - 1; + if (totalElements == 0) + totalElements = elements; + else + totalElements *= elements; + } + + int headerSize = 5 + dimensions * 2; + int elementSize = (offset - headerSize) / totalElements; + + int p = ptr + headerSize; + int[] values = new int[dimensions]; + for (int i = 0; i < values.length; i++) + values[i] = 0; + out: while (true) + { + text.append (variableName + " " + getDimensionText (values) + " = "); + if (elementSize == 2) + { + int intValue = HexFormatter.intValue (buffer[p + 1], buffer[p]); + if ((buffer[p] & 0x80) > 0) + intValue -= 65536; + text.append (intValue + "\n"); + } + else if (elementSize == 3) + { + int strLength = HexFormatter.intValue (buffer[p]); + if (strLength > 0) + { + strPtr -= strLength; + text.append (HexFormatter.getString (buffer, strPtr, strLength)); + } + text.append ("\n"); + } + else if (elementSize == 5) + { + if (hasValue (p)) + text.append (HexFormatter.floatValue (buffer, p)); + text.append ("\n"); + } + p += elementSize; + int cp = 0; + while (++values[cp] > dimensionSizes[cp]) + { + values[cp++] = 0; + if (cp >= values.length) + break out; + } + } + ptr += offset; + } + } + + private boolean hasValue (int p) + { + for (int i = 0; i < 5; i++) + if (buffer[p + i] != 0) + return true; + return false; + } + + public String getHexDump () + { + StringBuffer text = new StringBuffer (); + + text.append ("File length : " + HexFormatter.format4 (buffer.length)); + int totalLength = HexFormatter.intValue (buffer[0], buffer[1]); + text.append ("\nTotal length : " + HexFormatter.format4 (totalLength)); + + int varLength = HexFormatter.intValue (buffer[2], buffer[3]); + text.append ("\nVar length : " + HexFormatter.format4 (varLength)); + + int unknown = HexFormatter.intValue (buffer[4]); + text.append ("\nUnknown : " + HexFormatter.format2 (unknown)); + text.append ("\n\n"); + + int ptr = 5; + text.append ("Simple variables : \n\n"); + while (ptr < varLength + 5) + { + text.append (HexFormatter.format (buffer, ptr, 7, false, 0) + "\n"); + ptr += 7; + } + text.append ("\nArrays : \n\n"); + while (ptr < totalLength + 5) + { + int offset = HexFormatter.intValue (buffer[ptr + 2], buffer[ptr + 3]); + int dimensions = HexFormatter.intValue (buffer[ptr + 4]); + int[] dimensionSizes = new int[dimensions]; + int totalElements = 0; + for (int i = 0; i < dimensions; i++) + { + int p = i * 2 + 5 + ptr; + int elements = HexFormatter.intValue (buffer[p + 1], buffer[p]); + dimensionSizes[dimensions - i - 1] = elements; + if (totalElements == 0) + totalElements = elements; + else + totalElements *= elements; + } + int headerSize = 5 + dimensions * 2; + text.append (HexFormatter.format (buffer, ptr, headerSize, false, 0) + "\n\n"); + text.append (HexFormatter.format (buffer, ptr + headerSize, offset - headerSize, false, 0) + + "\n\n"); + ptr += offset; + } + text.append ("Strings : \n\n"); + int length = buffer.length - ptr; + text.append (HexFormatter.format (buffer, ptr, length, false, 0) + "\n\n"); + + return text.toString (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/applefile/TextBuffer.java b/src/com/bytezone/diskbrowser/applefile/TextBuffer.java new file mode 100644 index 0000000..5e1e7ed --- /dev/null +++ b/src/com/bytezone/diskbrowser/applefile/TextBuffer.java @@ -0,0 +1,43 @@ +package com.bytezone.diskbrowser.applefile; + +import com.bytezone.diskbrowser.HexFormatter; + +// only used by Prodos text files - note the fixed block size of 512 - bad! +public class TextBuffer +{ + public final byte[] buffer; + public final int reclen; + public final int firstRecNo; + + public TextBuffer (byte[] tempBuffer, int reclen, int firstBlock) + { + this.reclen = reclen; + + // calculate recNo of first full record + int firstByte = firstBlock * 512; // logical byte # + int rem = firstByte % reclen; + firstRecNo = firstByte / reclen + (rem > 0 ? 1 : 0); + int offset = (rem > 0) ? reclen - rem : 0; + + int availableBytes = tempBuffer.length - offset; + int totalRecords = (availableBytes - 1) / reclen + 1; + + // should check whether the two buffers are identical, and maybe skip this + // step + buffer = new byte[totalRecords * reclen]; + int copyBytes = Math.min (availableBytes, buffer.length); + System.arraycopy (tempBuffer, offset, buffer, 0, copyBytes); + } + + @Override + public String toString () + { + StringBuilder text = new StringBuilder (); + + text.append ("Record length : " + reclen + "\n"); + text.append ("First record : " + firstRecNo + "\n\n"); + text.append (HexFormatter.format (buffer, 0, buffer.length) + "\n"); + + return text.toString (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/applefile/TextFile.java b/src/com/bytezone/diskbrowser/applefile/TextFile.java new file mode 100755 index 0000000..531374b --- /dev/null +++ b/src/com/bytezone/diskbrowser/applefile/TextFile.java @@ -0,0 +1,159 @@ +package com.bytezone.diskbrowser.applefile; + +import java.util.List; + +import com.bytezone.diskbrowser.HexFormatter; + +public class TextFile extends AbstractFile +{ + private int recordLength; + private List buffers; // only used if it is a Prodos text file + private int eof; + + public TextFile (String name, byte[] buffer) + { + super (name, buffer); + } + + public TextFile (String name, byte[] buffer, int recordLength, int eof) + { + this (name, buffer); + this.eof = eof; + this.recordLength = recordLength; + } + + public TextFile (String name, List buffers, int recordLength, int eof) + { + super (name, null); + this.buffers = buffers; + this.eof = eof; + this.recordLength = recordLength; + } + + @Override + public String getHexDump () + { + if (buffers == null) + return (super.getHexDump ()); + + StringBuilder text = new StringBuilder (); + for (TextBuffer tb : buffers) + { + for (int i = 0, rec = 0; i < tb.buffer.length; i += tb.reclen, rec++) + { + text.append ("\nRecord #" + (tb.firstRecNo + rec) + "\n"); + text.append (HexFormatter.format (tb.buffer, i, tb.reclen) + "\n"); + } + } + return text.toString (); + } + + @Override + public String getText () + { + StringBuilder text = new StringBuilder (); + + text.append ("Name : " + name + "\n"); + if (recordLength > 0) // a prodos text file + { + text.append (String.format ("Record length : %,8d%n", recordLength)); + text.append (String.format ("End of file : %,8d%n", eof)); + } + else + text.append (String.format ("End of file : %,8d%n", buffer.length)); + text.append ("\n"); + + // check whether file is spread over multiple buffers + if (buffers != null) + return treeFileText (text); + + // check whether the record length is known + if (recordLength == 0) + return unknownLength (text); + + text.append ("Offset Record Text values\n"); + text.append ("------ ------- -------------------------------------------------------\n"); + return knownLength (text, 0).toString (); + } + + private String treeFileText (StringBuilder text) + { + for (TextBuffer tb : buffers) + { + this.buffer = tb.buffer; + knownLength (text, tb.firstRecNo); + } + return text.toString (); + } + + private StringBuilder knownLength (StringBuilder text, int recNo) + { + for (int ptr = 0; ptr < buffer.length; ptr += recordLength) + { + if (buffer[ptr] == 0) + { + recNo++; + continue; + } + int len = buffer.length - ptr; + int bytes = len < recordLength ? len : recordLength; + + while (buffer[ptr + bytes - 1] == 0) + bytes--; + + text.append (String.format ("%,6d %,8d %s%n", ptr, recNo++, + HexFormatter.getString (buffer, ptr, bytes))); + } + return text; + } + + private String unknownLength (StringBuilder text) + { + int nulls = 0; + int ptr = 0; + int size = buffer.length; + int lastVal = 0; + boolean newFormat = true; + + if (newFormat) + { + text.append ("Offset Text values\n"); + text.append ("------ -------------------------------------------------------" + + "-------------------\n"); + if (size == 0) + return text.toString (); + + if (buffer[ptr] != 0) + text.append (String.format ("%6d ", ptr)); + } + + while (ptr < size) + { + int val = buffer[ptr++] & 0x7F; // strip hi-order bit + if (val == 0) + ++nulls; + else if (val == 0x0D) // carriage return + text.append ("\n"); + else + { + if (nulls > 0) + { + if (newFormat) + text.append (String.format ("%6d ", ptr - 1)); + else + text.append ("\nNew record at : " + (ptr - 1) + "\n"); + nulls = 0; + } + else if (lastVal == 0x0D && newFormat) + text.append (" "); + + text.append ((char) val); + } + lastVal = val; + } + if (text.length () > 0 && text.charAt (text.length () - 1) == '\n') + text.deleteCharAt (text.length () - 1); + + return text.toString (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/applefile/VisicalcFile.java b/src/com/bytezone/diskbrowser/applefile/VisicalcFile.java new file mode 100644 index 0000000..d9656f7 --- /dev/null +++ b/src/com/bytezone/diskbrowser/applefile/VisicalcFile.java @@ -0,0 +1,57 @@ +package com.bytezone.diskbrowser.applefile; + +import com.bytezone.diskbrowser.HexFormatter; + +public class VisicalcFile extends AbstractFile +{ + private VisicalcSpreadsheet sheet; + + public VisicalcFile (String name, byte[] buffer) + { + super (name, buffer); + } + + @Override + public String getText () + { + if (sheet == null) + sheet = new VisicalcSpreadsheet (buffer); + + StringBuilder text = new StringBuilder (); + + text.append ("Visicalc : " + name + "\n"); + text.append ("Cells : " + sheet.size () + "\n\n"); + text.append (sheet.getCells ()); + + if (false) + { + text.append ("\n"); + for (String line : sheet.lines) + { + text.append ("\n"); + text.append (line); + } + } + + return text.toString (); + } + + public static boolean isVisicalcFile (byte[] buffer) + { + if (false) + System.out.println (HexFormatter.format (buffer)); + int firstByte = buffer[0] & 0xFF; + if (firstByte != 0xBE && firstByte != 0xAF) + return false; + + int last = buffer.length - 1; + + while (buffer[last] == 0) + last--; + + if (buffer[last] != (byte) 0x8D) + return false; + + return true; + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/applefile/VisicalcSpreadsheet.java b/src/com/bytezone/diskbrowser/applefile/VisicalcSpreadsheet.java new file mode 100644 index 0000000..e5136a5 --- /dev/null +++ b/src/com/bytezone/diskbrowser/applefile/VisicalcSpreadsheet.java @@ -0,0 +1,564 @@ +package com.bytezone.diskbrowser.applefile; + +import java.security.InvalidParameterException; +import java.text.DecimalFormat; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.applefile.VisicalcSpreadsheet.VisicalcCell; + +public class VisicalcSpreadsheet implements Iterable +{ + private static final Pattern addressPattern = Pattern.compile ("([A-B]?[A-Z])([0-9]{1,3}):"); + private static final Pattern cellContents = Pattern + .compile ("([-+/*]?)(([A-Z]{1,2}[0-9]{1,3})|([0-9.]+)|(@[^-+/*]+))"); + private static final Pattern functionPattern = Pattern + .compile ("\\(([A-B]?[A-Z])([0-9]{1,3})\\.\\.\\.([A-B]?[A-Z])([0-9]{1,3})\\)"); + + private final Map sheet = new TreeMap (); + private final Map functions = new HashMap (); + + final List lines = new ArrayList (); + VisicalcCell currentCell = null; + int columnWidth = 12; + char defaultFormat; + + // Maximum cell = BK254 + + //Commands: + // /G
goto + // /B blank the cell + // /C clear and home + // A-Z label (" to force a label) + // 0-9.+-()*/@# value (+ to force a value) + + // /S Storage (ISLDWR) + // /SI Storage init + // /SS Storage Save + // /SL Storage Load + // /SD Storage Delete + // /SW Storage Write (to cassette) + // /SR Storage Read (from cassette) + + // /R Replicate + + // /G Global (CORF) + // /GF Global Format (DGILR$*) + // /GFI Global Format Integer + // /GF$ Global Format Currency + // /GC Global Column + // /GR Global + // /GO Global + + // /T Titles (HVBN) + // /TH fix Horizontal Titles + // /TV fix Vertical Titles + // /TB fix Both Titles + // /TN fix Neither + + // /W Window (HV1SU) + // /WV Window Vertical (split on cursor column) + // /WH Window Horizontal (split on cursor row) + + public VisicalcSpreadsheet (byte[] buffer) + { + int ptr = 0; + int last = buffer.length - 1; + + while (buffer[last] == 0) + last--; + + while (ptr <= last) + { + int endPtr = findEndPtr (buffer, ptr); + add (HexFormatter.getString (buffer, ptr, endPtr - ptr)); + ptr = endPtr + 1; + } + + if (false) + for (VisicalcCell cell : sheet.values ()) + System.out.println (cell); + } + + public void add (String command) + { + lines.add (command); + String data; + + if (command.startsWith (">")) // GOTO cell + { + int pos = command.indexOf (':'); // end of cell address + Matcher m = addressPattern.matcher (command); + if (m.find ()) + { + Address address = new Address (m.group (1), m.group (2)); + VisicalcCell cell = sheet.get (address.sortValue); + command = command.substring (pos + 1); + + if (cell == null) + { + cell = new VisicalcCell (this, address); + sheet.put (cell.address.sortValue, cell); + currentCell = cell; + } + else + System.out.println ("Found " + cell); + } + else + System.out.printf ("Invalid cell address: %s%n", command); + } + + if (command.startsWith ("/")) // command + { + // System.out.printf ("Cmd: %s%n", command); + data = command.substring (1); + char subCommand = command.charAt (1); + switch (subCommand) + { + case 'W': + System.out.println (" Window command: " + data); + break; + + case 'G': + System.out.println (" Global command: " + data); + if (data.charAt (1) == 'C') + columnWidth = Integer.parseInt (data.substring (2)); + else if (data.charAt (1) == 'F') + defaultFormat = data.charAt (2); + break; + + case 'T': + System.out.println (" Title command: " + data); + break; + + default: + currentCell.doCommand (command); + } + } + else if (command.startsWith ("@")) + { + currentCell.doCommand (command); + } + else if (command.startsWith ("\"")) + { + currentCell.doCommand (command); + } + else if (command.startsWith ("+")) + { + currentCell.doCommand (command); + } + else if (command.matches ("^[0-9.]+$")) // value + { + currentCell.doCommand (command); + } + else if (command.matches ("^[-A-Z]+$")) // label + { + currentCell.doCommand (command); + } + else + currentCell.doCommand (command); // formula + } + + private int findEndPtr (byte[] buffer, int ptr) + { + while (buffer[ptr] != (byte) 0x8D) + ptr++; + return ptr; + } + + private double evaluateFunction (String function) + { + if (functions.containsKey (function)) + return functions.get (function); + + Range range = null; + Matcher m = functionPattern.matcher (function); + while (m.find ()) + { + Address fromAddress = new Address (m.group (1), m.group (2)); + Address toAddress = new Address (m.group (3), m.group (4)); + range = new Range (fromAddress, toAddress); + } + + double result = 0; + + if (function.startsWith ("@SUM")) + { + for (Address address : range) + result += getValue (address); + } + else if (function.startsWith ("@COUNT")) + { + int count = 0; + for (Address address : range) + { + VisicalcCell cell = getCell (address); + if (cell != null && cell.hasValue () && cell.value != 0.0) + ++count; + } + result = count; + } + else if (function.startsWith ("@MIN")) + { + double min = Double.MAX_VALUE; + for (Address address : range) + if (min > getValue (address)) + min = getValue (address); + result = min; + } + else if (function.startsWith ("@MAX")) + { + double max = Double.MIN_VALUE; + for (Address address : range) + if (max < getValue (address)) + max = getValue (address); + result = max; + } + else + System.out.println ("Unimplemented function: " + function); + // http://www.bricklin.com/history/refcard1.htm + // Functions: + // @AVERAGE + // @NPV + // @LOOKUP(v,range) + // @NA + // @ERROR + // @PI + // @ABS + // @INT + // @EXP + // @SQRT + // @LN + // @LOG10 + // @SIN + // @ASIN + // @COS + // @ACOS + // @TAN + // @ATAN + + functions.put (function, result); + return result; + } + + public double getValue (Address address) + { + VisicalcCell cell = sheet.get (address.sortValue); + return cell == null ? 0.0 : cell.value; + } + + public double getValue (String cellName) + { + Address address = new Address (cellName); + return getValue (address); + } + + public VisicalcCell getCell (Address address) + { + return sheet.get (address.sortValue); + } + + public int size () + { + return sheet.size (); + } + + @Override + public Iterator iterator () + { + return sheet.values ().iterator (); + } + + public String getCells () + { + StringBuilder text = new StringBuilder (); + + String format = String.format ("%%-%d.%ds", columnWidth, columnWidth); + String currencyFormat = String.format ("%%%d.%ds", columnWidth, columnWidth); + String integerFormat = String.format ("%%%d.0f", columnWidth); + String numberFormat = String.format ("%%%d.3f", columnWidth); + + DecimalFormat nf = new DecimalFormat ("$#####0.00"); + // NumberFormat nf = NumberFormat.getCurrencyInstance (); + int lastRow = 0; + int lastColumn = -1; + + for (VisicalcCell cell : sheet.values ()) + { + while (lastRow < cell.address.row) + { + text.append ("\n"); + ++lastRow; + lastColumn = -1; + } + + while (lastColumn < cell.address.column - 1) + { + text.append (" ".substring (0, columnWidth)); + ++lastColumn; + } + lastColumn = cell.address.column; + + if (cell.hasValue ()) + { + if (defaultFormat == 'I') + text.append (String.format (integerFormat, cell.getValue ())); + else if (defaultFormat == '$') + text.append (String.format (currencyFormat, nf.format (cell.getValue ()))); + else + text.append (String.format (numberFormat, cell.getValue ())); + } + else + text.append (String.format (format, cell.value ())); + } + return text.toString (); + } + + class VisicalcCell implements Comparable + { + private final Address address; + private final VisicalcSpreadsheet parent; + + private String label; + private double value; + private String formula; + private char format; + private int width; + private int columnWidth; + private char repeatingChar; + private String repeat = ""; + private boolean valid; + + public VisicalcCell (VisicalcSpreadsheet parent, Address address) + { + this.parent = parent; + this.address = address; + } + + public void doCommand (String command) + { + if (command.startsWith ("/")) + { + if (command.charAt (1) == 'F') // format cell + { + format = command.charAt (2); + if (command.length () > 3 && command.charAt (3) == '"') + label = command.substring (4); + } + else if (command.charAt (1) == '-') // repeating label + { + repeatingChar = command.charAt (2); + for (int i = 0; i < 20; i++) + repeat += repeatingChar; + } + else + System.out.println ("Unknown command: " + command); + } + else if (command.startsWith ("\"")) // starts with a quote + label = command.substring (1); + else if (command.matches ("^[0-9.]+$")) // contains only numbers or . + this.value = Float.parseFloat (command); + else + formula = command; + } + + public boolean hasValue () + { + return label == null && repeatingChar == 0; + } + + public double getValue () + { + if (valid || formula == null) + return value; + + double result = 0.0; + double interim; + + Matcher m = cellContents.matcher (formula); + while (m.find ()) + { + valid = true; + char operator = m.group (1).isEmpty () ? '+' : m.group (1).charAt (0); + + if (m.group (3) != null) // address + interim = parent.getValue (m.group (3)); + else if (m.group (4) != null) // constant + interim = Double.parseDouble (m.group (4)); + else + interim = parent.evaluateFunction (m.group (5)); // function + + if (operator == '+') + result += interim; + else if (operator == '-') + result -= interim; + else if (operator == '*') + result *= interim; + else if (operator == '/') + result = interim == 0.0 ? 0 : result / interim; + } + + if (valid) + { + value = result; + return result; + } + + System.out.println ("?? " + formula); + + return value; + } + + public String value () + { + if (label != null) + return label; + if (repeatingChar > 0) + return repeat; + if (formula != null) + if (formula.length () >= 12) + return formula.substring (0, 12); + else + return formula; + return value + ""; + } + + @Override + public String toString () + { + String value = + repeatingChar == 0 ? label == null ? formula == null ? ", Value: " + this.value + : ", Frmla: " + formula : ", Label: " + label : ", Rpeat: " + repeatingChar; + String format = this.format == 0 ? "" : ", Format: " + this.format; + String width = this.width == 0 ? "" : ", Width: " + this.width; + String columnWidth = this.columnWidth == 0 ? "" : ", Col Width: " + this.columnWidth; + return String.format ("[Cell:%5s%s%s%s%s]", address, format, width, columnWidth, value); + } + + @Override + public int compareTo (VisicalcCell o) + { + return address.compareTo (o.address); + } + } + + class Range implements Iterable
+ { + Address from, to; + List
range = new ArrayList
(); + + public Range (Address from, Address to) + { + this.from = from; + this.to = to; + + range.add (from); + + if (from.row == to.row) + { + while (from.compareTo (to) < 0) + { + from = from.nextColumn (); + range.add (from); + } + } + else if (from.column == to.column) + { + while (from.compareTo (to) < 0) + { + from = from.nextRow (); + range.add (from); + } + } + else + throw new InvalidParameterException (); + } + + @Override + public String toString () + { + return String.format (" %s -> %s", from.text, to.text); + } + + @Override + public Iterator
iterator () + { + return range.iterator (); + } + } + + class Address implements Comparable
+ { + int row, column; + int sortValue; + String text; + + public Address (String column, String row) + { + set (column, row); + } + + public Address (int column, int row) + { + assert column <= 64; + assert row <= 255; + this.row = row; + this.column = column; + sortValue = row * 64 + column; + + int col1 = column / 26; + int col2 = column % 26; + String col = + col1 > 0 ? (char) ('@' + col1) + ('A' + col2) + "" : (char) ('A' + col2) + ""; + text = col + (row + 1); + } + + public Address (String address) + { + if (address.charAt (1) < 'A') + set (address.substring (0, 1), address.substring (1)); + else + set (address.substring (0, 2), address.substring (2)); + } + + private void set (String sCol, String sRow) + { + if (sCol.length () == 1) + column = sCol.charAt (0) - 'A'; + else if (sCol.length () == 2) + column = (sCol.charAt (0) - '@') * 26 + sCol.charAt (1) - 'A'; + else + System.out.println ("Bollocks"); + + row = Integer.parseInt (sRow) - 1; + sortValue = row * 64 + column; + text = sCol + sRow; + } + + public Address nextRow () + { + Address next = new Address (column, row + 1); + return next; + } + + public Address nextColumn () + { + Address next = new Address (column + 1, row); + return next; + } + + @Override + public String toString () + { + return String.format ("%s %d %d %d", text, row, column, sortValue); + } + + @Override + public int compareTo (Address o) + { + return sortValue - o.sortValue; + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/applefile/WizardryTitle.java b/src/com/bytezone/diskbrowser/applefile/WizardryTitle.java new file mode 100755 index 0000000..5ca4c0d --- /dev/null +++ b/src/com/bytezone/diskbrowser/applefile/WizardryTitle.java @@ -0,0 +1,51 @@ +package com.bytezone.diskbrowser.applefile; + +import com.bytezone.diskbrowser.HexFormatter; + +public class WizardryTitle extends AbstractFile +{ + public WizardryTitle (String name, byte[] buffer) + { + super (name, buffer); + } + + @Override + public String getText () + { + int size = 20; + StringBuilder text = new StringBuilder (); + for (int i = 0; i < buffer.length; i += size) + { + for (int line = 0; line < size; line++) + { + int p = i + line; + if (p >= buffer.length) + break; + int value = HexFormatter.intValue (buffer[p]); + text = decode2 (value, text); + } + text.append ("\n"); + } + return text.toString (); + } + + private StringBuilder decode (int value, StringBuilder text) + { + for (int bit = 0; bit < 8; bit++) + { + text.append ((value & 0x01) == 1 ? "X" : " "); + value >>= 1; + } + return text; + } + + private StringBuilder decode2 (int value, StringBuilder text) + { + for (int bit = 7; bit >= 0; bit--) + { + text.append ((value & 0x01) == 1 ? "X" : " "); + value >>= 1; + } + return text; + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/applefile/equates.txt b/src/com/bytezone/diskbrowser/applefile/equates.txt new file mode 100644 index 0000000..d957dd8 --- /dev/null +++ b/src/com/bytezone/diskbrowser/applefile/equates.txt @@ -0,0 +1,158 @@ +* Zero page + +0020 WNDLFT +0021 WNDWDTH +0022 WNDTOP +0023 WNDBTM +0024 CH +0025 CV +0026 GBAS-LO +0027 GBAS-HI +0028 BAS-LO +0029 BAS-HI +0032 INVFLG +0035 YSAV1 +0036 CSWL +0037 CSHW +0044 A5L - volume number? +004E RND-LO +004F RND-HI +0050 LINNUM +0073 HIMEM +009D FAC +009E FAC mantissa hi order +009F FAC mantissa mid order hi +00B1 CHRGET +00B7 CHRGOT +00B8 TXTPTR + +0200 Input buffer + +03D0 Applesoft warm start +03EA VECT +A56E catalog routine + +C000 KYBD - last key pressed +C010 STROBE - Clear KYBD +C050 TXTCLR - Display Graphics +C051 TXTSET - Display Text +C052 MIXCLR - Display Full Screen +C053 MIXSET - Display Split Screen +C054 TXTPAGE1 - Display Page 1 +C055 TXTPAGE2 - If 80STORE Off: Display Page 2, If 80STORE On: Read/Write Aux Display Mem +C056 LORES - Display LoRes Graphics +C057 HIRES - Display HiRes Graphics + +C080 Read RAM bank 2; no write +C081 ROMIN - Read ROM; write RAM bank 2 +C082 Read ROM; no write +C083 LCBANK2 - Read/write RAM bank 2 +C084 Read RAM bank 2; no write +C085 ROMIN - Read ROM; write RAM bank 2 +C086 Read ROM; no write +C087 LCBANK2 - Read/write RAM bank 2 +C088 Read RAM bank 1; no write +C089 Read ROM; write RAM bank 1 +C08A Read ROM; no write +C08B Read/write RAM bank 1 +C08C Read RAM bank 1; no write +C08D Read ROM; write RAM bank 1 +C08E Read ROM; no write +C08F Read/write RAM bank 1 + +D52C INLIN numeric input +DB3A STROUT - output a string +DB5C output a character +DD67 FRMNUM +DD7B FRMEVAL +DEBE CHKCOM +DEC0 SYNCHR +DEC9 syntax error +DFE3 PTRGET + +E053 find a variable +E10C convert FP to INT +E2F2 convert ACC to FP +E301 SNGFLT +E3E7 FPSTR2 +E6F8 GETBYTE +E74C COMBYTE +E752 GETADR - get from FAC to LINNUM +E7A7 FSUB +E7BE FADD +E8D5 OVERFLOW +E913 ONE +E941 FLOG +E97F FMULT +E9E3 CONUPK +EA39 MUL10 +EA66 FDIV +EAE1 DIVERR +EAF9 MOVEFM - move (A,Y) to FAC +EB2B MOVEMF +EB93 FLOAT +EBA0 FLOAT1 - integer to FAC ($9D-$A2) +EBB2 FCOMP +EBF2 QINT +EC23 FINT +EC4A FIN +ED24 LINPRNT - print a decimal number +ED2E PRNTFAC +ED34 FOUT - FAC to FBUFFR ($100-$110) +EE8D SQR +EE97 FPWRT +EED0 NEGOP +EF09 FEXP +EFAE RND +EFEA FCOS +EFF1 FSIN + +F03A FTAN +F066 PIHALF +F09E FATN +F411 map x,y location on hi-res 1 ?? +F467 LEFT EQU +F48A RIGHT EQU +F4D5 UP EQU +F504 DOWN EQU +F6B9 HFNS +F940 PRINTYX +F941 PRINTAX - print a hex number +FAA6 reboot DOS +FAFF 0 = Autostart ROM, 1 = Old Monitor +FB1E PREAD - read game paddle +FB2F initialise text screen +FB39 text mode - SETTXT +FB5B TABV - monitor tab routine +FC66 CURSDWN - move cursor down +FBF4 CURSRIT - move cursor right +FC10 CURSLFT - move cursor left +FC1A CURSUP - move cursor up +FB6F set powerup checksum +FBC1 BASCALC - calculate video address +FBDD BEEP +FC22 VTAB +FC42 CLREOP - clear to end of page +FC58 HOME - clear screen +FC62 CR +FC9C CLREOL +FCA8 WAIT 1/2(26+27A+5A^2) microseconds +FD0C RDKEY - Blink cursor +FD1B KEYIN - Increment RNDL,H while polling keyboard +FD35 RDCHAR - Call RDKEY +FD6A GETLN +FD75 NXTCHAR +FD8B CROUT1 - generate a return with clear +FDDA PRBYTE - print A in hex +FDED COUT - output a character +FDF0 COUT1 - output a character to screen +FD8E CROUT - generate a return +FE2C move a block of memory +FE89 disconnect DOS from I/O links +FE93 disconnect DOS from I/O links +FF3A BELL +FF3F SAVE +FF4A RESTORE +FF59 Monitor cold entry point +FFA7 GETNUM - move num to A2L.A2H +FFC7 ZMODE - monitor get ASCII return diff --git a/src/com/bytezone/diskbrowser/appleworks/AppleworksADBFile.java b/src/com/bytezone/diskbrowser/appleworks/AppleworksADBFile.java new file mode 100644 index 0000000..aff9f6a --- /dev/null +++ b/src/com/bytezone/diskbrowser/appleworks/AppleworksADBFile.java @@ -0,0 +1,185 @@ +package com.bytezone.diskbrowser.appleworks; + +import java.util.ArrayList; +import java.util.List; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.applefile.AbstractFile; + +public class AppleworksADBFile extends AbstractFile +{ + static final String line = "-------------------------------------------------------" + + "-----------------------------------\n"; + + private final int headerSize; + private final int cursorDirectionSRL; + private final char cursorDirectionMRL; + private final char currentDisplay; + final int categories; + private final int totalReports; + private final int totalRecords; + private final int dbMinVersion; + final String[] categoryNames; + int maxCategoryName; + + private final int[] columnWidthsMRL = new int[30]; + private final int[] columnCategoryMRL = new int[30]; + private final int[] rowPositionSRL = new int[30]; + private final int[] columnPositionSRL = new int[30]; + private final int[] categorySRL = new int[30]; + + private final int firstFrozenColumn; + private final int lastFrozenColumn; + private final int leftmostActiveColumn; + private final int totalCategoriesMRL; + + private final int[] selectionRules = new int[3]; + private final int[] testTypes = new int[3]; + private final int[] continuation = new int[3]; + private final String[] comparison = new String[3]; + + private final List reports = new ArrayList (); + final List records = new ArrayList (); + private final Record standardRecord; + + public AppleworksADBFile (String name, byte[] buffer) + { + super (name, buffer); + + dbMinVersion = buffer[218] & 0xFF; + + headerSize = HexFormatter.getWord (buffer, 0); + cursorDirectionSRL = buffer[30]; + cursorDirectionMRL = (char) buffer[31]; + currentDisplay = (char) buffer[34]; + categories = buffer[35] & 0xFF; + categoryNames = new String[categories]; + + totalReports = buffer[38] & 0xFF; + int recs = HexFormatter.getWord (buffer, 36); + totalRecords = dbMinVersion == 0 ? recs : recs & 0x7FFF; + + for (int i = 0; i < 30; i++) + { + columnWidthsMRL[i] = buffer[42 + i] & 0xFF; + columnCategoryMRL[i] = buffer[78 + i] & 0xFF; + columnPositionSRL[i] = buffer[114 + i] & 0xFF; + rowPositionSRL[i] = buffer[150 + i] & 0xFF; + categorySRL[i] = buffer[186 + i] & 0xFF; + } + + firstFrozenColumn = buffer[219] & 0xFF; + lastFrozenColumn = buffer[220] & 0xFF; + leftmostActiveColumn = buffer[221] & 0xFF; + totalCategoriesMRL = buffer[222] & 0xFF; + + for (int i = 0; i < 3; i++) + { + selectionRules[i] = HexFormatter.getWord (buffer, 223 + i * 2); + testTypes[i] = HexFormatter.getWord (buffer, 229 + i * 2); + continuation[i] = HexFormatter.getWord (buffer, 235 + i * 2); + comparison[i] = new String (buffer, 241 + i * 20, 20); + } + + int ptr = 357; + for (int i = 0; i < categoryNames.length; i++) + { + categoryNames[i] = new String (buffer, ptr + 1, buffer[ptr] & 0xFF); + if (categoryNames[i].length () > maxCategoryName) + maxCategoryName = categoryNames[i].length (); + ptr += 22; + } + + for (int reportNo = 0; reportNo < totalReports; reportNo++) + { + int reportFormat = (char) buffer[ptr + 214]; + if (reportFormat == 'H') + reports.add (new TableReport (this, buffer, ptr)); + else if (reportFormat == 'V') + reports.add (new LabelReport (this, buffer, ptr)); + else + System.out.println ("Bollocks - report format not H or V : " + reportFormat); + ptr += 600; + } + + int length = HexFormatter.getWord (buffer, ptr); + ptr += 2; + + if (length == 0) + standardRecord = null; + else + { + standardRecord = new Record (this, buffer, ptr); + ptr += length; + + for (int recordNo = 0; recordNo < totalRecords; recordNo++) + { + length = HexFormatter.getWord (buffer, ptr); + ptr += 2; + if (length == 0) + break; + + records.add (new Record (this, buffer, ptr)); + ptr += length; + } + } + } + + @Override + public String getText () + { + StringBuilder text = new StringBuilder (); + + text.append (String.format ("Header size ........ %d%n", headerSize)); + text.append (String + .format ("SRL cursor ......... %d (1=default, 2=left->right, top->bottom)%n", + cursorDirectionSRL)); + text.append (String.format ("MRL cursor ......... %s (D=down, R=right)%n", + cursorDirectionMRL)); + text.append (String.format ("Display ............ %s (R=SRL, /=MRL)%n", currentDisplay)); + text.append (String.format ("Categories ......... %d%n", categories)); + text.append (String.format ("Reports ............ %d%n", totalReports)); + text.append (String.format ("Records ............ %d%n", totalRecords)); + text.append (String.format ("Min version ........ %d%n", dbMinVersion)); + text.append (String.format ("1st Frozen col ..... %d%n", firstFrozenColumn)); + text.append (String.format ("Last Frozen col .... %d%n", lastFrozenColumn)); + text.append (String.format ("Left active col .... %d%n", leftmostActiveColumn)); + text.append (String.format ("MRL categories ..... %d%n", totalCategoriesMRL)); + + text.append ("\n Categories:\n"); + for (int i = 0; i < categories; i++) + text.append (String.format (" %2d %-30s %n", (i + 1), categoryNames[i])); + text.append ("\n"); + + for (Report report : reports) + { + text.append (report); + text.append ("\n"); + } + + for (Report report : reports) + { + text.append (report.getText ()); + text.append ("\n"); + } + + // if (reports.size () == 0) + { + text.append (line); + for (Record record : records) + { + text.append (record.getReportLine () + "\n"); + text.append (line); + } + } + + removeTrailing (text, '\n'); + return text.toString (); + } + + private void removeTrailing (StringBuilder text, char c) + { + while (text.charAt (text.length () - 1) == c) + text.deleteCharAt (text.length () - 1); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/appleworks/AppleworksSSFile.java b/src/com/bytezone/diskbrowser/appleworks/AppleworksSSFile.java new file mode 100644 index 0000000..9cbb14b --- /dev/null +++ b/src/com/bytezone/diskbrowser/appleworks/AppleworksSSFile.java @@ -0,0 +1,308 @@ +package com.bytezone.diskbrowser.appleworks; + +import java.util.ArrayList; +import java.util.List; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.applefile.AbstractFile; + +public class AppleworksSSFile extends AbstractFile +{ + Header header; + List rows = new ArrayList (); + + public AppleworksSSFile (String name, byte[] buffer) + { + super (name, buffer); + + header = new Header (); + + int ptr = header.ssMinVers == 0 ? 300 : 302; + while (ptr < buffer.length) + { + int length = HexFormatter.getWord (buffer, ptr); + + if (length == 0xFFFF) + break; + + ptr += 2; + Row row = new Row (ptr); + rows.add (row); + ptr += length; + } + } + + @Override + public String getText () + { + StringBuilder text = new StringBuilder (header.toString ()); + + for (Row row : rows) + { + text.append ("\n"); + for (Cell cell : row.cells) + text.append (cell); + } + + return text.toString (); + } + + static String getCellName (int row, int column) + { + char c1 = (char) ('A' + column / 26 - 1); + char c2 = (char) ('A' + column % 26); + return "" + (c1 == '@' ? "" : c1) + c2 + row; + } + + private class Header + { + private final int[] columnWidths = new int[127]; + private final char calcOrder; + private final char calcFrequency; + private final int lastRow; + private final int lastColumn; + private final char windowLayout; + private final boolean windowSynch; + private final Window currentWindow; + private final Window secondWindow; + private final boolean cellProtection; + private final int platenWidth; + private final int leftMargin; + private final int rightMargin; + private final int charsPerInch; + + private final int paperLength; + private final int topMargin; + private final int bottomMargin; + private final int linesPerInch; + private final char spacing; + + private final byte[] printerCodes = new byte[14]; + private final boolean printDash; + private final boolean printHeader; + private final boolean zoomed; + + private final int ssMinVers; + + public Header () + { + int ptr = 4; + for (int i = 0; i < columnWidths.length; i++) + columnWidths[i] = buffer[ptr++] & 0xFF; + + calcOrder = (char) buffer[131]; + calcFrequency = (char) buffer[132]; + lastRow = HexFormatter.getWord (buffer, 133); + lastColumn = buffer[135] & 0xFF; + windowLayout = (char) buffer[136]; + windowSynch = buffer[137] != 0; + + currentWindow = new Window (138); + secondWindow = new Window (162); + + cellProtection = buffer[213] != 0; + platenWidth = buffer[215] & 0xFF; + leftMargin = buffer[216] & 0xFF; + rightMargin = buffer[217] & 0xFF; + charsPerInch = buffer[218] & 0xFF; + + paperLength = buffer[219] & 0xFF; + topMargin = buffer[220] & 0xFF; + bottomMargin = buffer[221] & 0xFF; + linesPerInch = buffer[222] & 0xFF; + spacing = (char) buffer[223]; + + System.arraycopy (buffer, 224, printerCodes, 0, printerCodes.length); + + printDash = buffer[238] != 0; + printHeader = buffer[239] != 0; + zoomed = buffer[240] != 0; + + ssMinVers = buffer[242]; + } + + @Override + public String toString () + { + StringBuilder text = new StringBuilder (); + + text.append (String.format ("Calc order ..... %s %n", calcOrder)); + text.append (String.format ("Calc freq ...... %s %n", calcFrequency)); + text.append (String.format ("Last row ....... %d %n", lastRow)); + text.append (String.format ("Last column .... %d %n", lastColumn)); + text.append (String.format ("Window layout .. %s %n", windowLayout)); + text.append (String.format ("Window synch ... %s %n", windowSynch)); + text.append (String.format ("Min version .... %s %n%n", ssMinVers)); + + String[] s1 = currentWindow.toString ().split ("\n"); + String[] s2 = secondWindow.toString ().split ("\n"); + for (int i = 0; i < s1.length; i++) + text.append (String.format ("%-30s %-30s%n", s1[i], s2[i])); + text.append ("\n"); + + text.append (String.format ("Cell protect ... %s %n", cellProtection)); + text.append (String.format ("Platen width ... %d %n", platenWidth)); + text.append (String.format ("Left margin .... %d %n", leftMargin)); + text.append (String.format ("Right margin ... %d %n", rightMargin)); + text.append (String.format ("Chars per inch . %d %n", charsPerInch)); + + text.append (String.format ("Paper length ... %d %n", paperLength)); + text.append (String.format ("Top margin ..... %d %n", topMargin)); + text.append (String.format ("Bottom margin .. %d %n", bottomMargin)); + text.append (String.format ("Lines per inch . %d %n", linesPerInch)); + text.append (String.format ("Spacing ........ %s %n", spacing)); + + String prC = HexFormatter.getHexString (printerCodes); + text.append (String.format ("Printer codes .. %s %n", prC)); + text.append (String.format ("Print dash ..... %s %n", printDash)); + text.append (String.format ("Print header ... %s %n", printHeader)); + text.append (String.format ("Zoomed ......... %s %n", zoomed)); + + return text.toString (); + } + } + + private class Window + { + private final int justification; + private final CellFormat format; + private final int r1; + private final int c1; + private final int r2; + private final int c2; + private final int r3; + private final int c3; + private final int r4; + private final int c4; + private final int r5; + private final int c5; + private final int r6; + private final int c6; + private final int r7; + private final int c7; + private final int bodyRows; + private final boolean rightColumnNotDisplayed; + private final boolean topTitleSwitch; + private final boolean sideTitleSwitch; + + public Window (int offset) + { + justification = buffer[offset] & 0xFF; + + format = new CellFormat (buffer[offset + 1], buffer[offset + 2]); + + r1 = buffer[offset + 3] & 0xFF; + c1 = buffer[offset + 4] & 0xFF; + r2 = HexFormatter.getWord (buffer, offset + 5); + c2 = buffer[offset + 7] & 0xFF; + r3 = HexFormatter.getWord (buffer, offset + 8); + c3 = buffer[offset + 10] & 0xFF; + r4 = HexFormatter.getWord (buffer, offset + 11); + c4 = buffer[offset + 13] & 0xFF; + r5 = buffer[offset + 14] & 0xFF; + c5 = buffer[offset + 15] & 0xFF; + r6 = HexFormatter.getWord (buffer, offset + 16); + c6 = buffer[offset + 18] & 0xFF; + r7 = buffer[offset + 19] & 0xFF; + c7 = buffer[offset + 20] & 0xFF; + + bodyRows = buffer[offset + 21] & 0xFF; + rightColumnNotDisplayed = buffer[offset + 21] != 0; + + int flags = buffer[offset + 23] & 0xFF; + topTitleSwitch = (flags & 0x80) != 0; + sideTitleSwitch = (flags & 0x40) != 0; + } + + @Override + public String toString () + { + StringBuilder text = new StringBuilder (); + + text.append (String.format ("Justification .. %s %n", justification)); + text.append (String.format ("Format ......... %s %n", format.mask ())); + text.append (String.format ("Decimals ....... %s %n", format.decimals)); + text.append (String.format ("Top line ....... %d %n", r1)); + text.append (String.format ("Left column .... %d %n", c1)); + text.append (String.format ("Title top line . %d %n", r2)); + text.append (String.format ("Title left col . %d %n", c2)); + text.append (String.format ("Top line ....... %d %n", r3)); + text.append (String.format ("Left column .... %d %n", c3)); + text.append (String.format ("Title top line . %d %n", r4)); + text.append (String.format ("Title left col . %d %n", c4)); + text.append (String.format ("Title top line . %d %n", r5)); + text.append (String.format ("Title left col . %d %n", c5)); + text.append (String.format ("Top line ....... %d %n", r6)); + text.append (String.format ("Left column .... %d %n", c6)); + text.append (String.format ("Title top line . %d %n", r7)); + text.append (String.format ("Title left col . %d %n", c7)); + text.append (String.format ("Body rows ...... %d %n", bodyRows)); + text.append (String.format ("Right col hidden %s %n", rightColumnNotDisplayed)); + text.append (String.format ("Top title sw ... %s %n", topTitleSwitch)); + text.append (String.format ("Left title sw .. %s %n", sideTitleSwitch)); + + return text.toString (); + } + } + + private class Row + { + private final int rowNumber; + private final List cells = new ArrayList (); + + public Row (int ptr) + { + rowNumber = HexFormatter.getWord (buffer, ptr); + ptr += 2; // first control byte + + int column = 0; + int val; + + while ((val = buffer[ptr++] & 0xFF) != 0xFF) + { + if (val > 0x80) + { + column += (val - 0x80); // skip columns + continue; + } + + if (ptr >= buffer.length) + { + System.out.println ("too long for buffer"); + break; + } + + int b1 = buffer[ptr] & 0xFF; + + if ((b1 & 0xE0) == 0 || (b1 & 0xA0) == 0x20) // Label - 000 or 0.1 + cells.add (new CellLabel (buffer, rowNumber, column++, ptr, val)); + else if ((b1 & 0xA0) == 0xA0) // Constant - 1.1 + { + if (val > 0) + cells.add (new CellConstant (buffer, rowNumber, column++, ptr, val)); + } + else if ((b1 & 0xA0) == 0x80) // Value - 1.0 + cells.add (new CellValue (buffer, rowNumber, column++, ptr, val)); + else + System.out.println ("Unknown Cell value : " + val); + + ptr += val; + } + } + + @Override + public String toString () + { + StringBuilder text = new StringBuilder (); + + text.append (String.format ("Row number ..... %s %n", rowNumber)); + for (Cell cell : cells) + { + text.append (cell); + text.append ("\n"); + } + + return text.toString (); + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/appleworks/AppleworksWPFile.java b/src/com/bytezone/diskbrowser/appleworks/AppleworksWPFile.java new file mode 100755 index 0000000..85264c5 --- /dev/null +++ b/src/com/bytezone/diskbrowser/appleworks/AppleworksWPFile.java @@ -0,0 +1,187 @@ +package com.bytezone.diskbrowser.appleworks; + +import com.bytezone.diskbrowser.applefile.AbstractFile; + +public class AppleworksWPFile extends AbstractFile +{ + Header header; + + public AppleworksWPFile (String name, byte[] buffer) + { + super (name, buffer); + + header = new Header (); + } + + @Override + public String getText () + { + int leftMargin = header.leftMargin; + int rightMargin; + int topMargin; + int bottomMargin; + int paperLength; + int indent; + + int ptr = 300; // skip the header + StringBuilder text = new StringBuilder (header.toString ()); + text.append ("\n"); + + while (true) + { + int b1 = buffer[ptr] & 0xFF; + int b2 = buffer[ptr + 1] & 0xFF; + // System.out.printf ("%02X%02X %n", (buffer[ptr] & 0xFF), (buffer[ptr + 1] & 0xFF)); + + if (b1 == 0xFF && b2 == 0xFF) + break; + + switch (b2) + { + case 0: + int len = b1; + int b3 = buffer[ptr + 2] & 0xFF; + int b4 = buffer[ptr + 3] & 0xFF; + + int lineMargin = b3 & 0x7F; + boolean containsTabs = (b3 & 0x80) != 0; + int textLen = b4 & 0x7F; + boolean cr = (b4 & 0x80) != 0; + + // System.out.printf ("%02X %02X %d %d %s %s%n", b3, b4, margin, textLen, + // containsTabs, cr); + if (b3 == 0xFF) + text.append ("--------- Ruler ----------\n"); + else + { + // left margin + for (int i = 0; i < leftMargin; i++) + text.append (" "); + for (int i = 0; i < lineMargin; i++) + text.append (" "); + + // check for tabs (I'm guessing about how this works) + if (false) + { + while (buffer[ptr + 4] == 0x16) // tab character + { + ptr++; + len--; + while (buffer[ptr + 4] == 0x17) // tab fill character + { + text.append (" "); + ptr++; + len--; + } + } + text.append (new String (buffer, ptr + 4, len - 2)); + ptr += len; + } + else + { + StringBuilder line = new StringBuilder (); + int p = ptr + 4; + ptr += len; + len -= 2; + + while (--len >= 0) + { + char c = (char) buffer[p++]; + if (c >= 0x20) + line.append (c); + else if (c == 0x17) + line.append (' '); + } + + text.append (line.toString ()); + } + } + + text.append ("\n"); + + if (cr) + text.append ("\n"); + + break; + + case 0xD0: + text.append ("\n"); + break; + + case 0xD9: + leftMargin = b1; + break; + + case 0xDA: + rightMargin = b1; + break; + + case 0xDE: + indent = b1; + break; + + case 0xE2: + paperLength = b1; + break; + + case 0xE3: + topMargin = b1; + break; + + case 0xE4: + bottomMargin = b1; + break; + + default: + System.out.printf ("Unknown value : %02X %02X%n", b1, b2); + } + ptr += 2; + } + return text.toString (); + } + + private class Header + { + private final char[] tabStops = new char[80]; + private final String tabs; + private final boolean zoom; + private final boolean paginated; + private final int leftMargin; + private final boolean mailMerge; + private final int sfMinVers; + + private final boolean multipleRulers; + + public Header () + { + assert buffer[4] == 0x4F; + + int ptr = 5; + for (int i = 0; i < 80; i++) + tabStops[i] = (char) buffer[ptr++]; + + tabs = new String (tabStops); + zoom = buffer[85] != 0; + paginated = buffer[90] != 0; + leftMargin = buffer[91] & 0xFF; + mailMerge = buffer[92] != 0; + + multipleRulers = buffer[176] != 0; + sfMinVers = buffer[183] & 0xFF; + } + + @Override + public String toString () + { + StringBuilder text = new StringBuilder (); + + text.append (String.format ("Tabs ......... %s %n", tabs)); + text.append (String.format ("Zoom ......... %s %n", zoom)); + text.append (String.format ("Mail merge ... %s %n", mailMerge)); + text.append (String.format ("Left margin .. %d %n", leftMargin)); + text.append (String.format ("Min version .. %d %n", sfMinVers)); + + return text.toString (); + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/appleworks/Cell.java b/src/com/bytezone/diskbrowser/appleworks/Cell.java new file mode 100644 index 0000000..2d11415 --- /dev/null +++ b/src/com/bytezone/diskbrowser/appleworks/Cell.java @@ -0,0 +1,31 @@ +package com.bytezone.diskbrowser.appleworks; + +class Cell +{ + final String cellName; + final int row; + final int column; + String value; + String type; + + static String getCellName (int row, int column) + { + char c1 = (char) ('A' + column / 26 - 1); + char c2 = (char) ('A' + column % 26); + return "" + (c1 == '@' ? "" : c1) + c2 + row; + } + + public Cell (int row, int column, int offset, int length) + { + this.row = row; + this.column = column; + + cellName = getCellName (row, column); + } + + @Override + public String toString () + { + return String.format ("%5s : %s %s%n", cellName, type, value); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/appleworks/CellAddress.java b/src/com/bytezone/diskbrowser/appleworks/CellAddress.java new file mode 100644 index 0000000..d569cfa --- /dev/null +++ b/src/com/bytezone/diskbrowser/appleworks/CellAddress.java @@ -0,0 +1,21 @@ +package com.bytezone.diskbrowser.appleworks; + +import com.bytezone.diskbrowser.HexFormatter; + +public class CellAddress +{ + int colRef; + int rowRef; + + public CellAddress (byte[] buffer, int offset) + { + colRef = buffer[offset]; + rowRef = HexFormatter.getSignedWord (buffer[offset + 1], buffer[offset + 2]); + } + + @Override + public String toString () + { + return String.format ("[Row=%04d, Col=%04d]", rowRef, colRef); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/appleworks/CellConstant.java b/src/com/bytezone/diskbrowser/appleworks/CellConstant.java new file mode 100644 index 0000000..ab4e492 --- /dev/null +++ b/src/com/bytezone/diskbrowser/appleworks/CellConstant.java @@ -0,0 +1,40 @@ +package com.bytezone.diskbrowser.appleworks; + +import com.bytezone.diskbrowser.HexFormatter; + +public class CellConstant extends Cell +{ + double saneDouble; + CellFormat format; + + public CellConstant (byte[] buffer, int row, int column, int offset, int length) + { + super (row, column, offset, length); + + type = "Const"; + + // assert length == 10; + + if (length != 10) + { + System.out.println ("Spreadsheet CellConstant with length != 10"); + System.out.printf ("Row %d, Col %d, Length %d %n", row, column, length); + System.out.println (HexFormatter.format (buffer, offset, length)); + type = "*** Invalid Constant ***"; + value = ""; + } + else + { + long bits = 0; + for (int i = 9; i >= 2; i--) + { + bits <<= 8; + bits |= buffer[offset + i] & 0xFF; + } + + saneDouble = Double.longBitsToDouble (bits); + format = new CellFormat (buffer[offset], buffer[offset + 1]); + value = String.format (format.mask (), saneDouble).trim (); + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/appleworks/CellFormat.java b/src/com/bytezone/diskbrowser/appleworks/CellFormat.java new file mode 100644 index 0000000..e52d67c --- /dev/null +++ b/src/com/bytezone/diskbrowser/appleworks/CellFormat.java @@ -0,0 +1,50 @@ +package com.bytezone.diskbrowser.appleworks; + +public class CellFormat +{ + boolean labelAllowed; + boolean valueAllowed; + boolean display; + boolean standard; + boolean fixed; + boolean dollars; + boolean commas; + boolean percent; + boolean appropriate; + int decimals; + + public CellFormat (byte format) + { + display = (format & 0x40) == 0; + labelAllowed = (format & 0x10) == 0; + valueAllowed = (format & 0x08) == 0; + + int formatting = format & 0x07; + + standard = formatting == 1; + fixed = formatting == 2; + dollars = formatting == 3; + commas = formatting == 4; + percent = formatting == 5; + appropriate = formatting == 6; + } + + public CellFormat (byte format, byte decimals) + { + this (format); + this.decimals = decimals & 0x07; + } + + public String mask () + { + String fmt = dollars ? "$%" : "%"; + if (commas) + fmt += ","; + fmt += "12." + decimals; + fmt += "f"; + if (percent) + fmt += "%%"; + + return fmt; + } +} diff --git a/src/com/bytezone/diskbrowser/appleworks/CellFormula.java b/src/com/bytezone/diskbrowser/appleworks/CellFormula.java new file mode 100644 index 0000000..d05c3f7 --- /dev/null +++ b/src/com/bytezone/diskbrowser/appleworks/CellFormula.java @@ -0,0 +1,59 @@ +package com.bytezone.diskbrowser.appleworks; + +import com.bytezone.diskbrowser.HexFormatter; + +public class CellFormula +{ + private static String[] tokens = {// + "@Deg", "@Rad", "@Pi", "@True", "@False", "@Not", "@IsBlank", "@IsNA", "@IsError", + "@Exp", "@Ln", "@Log", "@Cos", "@Sin", "@Tan", "@ACos", "@ASin", "@ATan2", + "@ATan", "@Mod", "@FV", "@PV", "@PMT", "@Term", "@Rate", "@Round", "@Or", "@And", + "@Sum", "@Avg", "@Choose", "@Count", "@Error", "@IRR", "@If", "@Int", "@Lookup", + "@Max", "@Min", "@NA", "@NPV", "@Sqrt", "@Abs", "", "<>", ">=", "<=", "=", ">", + "<", ",", "^", ")", "-", "+", "/", "*", "(", "-", "+", "..." }; + String value; + + public CellFormula (Cell cell, byte[] buffer, int offset, int length) + { + StringBuilder text = new StringBuilder (); + + for (int i = 0; i < length; i++) + { + int value = buffer[offset + i] & 0xFF; + if (value < 0xFD) + { + String token = tokens[value - 0xC0]; + text.append (token); + if (value == 0xE0 || value == 0xE7) + i += 3; + } + else if (value == 0xFD) + { + double d = HexFormatter.getSANEDouble (buffer, offset + i + 1); + String num = String.format ("%f", d).trim (); + while (num.endsWith ("0")) + num = num.substring (0, num.length () - 1); + if (num.endsWith (".")) + num = num.substring (0, num.length () - 1); + text.append (num); + i += 8; + } + else if (value == 0xFE) + { + CellAddress address = new CellAddress (buffer, offset + i + 1); + String cellName = + Cell.getCellName (cell.row + address.rowRef, cell.column + address.colRef); + i += 3; + text.append (cellName); + } + else if (value == 0xFF) + { + int len = buffer[offset + i + 1] & 0xFF; + String word = new String (buffer, offset + i + 2, len); + i += len + 1; + System.out.println ("Word: " + word); + } + } + value = text.toString (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/appleworks/CellLabel.java b/src/com/bytezone/diskbrowser/appleworks/CellLabel.java new file mode 100644 index 0000000..39cbf49 --- /dev/null +++ b/src/com/bytezone/diskbrowser/appleworks/CellLabel.java @@ -0,0 +1,30 @@ +package com.bytezone.diskbrowser.appleworks; + +import com.bytezone.diskbrowser.HexFormatter; + +public class CellLabel extends Cell +{ + boolean propagated; + String label; + + public CellLabel (byte[] buffer, int row, int column, int offset, int length) + { + super (row, column, offset, length); + + int b1 = buffer[offset] & 0xFF; + + // label = new String (buffer, offset + 1, length - 1); + + // MOUSE.TEXT.SS/TAWUG.22/TAWUG 21 to 25.2mg has funny characters + label = HexFormatter.sanitiseString (buffer, offset + 1, length - 1); + + // int columnWidth = header.columnWidths[column]; + + value = "[" + label + "]"; + type = "Label"; + propagated = (b1 & 0xA0) == 0x20; + + if (propagated) + value += "+"; + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/appleworks/CellValue.java b/src/com/bytezone/diskbrowser/appleworks/CellValue.java new file mode 100644 index 0000000..98ec223 --- /dev/null +++ b/src/com/bytezone/diskbrowser/appleworks/CellValue.java @@ -0,0 +1,36 @@ +package com.bytezone.diskbrowser.appleworks; + +import com.bytezone.diskbrowser.HexFormatter; + +public class CellValue extends Cell +{ + CellFormat format; + CellFormula formula; + boolean lastEvalNA; + boolean lastEvalError; + double saneDouble; + + public CellValue (byte[] buffer, int row, int column, int offset, int length) + { + super (row, column, offset, length); + + type = "Value"; + + // if (header.ssMinVers != 0) + // { + // System.out.println ("AppleWorks v" + header.ssMinVers + " required!"); + // value = HexFormatter.getHexString (buffer, offset, length); + // } + // else + { + format = new CellFormat (buffer[offset]); + int b1 = buffer[offset + 1] & 0xFF; + lastEvalNA = (b1 & 0x40) != 0; + lastEvalError = (b1 & 0x20) != 0; + saneDouble = HexFormatter.getSANEDouble (buffer, offset + 2); + value = String.format (format.mask (), saneDouble).trim (); + formula = new CellFormula (this, buffer, offset + 10, length - 10); + value = String.format ("%-15s %s", value, formula.value); + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/appleworks/LabelReport.java b/src/com/bytezone/diskbrowser/appleworks/LabelReport.java new file mode 100644 index 0000000..d64b31c --- /dev/null +++ b/src/com/bytezone/diskbrowser/appleworks/LabelReport.java @@ -0,0 +1,16 @@ +package com.bytezone.diskbrowser.appleworks; + +class LabelReport extends Report +{ + + public LabelReport (AppleworksADBFile parent, byte[] buffer, int offset) + { + super (parent, buffer, offset); + } + + @Override + public String getText () + { + return "Skipping vertical report\n"; + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/appleworks/Record.java b/src/com/bytezone/diskbrowser/appleworks/Record.java new file mode 100644 index 0000000..b4d017b --- /dev/null +++ b/src/com/bytezone/diskbrowser/appleworks/Record.java @@ -0,0 +1,178 @@ +package com.bytezone.diskbrowser.appleworks; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +class Record +{ + AppleworksADBFile parent; + int length; + List items = new ArrayList (); + Map calculatedItems = new HashMap ();// move to TableReport + + public Record (AppleworksADBFile parent, byte[] buffer, int ptr) + { + this.parent = parent; + int count; + + while ((count = buffer[ptr++] & 0xFF) != 0xFF) + { + if (count < 0x80) // size of category data + { + if (buffer[ptr] == (byte) 0xC0) // date + { + String year = new String (buffer, ptr + 1, 2); + int month = buffer[ptr + 3] - '@'; + String day = new String (buffer, ptr + 4, 2).trim (); + if (day.length () == 1) + day = "0" + day; + items.add (String.format ("%2s/%02d/%2s", year, month, day)); + } + else if (buffer[ptr] == (byte) 0xD4) // time + { + int hour = buffer[ptr + 1] - '@'; + String minute = new String (buffer, ptr + 2, 2); + items.add (String.format ("%02d:%s", hour, minute)); + } + else + items.add (new String (buffer, ptr, count)); + + ptr += count; + } + else + while (count-- > 0x80) + items.add (""); + } + + if (items.size () > parent.categories) + System.out.println ("Too many items"); + + while (items.size () < parent.categories) + items.add (""); + } + + public String getItem (int index) + { + return items.get (index); + } + + public double calculateItem (int pos, int name, String condition) + { + try + { + // System.out.printf ("%nCalculating %d (%s): %s%n", pos, (char) name, condition); + Pattern p = Pattern.compile ("([A-Za-z]{1,2})(([-+*/]([A-Za-z]{1,2}|[0-9]))*)"); + Matcher m = p.matcher (condition); + if (m.matches ()) + { + String init = m.group (1); + String rest = m.group (2); + + double val = Double.parseDouble (valueOf (init.charAt (0))); + + Pattern p2 = Pattern.compile ("([-+*/])(([A-Za-z]{1,2})|([0-9]{1,6}))"); + Matcher m2 = p2.matcher (rest); + + while (m2.find ()) + { + String operator = m2.group (1).trim (); + + double nextVal; + if (m2.group (3) != null) + nextVal = Double.parseDouble (valueOf (m2.group (3).charAt (0))); + else + nextVal = Double.parseDouble (m2.group (4)); + + if (operator.equals ("+")) + val += nextVal; + else if (operator.equals ("-")) + val -= nextVal; + else if (operator.equals ("/")) + val /= nextVal; + else if (operator.equals ("*")) + val *= nextVal; + else + System.out.println ("Unknown operator : " + operator); + } + + calculatedItems.put (name, val); + // System.out.printf ("Putting %s : %f%n", (char) name, val); + return val; + } + } + catch (Exception e) + { + e.printStackTrace (); + return 0.0; + } + return 0.0; + } + + private String valueOf (int field) + { + int itemNo = field - 'A'; + + if (itemNo > 26) // lowercase - calculated + { + if (calculatedItems.containsKey (field)) + { + // System.out.printf (" Value of : [%s]", (char) field); + // System.out.println (" -> " + calculatedItems.get (field)); + return Double.toString (calculatedItems.get (field)); + } + System.out.println ("Didn't find : " + field); + } + + if (itemNo < items.size ()) + { + // System.out.printf (" Value of : [%s]", (char) field); + // System.out.println (" -> " + items.get (itemNo)); + return items.get (itemNo); + } + + System.out.printf (" -> can't find: %d out of %d%n", (itemNo + 1), items.size ()); + return "0.0"; + } + + public String getReportLine (String format) + { + return String.format (format, (Object[]) items.toArray (new String[items.size ()])); + } + + public String getReportLine () + { + StringBuilder text = new StringBuilder (); + String format = String.format ("%%-%ds : %%s%%n", parent.maxCategoryName); + + int count = 0; + for (String item : items) + { + if (count < parent.categoryNames.length) + text.append (String.format (format, parent.categoryNames[count++], item)); + else + text.append (item + "\n"); + } + + if (text.length () > 0) + text.deleteCharAt (text.length () - 1); + + return text.toString (); + } + + @Override + public String toString () + { + StringBuilder text = new StringBuilder (); + String format = "%-" + parent.maxCategoryName + "s [%s]%n"; + + int category = 0; + for (String item : items) + text.append (String.format (format, parent.categoryNames[category++], item)); + + return text.toString (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/appleworks/Report.java b/src/com/bytezone/diskbrowser/appleworks/Report.java new file mode 100644 index 0000000..7106f03 --- /dev/null +++ b/src/com/bytezone/diskbrowser/appleworks/Report.java @@ -0,0 +1,182 @@ +package com.bytezone.diskbrowser.appleworks; + +import com.bytezone.diskbrowser.HexFormatter; + +abstract class Report +{ + static final String line = "-------------------------------------------------------" + + "-----------------------------------\n"; + static final String gap = " " + + " "; + static final String[] testText = { "", "=", ">", "<", "?4", "?5", "<>", "?7", "?8", "?9", + "?10", "?11", "?12", "?13" }; + static final String[] continuationText = { "", "And", "Or", "Through" }; + + protected final AppleworksADBFile parent; + protected final String name; + private final char reportFormat; + private final char spacing; + protected final int categoriesOnThisReport; + protected final String titleLine; + protected final boolean printHeader; + private final int platenWidth; + private final int leftMargin; + private final int rightMargin; + private final int charsPerInch; + private final int paperLength; + private final int topMargin; + private final int bottomMargin; + private final int linesPerInch; + + private final int[] selectionRules = new int[3]; + private final int[] testTypes = new int[3]; + private final int[] continuation = new int[3]; + private final String[] comparison = new String[3]; + + private final String printerRules; + private final boolean printDash; + private String fudgeReason; + + public Report (AppleworksADBFile parent, byte[] buffer, int offset) + { + this.parent = parent; + + name = pascalString (buffer, offset); + categoriesOnThisReport = buffer[offset + 200] & 0xFF; + platenWidth = buffer[offset + 205] & 0xFF; + leftMargin = buffer[offset + 206] & 0xFF; + rightMargin = buffer[offset + 207] & 0xFF; + charsPerInch = buffer[offset + 208] & 0xFF; + paperLength = buffer[offset + 209] & 0xFF; + topMargin = buffer[offset + 210] & 0xFF; + bottomMargin = buffer[offset + 211] & 0xFF; + linesPerInch = buffer[offset + 212] & 0xFF; + + reportFormat = (char) buffer[offset + 214]; + spacing = (char) buffer[offset + 215]; + printHeader = buffer[offset + 216] != 0; + + titleLine = pascalString (buffer, offset + 220); + int printerLength = buffer[offset + 464] & 0xFF; + if (printerLength > 13) + System.out.println ("*** Dodgy printer rules ***"); + + printerRules = pascalString (buffer, offset + 464); + printDash = buffer[offset + 478] != 0; + + int fudge = 0; + if (buffer[offset + 480] != 0) + { + fudge = 1; + fudgeReason = "*** Report rules ***"; + if (buffer[offset + 481] != 0) + fudgeReason = "*** Bollocksed ***"; + } + else if (buffer[offset + 464] == 0 && buffer[offset + 465] != 0) + { + fudgeReason = "*** Printer codes ***"; + fudge = 1; + } + else if (buffer[offset + 479] != 0 && buffer[offset + 485] == 0) + { + fudgeReason = "*** Test codes ***"; + fudge = 1; + } + else + fudgeReason = ""; + + if (false) + { + System.out.println ("=============================================================="); + System.out.println ("Header"); + System.out.println (HexFormatter.formatNoHeader (buffer, offset + 213, 7)); + System.out.println (); + System.out.println ("Title line:"); + System.out.println (HexFormatter.formatNoHeader (buffer, offset + 220, 81)); + System.out.println (); + System.out.println ("Calculated categories:"); + System.out.println (HexFormatter.formatNoHeader (buffer, offset + 302, 22)); + System.out.println (HexFormatter.formatNoHeader (buffer, offset + 324, 32)); + System.out.println (); + System.out.println (HexFormatter.formatNoHeader (buffer, offset + 356, 22)); + System.out.println (HexFormatter.formatNoHeader (buffer, offset + 378, 32)); + System.out.println (); + System.out.println (HexFormatter.formatNoHeader (buffer, offset + 410, 22)); + System.out.println (HexFormatter.formatNoHeader (buffer, offset + 432, 32)); + System.out.println (); + System.out.println ("Printer rules:"); + System.out.println (HexFormatter.formatNoHeader (buffer, offset + 464 + fudge, 14)); + System.out.println (); + System.out.println ("Printer dash:"); + System.out.println (HexFormatter.formatNoHeader (buffer, offset + 478 + fudge, 1)); + System.out.println (); + System.out.println ("Selection rules:"); + System.out.println (" categories:"); + System.out.println (HexFormatter.formatNoHeader (buffer, offset + 479 + fudge, 6)); + System.out.println (" tests:"); + System.out.println (HexFormatter.formatNoHeader (buffer, offset + 485 + fudge, 6)); + System.out.println (" continuation:"); + System.out.println (HexFormatter.formatNoHeader (buffer, offset + 491 + fudge, 6)); + System.out.println (" comparison:"); + System.out.println (HexFormatter.formatNoHeader (buffer, offset + 497 + fudge, 32)); + System.out.println (); + } + + if (buffer[offset + 480 + fudge] == 0) // test high byte + for (int i = 0; i < 3; i++) + { + selectionRules[i] = HexFormatter.getWord (buffer, offset + 479 + i * 2 + fudge); + testTypes[i] = HexFormatter.getWord (buffer, offset + 485 + i * 2 + fudge); + continuation[i] = HexFormatter.getWord (buffer, offset + 491 + i * 2 + fudge); + comparison[i] = pascalString (buffer, offset + 497 + i * 32 + fudge); + } + else + System.out.println ("*** Invalid value in report rules ***"); + } + + public abstract String getText (); + + protected String pascalString (byte[] buffer, int ptr) + { + return new String (buffer, ptr + 1, buffer[ptr] & 0xFF); + } + + @Override + public String toString () + { + StringBuilder text = new StringBuilder (); + + text.append (String.format ("Report name ........ %s%n", name)); + text.append (String.format ("Report type ........ %s%n", reportFormat)); + text.append (String.format ("Spacing ............ %s (Single/Double/Triple)%n", spacing)); + text.append (String.format ("Print header ....... %s%n", printHeader)); + text.append (String.format ("Title .............. %s%n", titleLine)); + text.append (String.format ("L/R/T/B margin ..... %d/%d/%d/%d%n", leftMargin, rightMargin, + topMargin, bottomMargin)); + text.append (String.format ("Categories ......... %s%n", categoriesOnThisReport)); + text.append (String.format ("Platen width ....... %d%n", platenWidth)); + text.append (String.format ("Chars per inch ..... %d%n", charsPerInch)); + text.append (String.format ("Paper length ....... %d%n", paperLength)); + text.append (String.format ("Lines per inch ..... %d%n", linesPerInch)); + text.append (String.format ("Print dash ......... %s%n", printDash)); + text.append (String.format ("Printer rules ...... %s%n", printerRules)); + + text.append ("Report rules ....... "); + for (int i = 0; i < 3; i++) + { + if (selectionRules[i] == 0) + break; + int category = selectionRules[i] - 1; + int test = testTypes[i]; + int cont = continuation[i]; + text.append (String.format ("[%s] %s [%s] %s ", parent.categoryNames[category], + testText[test], comparison[i], continuationText[cont])); + } + text.append ("\n"); + + if (!fudgeReason.isEmpty ()) + text.append ("Fudge .............. " + fudgeReason + "\n"); + + return text.toString (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/appleworks/TableReport.java b/src/com/bytezone/diskbrowser/appleworks/TableReport.java new file mode 100644 index 0000000..eb89940 --- /dev/null +++ b/src/com/bytezone/diskbrowser/appleworks/TableReport.java @@ -0,0 +1,211 @@ +package com.bytezone.diskbrowser.appleworks; + +class TableReport extends Report +{ + private final int[] columnWidths = new int[33]; + private final int[] spaces = new int[33]; + private final int[] footTotals = new int[33]; + private final int[] justification = new int[33]; + private final int[] reportCategoryNames = new int[33]; + + private final int[] calculatedColumn = new int[3]; + private final String[] calculatedCategory = new String[3]; + private final String[] calculatedRules = new String[3]; + + private final int groupTotalColumn; + private final boolean printGroupTotals; + + public TableReport (AppleworksADBFile parent, byte[] buffer, int offset) + { + super (parent, buffer, offset); + + for (int i = 0; i < categoriesOnThisReport; i++) + { + columnWidths[i] = buffer[offset + 20 + i] & 0xFF; + spaces[i] = buffer[offset + 56 + i] & 0xFF; + reportCategoryNames[i] = buffer[offset + 92 + i] & 0xFF; + footTotals[i] = buffer[offset + 128 + i] & 0xFF; + justification[i] = buffer[offset + 164 + i] & 0xFF; + } + + for (int i = 0; i < 3; i++) + { + calculatedColumn[i] = buffer[offset + 201 + i] & 0xFF; + calculatedCategory[i] = pascalString (buffer, offset + 302 + i * 54); + calculatedRules[i] = pascalString (buffer, offset + 324 + i * 54); + } + + groupTotalColumn = buffer[offset + 204] & 0xFF; + printGroupTotals = buffer[offset + 217] != 0; + } + + @Override + public String getText () + { + StringBuilder text = new StringBuilder (); + + if (printHeader && !titleLine.isEmpty ()) + text.append (titleLine); + else + text.append ("Report name: " + name); + text.append ("\n\n"); + + StringBuilder header = new StringBuilder (); + StringBuilder underline = new StringBuilder (); + + for (int i = 0; i < categoriesOnThisReport; i++) + { + int category = reportCategoryNames[i]; + String categoryName; + if (category < 0x7F) + categoryName = parent.categoryNames[category - 1]; + else + { + int calcField = (category - 0x80); + categoryName = calculatedCategory[calcField]; + } + + if (categoryName.length () > columnWidths[i]) + categoryName = categoryName.substring (0, columnWidths[i]); + + header.append (categoryName); + header.append (gap.substring (0, columnWidths[i] + spaces[i] - categoryName.length ())); + underline.append (line.substring (0, columnWidths[i])); + underline.append (gap.substring (0, spaces[i])); + } + + header = trimRight (header); + text.append (header.toString ()); + text.append ("\n"); + + underline = trimRight (underline); + text.append (underline.toString ()); + text.append ("\n"); + + float[] totals = new float[33]; + + for (Record record : parent.records) + { + for (int i = 0; i < categoriesOnThisReport; i++) + { + int category = reportCategoryNames[i]; + String item; + if (category < 0x7F) + item = record.getItem (category - 1).trim (); + else + { + int calcField = (category - 0x80); + String cond = calculatedRules[calcField]; + int col = calculatedColumn[calcField] - 1; + String format = "%12." + justification[col] + "f"; + item = + String.format (format, record.calculateItem (calcField, i + 97, cond)).trim (); + // System.out.println (item); + } + + if (item.length () > columnWidths[i]) + item = item.substring (0, columnWidths[i]); + + if (footTotals[i] != 0xFF && !item.isEmpty () && !item.equals (" ")) + { + try + { + totals[i] += Float.parseFloat (item); + } + catch (NumberFormatException e) + { + // ignore this value + } + } + + if (justification[i] != 0xFF) + { + text.append (gap.substring (0, columnWidths[i] - item.length ())); + text.append (item); + } + else + { + text.append (item); + text.append (gap.substring (0, columnWidths[i] - item.length ())); + } + + text.append (gap.substring (0, spaces[i])); + } + + text = trimRight (text); + text.append ("\n"); + } + + text.append (underline.toString ()); + text.append ("\n"); + + StringBuilder totalLine = new StringBuilder (); + underline = new StringBuilder (); + boolean hasTotals = false; + + for (int i = 0; i < categoriesOnThisReport; i++) + { + if (footTotals[i] == 0xFF) // not totalled + { + totalLine.append (gap.substring (0, columnWidths[i])); + underline.append (gap.substring (0, columnWidths[i])); + } + else + { + hasTotals = true; + String format = "%12." + footTotals[i] + "f"; + String value = String.format (format, totals[i]).trim (); + if (value.length () > columnWidths[i]) + value = value.substring (0, columnWidths[i]); // wrong end + + totalLine.append (gap.substring (0, columnWidths[i] - value.length ())); + totalLine.append (value); + underline.append (line.substring (0, columnWidths[i])); + } + + totalLine.append (gap.substring (0, spaces[i])); + underline.append (gap.substring (0, spaces[i])); + } + + if (hasTotals) + { + text.append (totalLine.toString ()); + text.append ("\n"); + text.append (underline.toString ()); + text.append ("\n"); + } + + return text.toString (); + } + + private StringBuilder trimRight (StringBuilder text) + { + while (text.length () > 0 && text.charAt (text.length () - 1) == ' ') + text.deleteCharAt (text.length () - 1); + return text; + } + + @Override + public String toString () + { + StringBuilder text = new StringBuilder (super.toString ()); + text.append (String.format ("Calculated ......... %d %d %d%n", calculatedColumn[0], + calculatedColumn[1], calculatedColumn[2])); + text.append (String.format ("Group total ........ %d%n", groupTotalColumn)); + text.append (String.format ("Print gr totals .... %s%n", printGroupTotals)); + text.append (String.format ("Calc category1 ..... %s%n", calculatedCategory[0])); + text.append (String.format ("Calc rules1 ........ %s%n", calculatedRules[0])); + text.append (String.format ("Calc category2 ..... %s%n", calculatedCategory[1])); + text.append (String.format ("Calc rules2 ........ %s%n", calculatedRules[1])); + text.append (String.format ("Calc category3 ..... %s%n", calculatedCategory[2])); + text.append (String.format ("Calc rules3 ........ %s%n", calculatedRules[2])); + + text.append (String.format ("%n Width Space Name Foot Just%n")); + for (int i = 0; i < categoriesOnThisReport; i++) + text.append (String.format (" %2d %2d %02X %02X %02X %n", columnWidths[i], + spaces[i], reportCategoryNames[i], footTotals[i], + justification[i])); + + return text.toString (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/catalog/AbstractCatalogCreator.java b/src/com/bytezone/diskbrowser/catalog/AbstractCatalogCreator.java new file mode 100755 index 0000000..41c4a79 --- /dev/null +++ b/src/com/bytezone/diskbrowser/catalog/AbstractCatalogCreator.java @@ -0,0 +1,13 @@ +package com.bytezone.diskbrowser.catalog; + +import javax.swing.tree.DefaultMutableTreeNode; + +public abstract class AbstractCatalogCreator implements CatalogLister +{ + DefaultMutableTreeNode node; + + public void setNode (DefaultMutableTreeNode node) + { + this.node = node; + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/catalog/AbstractDiskCreator.java b/src/com/bytezone/diskbrowser/catalog/AbstractDiskCreator.java new file mode 100755 index 0000000..05a7a61 --- /dev/null +++ b/src/com/bytezone/diskbrowser/catalog/AbstractDiskCreator.java @@ -0,0 +1,35 @@ +package com.bytezone.diskbrowser.catalog; + +import java.util.Enumeration; + +import javax.swing.JTree; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; + +import com.bytezone.diskbrowser.disk.FormattedDisk; +import com.bytezone.diskbrowser.gui.DiskSelectedEvent; + +public abstract class AbstractDiskCreator implements DiskLister +{ + FormattedDisk disk; + + public void setDisk (FormattedDisk disk) + { + this.disk = disk; + } + + // should return List + @SuppressWarnings("unchecked") + public Enumeration getEnumeration () + { + JTree tree = disk.getCatalogTree (); + DefaultTreeModel treeModel = (DefaultTreeModel) tree.getModel (); + DefaultMutableTreeNode node = (DefaultMutableTreeNode) treeModel.getRoot (); + return node.breadthFirstEnumeration (); + } + + public void diskSelected (DiskSelectedEvent e) + { + setDisk (e.getFormattedDisk ()); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/catalog/CatalogLister.java b/src/com/bytezone/diskbrowser/catalog/CatalogLister.java new file mode 100755 index 0000000..6620542 --- /dev/null +++ b/src/com/bytezone/diskbrowser/catalog/CatalogLister.java @@ -0,0 +1,12 @@ +package com.bytezone.diskbrowser.catalog; + +import javax.swing.tree.DefaultMutableTreeNode; + +public interface CatalogLister +{ + public void setNode (DefaultMutableTreeNode node); + + public void createCatalog (); + + public String getMenuText (); +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/catalog/DiskLister.java b/src/com/bytezone/diskbrowser/catalog/DiskLister.java new file mode 100755 index 0000000..81e2003 --- /dev/null +++ b/src/com/bytezone/diskbrowser/catalog/DiskLister.java @@ -0,0 +1,19 @@ +package com.bytezone.diskbrowser.catalog; + +import java.util.Enumeration; + +import javax.swing.tree.DefaultMutableTreeNode; + +import com.bytezone.diskbrowser.disk.FormattedDisk; +import com.bytezone.diskbrowser.gui.DiskSelectionListener; + +public interface DiskLister extends DiskSelectionListener +{ + public void setDisk (FormattedDisk disk); + + public void createDisk (); + + public Enumeration getEnumeration (); + + public String getMenuText (); +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/catalog/DocumentCreatorFactory.java b/src/com/bytezone/diskbrowser/catalog/DocumentCreatorFactory.java new file mode 100755 index 0000000..eb0cbc1 --- /dev/null +++ b/src/com/bytezone/diskbrowser/catalog/DocumentCreatorFactory.java @@ -0,0 +1,52 @@ +package com.bytezone.diskbrowser.catalog; + +/******************************************************************************* + * Factory object that determines whether iText is available, and creates a + * CatalogLister and a DiskLister accordingly. Also links the two xxxListers to + * menu items. + ******************************************************************************/ + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import com.bytezone.diskbrowser.gui.MenuHandler; + +public class DocumentCreatorFactory +{ + public CatalogLister catalogLister; + public DiskLister diskLister; + + public DocumentCreatorFactory (MenuHandler mh) + { + // try + // { + // Class.forName ("com.lowagie.text.Document"); + // catalogLister = new PDFCatalogCreator (); + // diskLister = new PDFDiskCreator (); + // } + // catch (ClassNotFoundException e) + { + catalogLister = new TextCatalogCreator (); + diskLister = new TextDiskCreator (); + } + + mh.createCatalogFileItem.setText (catalogLister.getMenuText ()); + mh.createDiskFileItem.setText (diskLister.getMenuText ()); + + mh.createCatalogFileItem.addActionListener (new ActionListener () + { + public void actionPerformed (ActionEvent e) + { + catalogLister.createCatalog (); + } + }); + + mh.createDiskFileItem.addActionListener (new ActionListener () + { + public void actionPerformed (ActionEvent e) + { + diskLister.createDisk (); + } + }); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/catalog/TextCatalogCreator.java b/src/com/bytezone/diskbrowser/catalog/TextCatalogCreator.java new file mode 100755 index 0000000..60c0784 --- /dev/null +++ b/src/com/bytezone/diskbrowser/catalog/TextCatalogCreator.java @@ -0,0 +1,90 @@ +package com.bytezone.diskbrowser.catalog; + +import java.awt.EventQueue; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Enumeration; + +import javax.swing.JOptionPane; +import javax.swing.tree.DefaultMutableTreeNode; + +import com.bytezone.diskbrowser.disk.DiskFactory; +import com.bytezone.diskbrowser.disk.FormattedDisk; +import com.bytezone.diskbrowser.gui.TreeBuilder.FileNode; + +public class TextCatalogCreator extends AbstractCatalogCreator +{ + @Override + public void createCatalog () + { + Object o = node.getUserObject (); + if (!(o instanceof FileNode)) + { + JOptionPane.showMessageDialog (null, "Please select a folder from the Disk Tree", + "Info", JOptionPane.INFORMATION_MESSAGE); + return; + } + File f = ((FileNode) o).file; + final File f2 = new File (f.getAbsolutePath () + "/Catalog.txt"); + JOptionPane.showMessageDialog (null, "About to create file : " + f2.getAbsolutePath (), + "Info", JOptionPane.INFORMATION_MESSAGE); + + EventQueue.invokeLater (new Runnable () + { + @Override + public void run () + { + FileWriter out = null; + try + { + out = new FileWriter (f2); + printDescendants (node, out); + } + catch (IOException e) + { + JOptionPane.showMessageDialog (null, "Error creating catalog : " + e.getMessage (), + "Bugger", JOptionPane.INFORMATION_MESSAGE); + } + finally + { + try + { + if (out != null) + out.close (); + } + catch (IOException e) + { + e.printStackTrace (); + } + } + } + + private void printDescendants (DefaultMutableTreeNode root, FileWriter out) + throws IOException + { + Object o = root.getUserObject (); + if (o instanceof FileNode) + { + File f = ((FileNode) root.getUserObject ()).file; + if (!f.isDirectory ()) + { + FormattedDisk fd = DiskFactory.createDisk (f.getAbsolutePath ()); + out.write (fd.getCatalog ().getDataSource ().getText () + String.format ("%n")); + } + } + + Enumeration children = root.children (); + if (children != null) + while (children.hasMoreElements ()) + printDescendants (children.nextElement (), out); + } + }); + } + + @Override + public String getMenuText () + { + return "Create catalog text"; + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/catalog/TextDiskCreator.java b/src/com/bytezone/diskbrowser/catalog/TextDiskCreator.java new file mode 100755 index 0000000..d3880fd --- /dev/null +++ b/src/com/bytezone/diskbrowser/catalog/TextDiskCreator.java @@ -0,0 +1,61 @@ +package com.bytezone.diskbrowser.catalog; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Enumeration; + +import javax.swing.tree.DefaultMutableTreeNode; + +import com.bytezone.diskbrowser.applefile.AppleFileSource; + +public class TextDiskCreator extends AbstractDiskCreator +{ + public void createDisk () + { + File f = new File ("D:\\DiskDetails.txt"); + FileWriter out = null; + + try + { + out = new FileWriter (f); + printDisk (out); + } + catch (IOException e) + { + e.printStackTrace (); + } + finally + { + try + { + out.close (); + } + catch (IOException e) + { + e.printStackTrace (); + } + } + } + + private void printDisk (FileWriter out) throws IOException + { + Enumeration children = getEnumeration (); + + if (children == null) + return; + + while (children.hasMoreElements ()) + { + DefaultMutableTreeNode node = (DefaultMutableTreeNode) children.nextElement (); + AppleFileSource afs = (AppleFileSource) node.getUserObject (); + out.write (afs.getDataSource ().getText () + String.format ("%n")); + } + + } + + public String getMenuText () + { + return "create text disk"; + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/cpm/CPMDisk.java b/src/com/bytezone/diskbrowser/cpm/CPMDisk.java new file mode 100644 index 0000000..192129a --- /dev/null +++ b/src/com/bytezone/diskbrowser/cpm/CPMDisk.java @@ -0,0 +1,66 @@ +package com.bytezone.diskbrowser.cpm; + +import java.awt.Color; +import java.util.List; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.applefile.BootSector; +import com.bytezone.diskbrowser.disk.AbstractFormattedDisk; +import com.bytezone.diskbrowser.disk.AppleDisk; +import com.bytezone.diskbrowser.disk.Disk; +import com.bytezone.diskbrowser.disk.DiskAddress; +import com.bytezone.diskbrowser.disk.SectorType; + +public class CPMDisk extends AbstractFormattedDisk +{ + private final Color green = new Color (0, 200, 0); + public final SectorType catalogSector = new SectorType ("Catalog", green); + public final SectorType cpmSector = new SectorType ("CPM", Color.lightGray); + + public CPMDisk (Disk disk) + { + super (disk); + sectorTypesList.add (catalogSector); + sectorTypesList.add (cpmSector); + + byte[] sectorBuffer = disk.readSector (0, 0); // Boot sector + bootSector = new BootSector (disk, sectorBuffer, "CPM"); + sectorTypes[0] = cpmSector; + + for (int sector = 0; sector < 8; sector++) + { + DiskAddress da = disk.getDiskAddress (3, sector); + sectorTypes[da.getBlock ()] = catalogSector; + } + } + + @Override + public List getFileSectors (int fileNo) + { + return null; + } + + public static boolean isCorrectFormat (AppleDisk disk) + { + disk.setInterleave (3); + + for (int sector = 0; sector < 8; sector++) + { + byte[] buffer = disk.readSector (3, sector); + for (int i = 0; i < buffer.length; i += 32) + { + if (buffer[i] != 0 && buffer[i] != (byte) 0xE5) + return false; + if (buffer[i] == 0) + { + String filename = HexFormatter.getString (buffer, i + 1, 8); + String filetype = HexFormatter.getString (buffer, i + 9, 3); + String bytes = HexFormatter.getHexString (buffer, i + 12, 20); + System.out.println (filename + " " + filetype + " " + bytes); + } + } + } + + return true; + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/disk/AbstractFormattedDisk.java b/src/com/bytezone/diskbrowser/disk/AbstractFormattedDisk.java new file mode 100755 index 0000000..0b07348 --- /dev/null +++ b/src/com/bytezone/diskbrowser/disk/AbstractFormattedDisk.java @@ -0,0 +1,411 @@ +package com.bytezone.diskbrowser.disk; + +import java.awt.AWTEventMulticaster; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.Enumeration; +import java.util.List; + +import javax.swing.JTree; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreePath; + +import com.bytezone.diskbrowser.applefile.AbstractFile; +import com.bytezone.diskbrowser.applefile.AppleFileSource; +import com.bytezone.diskbrowser.applefile.BootSector; +import com.bytezone.diskbrowser.gui.DataSource; + +public abstract class AbstractFormattedDisk implements FormattedDisk +{ + protected Disk disk; + protected FormattedDisk parent; // used by Dual-dos disks + + protected ActionListener actionListenerList; + protected JTree catalogTree; + protected Path originalPath; + // protected String originalName; + + protected List sectorTypesList = new ArrayList (); + protected List fileEntries = new ArrayList (); + + public SectorType[] sectorTypes; + + protected BootSector bootSector; + + public final SectorType emptySector = new SectorType ("Unused (empty)", Color.white); + public final SectorType usedSector = new SectorType ("Unused (data)", Color.yellow); + // public final SectorType dosSector = new SectorType ("DOS", Color.lightGray); + + protected int falsePositives; + protected int falseNegatives; + + protected Dimension gridLayout; + + protected BitSet freeBlocks; + protected BitSet usedBlocks; // still to be populated - currently using stillAvailable () + + public AbstractFormattedDisk (Disk disk) + { + this.disk = disk; + freeBlocks = new BitSet (disk.getTotalBlocks ()); + usedBlocks = new BitSet (disk.getTotalBlocks ()); + /* + * All formatted disks will have empty and/or used sectors, so set them + * here, and let the actual subclass add its sector types later. This list + * is used to hold one of each sector type so that the DiskLayoutPanel can + * draw its grid and key correctly. Every additional type that the instance + * creates should be added here too. + */ + sectorTypesList.add (emptySector); + sectorTypesList.add (usedSector); + /* + * Hopefully every used sector will be changed by the subclass to something + * sensible, but deleted files will always leave the sector as used/unknown + * as it contains data. + */ + setSectorTypes (); + setGridLayout (); + /* + * Create the disk name as the root for the catalog tree. Subclasses will + * have to append their catalog entries to this node. + */ + DefaultAppleFileSource afs = + new DefaultAppleFileSource (getName (), disk.toString (), this); + DefaultMutableTreeNode root = new DefaultMutableTreeNode (afs); + DefaultTreeModel treeModel = new DefaultTreeModel (root); + catalogTree = new JTree (treeModel); + treeModel.setAsksAllowsChildren (true); // allows empty nodes to appear as folders + /* + * Add an ActionListener to the disk in case the interleave or blocksize + * changes + */ + disk.addActionListener (new ActionListener () + { + @Override + public void actionPerformed (ActionEvent e) + { + setSectorTypes (); + } + }); + } + + private void setSectorTypes () + { + sectorTypes = new SectorType[disk.getTotalBlocks ()]; + + for (DiskAddress da : disk) + sectorTypes[da.getBlock ()] = disk.isSectorEmpty (da) ? emptySector : usedSector; + } + + private void setGridLayout () + { + int totalBlocks = disk.getTotalBlocks (); + switch (totalBlocks) + { + case 280: + gridLayout = new Dimension (8, 35); + break; + case 455: + gridLayout = new Dimension (13, 35); + break; + case 560: + gridLayout = new Dimension (16, 35); + break; + case 1600: + gridLayout = new Dimension (16, 100); + break; + default: + int[] sizes = { 32, 20, 16, 8 }; + for (int size : sizes) + if ((totalBlocks % size) == 0) + { + gridLayout = new Dimension (size, totalBlocks / size); + break; + } + if (gridLayout == null) + System.out.println ("Unusable total blocks : " + totalBlocks); + } + } + + @Override + public Disk getDisk () + { + return disk; + } + + @Override + public FormattedDisk getParent () + { + return parent; + } + + @Override + public void setParent (FormattedDisk disk) + { + parent = disk; + } + + @Override + public void setOriginalPath (Path path) + { + this.originalPath = path; + + DefaultMutableTreeNode root = (DefaultMutableTreeNode) catalogTree.getModel ().getRoot (); + DefaultAppleFileSource afs = + new DefaultAppleFileSource (getName (), disk.toString (), this); + root.setUserObject (afs); + } + + @Override + public String getAbsolutePath () + { + if (originalPath != null) + return originalPath.toString (); + return disk.getFile ().getAbsolutePath (); + } + + @Override + public String getName () + { + if (originalPath != null) + return originalPath.getFileName ().toString (); + return disk.getFile ().getName (); + } + + @Override + public void writeFile (AbstractFile file) + { + System.out.println ("not implemented yet"); + } + + @Override + public List getCatalogList () + { + return fileEntries; + } + + @Override + public AppleFileSource getFile (String uniqueName) + { + for (AppleFileSource afs : fileEntries) + if (afs.getUniqueName ().equals (uniqueName)) + return afs; + return null; + } + + /* + * Catalog Tree routines + */ + @Override + public JTree getCatalogTree () + { + return catalogTree; + } + + public DefaultMutableTreeNode getCatalogTreeRoot () + { + return (DefaultMutableTreeNode) catalogTree.getModel ().getRoot (); + } + + public void makeNodeVisible (DefaultMutableTreeNode node) + { + catalogTree.makeVisible (new TreePath (((DefaultTreeModel) catalogTree.getModel ()) + .getPathToRoot (node))); + } + + protected DefaultMutableTreeNode findNode (DefaultMutableTreeNode node, String name) + { + Enumeration children = node.breadthFirstEnumeration (); + if (children != null) + { + while (children.hasMoreElements ()) + { + DefaultMutableTreeNode childNode = children.nextElement (); + if (childNode.getUserObject ().toString ().indexOf (name) > 0) + return childNode; + } + } + System.out.println ("Node not found : " + name); + return null; + } + + /* + * These routines just hand back the information that was created above, and + * added to by the subclass. + */ + @Override + public SectorType getSectorType (int block) + { + return getSectorType (disk.getDiskAddress (block)); + } + + @Override + public SectorType getSectorType (int track, int sector) + { + return getSectorType (disk.getDiskAddress (track, sector)); + } + + @Override + public SectorType getSectorType (DiskAddress da) + { + return sectorTypes[da.getBlock ()]; + } + + @Override + public List getSectorTypeList () + { + return sectorTypesList; + } + + @Override + public void setSectorType (int block, SectorType type) + { + if (block < sectorTypes.length) + sectorTypes[block] = type; + else + System.out.println ("Invalid block number: " + block); + } + + // Override this so that the correct sector type can be displayed + @Override + public DataSource getFormattedSector (DiskAddress da) + { + if (da.getBlock () == 0 && bootSector != null) + return bootSector; + + SectorType sectorType = sectorTypes[da.getBlock ()]; + byte[] buffer = disk.readSector (da); + String address = String.format ("%02X %02X", da.getTrack (), da.getSector ()); + + if (sectorType == emptySector) + return new DefaultSector ("Empty sector at " + address, disk, buffer); + if (sectorType == usedSector) + return new DefaultSector ("Orphan sector at " + address, disk, buffer); + + return new DefaultSector ("Data sector at " + address, disk, buffer); + } + + /* + * Override this with something useful + */ + @Override + public AppleFileSource getCatalog () + { + return new DefaultAppleFileSource (disk.toString (), this); + } + + @Override + public String getSectorFilename (DiskAddress da) + { + return "unknown"; + } + + @Override + public int clearOrphans () + { + System.out.println ("Not implemented yet"); + return 0; + } + + @Override + public boolean isSectorFree (DiskAddress da) + { + return freeBlocks.get (da.getBlock ()); + } + + @Override + public boolean isSectorFree (int blockNo) + { + return freeBlocks.get (blockNo); + } + + // representation of the Free Sector Table + @Override + public void setSectorFree (int block, boolean free) + { + if (block < 0 || block >= freeBlocks.size ()) + { + System.out.printf ("Block %d not in range : 0-%d%n", block, freeBlocks.size () - 1); + return; + } + // assert block < freeBlocks.size () : String.format ("Set free block # %6d, size %6d", + // block, freeBlocks.size ()); + freeBlocks.set (block, free); + } + + // Check that the sector hasn't already been flagged as part of the disk structure + @Override + public boolean stillAvailable (DiskAddress da) + { + return sectorTypes[da.getBlock ()] == usedSector + || sectorTypes[da.getBlock ()] == emptySector; + } + + @Override + public boolean stillAvailable (int blockNo) + { + return sectorTypes[blockNo] == usedSector || sectorTypes[blockNo] == emptySector; + } + + @Override + public void verify () + { + System.out.println ("Sectors to clean :"); + for (int i = 0, max = disk.getTotalBlocks (); i < max; i++) + { + if (freeBlocks.get (i)) + { + if (sectorTypes[i] == usedSector) + System.out.printf ("%04X clean%n", i); + } + else + { + if (sectorTypes[i] == usedSector) + System.out.printf ("%04X *** error ***%n", i); + } + } + } + + @Override + public Dimension getGridLayout () + { + return gridLayout; + } + + // VTOC flags sector as free, but it is in use by a file + @Override + public int falsePositiveBlocks () + { + return falsePositives; + } + + // VTOC flags sector as in use, but no file is using it (and not in the DOS tracks) + @Override + public int falseNegativeBlocks () + { + return falseNegatives; + } + + public void addActionListener (ActionListener actionListener) + { + actionListenerList = AWTEventMulticaster.add (actionListenerList, actionListener); + } + + public void removeActionListener (ActionListener actionListener) + { + actionListenerList = AWTEventMulticaster.remove (actionListenerList, actionListener); + } + + public void notifyListeners (String text) + { + if (actionListenerList != null) + actionListenerList.actionPerformed (new ActionEvent (this, ActionEvent.ACTION_PERFORMED, + text)); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/disk/AbstractSector.java b/src/com/bytezone/diskbrowser/disk/AbstractSector.java new file mode 100755 index 0000000..b630968 --- /dev/null +++ b/src/com/bytezone/diskbrowser/disk/AbstractSector.java @@ -0,0 +1,123 @@ +package com.bytezone.diskbrowser.disk; + +import java.awt.image.BufferedImage; + +import javax.swing.JComponent; +import javax.swing.JPanel; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.applefile.AssemblerProgram; +import com.bytezone.diskbrowser.gui.DataSource; + +public abstract class AbstractSector implements DataSource +{ + private static String newLine = String.format ("%n"); + private static String newLine2 = newLine + newLine; + + public byte[] buffer; + protected Disk disk; + AssemblerProgram assembler; + String description; + + // maybe this should just use a DiskAddress + public AbstractSector (Disk disk, byte[] buffer) + { + this.buffer = buffer; + this.disk = disk; + } + + @Override + public String getAssembler () + { + if (assembler == null) + assembler = new AssemblerProgram ("noname", buffer, 0); + return assembler.getText (); + } + + @Override + public String getHexDump () + { + return HexFormatter.format (buffer, 0, buffer.length); + } + + @Override + public BufferedImage getImage () + { + return null; + } + + @Override + public String getText () + { + if (description == null) + description = createText (); + return description; + } + + public abstract String createText (); + + protected StringBuilder getHeader (String title) + { + StringBuilder text = new StringBuilder (); + + text.append (title + newLine2); + text.append ("Offset Value Description" + newLine); + text.append ("======= =========== " + + "===============================================================" + newLine); + return text; + } + + @Override + public JComponent getComponent () + { + System.out.println ("In AbstractSector.getComponent()"); + JPanel panel = new JPanel (); + return panel; + } + + protected void addText (StringBuilder text, byte[] b, int offset, int size, String desc) + { + if ((offset + size - 1) > b.length) + { + // System.out.printf ("Offset : %d, Size : %d, Buffer : %d%n", offset, size, buffer.length); + return; + } + + switch (size) + { + case 1: + text.append (String + .format ("%03X %02X %s%n", offset, b[offset], desc)); + break; + case 2: + text.append (String.format ("%03X-%03X %02X %02X %s%n", offset, offset + 1, + b[offset], b[offset + 1], desc)); + break; + case 3: + text.append (String.format ("%03X-%03X %02X %02X %02X %s%n", offset, + offset + 2, b[offset], b[offset + 1], b[offset + 2], desc)); + break; + case 4: + text.append (String.format ("%03X-%03X %02X %02X %02X %02X %s%n", offset, + offset + 3, b[offset], b[offset + 1], b[offset + 2], + b[offset + 3], desc)); + break; + default: + System.out.println ("Invalid length : " + size); + } + } + + protected void addTextAndDecimal (StringBuilder text, byte[] b, int offset, int size, + String desc) + { + if (size == 1) + desc += " (" + (b[offset] & 0xFF) + ")"; + else if (size == 2) + desc += " (" + ((b[offset + 1] & 0xFF) * 256 + (b[offset] & 0xFF)) + ")"; + else if (size == 3) + desc += + String.format (" (%,d)", ((b[offset + 2] & 0xFF) * 65536) + + ((b[offset + 1] & 0xFF) * 256) + (b[offset] & 0xFF)); + addText (text, b, offset, size, desc); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/disk/AppleDisk.java b/src/com/bytezone/diskbrowser/disk/AppleDisk.java new file mode 100755 index 0000000..4b93e3a --- /dev/null +++ b/src/com/bytezone/diskbrowser/disk/AppleDisk.java @@ -0,0 +1,441 @@ +package com.bytezone.diskbrowser.disk; + +import java.awt.AWTEventMulticaster; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.zip.CRC32; +import java.util.zip.Checksum; + +import com.bytezone.diskbrowser.FileFormatException; +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.applefile.AppleFileSource; + +public class AppleDisk implements Disk +{ + static final String newLine = String.format ("%n"); + static final int MAX_INTERLEAVE = 3; + + public final File path; + private final byte[] diskBuffer; // contains the disk contents in memory + + private final int tracks; // usually 35 for floppy disks + private int sectors; // 8 or 16 + private int blocks; // 280 or 560 + + private final int trackSize; // 4096 + public int sectorSize; // 256 or 512 + + private int interleave = 0; + private static int[][] interleaveSector = // + { { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }, // Dos + { 0, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 15 }, // Prodos + { 0, 13, 11, 9, 7, 5, 3, 1, 14, 12, 10, 8, 6, 4, 2, 15 }, // Infocom + { 0, 6, 12, 3, 9, 15, 14, 5, 11, 2, 8, 7, 13, 4, 10, 1 } }; // CPM + + private boolean[] hasData; + private ActionListener actionListenerList; + private List blockList; + + public AppleDisk (File path, int tracks, int sectors) throws FileFormatException + { + assert (path.exists ()) : "No such path :" + path.getAbsolutePath (); + assert (!path.isDirectory ()) : "File is directory :" + path.getAbsolutePath (); + assert (path.length () <= Integer.MAX_VALUE) : "File too large"; + assert (path.length () != 0) : "File empty"; + + int skip = 0; + + String name = path.getName (); + int pos = name.lastIndexOf ('.'); + if (pos > 0 && name.substring (pos + 1).equalsIgnoreCase ("2mg")) + { + byte[] buffer = getPrefix (path); + this.blocks = HexFormatter.intValue (buffer[20], buffer[21]); // 1600 + this.sectorSize = 512; + this.trackSize = 8 * sectorSize; + tracks = this.blocks / 8; + sectors = 8; + skip = HexFormatter.intValue (buffer[8], buffer[9]); + } + else if (pos > 0 && name.substring (pos + 1).equalsIgnoreCase ("HDV")) + { + this.blocks = (int) path.length () / 4096 * 8; // reduces blocks to a legal multiple + this.sectorSize = 512; + this.trackSize = sectors * sectorSize; + } + else + { + this.blocks = tracks * sectors; + this.sectorSize = (int) path.length () / blocks; + this.trackSize = sectors * sectorSize; + } + + if (false) + { + System.out.println ("Tracks : " + tracks); + System.out.println ("Sectors : " + sectors); + System.out.println ("Blocks : " + blocks); + System.out.println ("Sector size : " + sectorSize); + System.out.println ("Track size : " + trackSize); + System.out.println (); + } + + if (sectorSize != 256 && sectorSize != 512) + { + System.out.println ("Invalid sector size : " + sectorSize); + new Exception ().printStackTrace (); + } + + if (sectorSize != 256 && sectorSize != 512) + throw new FileFormatException ("Invalid sector size : " + sectorSize); + + this.path = path; + this.tracks = tracks; + this.sectors = sectors; + + diskBuffer = new byte[tracks * sectors * sectorSize]; + hasData = new boolean[blocks]; + + try + { + BufferedInputStream file = new BufferedInputStream (new FileInputStream (path)); + if (skip > 0) + file.skip (skip); + file.read (diskBuffer); + file.close (); + } + catch (IOException e) + { + e.printStackTrace (); + System.exit (1); + } + + checkSectorsForData (); + } + + private byte[] getPrefix (File path) + { + byte[] buffer = new byte[64]; + try + { + BufferedInputStream file = new BufferedInputStream (new FileInputStream (path)); + file.read (buffer); + file.close (); + } + catch (IOException e) + { + e.printStackTrace (); + System.exit (1); + } + return buffer; + } + + private void checkSectorsForData () + { + blockList = null; // force blockList to be rebuilt with the correct number/size of blocks + for (DiskAddress da : this) + { + byte[] buffer = readSector (da); + hasData[da.getBlock ()] = false; + for (int i = 0; i < sectorSize; i++) + if (buffer[i] != 0) + { + hasData[da.getBlock ()] = true; + break; + } + } + } + + /* + * Routines that implement the Disk interface + */ + + @Override + public int getSectorsPerTrack () + { + return trackSize / sectorSize; + } + + @Override + public int getTrackSize () + { + return trackSize; + } + + @Override + public int getBlockSize () + { + return sectorSize; + } + + @Override + public int getTotalBlocks () + { + return blocks; + } + + @Override + public int getTotalTracks () + { + return tracks; + } + + @Override + public boolean isSectorEmpty (DiskAddress da) + { + return !hasData[da.getBlock ()]; + } + + @Override + public boolean isSectorEmpty (int block) + { + return !hasData[block]; + } + + @Override + public boolean isSectorEmpty (int track, int sector) + { + return !hasData[getDiskAddress (track, sector).getBlock ()]; + } + + @Override + public File getFile () + { + return path; + } + + @Override + public byte[] readSector (DiskAddress da) + { + byte[] buffer = new byte[sectorSize]; + readBuffer (da, buffer, 0); + return buffer; + } + + @Override + public byte[] readSectors (List daList) + { + byte[] buffer = new byte[daList.size () * sectorSize]; + int bufferOffset = 0; + for (DiskAddress da : daList) + { + if (da != null) // text files may have gaps + readBuffer (da, buffer, bufferOffset); + bufferOffset += sectorSize; + } + return buffer; + } + + @Override + public byte[] readSector (int track, int sector) + { + return readSector (getDiskAddress (track, sector)); + } + + @Override + public byte[] readSector (int block) + { + return readSector (getDiskAddress (block)); + } + + @Override + public int writeSector (DiskAddress da, byte[] buffer) + { + System.out.println ("Not yet implemented"); + return -1; + } + + @Override + public void setInterleave (int interleave) + { + assert (interleave >= 0 && interleave <= MAX_INTERLEAVE) : "Invalid interleave"; + this.interleave = interleave; + checkSectorsForData (); + if (actionListenerList != null) + notifyListeners ("Interleave changed"); + } + + @Override + public int getInterleave () + { + return interleave; + } + + @Override + public void setBlockSize (int size) + { + assert (size == 256 || size == 512) : "Invalid sector size : " + size; + if (sectorSize == size) + return; + sectorSize = size; + sectors = trackSize / sectorSize; + blocks = tracks * sectors; + System.out.printf ("New blocks: %d%n", blocks); + hasData = new boolean[blocks]; + checkSectorsForData (); + if (actionListenerList != null) + notifyListeners ("Sector size changed"); + } + + @Override + public DiskAddress getDiskAddress (int block) + { + if (!isValidAddress (block)) + { + System.out.println ("Invalid block : " + block); + return null; + } + return new AppleDiskAddress (block, this); + } + + @Override + public List getDiskAddressList (int... blocks) + { + List addressList = new ArrayList (); + + for (int block : blocks) + { + assert (isValidAddress (block)) : "Invalid block : " + block; + addressList.add (new AppleDiskAddress (block, this)); + } + return addressList; + } + + @Override + public DiskAddress getDiskAddress (int track, int sector) + { + // should this return null for invalid addresses? + assert (isValidAddress (track, sector)) : "Invalid address : " + track + ", " + sector; + return new AppleDiskAddress (track, sector, this); + } + + @Override + public boolean isValidAddress (int block) + { + if (block < 0 || block >= this.blocks) + return false; + return true; + } + + @Override + public boolean isValidAddress (int track, int sector) + { + if (track < 0 || track >= this.tracks) + return false; + if (sector < 0 || sector >= this.sectors) + return false; + return true; + } + + @Override + public boolean isValidAddress (DiskAddress da) + { + return isValidAddress (da.getTrack (), da.getSector ()); + } + + /* + * This is the only method that transfers data from the disk buffer to an output buffer. + * It handles sectors of 256 or 512 bytes, and both linear and interleaved sectors. + */ + private void readBuffer (DiskAddress da, byte[] buffer, int bufferOffset) + { + if (da.getDisk () != this) + { + System.out.println (da.getDisk ()); + System.out.println (this); + } + + assert da.getDisk () == this : "Disk address not applicable to this disk"; + assert sectorSize == 256 || sectorSize == 512 : "Invalid sector size : " + sectorSize; + assert interleave >= 0 && interleave <= MAX_INTERLEAVE : "Invalid interleave : " + + interleave; + int diskOffset; + + if (sectorSize == 256) + { + diskOffset = + da.getTrack () * trackSize + interleaveSector[interleave][da.getSector ()] + * sectorSize; + System.arraycopy (diskBuffer, diskOffset, buffer, bufferOffset, sectorSize); + } + else if (sectorSize == 512) + { + diskOffset = + da.getTrack () * trackSize + interleaveSector[interleave][da.getSector () * 2] + * 256; + System.arraycopy (diskBuffer, diskOffset, buffer, bufferOffset, 256); + diskOffset = + da.getTrack () * trackSize + interleaveSector[interleave][da.getSector () * 2 + 1] + * 256; + System.arraycopy (diskBuffer, diskOffset, buffer, bufferOffset + 256, 256); + } + } + + @Override + public void addActionListener (ActionListener actionListener) + { + actionListenerList = AWTEventMulticaster.add (actionListenerList, actionListener); + } + + @Override + public void removeActionListener (ActionListener actionListener) + { + actionListenerList = AWTEventMulticaster.remove (actionListenerList, actionListener); + } + + public void notifyListeners (String text) + { + if (actionListenerList != null) + actionListenerList.actionPerformed (new ActionEvent (this, ActionEvent.ACTION_PERFORMED, + text)); + } + + public AppleFileSource getDetails () + { + return new DefaultAppleFileSource (toString (), path.getName (), null); + } + + @Override + public String toString () + { + StringBuilder text = new StringBuilder (); + + text.append ("Path............ " + path.getAbsolutePath () + newLine); + text.append (String.format ("File size....... %,d%n", path.length ())); + text.append ("Tracks.......... " + tracks + newLine); + text.append ("Sectors......... " + sectors + newLine); + text.append (String.format ("Blocks.......... %,d%n", blocks)); + text.append ("Sector size..... " + sectorSize + newLine); + text.append ("Interleave...... " + interleave + newLine); + + return text.toString (); + } + + @Override + public Iterator iterator () + { + if (blockList == null) + { + blockList = new ArrayList (blocks); + for (int block = 0; block < blocks; block++) + blockList.add (new AppleDiskAddress (block, this)); + } + return blockList.iterator (); + } + + @Override + public long getBootChecksum () + { + byte[] buffer = readSector (0, 0); + Checksum checksum = new CRC32 (); + checksum.update (buffer, 0, buffer.length); + return checksum.getValue (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/disk/AppleDiskAddress.java b/src/com/bytezone/diskbrowser/disk/AppleDiskAddress.java new file mode 100755 index 0000000..028dc34 --- /dev/null +++ b/src/com/bytezone/diskbrowser/disk/AppleDiskAddress.java @@ -0,0 +1,65 @@ +package com.bytezone.diskbrowser.disk; + +public class AppleDiskAddress implements DiskAddress +{ + private final int block; + private final int track; + private final int sector; + public final Disk owner; + + public AppleDiskAddress (int block, Disk owner) + { + this.owner = owner; + this.block = block; + int sectorsPerTrack = owner.getSectorsPerTrack (); + this.track = block / sectorsPerTrack; + this.sector = block % sectorsPerTrack; + } + + public AppleDiskAddress (int track, int sector, Disk owner) + { + this.owner = owner; + this.track = track; + this.sector = sector; + this.block = track * owner.getSectorsPerTrack () + sector; + } + + @Override + public String toString () + { + return String.format ("[Block=%3d, Track=%2d, Sector=%2d]", block, track, sector); + } + + public int compareTo (DiskAddress that) + { + return this.block - that.getBlock (); + } + + public int getBlock () + { + return block; + } + + public int getSector () + { + return sector; + } + + public int getTrack () + { + return track; + } + + public Disk getDisk () + { + return owner; + } + + @Override + public boolean equals (Object other) + { + if (other == null || getClass () != other.getClass ()) + return false; + return this.block == ((AppleDiskAddress) other).block; + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/disk/DataDisk.java b/src/com/bytezone/diskbrowser/disk/DataDisk.java new file mode 100755 index 0000000..9ff9b44 --- /dev/null +++ b/src/com/bytezone/diskbrowser/disk/DataDisk.java @@ -0,0 +1,52 @@ +package com.bytezone.diskbrowser.disk; + +import java.util.List; + +import com.bytezone.diskbrowser.gui.DataSource; + +public class DataDisk extends AbstractFormattedDisk +{ + // static final byte[] dos = { 0x01, (byte) 0xA5, 0x27, (byte) 0xC9, 0x09 }; + + // this should somehow tie in with the checksum from DiskFactory to determine + // whether it has a bootloader + + public DataDisk (AppleDisk disk) + { + super (disk); + + // byte[] buffer = disk.readSector (0, 0); // Boot sector + // boolean ok = true; + // for (int i = 0; i < dos.length; i++) + // if (buffer[i] != dos[i]) + // { + // ok = false; + // break; + // } + // if (buffer[0] == 0x01) + // { + // bootSector = new BootSector (disk, buffer, "DOS"); + // sectorTypesList.add (dosSector); + // sectorTypes[0] = dosSector; + // } + } + + // no files on data disks + @Override + public List getFileSectors (int fileNo) + { + return null; + } + + // no files on data disks + public DataSource getFile (int fileNo) + { + return null; + } + + @Override + public String toString () + { + return disk.toString (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/disk/DefaultAppleFileSource.java b/src/com/bytezone/diskbrowser/disk/DefaultAppleFileSource.java new file mode 100755 index 0000000..ed34937 --- /dev/null +++ b/src/com/bytezone/diskbrowser/disk/DefaultAppleFileSource.java @@ -0,0 +1,88 @@ +package com.bytezone.diskbrowser.disk; + +import java.util.List; + +import com.bytezone.diskbrowser.applefile.AppleFileSource; +import com.bytezone.diskbrowser.gui.DataSource; + +/* + * Most AppleFileSource objects are CatalogEntry types. In order to allow Disk + * and Volume nodes in the tree to show some text in the centre panel, use a + * DefaultAppleFileSource which returns a DefaultDataSource (just some text). + */ +public class DefaultAppleFileSource implements AppleFileSource +{ + final String title; + final DataSource file; + final FormattedDisk owner; + List blocks; + + public DefaultAppleFileSource (String text, FormattedDisk owner) + { + this ("", text, owner); + } + + public DefaultAppleFileSource (String title, String text, FormattedDisk owner) + { + this (title, new DefaultDataSource (text), owner); + } + + public DefaultAppleFileSource (String title, DataSource file, FormattedDisk owner) + { + this.title = title; + this.file = file; + this.owner = owner; + } + + public DefaultAppleFileSource (String title, DataSource file, FormattedDisk owner, + List blocks) + { + this (title, file, owner); + this.blocks = blocks; + if (file instanceof DefaultDataSource) + ((DefaultDataSource) file).buffer = owner.getDisk ().readSectors (blocks); + } + + public void setSectors (List blocks) + { + this.blocks = blocks; + if (file instanceof DefaultDataSource) + ((DefaultDataSource) file).buffer = owner.getDisk ().readSectors (blocks); + } + + public DataSource getDataSource () + { + return file; + } + + public FormattedDisk getFormattedDisk () + { + return owner; + } + + public List getSectors () + { + return blocks; + } + + /* + * See similar routine in CatalogPanel.DiskNode + */ + @Override + public String toString () + { + final int MAX_NAME_LENGTH = 40; + final int SUFFIX_LENGTH = 12; + final int PREFIX_LENGTH = MAX_NAME_LENGTH - SUFFIX_LENGTH - 3; + + if (title.length () > MAX_NAME_LENGTH) + return title.substring (0, PREFIX_LENGTH) + "..." + + title.substring (title.length () - SUFFIX_LENGTH); + return title; + } + + public String getUniqueName () + { + return title; + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/disk/DefaultDataSource.java b/src/com/bytezone/diskbrowser/disk/DefaultDataSource.java new file mode 100755 index 0000000..6f9fca2 --- /dev/null +++ b/src/com/bytezone/diskbrowser/disk/DefaultDataSource.java @@ -0,0 +1,49 @@ +package com.bytezone.diskbrowser.disk; + +import java.awt.image.BufferedImage; + +import javax.swing.JComponent; +import javax.swing.JPanel; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.gui.DataSource; + +public class DefaultDataSource implements DataSource +{ + public String text; + byte[] buffer; + + public DefaultDataSource (String text) + { + this.text = text; + } + + public String getAssembler () + { + return null; + } + + public String getHexDump () + { + if (buffer != null) + return HexFormatter.format (buffer, 0, buffer.length); + return null; + } + + public BufferedImage getImage () + { + return null; + } + + public String getText () + { + return text; + } + + public JComponent getComponent () + { + System.out.println ("In DefaultDataSource.getComponent()"); + JPanel panel = new JPanel (); + return panel; + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/disk/DefaultSector.java b/src/com/bytezone/diskbrowser/disk/DefaultSector.java new file mode 100755 index 0000000..2432682 --- /dev/null +++ b/src/com/bytezone/diskbrowser/disk/DefaultSector.java @@ -0,0 +1,26 @@ +package com.bytezone.diskbrowser.disk; + +import com.bytezone.diskbrowser.HexFormatter; + +public class DefaultSector extends AbstractSector +{ + String name; + + public DefaultSector (String name, Disk disk, byte[] buffer) + { + super (disk, buffer); + this.name = name; + } + + @Override + public String createText () + { + return name + "\n\n" + HexFormatter.format (buffer, 0, buffer.length); + } + + @Override + public String toString () + { + return name; + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/disk/Disk.java b/src/com/bytezone/diskbrowser/disk/Disk.java new file mode 100755 index 0000000..153b599 --- /dev/null +++ b/src/com/bytezone/diskbrowser/disk/Disk.java @@ -0,0 +1,60 @@ +package com.bytezone.diskbrowser.disk; + +import java.awt.event.ActionListener; +import java.io.File; +import java.util.List; + +public interface Disk extends Iterable +{ + public long getBootChecksum (); + + public int getTotalBlocks (); // blocks per disk - usually 560 or 280 + + public int getTotalTracks (); // usually 35 + + public int getBlockSize (); // bytes per block - 256 or 512 + + public void setBlockSize (int blockSize); + + public int getTrackSize (); // bytes per track - 4096 + + public int getSectorsPerTrack (); // 8 or 16 + + public void setInterleave (int interleave); + + public int getInterleave (); + + public DiskAddress getDiskAddress (int block); + + public List getDiskAddressList (int... blocks); + + public DiskAddress getDiskAddress (int track, int sector); + + public byte[] readSector (int block); + + public byte[] readSector (int track, int sector); + + public byte[] readSector (DiskAddress da); + + public byte[] readSectors (List daList); + + public int writeSector (DiskAddress da, byte[] buffer); + + public boolean isSectorEmpty (DiskAddress da); + + public boolean isSectorEmpty (int block); + + public boolean isSectorEmpty (int track, int sector); + + public boolean isValidAddress (int block); + + public boolean isValidAddress (int track, int sector); + + public boolean isValidAddress (DiskAddress da); + + public File getFile (); + + public void addActionListener (ActionListener listener); + + public void removeActionListener (ActionListener listener); +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/disk/DiskAddress.java b/src/com/bytezone/diskbrowser/disk/DiskAddress.java new file mode 100755 index 0000000..b70d948 --- /dev/null +++ b/src/com/bytezone/diskbrowser/disk/DiskAddress.java @@ -0,0 +1,12 @@ +package com.bytezone.diskbrowser.disk; + +public interface DiskAddress extends Comparable +{ + public int getBlock (); + + public int getTrack (); + + public int getSector (); + + public Disk getDisk (); +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/disk/DiskFactory.java b/src/com/bytezone/diskbrowser/disk/DiskFactory.java new file mode 100755 index 0000000..85f6584 --- /dev/null +++ b/src/com/bytezone/diskbrowser/disk/DiskFactory.java @@ -0,0 +1,362 @@ +package com.bytezone.diskbrowser.disk; + +import java.io.*; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.zip.GZIPInputStream; + +import com.bytezone.diskbrowser.FileFormatException; +import com.bytezone.diskbrowser.NuFX; +import com.bytezone.diskbrowser.cpm.CPMDisk; +import com.bytezone.diskbrowser.dos.DosDisk; +import com.bytezone.diskbrowser.infocom.InfocomDisk; +import com.bytezone.diskbrowser.pascal.PascalDisk; +import com.bytezone.diskbrowser.prodos.ProdosDisk; +import com.bytezone.diskbrowser.wizardry.WizardryScenarioDisk; + +public class DiskFactory +{ + private static boolean debug = false; + + private DiskFactory () + { + } + + public static FormattedDisk createDisk (File file) + { + return createDisk (file.getAbsolutePath ()); + } + + public static FormattedDisk createDisk (String path) + { + if (debug) + System.out.println ("Factory : " + path); + + File file = new File (path); + if (!file.exists ()) + return null; + + String suffix = path.substring (path.lastIndexOf (".") + 1).toLowerCase (); + Boolean compressed = false; + Path p = Paths.get (path); + + if (suffix.equalsIgnoreCase ("sdk")) + { + try + { + NuFX nuFX = new NuFX (p); + File tmp = File.createTempFile ("sdk", null); + FileOutputStream fos = new FileOutputStream (tmp); + fos.write (nuFX.getBuffer ()); + fos.close (); + tmp.deleteOnExit (); + file = tmp; + suffix = "dsk"; + compressed = true; + } + catch (IOException e) + { + e.printStackTrace (); + return null; + } + catch (FileFormatException e) + { + return null; + } + } + else if (suffix.equalsIgnoreCase ("gz")) // will be .dsk.gz + { + try + { + InputStream in = new GZIPInputStream (new FileInputStream (path)); + File tmp = File.createTempFile ("gzip", null); + FileOutputStream fos = new FileOutputStream (tmp); + + int bytesRead; + byte[] buffer = new byte[1024]; + while ((bytesRead = in.read (buffer)) > 0) + fos.write (buffer, 0, bytesRead); + + fos.close (); + in.close (); + tmp.deleteOnExit (); + file = tmp; + suffix = "dsk"; + compressed = true; + } + catch (IOException e) + { + e.printStackTrace (); + return null; + } + } + + FormattedDisk disk = null; + FormattedDisk disk2 = null; + + if (suffix.equals ("hdv")) + return checkHardDisk (file); + + if (suffix.equals ("2mg")) + return check2mgDisk (file); + + // if (((suffix.equals ("po") || suffix.equals ("dsk")) && file.length () > 116480)) + if (((suffix.equals ("po") || suffix.equals ("dsk")) && file.length () > 143360)) + { + disk = checkHardDisk (file); + if (disk != null) + { + if (compressed) + disk.setOriginalPath (p); + return disk; + } + } + + long length = file.length (); + if (length != 143360 && length != 116480) + { + System.out.println ("invalid file length : " + file.length ()); + return null; + } + + int sectors = file.length () == 143360 ? 16 : 13; + if (true) + { + AppleDisk appleDisk = new AppleDisk (file, 35, sectors); + long checksum = appleDisk.getBootChecksum (); + + if (checksum == 3176296590L || checksum == 108825457L || checksum == 1439356606L + || checksum == 1550012074L || checksum == 1614602459L || checksum == 940889336L + || checksum == 990032697 || checksum == 2936955085L || checksum == 1348415927L + || checksum == 3340889101L || checksum == 18315788L || checksum == 993895235L) + { + disk = checkDos (file); + disk2 = checkProdos (file); + if (disk2 != null && disk != null) + disk = new DualDosDisk (disk, disk2); + } + + else if (checksum == 1737448647L || checksum == 170399908L) + { + disk = checkProdos (file); + disk2 = checkDos (file); + if (disk2 != null && disk != null) + disk = new DualDosDisk (disk, disk2); + } + + else if (checksum == 2803644711L || checksum == 3317783349L || checksum == 1728863694L + || checksum == 198094178L) + disk = checkPascalDisk (file); + + else if (checksum == 3028642627L || checksum == 2070151659L) + disk = checkInfocomDisk (file); + + else if (checksum == 1212926910L || checksum == 1365043894L) + disk = checkCPMDisk (file); + + if (disk != null) + { + if (compressed) + disk.setOriginalPath (p); + return disk; + } + + // empty boot sector + if (checksum != 227968344L && false) + System.out.println ("Unknown checksum : " + checksum + " : " + path); + } + + if (suffix.equals ("dsk") || suffix.equals ("do") || suffix.equals ("d13")) + { + disk = checkDos (file); + if (disk == null) + disk = checkProdos (file); + else if (sectors == 16) + { + if (debug) + System.out.println ("Checking DualDos disk"); + disk2 = checkProdos (file); + if (disk2 != null) + disk = new DualDosDisk (disk, disk2); + } + } + else if (suffix.equals ("po")) + { + disk = checkProdos (file); + if (disk == null) + disk = checkDos (file); + } + + if (disk == null) + disk = checkPascalDisk (file); + + if (disk == null) + { + disk2 = checkInfocomDisk (file); + if (disk2 != null) + disk = disk2; + } + + if (disk == null) + disk = new DataDisk (new AppleDisk (file, 35, 16)); + + if (debug) + System.out.println (" Factory creating disk : " + + disk.getDisk ().getFile ().getAbsolutePath ()); + + if (disk != null && compressed) + disk.setOriginalPath (p); + + return disk; + } + + private static DosDisk checkDos (File file) + { + if (debug) + System.out.println ("Checking DOS disk"); + try + { + int sectors = file.length () == 143360 ? 16 : 13; + AppleDisk disk = new AppleDisk (file, 35, sectors); + if (DosDisk.isCorrectFormat (disk)) + return new DosDisk (disk); + } + catch (Exception e) + { + } + if (debug) + System.out.println ("Not a DOS disk"); + return null; + } + + private static ProdosDisk checkProdos (File file) + { + if (debug) + System.out.println ("Checking Prodos disk"); + try + { + AppleDisk disk = new AppleDisk (file, 35, 8); + if (ProdosDisk.isCorrectFormat (disk)) + return new ProdosDisk (disk); + } + catch (Exception e) + { + } + if (debug) + System.out.println ("Not a Prodos disk"); + return null; + } + + private static ProdosDisk checkHardDisk (File file) + { + if (debug) + { + System.out.println ("\nChecking Prodos hard disk"); + System.out.printf ("Total blocks : %f%n", (float) file.length () / 512); + System.out.printf ("Total tracks : %f%n", (float) file.length () / 4096); + System.out.printf ("File length : %d%n", file.length ()); + System.out.println (); + } + + // assumes a sector is 512 bytes + if ((file.length () % 512) != 0) + { + if (debug) + System.out.printf ("file length not divisible by 512 : %d%n%", file.length ()); + return null; + } + + // assumes a track is 4096 bytes + // if ((file.length () % 4096) != 0) + // { + // if (debug) + // { + // System.out.printf ("file length not divisible by 4096 : %d%n%n", file.length ()); + // int usableLength = (int) (file.length () / 4096); + // } + // return null; + // } + + try + { + AppleDisk disk = new AppleDisk (file, (int) file.length () / 4096, 8); + if (ProdosDisk.isCorrectFormat (disk)) + { + if (debug) + { + System.out.println ("Yay, it's a prodos hard disk"); + System.out.println (disk); + } + return new ProdosDisk (disk); + } + } + catch (Exception e) + { + e.printStackTrace (); + } + + if (debug) + System.out.println ("Not a Prodos hard disk"); + + return null; + } + + private static FormattedDisk check2mgDisk (File file) + { + if (debug) + System.out.println ("Checking Prodos 2mg disk"); + + try + { + AppleDisk disk = new AppleDisk (file, 0, 0); + if (ProdosDisk.isCorrectFormat (disk)) + return new ProdosDisk (disk); + } + catch (Exception e) + { + } + if (debug) + System.out.println ("Not a Prodos 2mg disk"); + return null; + } + + private static FormattedDisk checkPascalDisk (File file) + { + if (debug) + System.out.println ("Checking Pascal disk"); + AppleDisk disk = new AppleDisk (file, 35, 8); + if (!PascalDisk.isCorrectFormat (disk, debug)) + return null; + if (debug) + System.out.println ("Pascal disk OK - Checking Wizardry disk"); + if (WizardryScenarioDisk.isWizardryFormat (disk, debug)) + return new WizardryScenarioDisk (disk); + if (debug) + System.out.println ("Not a Wizardry disk"); + return new PascalDisk (disk); + } + + private static InfocomDisk checkInfocomDisk (File file) + { + if (debug) + System.out.println ("Checking Infocom disk"); + AppleDisk disk = new AppleDisk (file, 35, 16); + if (InfocomDisk.isCorrectFormat (disk)) + return new InfocomDisk (disk); + if (debug) + System.out.println ("Not an InfocomDisk disk"); + return null; + } + + private static CPMDisk checkCPMDisk (File file) + { + if (debug) + System.out.println ("Checking CPM disk"); + AppleDisk disk = new AppleDisk (file, 35, 16); + if (CPMDisk.isCorrectFormat (disk)) + return new CPMDisk (disk); + if (debug) + System.out.println ("Not a CPM disk"); + return null; + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/disk/DualDosDisk.java b/src/com/bytezone/diskbrowser/disk/DualDosDisk.java new file mode 100755 index 0000000..6e135c9 --- /dev/null +++ b/src/com/bytezone/diskbrowser/disk/DualDosDisk.java @@ -0,0 +1,274 @@ +package com.bytezone.diskbrowser.disk; + +import java.awt.Dimension; +import java.nio.file.Path; +import java.util.List; + +import javax.swing.JTree; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; + +import com.bytezone.diskbrowser.applefile.AbstractFile; +import com.bytezone.diskbrowser.applefile.AppleFileSource; +import com.bytezone.diskbrowser.gui.DataSource; +import com.bytezone.diskbrowser.gui.FileSelectedEvent; +import com.bytezone.diskbrowser.gui.FileSelectionListener; + +// Apple Assembly Lines disks are dual-dos + +public class DualDosDisk implements FormattedDisk, FileSelectionListener +{ + private final FormattedDisk[] disks = new FormattedDisk[2]; + private int currentDisk; + private final JTree tree; + + public DualDosDisk (FormattedDisk disk0, FormattedDisk disk1) + { + String diskName = disk0.getDisk ().getFile ().getName (); + String text = + "This disk contains both DOS and Prodos files. Isn't that clever?\n\n" + + disk0.getDisk () + "\n" + disk1.getDisk (); + DefaultMutableTreeNode root = + new DefaultMutableTreeNode (new DefaultAppleFileSource (diskName, text, this)); + DefaultTreeModel treeModel = new DefaultTreeModel (root); + tree = new JTree (treeModel); + treeModel.setAsksAllowsChildren (true); // allows empty nodes to appear as folders + + this.disks[0] = disk0; + this.disks[1] = disk1; + disk0.setParent (this); + disk1.setParent (this); + + DefaultMutableTreeNode root0 = + (DefaultMutableTreeNode) disk0.getCatalogTree ().getModel ().getRoot (); + DefaultMutableTreeNode root1 = + (DefaultMutableTreeNode) disk1.getCatalogTree ().getModel ().getRoot (); + root.add ((DefaultMutableTreeNode) root0.getChildAt (0)); + root.add ((DefaultMutableTreeNode) root1.getChildAt (0)); + + // TreeNode[] nodes = ((DefaultTreeModel) tree.getModel ()).getPathToRoot (child); + // tree.setSelectionPath (new TreePath (nodes)); + } + + @Override + public JTree getCatalogTree () + { + return tree; + } + + @Override + public List getFileSectors (int fileNo) + { + return disks[currentDisk].getFileSectors (fileNo); + } + + @Override + public List getCatalogList () + { + return disks[currentDisk].getCatalogList (); + } + + @Override + public DataSource getFormattedSector (DiskAddress da) + { + return disks[currentDisk].getFormattedSector (da); + } + + @Override + public SectorType getSectorType (DiskAddress da) + { + return disks[currentDisk].getSectorType (da); + } + + @Override + public SectorType getSectorType (int track, int sector) + { + return disks[currentDisk].getSectorType (track, sector); + } + + @Override + public SectorType getSectorType (int block) + { + return disks[currentDisk].getSectorType (block); + } + + @Override + public List getSectorTypeList () + { + return disks[currentDisk].getSectorTypeList (); + } + + @Override + public Disk getDisk () + { + return disks[currentDisk].getDisk (); + } + + public void setCurrentDisk (AppleFileSource afs) + { + FormattedDisk fd = afs.getFormattedDisk (); + if (disks[0] == fd && currentDisk != 0) + currentDisk = 0; + else if (disks[1] == fd && currentDisk != 1) + currentDisk = 1; + + System.out.println ("AFS : " + afs); + System.out.println ("1. Setting current disk to : " + currentDisk); + } + + public void setCurrentDiskNo (int n) + { + currentDisk = n; + System.out.println ("2. Setting current disk to : " + currentDisk); + } + + public int getCurrentDiskNo () + { + return currentDisk; + } + + public FormattedDisk getCurrentDisk () + { + return disks[currentDisk]; + } + + @Override + public void writeFile (AbstractFile file) + { + disks[currentDisk].writeFile (file); + } + + @Override + public AppleFileSource getCatalog () + { + return new DefaultAppleFileSource ("text", disks[0].getCatalog ().getDataSource () + .getText () + + "\n\n" + disks[1].getCatalog ().getDataSource ().getText (), this); + } + + @Override + public AppleFileSource getFile (String uniqueName) + { + if (true) + return disks[currentDisk].getFile (uniqueName); + System.out.println ("Searching for : " + uniqueName); + for (int i = 0; i < 2; i++) + { + AppleFileSource afs = disks[i].getFile (uniqueName); + if (afs != null) + { + setCurrentDiskNo (i); + return afs; + } + } + return null; + } + + @Override + public int clearOrphans () + { + return disks[currentDisk].clearOrphans (); + } + + @Override + public boolean isSectorFree (DiskAddress da) + { + return disks[currentDisk].isSectorFree (da); + } + + @Override + public void verify () + { + disks[currentDisk].verify (); + } + + @Override + public boolean stillAvailable (DiskAddress da) + { + return disks[currentDisk].stillAvailable (da); + } + + @Override + public void setSectorType (int block, SectorType type) + { + disks[currentDisk].setSectorType (block, type); + } + + @Override + public void setSectorFree (int block, boolean free) + { + disks[currentDisk].setSectorFree (block, free); + } + + @Override + public int falseNegativeBlocks () + { + return disks[currentDisk].falseNegativeBlocks (); + } + + @Override + public int falsePositiveBlocks () + { + return disks[currentDisk].falsePositiveBlocks (); + } + + @Override + public Dimension getGridLayout () + { + return disks[currentDisk].getGridLayout (); + } + + @Override + public boolean isSectorFree (int block) + { + return disks[currentDisk].isSectorFree (block); + } + + @Override + public boolean stillAvailable (int block) + { + return disks[currentDisk].stillAvailable (block); + } + + @Override + public void setOriginalPath (Path path) + { + disks[currentDisk].setOriginalPath (path); + } + + @Override + public String getAbsolutePath () + { + return disks[currentDisk].getAbsolutePath (); + } + + @Override + public FormattedDisk getParent () + { + return disks[currentDisk].getParent (); + } + + @Override + public void setParent (FormattedDisk disk) + { + disks[currentDisk].setParent (disk); + } + + @Override + public void fileSelected (FileSelectedEvent event) + { + System.out.println ("In DDD - file selected : " + event.file); + } + + @Override + public String getSectorFilename (DiskAddress da) + { + return disks[currentDisk].getSectorFilename (da); + } + + @Override + public String getName () + { + return disks[currentDisk].getName (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/disk/FormattedDisk.java b/src/com/bytezone/diskbrowser/disk/FormattedDisk.java new file mode 100755 index 0000000..8b533f4 --- /dev/null +++ b/src/com/bytezone/diskbrowser/disk/FormattedDisk.java @@ -0,0 +1,79 @@ +package com.bytezone.diskbrowser.disk; + +import java.awt.Dimension; +import java.nio.file.Path; +import java.util.List; + +import javax.swing.JTree; + +import com.bytezone.diskbrowser.applefile.AbstractFile; +import com.bytezone.diskbrowser.applefile.AppleFileSource; +import com.bytezone.diskbrowser.gui.DataSource; + +public interface FormattedDisk +{ + // Methods to be implemented by the implementer + public DataSource getFormattedSector (DiskAddress da); + + public List getFileSectors (int fileNo); + + // Methods implemented by AbstractFormattedDisk + public JTree getCatalogTree (); // each node is an AppleFileSource + + public List getCatalogList (); + + public void writeFile (AbstractFile file); + + public SectorType getSectorType (int track, int sector); + + public SectorType getSectorType (int block); + + public SectorType getSectorType (DiskAddress da); + + public void setSectorType (int block, SectorType type); + + public String getSectorFilename (DiskAddress da); + + public List getSectorTypeList (); + + public Disk getDisk (); + + public FormattedDisk getParent (); + + public void setParent (FormattedDisk disk); + + public AppleFileSource getCatalog (); + + public AppleFileSource getFile (String uniqueName); + + public int clearOrphans (); + + public void setSectorFree (int block, boolean free); + + public boolean isSectorFree (DiskAddress da); + + public boolean isSectorFree (int block); + + public void verify (); + + public boolean stillAvailable (DiskAddress da); + + public boolean stillAvailable (int block); + + public Dimension getGridLayout (); + + public String getAbsolutePath (); + + public void setOriginalPath (Path path); + + // VTOC flags sector as free, but it is in use by a file + public int falsePositiveBlocks (); + + // VTOC flags sector as in use, but no file is using it + public int falseNegativeBlocks (); + + public String getName (); +} + +// getFileTypeList () +// getFiles (FileType type) \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/disk/SectorList.java b/src/com/bytezone/diskbrowser/disk/SectorList.java new file mode 100755 index 0000000..ba8bd01 --- /dev/null +++ b/src/com/bytezone/diskbrowser/disk/SectorList.java @@ -0,0 +1,48 @@ +package com.bytezone.diskbrowser.disk; + +import java.util.List; + +import com.bytezone.diskbrowser.applefile.AbstractFile; + +public class SectorList extends AbstractFile +{ + List sectors; + FormattedDisk formattedDisk; + + public SectorList (FormattedDisk formattedDisk, List sectors) + { + super ("noname", null); + + this.sectors = sectors; + this.formattedDisk = formattedDisk; + + Disk disk = formattedDisk.getDisk (); + int ptr = 0; + buffer = new byte[sectors.size () * disk.getBlockSize ()]; + + for (DiskAddress da : sectors) + { + byte[] tempBuffer = disk.readSector (da); + System.arraycopy (tempBuffer, 0, buffer, ptr, disk.getBlockSize ()); + ptr += disk.getBlockSize (); + } + } + + @Override + public String getText () + { + StringBuilder text = new StringBuilder ("Block Sector Type Owner\n"); + text.append ("----- ------------------ ---------------------------------------------\n"); + + for (DiskAddress da : sectors) + { + SectorType sectorType = formattedDisk.getSectorType (da); + String owner = formattedDisk.getSectorFilename (da); + if (owner == null) + owner = ""; + text.append (String.format (" %04X %-18s %s%n", da.getBlock (), sectorType.name, owner)); + } + + return text.toString (); + } +} diff --git a/src/com/bytezone/diskbrowser/disk/SectorListConverter.java b/src/com/bytezone/diskbrowser/disk/SectorListConverter.java new file mode 100644 index 0000000..4b6e599 --- /dev/null +++ b/src/com/bytezone/diskbrowser/disk/SectorListConverter.java @@ -0,0 +1,65 @@ +package com.bytezone.diskbrowser.disk; + +import java.util.ArrayList; +import java.util.List; + +public class SectorListConverter +{ + public final List sectors; + public final String sectorText; + + public SectorListConverter (String text, Disk disk) + { + sectors = new ArrayList (); + sectorText = text; + + String[] blocks = text.split (";"); + for (String s : blocks) + { + int pos = s.indexOf ('-'); + if (pos > 0) + { + int lo = Integer.parseInt (s.substring (0, pos)); + int hi = Integer.parseInt (s.substring (pos + 1)); + for (int i = lo; i <= hi; i++) + sectors.add (disk.getDiskAddress (i)); + } + else + sectors.add (disk.getDiskAddress (Integer.parseInt (s))); + } + } + + public SectorListConverter (List sectors) + { + this.sectors = sectors; + StringBuilder text = new StringBuilder (); + + int firstBlock = -2; + int runLength = 0; + + for (DiskAddress da : sectors) + { + if (da.getBlock () == firstBlock + 1 + runLength) + { + ++runLength; + continue; + } + + if (firstBlock >= 0) + addToText (text, firstBlock, runLength); + + firstBlock = da.getBlock (); + runLength = 0; + } + addToText (text, firstBlock, runLength); + sectorText = text.deleteCharAt (text.length () - 1).toString (); + } + + private void addToText (StringBuilder text, int firstBlock, int runLength) + { + if (runLength == 0) + text.append (firstBlock + ";"); + else + text.append (firstBlock + "-" + (firstBlock + runLength) + ";"); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/disk/SectorType.java b/src/com/bytezone/diskbrowser/disk/SectorType.java new file mode 100755 index 0000000..f8d56ee --- /dev/null +++ b/src/com/bytezone/diskbrowser/disk/SectorType.java @@ -0,0 +1,21 @@ +package com.bytezone.diskbrowser.disk; + +import java.awt.Color; + +public class SectorType +{ + public final String name; + public final Color colour; + + public SectorType (String name, Color colour) + { + this.name = name; + this.colour = colour; + } + + @Override + public String toString () + { + return String.format ("[SectorType : %s, %s]", name, colour); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/dos/AbstractCatalogEntry.java b/src/com/bytezone/diskbrowser/dos/AbstractCatalogEntry.java new file mode 100644 index 0000000..ab895af --- /dev/null +++ b/src/com/bytezone/diskbrowser/dos/AbstractCatalogEntry.java @@ -0,0 +1,277 @@ +package com.bytezone.diskbrowser.dos; + +import java.util.ArrayList; +import java.util.List; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.applefile.*; +import com.bytezone.diskbrowser.disk.Disk; +import com.bytezone.diskbrowser.disk.DiskAddress; +import com.bytezone.diskbrowser.disk.FormattedDisk; +import com.bytezone.diskbrowser.dos.DosDisk.FileType; +import com.bytezone.diskbrowser.gui.DataSource; + +abstract class AbstractCatalogEntry implements AppleFileSource +{ + Disk disk; + DosDisk dosDisk; + String name; + String catalogName; + + FileType fileType; + int reportedSize; + boolean locked; + protected DataSource appleFile; + + protected DiskAddress catalogSectorDA; + protected final List dataSectors = new ArrayList (); + protected final List tsSectors = new ArrayList (); + + public AbstractCatalogEntry (DosDisk dosDisk, DiskAddress catalogSector, byte[] entryBuffer) + { + this.dosDisk = dosDisk; + this.disk = dosDisk.getDisk (); + this.catalogSectorDA = catalogSector; + reportedSize = HexFormatter.intValue (entryBuffer[33], entryBuffer[34]); + int type = entryBuffer[2] & 0xFF; + locked = (type & 0x80) > 0; + this.disk = dosDisk.getDisk (); + + if ((type & 0x7F) == 0) + fileType = FileType.Text; + else if ((type & 0x01) > 0) + fileType = FileType.IntegerBasic; + else if ((type & 0x02) > 0) + fileType = FileType.ApplesoftBasic; + else if ((type & 0x04) > 0) + fileType = FileType.Binary; + else if ((type & 0x08) > 0) + fileType = FileType.SS; + else if ((type & 0x10) > 0) + fileType = FileType.Relocatable; + else if ((type & 0x20) > 0) + fileType = FileType.AA; + else if ((type & 0x40) > 0) + fileType = FileType.BB; + else + System.out.println ("Unknown file type : " + (type & 0x7F)); + + name = getName ("", entryBuffer); + // CATALOG command only formats the LO byte - see Beneath Apple DOS pp4-6 + String base = + String.format ("%s%s %03d ", (locked) ? "*" : " ", getFileType (), + (entryBuffer[33] & 0xFF)); + catalogName = getName (base, entryBuffer); + } + + private String getName (String base, byte[] buffer) + { + StringBuilder text = new StringBuilder (base); + int max = buffer[0] == (byte) 0xFF ? 32 : 33; + for (int i = 3; i < max; i++) + { + int c = buffer[i] & 0xFF; + if (c == 136 && !base.isEmpty ()) // allow backspaces + { + if (text.length () > 0) + text.deleteCharAt (text.length () - 1); + continue; + } + if (c > 127) + c -= c < 160 ? 64 : 128; + if (c < 32) + text.append ("^" + (char) (c + 64)); // non-printable ascii + else + text.append ((char) c); // standard ascii + } + while (text.length () > 0 && text.charAt (text.length () - 1) == ' ') + text.deleteCharAt (text.length () - 1); // rtrim() + return text.toString (); + } + + protected String getFileType () + { + switch (this.fileType) + { + case Text: + return "T"; + case IntegerBasic: + return "I"; + case ApplesoftBasic: + return "A"; + case Binary: + return "B"; + case SS: // what is this? + return "S"; + case Relocatable: + return "R"; + case AA: // what is this? + return "A"; + case BB: // what is this? + return "B"; + default: + System.out.println ("Unknown file type : " + fileType); + return "?"; + } + } + + // maybe this should be in the FormattedDisk + // maybe DiskAddress should have a 'valid' flag + protected DiskAddress getValidAddress (byte[] buffer, int offset) + { + if (disk.isValidAddress (buffer[offset], buffer[offset + 1])) + return disk.getDiskAddress (buffer[offset], buffer[offset + 1]); + return null; + } + + @Override + public DataSource getDataSource () + { + if (appleFile != null) + return appleFile; + + byte[] buffer = disk.readSectors (dataSectors); + int reportedLength; + if (buffer.length == 0) + { + appleFile = new DefaultAppleFile (name, buffer); + return appleFile; + } + + try + { + switch (this.fileType) + { + case Text: + if (VisicalcFile.isVisicalcFile (buffer)) + appleFile = new VisicalcFile (name, buffer); + else + appleFile = new TextFile (name, buffer); + break; + case IntegerBasic: + reportedLength = HexFormatter.intValue (buffer[0], buffer[1]); + byte[] exactBuffer = new byte[reportedLength]; + System.arraycopy (buffer, 2, exactBuffer, 0, reportedLength); + appleFile = new IntegerBasicProgram (name, exactBuffer); + break; + case ApplesoftBasic: + reportedLength = HexFormatter.intValue (buffer[0], buffer[1]); + exactBuffer = new byte[reportedLength]; + if (reportedLength > buffer.length) + reportedLength = buffer.length - 2; + System.arraycopy (buffer, 2, exactBuffer, 0, reportedLength); + // appleFile = new ApplesoftBasicProgram (name, exactBuffer); + appleFile = new BasicProgram (name, exactBuffer); + break; + case Binary: // binary file + case Relocatable: // relocatable binary file + if (buffer.length == 0) + appleFile = new AssemblerProgram (name, buffer, 0); + else + { + int loadAddress = HexFormatter.intValue (buffer[0], buffer[1]); + reportedLength = HexFormatter.intValue (buffer[2], buffer[3]); + if (reportedLength == 0) + { + System.out.println (name.trim () + " reported length : 0 - reverting to " + + (buffer.length - 4)); + reportedLength = buffer.length - 4; + } + + // buffer is a multiple of the block size, so it usually needs to be reduced + if ((reportedLength + 4) <= buffer.length) + exactBuffer = new byte[reportedLength]; + else + exactBuffer = new byte[buffer.length - 4]; // reported length is too long + System.arraycopy (buffer, 4, exactBuffer, 0, exactBuffer.length); + + if (ShapeTable.isShapeTable (exactBuffer)) + appleFile = new ShapeTable (name, exactBuffer); + else if (HiResImage.isGif (exactBuffer)) + appleFile = new HiResImage (name, exactBuffer); + else if (loadAddress == 0x2000 || loadAddress == 0x4000) + { + if ((reportedLength > 0x1F00 && reportedLength <= 0x4000) + || ((name.equals ("FLY LOGO") && reportedLength == 0x14FA))) + appleFile = new HiResImage (name, exactBuffer); + // else if + // appleFile = new HiResImage (name, unscrunch (exactBuffer)); + else + appleFile = new AssemblerProgram (name, exactBuffer, loadAddress); + } + else if (name.endsWith (".S")) + appleFile = new MerlinSource (name, exactBuffer); + else + appleFile = new AssemblerProgram (name, exactBuffer, loadAddress); + } + break; + case SS: // what is this? + System.out.println ("SS file"); + appleFile = new DefaultAppleFile (name, buffer); + break; + case AA: // what is this? + System.out.println ("AA file"); + appleFile = new DefaultAppleFile (name, buffer); + break; + case BB: // what is this? + int loadAddress = HexFormatter.intValue (buffer[0], buffer[1]); + reportedLength = HexFormatter.intValue (buffer[2], buffer[3]); + exactBuffer = new byte[reportedLength]; + System.arraycopy (buffer, 4, exactBuffer, 0, reportedLength); + appleFile = new SimpleText2 (name, exactBuffer, loadAddress); + break; + default: + System.out.println ("Unknown file type : " + fileType); + appleFile = new DefaultAppleFile (name, buffer); + break; + } + } + catch (Exception e) + { + appleFile = new ErrorMessageFile (name, buffer, e); + e.printStackTrace (); + } + return appleFile; + } + + boolean contains (DiskAddress da) + { + for (DiskAddress sector : tsSectors) + if (sector.compareTo (da) == 0) + return true; + for (DiskAddress sector : dataSectors) + // random access files may have gaps, and thus null sectors + if (sector != null && sector.compareTo (da) == 0) + return true; + return false; + } + + @Override + public String getUniqueName () + { + // this might not be unique if the file has been deleted + return name; + } + + @Override + public FormattedDisk getFormattedDisk () + { + return dosDisk; + } + + @Override + public List getSectors () + { + List sectors = new ArrayList (); + sectors.add (catalogSectorDA); + sectors.addAll (tsSectors); + sectors.addAll (dataSectors); + return sectors; + } + + @Override + public String toString () + { + return catalogName; + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/dos/CatalogEntry.java b/src/com/bytezone/diskbrowser/dos/CatalogEntry.java new file mode 100644 index 0000000..bbccfd0 --- /dev/null +++ b/src/com/bytezone/diskbrowser/dos/CatalogEntry.java @@ -0,0 +1,135 @@ +package com.bytezone.diskbrowser.dos; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.disk.DiskAddress; +import com.bytezone.diskbrowser.dos.DosDisk.FileType; + +class CatalogEntry extends AbstractCatalogEntry +{ + int textFileGaps; + int length; + int address; + + public CatalogEntry (DosDisk dosDisk, DiskAddress catalogSector, byte[] entryBuffer) + { + super (dosDisk, catalogSector, entryBuffer); // build lists of ts and data sectors + if (reportedSize > 0 && disk.isValidAddress (entryBuffer[0], entryBuffer[1])) + { + // Get address of first TS-list sector + DiskAddress da = disk.getDiskAddress (entryBuffer[0], entryBuffer[1]); + + // Loop through all TS-list sectors + loop: while (da.getBlock () > 0) + { + if (dosDisk.stillAvailable (da)) + dosDisk.sectorTypes[da.getBlock ()] = dosDisk.tsListSector; + else + { + System.out.print ("Attempt to assign TS sector to occupied sector : " + da); + System.out.println (" from " + name); + } + tsSectors.add (da); + byte[] sectorBuffer = disk.readSector (da); + + int startPtr = 12; + // the tsList *should* start at 0xC0, but some disks start in the unused bytes + if (false) + for (int i = 7; i < startPtr; i++) + if (sectorBuffer[i] != 0) + { + startPtr = i; + break; + } + + for (int i = startPtr, max = disk.getBlockSize (); i < max; i += 2) + { + da = getValidAddress (sectorBuffer, i); + if (da == null) + { + System.out.print ("T/S list in sector " + i); + System.out.printf (" contains an invalid address : %02X, %02X (file %s)%n", + sectorBuffer[i], sectorBuffer[i + 1], name.trim ()); + break loop; + } + if (da.getBlock () == 0) + { + if (fileType != FileType.Text) + break; + ++textFileGaps; + dataSectors.add (null); + } + else + { + dataSectors.add (da); + if (dosDisk.stillAvailable (da)) + dosDisk.sectorTypes[da.getBlock ()] = dosDisk.dataSector; + else + { + System.out.print ("Attempt to assign Data sector to occupied sector : " + da); + System.out.println (" from " + name); + } + } + } + + da = getValidAddress (sectorBuffer, 1); + if (da == null) + { + System.out.print ("Next T/S list in sector " + da); + System.out.printf (" is invalid : %02X, %02X%n", sectorBuffer[1], sectorBuffer[2]); + break; + } + } + } + + // remove trailing empty sectors + if (fileType == FileType.Text) + { + while (dataSectors.size () > 0) + { + DiskAddress da = dataSectors.get (dataSectors.size () - 1); + if (da == null) + { + dataSectors.remove (dataSectors.size () - 1); + --textFileGaps; + } + else + break; + } + } + + // get the file length + if (dataSectors.size () > 0 && fileType != FileType.Text) + { + byte[] buffer = disk.readSector (dataSectors.get (0)); + switch (fileType) + { + case IntegerBasic: + // length = HexFormatter.intValue (buffer[0], buffer[1]); + // break; + case ApplesoftBasic: + length = HexFormatter.intValue (buffer[0], buffer[1]); + break; + default: + address = HexFormatter.intValue (buffer[0], buffer[1]); + length = HexFormatter.intValue (buffer[2], buffer[3]); + } + } + } + + public String getDetails () + { + int actualSize = dataSectors.size () + tsSectors.size () - textFileGaps; + String addressText = address == 0 ? "" : String.format ("$%4X", address); + String lengthText = length == 0 ? "" : String.format ("$%4X %,6d", length, length); + String message = ""; + String lockedFlag = (locked) ? "*" : " "; + if (reportedSize != actualSize) + message += "Bad size (" + reportedSize + ") "; + if (dataSectors.size () == 0) + message += "No data "; + return String.format ("%1s %1s %03d %-30.30s %-5s %-13s %2d %3d %s", lockedFlag, + getFileType (), actualSize, name, addressText, lengthText, + tsSectors.size (), (dataSectors.size () - textFileGaps), + message.trim ()); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/dos/DeletedCatalogEntry.java b/src/com/bytezone/diskbrowser/dos/DeletedCatalogEntry.java new file mode 100644 index 0000000..ae006b2 --- /dev/null +++ b/src/com/bytezone/diskbrowser/dos/DeletedCatalogEntry.java @@ -0,0 +1,105 @@ +package com.bytezone.diskbrowser.dos; + +import com.bytezone.diskbrowser.applefile.DefaultAppleFile; +import com.bytezone.diskbrowser.disk.DiskAddress; +import com.bytezone.diskbrowser.gui.DataSource; + +class DeletedCatalogEntry extends AbstractCatalogEntry +{ + boolean allSectorsAvailable = true; + boolean debug = false; + + public DeletedCatalogEntry (DosDisk dosDisk, DiskAddress catalogSector, byte[] entryBuffer) + { + super (dosDisk, catalogSector, entryBuffer); + // reportedSize = HexFormatter.intValue (entryBuffer[33], entryBuffer[34]); + if (debug) + { + System.out.println ("Deleted file : " + name); + System.out.printf ("Reported size : %d%n", reportedSize); + } + + if (reportedSize <= 1 || !disk.isValidAddress (entryBuffer[32], entryBuffer[1])) + { + if (debug) + System.out.println ("invalid catalog entry"); + allSectorsAvailable = false; + return; + } + + // Get address of first TS-list sector + DiskAddress da = disk.getDiskAddress (entryBuffer[32], entryBuffer[1]); + int totalBlocks = 0; + + // Loop through all TS-list sectors + loop: while (da.getBlock () > 0) + { + if (!dosDisk.stillAvailable (da)) + { + allSectorsAvailable = false; + break; + } + tsSectors.add (da); + totalBlocks++; + + byte[] sectorBuffer = disk.readSector (da); + for (int i = 12, max = disk.getBlockSize (); i < max; i += 2) + { + da = getValidAddress (sectorBuffer, i); + if (da == null) + break loop; + if (da.getBlock () > 0 && debug) + System.out.println (da); + + if (da.getBlock () > 0) + { + if (!dosDisk.stillAvailable (da)) + { + allSectorsAvailable = false; + break loop; + } + dataSectors.add (da); + totalBlocks++; + } + } + + da = getValidAddress (sectorBuffer, 1); + + if (da == null) + { + System.out.printf ("Next T/S list in sector %s is invalid : %02X, %02X%n", da, + sectorBuffer[1], sectorBuffer[2]); + break; + } + } + if (debug) + System.out.printf ("Total blocks recoverable : %d%n", totalBlocks); + if (totalBlocks != reportedSize) + allSectorsAvailable = false; + } + + @Override + public String getUniqueName () + { + // name might not be unique if the file has been deleted + return "!" + name; + } + + @Override + public DataSource getDataSource () + { + if (!allSectorsAvailable && appleFile == null) + { + DefaultAppleFile daf = new DefaultAppleFile (name, null); + daf.setText ("This file cannot be recovered"); + appleFile = daf; + } + return super.getDataSource (); + } + + public String getDetails () + { + return String.format ("%-30s %s", name, allSectorsAvailable ? "Recoverable" + : "Not recoverable"); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/dos/DosCatalogSector.java b/src/com/bytezone/diskbrowser/dos/DosCatalogSector.java new file mode 100755 index 0000000..c4683ea --- /dev/null +++ b/src/com/bytezone/diskbrowser/dos/DosCatalogSector.java @@ -0,0 +1,105 @@ +package com.bytezone.diskbrowser.dos; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.disk.AbstractSector; +import com.bytezone.diskbrowser.disk.Disk; + +class DosCatalogSector extends AbstractSector +{ + private static final String[] fileTypes = { "Text file", "Integer Basic program", + "Applesoft Basic program", "Binary file", + "SS file", "Relocatable file", "AA file", + "BB file" }; + private static int CATALOG_ENTRY_SIZE = 35; + + public DosCatalogSector (Disk disk, byte[] buffer) + { + super (disk, buffer); + } + + @Override + public String createText () + { + StringBuilder text = getHeader ("Catalog Sector"); + addText (text, buffer, 0, 1, "Not used"); + addText (text, buffer, 1, 2, "Next catalog track/sector"); + addText (text, buffer, 3, 4, "Not used"); + addText (text, buffer, 7, 4, "Not used"); + + for (int i = 11; i <= 255; i += CATALOG_ENTRY_SIZE) + { + text.append ("\n"); + if (true) + { + if (buffer[i] == (byte) 0xFF) + { + addText (text, + buffer, + i + 0, + 2, + "DEL: file @ " + HexFormatter.format2 (buffer[i + 32]) + " " + + HexFormatter.format2 (buffer[i + 1])); + addText (text, buffer, i + 2, 1, "DEL: File type " + getType (buffer[i + 2])); + if (buffer[i + 3] == 0) + addText (text, buffer, i + 3, 4, ""); + else + addText (text, buffer, i + 3, 4, "DEL: " + getName (buffer, i)); + addTextAndDecimal (text, buffer, i + 33, 2, "DEL: Sector count"); + } + else if (buffer[i] > 0) + { + addText (text, buffer, i + 0, 2, "TS list track/sector"); + addText (text, buffer, i + 2, 1, "File type " + getType (buffer[i + 2])); + if (buffer[i + 3] == 0) + addText (text, buffer, i + 3, 4, ""); + else + addText (text, buffer, i + 3, 4, getName (buffer, i)); + addTextAndDecimal (text, buffer, i + 33, 2, "Sector count"); + } + else + { + addText (text, buffer, i + 0, 2, ""); + addText (text, buffer, i + 2, 1, ""); + addText (text, buffer, i + 3, 4, ""); + addText (text, buffer, i + 33, 2, ""); + } + } + } + + text.deleteCharAt (text.length () - 1); + return text.toString (); + } + + private String getName (byte[] buffer, int offset) + { + StringBuilder text = new StringBuilder (); + int max = buffer[offset] == (byte) 0xFF ? 32 : 33; + for (int i = 3; i < max; i++) + { + int c = HexFormatter.intValue (buffer[i + offset]); + if (c == 136) + { + if (text.length () > 0) + text.deleteCharAt (text.length () - 1); + continue; + } + if (c > 127) + c -= c < 160 ? 64 : 128; + if (c < 32) // non-printable + text.append ("^" + (char) (c + 64)); + else + text.append ((char) c); // standard ascii + } + return text.toString (); + } + + private String getType (byte value) + { + int type = value & 0x7F; + boolean locked = (value & 0x80) > 0; + int val = 7; + for (int i = 64; i > type; val--, i /= 2) + ; + return "(" + fileTypes[val] + (locked ? ", locked)" : ", unlocked)"); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/dos/DosDisk.java b/src/com/bytezone/diskbrowser/dos/DosDisk.java new file mode 100755 index 0000000..58c7584 --- /dev/null +++ b/src/com/bytezone/diskbrowser/dos/DosDisk.java @@ -0,0 +1,407 @@ +package com.bytezone.diskbrowser.dos; + +import java.awt.Color; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.tree.DefaultMutableTreeNode; + +import com.bytezone.diskbrowser.applefile.AppleFileSource; +import com.bytezone.diskbrowser.applefile.BootSector; +import com.bytezone.diskbrowser.disk.*; +import com.bytezone.diskbrowser.gui.DataSource; + +public class DosDisk extends AbstractFormattedDisk +{ + private static final int ENTRY_SIZE = 35; + private static final int CATALOG_TRACK = 17; + + // private static final boolean CHECK_SELF_POINTER = true; + + private final DosVTOCSector dosVTOCSector; + private final Color green = new Color (0, 200, 0); + private final DefaultMutableTreeNode volumeNode; + + private int freeSectors; + private int usedSectors; + + public final SectorType vtocSector = new SectorType ("VTOC", Color.magenta); + public final SectorType catalogSector = new SectorType ("Catalog", green); + public final SectorType tsListSector = new SectorType ("TSList", Color.blue); + public final SectorType dataSector = new SectorType ("Data", Color.red); + public final SectorType dosSector = new SectorType ("DOS", Color.lightGray); + + protected List deletedFileEntries = new ArrayList (); + + enum FileType + { + Text, ApplesoftBasic, IntegerBasic, Binary, Relocatable, SS, AA, BB + } + + public DosDisk (Disk disk) + { + super (disk); + + sectorTypesList.add (dosSector); + sectorTypesList.add (vtocSector); + sectorTypesList.add (catalogSector); + sectorTypesList.add (tsListSector); + sectorTypesList.add (dataSector); + + byte[] sectorBuffer = disk.readSector (0, 0); // Boot sector + bootSector = new BootSector (disk, sectorBuffer, "DOS"); + + sectorBuffer = disk.readSector (CATALOG_TRACK, 0); // VTOC + dosVTOCSector = new DosVTOCSector (this, disk, sectorBuffer); + + DiskAddress catalogStart = disk.getDiskAddress (sectorBuffer[1], sectorBuffer[2]); + + if (dosVTOCSector.sectorSize != disk.getBlockSize ()) + System.out.println ("Invalid sector size : " + dosVTOCSector.sectorSize); + if (dosVTOCSector.maxSectors != disk.getSectorsPerTrack ()) + System.out.println ("Invalid sectors per track : " + dosVTOCSector.maxSectors); + + sectorTypes[CATALOG_TRACK * dosVTOCSector.maxSectors] = vtocSector; + + // assert (maxTracks == disk.getTotalTracks ()); + // assert (dosVTOCSector.maxSectors == disk.getSectorsPerTrack ()); + // assert (sectorSize == disk.getBlockSize ()); HFSAssembler.dsk fails this + // assert (catalogStart.getTrack () == CATALOG_TRACK); + + // arcboot.dsk starts the catalog at 17/13 + // assert (catalogStart.getSector () == 15); + + // build list of CatalogEntry objects + DefaultMutableTreeNode rootNode = getCatalogTreeRoot (); + volumeNode = new DefaultMutableTreeNode (); + DefaultMutableTreeNode deletedFilesNode = new DefaultMutableTreeNode (); + rootNode.add (volumeNode); + + // flag the catalog sectors before any file mistakenly grabs them + DiskAddress da = disk.getDiskAddress (catalogStart.getBlock ()); + do + { + if (!disk.isValidAddress (da)) + break; + sectorBuffer = disk.readSector (da); + if (!disk.isValidAddress (sectorBuffer[1], sectorBuffer[2])) + break; + + // The first byte is officially unused, but it always seems to contain 0x00 or 0xFF + // See beautifulboot.dsk. + if (sectorBuffer[0] != 0 && (sectorBuffer[0] & 0xFF) != 0xFF && false) + { + System.out.println ("Dos catalog sector buffer byte #0 invalid : " + sectorBuffer[0]); + break; + } + + sectorTypes[da.getBlock ()] = catalogSector; + + int track = sectorBuffer[1] & 0xFF; + int sector = sectorBuffer[2] & 0xFF; + if (!disk.isValidAddress (track, sector)) + break; + + // int thisBlock = da.getBlock (); + da = disk.getDiskAddress (track, sector); + + // if (CHECK_SELF_POINTER && da.getBlock () == thisBlock) + // break; + + } while (da.getBlock () != 0); + + // same loop, but now all the catalog sectors are properly flagged + da = disk.getDiskAddress (catalogStart.getBlock ()); + loop: do + { + if (!disk.isValidAddress (da)) + break; + sectorBuffer = disk.readSector (da); + if (!disk.isValidAddress (sectorBuffer[1], sectorBuffer[2])) + break; + + for (int ptr = 11; ptr < 256; ptr += ENTRY_SIZE) + { + if (sectorBuffer[ptr] == 0) // empty slot, no more catalog entries + continue; + // break loop; + + byte[] entry = new byte[ENTRY_SIZE]; + System.arraycopy (sectorBuffer, ptr, entry, 0, ENTRY_SIZE); + + if (entry[0] == (byte) 0xFF) // deleted file + { + DeletedCatalogEntry deletedCatalogEntry = new DeletedCatalogEntry (this, da, entry); + deletedFileEntries.add (deletedCatalogEntry); + DefaultMutableTreeNode node = new DefaultMutableTreeNode (deletedCatalogEntry); + node.setAllowsChildren (false); + deletedFilesNode.add (node); + } + else + { + CatalogEntry catalogEntry = new CatalogEntry (this, da, entry); + fileEntries.add (catalogEntry); + DefaultMutableTreeNode node = new DefaultMutableTreeNode (catalogEntry); + node.setAllowsChildren (false); + volumeNode.add (node); + } + } + + int track = sectorBuffer[1] & 0xFF; + int sector = sectorBuffer[2] & 0xFF; + if (!disk.isValidAddress (track, sector)) + break; + + // int thisBlock = da.getBlock (); + da = disk.getDiskAddress (sectorBuffer[1], sectorBuffer[2]); + + // if (CHECK_SELF_POINTER && da.getBlock () == thisBlock) + // break; + + } while (da.getBlock () != 0); + + // add up all the free and used sectors, and label DOS sectors while we're here + int lastDosSector = dosVTOCSector.maxSectors * 3; // first three tracks + for (DiskAddress da2 : disk) + { + int blockNo = da2.getBlock (); + if (blockNo < lastDosSector) // in the DOS region + { + if (freeBlocks.get (blockNo)) // according to the VTOC + ++freeSectors; + else + { + ++usedSectors; + if (sectorTypes[blockNo] == usedSector) + sectorTypes[blockNo] = dosSector; + } + } + else + { + if (stillAvailable (da2)) // free or used, ie not specifically labelled + ++freeSectors; + else + ++usedSectors; + } + + if (freeBlocks.get (blockNo) && !stillAvailable (da2)) + falsePositives++; + if (!freeBlocks.get (blockNo) && stillAvailable (da2)) + falseNegatives++; + } + + if (deletedFilesNode.getDepth () > 0) + { + rootNode.add (deletedFilesNode); + deletedFilesNode.setUserObject (getDeletedList ()); + makeNodeVisible (deletedFilesNode.getFirstLeaf ()); + } + volumeNode.setUserObject (getCatalog ()); + makeNodeVisible (volumeNode.getFirstLeaf ()); + } + + @Override + public void setOriginalPath (Path path) + { + super.setOriginalPath (path); + volumeNode.setUserObject (getCatalog ()); // this has already been set in the constructor + } + + // Beagle Bros FRAMEUP disk only has one catalog block + // ARCBOOT.DSK has a catalog which starts at sector 0C + public static boolean isCorrectFormat (AppleDisk disk) + { + disk.setInterleave (0); + int catalogBlocks = checkFormat (disk); + if (catalogBlocks > 3) + return true; + disk.setInterleave (1); + int cb2 = checkFormat (disk); + // if (cb2 > catalogBlocks) + if (cb2 > 3) + return true; + disk.setInterleave (2); + if (false) + { + int cb3 = checkFormat (disk); + if (cb3 > 3) + return true; + } + if (catalogBlocks > 0) + { + disk.setInterleave (0); + return true; + } + if (cb2 > 0) + return true; + return false; + } + + private static int checkFormat (AppleDisk disk) + { + byte[] buffer = disk.readSector (0x11, 0x00); + + // DISCCOMMANDER.DSK uses track 0x17 for the catalog + // if (buffer[1] != 0x11) // first catalog track + // return 0; + + if (buffer[53] != 16 && buffer[53] != 13) // tracks per sector + { + // System.out.println ("VTOC tracks per sector : " + (buffer[53 & 0xFF])); + return 0; + } + if (buffer[49] < -1 || buffer[49] > 1) // direction of next file save + { + System.out.println ("Bad direction : " + buffer[49]); + // Visicalc data disk had 0xF8 + // return 0; + } + + int version = buffer[3]; + if (version < -1 || version > 4) + { + System.out.println ("Bad version : " + buffer[3]); + return 0; + } + + return countCatalogBlocks (disk, buffer); + } + + private static int countCatalogBlocks (AppleDisk disk, byte[] buffer) + { + DiskAddress catalogStart = disk.getDiskAddress (buffer[1], buffer[2]); + // int catalogBlocks = 0; + DiskAddress da = disk.getDiskAddress (catalogStart.getBlock ()); + List catalogAddresses = new ArrayList (); + + do + { + // System.out.printf ("%5d %s%n", catalogBlocks, da); + if (!disk.isValidAddress (da)) + break; + + if (catalogAddresses.contains (da)) + { + System.out.println ("Catalog looping"); + return 0; + } + + buffer = disk.readSector (da); + if (!disk.isValidAddress (buffer[1], buffer[2])) + { + System.out.printf ("Invalid address : %02X / %02X%n", buffer[1], buffer[2]); + // System.out.println (HexFormatter.format (buffer)); + // System.out.println (da); + break; + } + + catalogAddresses.add (da); + // catalogBlocks++; + // if (catalogBlocks > 1000) // looping + // { + // System.out.println ("Disk appears to be looping in countCatalogBlocks()"); + // return 0; + // } + + // int thisBlock = da.getBlock (); + da = disk.getDiskAddress (buffer[1], buffer[2]); + + // if (CHECK_SELF_POINTER && da.getBlock () == thisBlock) + // break; + + } while (da.getBlock () != 0); + + // if (catalogBlocks != catalogAddresses.size ()) + // System.out.printf ("CB: %d, size: %d%n", catalogBlocks, catalogAddresses.size ()); + return catalogAddresses.size (); + } + + @Override + public String toString () + { + StringBuffer text = new StringBuffer (dosVTOCSector.toString ()); + return text.toString (); + } + + @Override + public DataSource getFormattedSector (DiskAddress da) + { + SectorType type = sectorTypes[da.getBlock ()]; + if (type == vtocSector) + return dosVTOCSector; + if (da.getBlock () == 0) + return bootSector; + + byte[] buffer = disk.readSector (da); + String address = String.format ("%02X %02X", da.getTrack (), da.getSector ()); + + if (type == tsListSector) + return new DosTSListSector (getSectorFilename (da), disk, buffer); + if (type == catalogSector) + return new DosCatalogSector (disk, buffer); + if (type == dataSector) + return new DefaultSector ("Data Sector at " + address + " : " + getSectorFilename (da), + disk, buffer); + if (type == dosSector) + return new DefaultSector ("DOS sector at " + address, disk, buffer); + return super.getFormattedSector (da); + } + + @Override + public String getSectorFilename (DiskAddress da) + { + for (AppleFileSource ce : fileEntries) + if (((CatalogEntry) ce).contains (da)) + return ((CatalogEntry) ce).name; + return null; + } + + @Override + public List getFileSectors (int fileNo) + { + if (fileEntries.size () > 0 && fileEntries.size () > fileNo) + return fileEntries.get (fileNo).getSectors (); + return null; + } + + @Override + public AppleFileSource getCatalog () + { + String newLine = String.format ("%n"); + String line = + "- --- --- ------------------------------ ----- -------------" + + " -- ---- ----------------" + newLine; + StringBuilder text = new StringBuilder (); + text.append (String.format ("Disk : %s%n%n", getAbsolutePath ())); + text.append ("L Typ Len Name Addr" + + " Length TS Data Comment" + newLine); + text.append (line); + + for (AppleFileSource ce : fileEntries) + text.append (((CatalogEntry) ce).getDetails () + newLine); + + text.append (line); + text.append (String + .format (" Free sectors: %3d Used sectors: %3d Total sectors: %3d", + dosVTOCSector.freeSectors, dosVTOCSector.usedSectors, + (dosVTOCSector.freeSectors + dosVTOCSector.usedSectors))); + if (dosVTOCSector.freeSectors != freeSectors) + text.append (String + .format ("%nActual: Free sectors: %3d Used sectors: %3d Total sectors: %3d", + freeSectors, usedSectors, (usedSectors + freeSectors))); + return new DefaultAppleFileSource ("Volume " + dosVTOCSector.volume, text.toString (), + this); + } + + private AppleFileSource getDeletedList () + { + StringBuilder text = + new StringBuilder ("List of files that were deleted from this disk\n"); + + for (AppleFileSource afs : deletedFileEntries) + text.append (((DeletedCatalogEntry) afs).getDetails () + "\n"); + + return new DefaultAppleFileSource ("Deleted files", text.toString (), this); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/dos/DosTSListSector.java b/src/com/bytezone/diskbrowser/dos/DosTSListSector.java new file mode 100755 index 0000000..5c85946 --- /dev/null +++ b/src/com/bytezone/diskbrowser/dos/DosTSListSector.java @@ -0,0 +1,78 @@ +package com.bytezone.diskbrowser.dos; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.disk.AbstractSector; +import com.bytezone.diskbrowser.disk.Disk; +import com.bytezone.diskbrowser.disk.DiskAddress; + +class DosTSListSector extends AbstractSector +{ + String name; + + public DosTSListSector (String name, Disk disk, byte[] buffer) + { + super (disk, buffer); + this.name = name; + } + + public boolean isValid (DosDisk dosDisk) + { + System.out.println ("Validating TS List sector"); + + // what is the count of blocks? does it match? this sector can't tell, there + // might be more than one TS list + + // validate the sector, throw an exception if invalid + for (int i = 12; i < buffer.length; i += 2) + { + DiskAddress da = getValidAddress (buffer, i); + if (da == null) + { + System.out.println ("Invalid sector address : null"); + break; // throw exception + } + + if (da.getBlock () > 0 && dosDisk.stillAvailable (da)) + { + System.out.println ("Invalid sector address : " + da); + break; // throw exception + } + } + return true; + } + + // this is in too many places + protected DiskAddress getValidAddress (byte[] buffer, int offset) + { + if (disk.isValidAddress (buffer[offset], buffer[offset + 1])) + return disk.getDiskAddress (buffer[offset], buffer[offset + 1]); + return null; + } + + @Override + public String createText () + { + StringBuilder text = getHeader ("TS List Sector : " + name); + addText (text, buffer, 0, 1, "Not used"); + addText (text, buffer, 1, 2, "Next TS list track/sector"); + addText (text, buffer, 3, 2, "Not used"); + addTextAndDecimal (text, buffer, 5, 2, "Sector base number"); + addText (text, buffer, 7, 4, "Not used"); + addText (text, buffer, 11, 1, "Not used"); + + String message; + int sectorBase = HexFormatter.intValue (buffer[5], buffer[6]); + + for (int i = 12; i <= 255; i += 2) + { + if (buffer[i] == 0 && buffer[i + 1] == 0) + message = ""; + else + message = "Track/sector of file sector " + ((i - 10) / 2 + sectorBase); + addText (text, buffer, i, 2, message); + } + + text.deleteCharAt (text.length () - 1); + return text.toString (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/dos/DosVTOCSector.java b/src/com/bytezone/diskbrowser/dos/DosVTOCSector.java new file mode 100755 index 0000000..9959401 --- /dev/null +++ b/src/com/bytezone/diskbrowser/dos/DosVTOCSector.java @@ -0,0 +1,146 @@ +package com.bytezone.diskbrowser.dos; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.disk.AbstractSector; +import com.bytezone.diskbrowser.disk.Disk; + +class DosVTOCSector extends AbstractSector +{ + DosDisk parentDisk; + int volume; + int DOSVersion; + int maxTSPairs; + int lastAllocTrack; + int direction; + int freeSectors; + int usedSectors; + int sectorSize; + int maxSectors; + int maxTracks; + + public DosVTOCSector (DosDisk parentDisk, Disk disk, byte[] buffer) + { + super (disk, buffer); + + this.parentDisk = parentDisk; + DOSVersion = buffer[3]; + volume = HexFormatter.intValue (buffer[6]); + maxTSPairs = buffer[39]; + lastAllocTrack = buffer[48]; + direction = buffer[49]; + maxTracks = buffer[52] & 0xFF; + maxSectors = buffer[53] & 0xFF; + sectorSize = HexFormatter.intValue (buffer[54], buffer[55]); + flagSectors (); + } + + @Override + public String createText () + { + StringBuilder text = getHeader ("VTOC Sector"); + addText (text, buffer, 0, 1, "Not used"); + addText (text, buffer, 1, 2, "First directory track/sector"); + addText (text, buffer, 3, 1, "DOS release number"); + addText (text, buffer, 4, 2, "Not used"); + addTextAndDecimal (text, buffer, 6, 1, "Diskette volume"); + addTextAndDecimal (text, buffer, 39, 1, "Maximum TS pairs"); + addText (text, buffer, 40, 4, "Not used"); + addText (text, buffer, 44, 4, "Not used"); + addText (text, buffer, 48, 1, "Last allocated track"); + addText (text, buffer, 49, 1, "Direction to look when allocating the next file"); + addText (text, buffer, 50, 2, "Not used"); + addTextAndDecimal (text, buffer, 52, 1, "Maximum tracks"); + addTextAndDecimal (text, buffer, 53, 1, "Maximum sectors"); + addTextAndDecimal (text, buffer, 54, 2, "Bytes per sector"); + + for (int i = 56; i <= 0xc3; i += 4) + { + String extra; + if (i <= 64) + extra = "(DOS)"; + else if (i == 124) + extra = "(VTOC and Catalog)"; + else + extra = ""; + addText (text, + buffer, + i, + 4, + String.format ("Track %02X %s %s", (i - 56) / 4, + getBitmap (buffer[i], buffer[i + 1]), extra)); + } + + text.deleteCharAt (text.length () - 1); + + return text.toString (); + } + + private String getBitmap (byte left, byte right) + { + int base = maxSectors == 13 ? 3 : 0; + right >>= base; + StringBuilder text = new StringBuilder (); + for (int i = base; i < 8; i++) + { + if ((right & 0x01) == 1) + text.append ("."); + else + text.append ("X"); + right >>= 1; + } + for (int i = 0; i < 8; i++) + { + if ((left & 0x01) == 1) + text.append ("."); + else + text.append ("X"); + left >>= 1; + } + return text.toString (); + } + + public void flagSectors () + { + int block = 0; + int base = maxSectors == 13 ? 3 : 0; + for (int i = 56; i <= 0xc3; i += 4) + { + block = check (buffer[i + 1], block, base); + block = check (buffer[i], block, 0); + } + } + + private int check (byte b, int block, int base) + { + b >>= base; + for (int i = base; i < 8; i++) + { + if ((b & 0x01) == 1) + { + parentDisk.setSectorFree (block, true); + ++freeSectors; + } + else + { + parentDisk.setSectorFree (block, false); + ++usedSectors; + } + block++; + b >>= 1; + } + return block; + } + + @Override + public String toString () + { + StringBuffer text = new StringBuffer (); + text.append ("DOS version : 3." + DOSVersion); + text.append ("\nVolume : " + volume); + text.append ("\nMax TS pairs : " + maxTSPairs); + text.append ("\nLast allocated T : " + lastAllocTrack); + text.append ("\nDirection : " + direction); + + return text.toString (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/AboutAction.java b/src/com/bytezone/diskbrowser/gui/AboutAction.java new file mode 100755 index 0000000..b81fa5b --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/AboutAction.java @@ -0,0 +1,60 @@ +package com.bytezone.diskbrowser.gui; + +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +import javax.swing.Action; +import javax.swing.JOptionPane; +import javax.swing.KeyStroke; + +import com.bytezone.common.DefaultAction; + +public class AboutAction extends DefaultAction +{ + public AboutAction () + { + super ("About...", "Display build information", "/com/bytezone/diskbrowser/icons/"); + putValue (Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke ("alt A")); + putValue (Action.MNEMONIC_KEY, KeyEvent.VK_A); + + setIcon (Action.SMALL_ICON, "information_16.png"); + setIcon (Action.LARGE_ICON_KEY, "information_32.png"); + } + + @Override + public void actionPerformed (ActionEvent e) + { + about (); + } + + public void about () + { + int build = 0; + String buildDate = ""; + Properties props = new Properties (); + InputStream in = this.getClass ().getResourceAsStream ("build.properties"); + if (in != null) + { + try + { + props.load (in); + in.close (); + build = Integer.parseInt (props.getProperty ("build.number")); + buildDate = props.getProperty ("build.date"); + } + catch (IOException e1) + { + System.out.println ("Properties file not found"); + } + } + + JOptionPane.showMessageDialog (null, + "Author - Denis Molony\nBuild #" + + String.format ("%d", build) + " - " + buildDate + + "\n" + "\nContact - dmolony@iinet.net.au", + "About DiskBrowser", JOptionPane.INFORMATION_MESSAGE); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/AbstractTab.java b/src/com/bytezone/diskbrowser/gui/AbstractTab.java new file mode 100755 index 0000000..3b1d9e1 --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/AbstractTab.java @@ -0,0 +1,160 @@ +package com.bytezone.diskbrowser.gui; + +/*********************************************************************************************** + * Parent class of FileSystemTab and AppleDiskTab. + * + * + ***********************************************************************************************/ + +import static javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER; +import static javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS; + +import java.awt.BorderLayout; +import java.awt.Cursor; +import java.awt.Font; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTree; +import javax.swing.tree.*; + +import com.bytezone.diskbrowser.gui.RedoHandler.RedoData; +import com.bytezone.diskbrowser.gui.TreeBuilder.FileNode; + +abstract class AbstractTab extends JPanel implements Tab +{ + private final static Cursor handCursor = new Cursor (Cursor.HAND_CURSOR); + private final List adapters = new ArrayList (); + private Font font; + private final JScrollPane scrollpane; + final DiskAndFileSelector eventHandler; + final RedoHandler navMan; + final RedoData redoData; + protected JTree tree; + + public AbstractTab (RedoHandler navMan, DiskAndFileSelector selector, Font font) + { + super (new BorderLayout ()); + this.eventHandler = selector; + this.font = font; + this.navMan = navMan; + this.redoData = navMan.createData (); + + scrollpane = new JScrollPane (null, VERTICAL_SCROLLBAR_ALWAYS, HORIZONTAL_SCROLLBAR_NEVER); + scrollpane.setBorder (null); + add (scrollpane, BorderLayout.CENTER); + } + + protected void setTree (JTree tree) + { + this.tree = tree; + tree.setFont (font); + scrollpane.setViewportView (tree); + TreeSelectionModel tsm = tree.getSelectionModel (); + tsm.setSelectionMode (TreeSelectionModel.SINGLE_TREE_SELECTION); + + if (adapters.size () > 0) + restoreAdapters (); + else + addTreeMouseListener (new CursorHandler ()); + } + + protected void setTreeFont (Font font) + { + tree.setFont (font); + this.font = font; + } + + public void addTreeMouseListener (MouseAdapter adapter) + { + tree.addMouseListener (adapter); + adapters.add (adapter); + } + + private void restoreAdapters () + { + for (MouseAdapter ma : adapters) + tree.addMouseListener (ma); + } + + protected Object getSelectedObject () + { + DefaultMutableTreeNode node = + (DefaultMutableTreeNode) tree.getLastSelectedPathComponent (); + return node == null ? null : node.getUserObject (); + } + + @Override + public DefaultMutableTreeNode getRootNode () + { + return (DefaultMutableTreeNode) tree.getModel ().getRoot (); + } + + protected DefaultMutableTreeNode findNode (int nodeNo) + { + DefaultMutableTreeNode rootNode = getRootNode (); + Enumeration children = rootNode.breadthFirstEnumeration (); + int count = 0; + DefaultMutableTreeNode selectNode = null; + while (children.hasMoreElements () && ++count <= nodeNo) + selectNode = children.nextElement (); + return selectNode; + } + + protected DefaultMutableTreeNode findFirstLeafNode () + { + DefaultMutableTreeNode rootNode = getRootNode (); + Enumeration children = rootNode.depthFirstEnumeration (); + DefaultMutableTreeNode selectNode = null; + while (children.hasMoreElements ()) + { + selectNode = children.nextElement (); + if (selectNode.isLeaf ()) + { + FileNode node = (FileNode) selectNode.getUserObject (); + if (node.file.isFile ()) + return selectNode; + } + } + return null; + } + + // Trigger the TreeSelectionListener set by the real Tab (if the value is different) + protected void showNode (DefaultMutableTreeNode showNode) + { + TreePath tp = getPathToNode (showNode); + tree.setSelectionPath (tp); + if (!tree.isVisible (tp)) + tree.scrollPathToVisible (tp); + } + + protected TreePath getPathToNode (DefaultMutableTreeNode selectNode) + { + DefaultTreeModel treeModel = (DefaultTreeModel) tree.getModel (); + TreeNode[] nodes = treeModel.getPathToRoot (selectNode); + return new TreePath (nodes); + } + + private class CursorHandler extends MouseAdapter + { + private Cursor oldCursor; + + @Override + public void mouseEntered (MouseEvent e) + { + oldCursor = getCursor (); + setCursor (handCursor); + } + + @Override + public void mouseExited (MouseEvent e) + { + setCursor (oldCursor); + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/AppleDiskTab.java b/src/com/bytezone/diskbrowser/gui/AppleDiskTab.java new file mode 100755 index 0000000..bf6093b --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/AppleDiskTab.java @@ -0,0 +1,163 @@ +package com.bytezone.diskbrowser.gui; + +/*********************************************************************************************** + * JPanel used to display a scrolling JTree containing details of a single disk. The JTree + * consists entirely of AppleFileSource objects. Any number of these objects are contained + * in Catalog Panel, along with a single FileSystemTab. + ***********************************************************************************************/ + +import java.awt.Font; +import java.util.Enumeration; + +import javax.swing.JTree; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; +import javax.swing.tree.DefaultMutableTreeNode; + +import com.bytezone.diskbrowser.applefile.AppleFileSource; +import com.bytezone.diskbrowser.disk.DiskFactory; +import com.bytezone.diskbrowser.disk.FormattedDisk; +import com.bytezone.diskbrowser.gui.RedoHandler.RedoEvent; + +class AppleDiskTab extends AbstractTab +{ + FormattedDisk disk; + + public AppleDiskTab (FormattedDisk disk, DiskAndFileSelector selector, RedoHandler navMan, + Font font, FileSelectedEvent event) + { + super (navMan, selector, font); + create (disk); + navMan.fileSelected (event); + // System.out.println ("restoring a previous disk with a file selected"); + } + + public AppleDiskTab (FormattedDisk disk, DiskAndFileSelector selector, RedoHandler navMan, + Font font, SectorSelectedEvent event) + { + super (navMan, selector, font); + create (disk); + navMan.sectorSelected (event); + // System.out.println ("restoring a previous disk with a sector selected"); + } + + // This constructor is only called when lastFileUsed is not null, but the disk + // couldn't find the file entry. Either the file has been deleted, or it is a disk + // with redefined files (Wizardry, Infocom etc). + public AppleDiskTab (FormattedDisk disk, DiskAndFileSelector selector, RedoHandler navMan, + Font font, String lastFileUsed) + { + super (navMan, selector, font); + create (disk); + // System.out.println ("ooh - couldn't find the previous file"); + DefaultMutableTreeNode node = findNode (lastFileUsed); + if (node != null) + { + AppleFileSource afs = (AppleFileSource) node.getUserObject (); + FileSelectedEvent event = new FileSelectedEvent (this, afs); + navMan.fileSelected (event); + } + } + + // User is selecting a new disk from the catalog + public AppleDiskTab (FormattedDisk disk, DiskAndFileSelector selector, RedoHandler navMan, + Font font) + { + super (navMan, selector, font); + create (disk); + + AppleFileSource afs = (AppleFileSource) findNode (2).getUserObject (); // select Catalog + if (afs == null) + afs = (AppleFileSource) findNode (1).getUserObject (); // select Disk + navMan.fileSelected (new FileSelectedEvent (this, afs)); + } + + private void create (FormattedDisk disk) + { + this.disk = disk; + setTree (disk.getCatalogTree ()); + setSelectionListener (tree); + } + + @Override + public void activate () + { + // System.out.println ("=========== Activating AppleDiskTab ============="); + eventHandler.redo = true; + eventHandler.fireDiskSelectionEvent (disk); + eventHandler.redo = false; + tree.setSelectionPath (null); // turn off any current selection to force an event + navMan.setCurrentData (redoData); + } + + @Override + public void refresh () // called when the user gives ALT-R command + { + Object o = getSelectedObject (); + String currentFile = (o == null) ? null : ((AppleFileSource) o).getUniqueName (); + disk = DiskFactory.createDisk (disk.getAbsolutePath ()); + setTree (disk.getCatalogTree ()); + setSelectionListener (tree); + selectNode (currentFile); + } + + private void selectNode (String nodeName) + { + DefaultMutableTreeNode selectNode = null; + if (nodeName != null) + selectNode = findNode (nodeName); + if (selectNode == null) + selectNode = findNode (2); + if (selectNode != null) + showNode (selectNode); + else + System.out.println ("First node not found"); + } + + void redoEvent (RedoEvent event) + { + selectNode (((FileSelectedEvent) event.value).file.getUniqueName ()); + } + + private DefaultMutableTreeNode findNode (String nodeName) + { + DefaultMutableTreeNode rootNode = getRootNode (); + Enumeration children = rootNode.breadthFirstEnumeration (); + while (children.hasMoreElements ()) + { + DefaultMutableTreeNode node = children.nextElement (); + Object o = node.getUserObject (); + if (o instanceof AppleFileSource) + { + AppleFileSource afs = (AppleFileSource) node.getUserObject (); + if (nodeName.equals (afs.getUniqueName ())) + return node; + } + } + return null; + } + + public boolean contains (FormattedDisk disk) + { + return this.disk.getAbsolutePath ().equals (disk.getAbsolutePath ()); + } + + // This action is triggered by AppleDiskTab.selectNode (String), which calls + // AbstractTab.showNode (DefaultMutableTreeNode). That will trigger this listener + // ONLY if the value is different, so it is set to null first to force the event. + private void setSelectionListener (JTree tree) + { + tree.addTreeSelectionListener (new TreeSelectionListener () + { + @Override + public void valueChanged (TreeSelectionEvent e) + { + // A null happens when there is a click in the DiskLayoutPanel, in order + // to turn off the currently selected file + AppleFileSource afs = (AppleFileSource) getSelectedObject (); + if (afs != null) + eventHandler.fireFileSelectionEvent (afs); + } + }); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/CatalogPanel.java b/src/com/bytezone/diskbrowser/gui/CatalogPanel.java new file mode 100755 index 0000000..fdd708d --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/CatalogPanel.java @@ -0,0 +1,448 @@ +package com.bytezone.diskbrowser.gui; + +/*********************************************************************************************** + * Contains a single instance of FileSystemTab, and any number of AppleDiskTab instances. + * + * + ***********************************************************************************************/ + +import java.awt.Dimension; +import java.awt.Font; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.io.File; +import java.util.ArrayList; +import java.util.EventObject; +import java.util.List; +import java.util.prefs.Preferences; + +import javax.swing.JTabbedPane; +import javax.swing.JTree; +import javax.swing.SwingConstants; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.TreePath; + +import com.bytezone.common.FontAction.FontChangeEvent; +import com.bytezone.common.FontAction.FontChangeListener; +import com.bytezone.common.QuitAction.QuitListener; +import com.bytezone.diskbrowser.applefile.AppleFileSource; +import com.bytezone.diskbrowser.catalog.DocumentCreatorFactory; +import com.bytezone.diskbrowser.disk.DualDosDisk; +import com.bytezone.diskbrowser.disk.FormattedDisk; +import com.bytezone.diskbrowser.gui.RedoHandler.RedoEvent; +import com.bytezone.diskbrowser.gui.RedoHandler.RedoListener; +import com.bytezone.diskbrowser.gui.TreeBuilder.FileNode; + +class CatalogPanel extends JTabbedPane implements RedoListener, SectorSelectionListener, + QuitListener, FontChangeListener +// PreferenceChangeListener +{ + private static final String prefsLastDiskUsed = "Last disk used"; + private static final String prefsLastDosUsed = "Last dos used"; + private static final String prefsLastFileUsed = "Last file used"; + private static final String prefsLastSectorsUsed = "Last sectors used"; + private static final String prefsRootDirectory = "Root directory"; + + private Font font; + private FileSystemTab fileTab; + private final List diskTabs = new ArrayList (); + private final DocumentCreatorFactory lister; + private final DiskAndFileSelector selector = new DiskAndFileSelector (); + private final RedoHandler navMan; + private DuplicateAction duplicateAction; // this sux + private CloseTabAction closeTabAction; + + public CatalogPanel (MenuHandler mh, RedoHandler navMan, Preferences prefs) + { + // String catalogFontName = + // prefs.get (PreferencesDialog.prefsCatalogFont, PreferencesDialog.defaultFontName); + // int catalogFontSize = + // prefs.getInt (PreferencesDialog.prefsCatalogFontSize, + // PreferencesDialog.defaultFontSize); + // this.font = new Font (catalogFontName, Font.PLAIN, catalogFontSize); + this.lister = new DocumentCreatorFactory (mh); + this.navMan = navMan; + + selector.addDiskSelectionListener (lister.diskLister); + + setTabPlacement (SwingConstants.BOTTOM); + setPreferredSize (new Dimension (360, 802)); // width, height + // setPreferredSize (new Dimension (360, 523)); // width, height + + createTabs (prefs); + addChangeListener (new TabChangeListener ()); + } + + private void createTabs (Preferences prefs) + { + String rootDirectory = prefs.get (prefsRootDirectory, ""); + + File rootDirectoryFile = new File (rootDirectory); + if (!rootDirectoryFile.exists () || !rootDirectoryFile.isDirectory ()) + { + System.out.println ("No root directory"); + return; + } + + String lastDiskUsed = prefs.get (prefsLastDiskUsed, ""); + int lastDosUsed = prefs.getInt (prefsLastDosUsed, -1); + String lastFileUsed = prefs.get (prefsLastFileUsed, ""); + String lastSectorsUsed = prefs.get (prefsLastSectorsUsed, ""); + + if (false) + { + System.out.println ("Last disk : " + lastDiskUsed); + System.out.println ("Last dos : " + lastDosUsed); + System.out.println ("Last file : " + lastFileUsed); + System.out.println ("Last sectors : " + lastSectorsUsed); + } + + DiskSelectedEvent diskEvent = null; + if (!lastDiskUsed.isEmpty ()) + { + diskEvent = DiskSelectedEvent.create (this, lastDiskUsed); + if (diskEvent != null) + { + FormattedDisk fd = diskEvent.getFormattedDisk (); + if (lastDosUsed >= 0 && fd instanceof DualDosDisk) + ((DualDosDisk) fd).setCurrentDiskNo (lastDosUsed); + } + } + else + System.out.println ("no disk selected"); + + fileTab = new FileSystemTab (rootDirectoryFile, selector, navMan, font, diskEvent); + fileTab.addTreeMouseListener (new MouseListener ()); // listen for disk selection + lister.catalogLister.setNode (fileTab.getRootNode ()); + insertTab ("Disk Tree", null, fileTab, "Display Apple disks", 0); + + if (diskEvent != null) + { + AppleDiskTab tab = null; + FormattedDisk fd = diskEvent.getFormattedDisk (); + + if (!lastFileUsed.isEmpty ()) + { + AppleFileSource afs = fd.getFile (lastFileUsed); + if (afs != null) + { + FileSelectedEvent fileEvent = FileSelectedEvent.create (this, afs); + tab = new AppleDiskTab (fd, selector, navMan, font, fileEvent); + } + else + tab = new AppleDiskTab (fd, selector, navMan, font, lastFileUsed); + } + else if (!lastSectorsUsed.isEmpty ()) + { + SectorSelectedEvent sectorEvent = + SectorSelectedEvent.create (this, fd, lastSectorsUsed); + tab = new AppleDiskTab (fd, selector, navMan, font, sectorEvent); + } + else + tab = new AppleDiskTab (fd, selector, navMan, font); + + if (tab != null) + { + diskTabs.add (tab); + add (tab, "D" + diskTabs.size ()); + } + else + System.out.println ("No disk tab created"); + } + } + + public void activate () + { + if (fileTab == null) + { + System.out.println ("No file tab"); + return; + } + + if (diskTabs.size () > 0) + setSelectedIndex (1); + else if (fileTab != null) + setSelectedIndex (0); + } + + void setDuplicateAction (DuplicateAction action) + { + this.duplicateAction = action; + if (fileTab != null && fileTab.rootFolder != null) + action.setDuplicates (fileTab.rootFolder, fileTab.duplicateDisks); + } + + void setCloseTabAction (CloseTabAction action) + { + this.closeTabAction = action; + } + + // called by RootDirectoryAction + public void changeRootPanel (File root) + { + // try + // { + // This might throw a NoDisksFoundException + FileSystemTab newFileTab = new FileSystemTab (root, selector, navMan, font); + + // is the user replacing an existing root folder? + if (fileTab != null) + removeTabAt (0); + + fileTab = newFileTab; + fileTab.addTreeMouseListener (new MouseListener ()); // listen for disk selection + lister.catalogLister.setNode (fileTab.getRootNode ()); + + insertTab ("Disk Tree", null, fileTab, null, 0); + setSelectedIndex (0); + duplicateAction.setDuplicates (fileTab.rootFolder, fileTab.duplicateDisks); + // } + // catch (NoDisksFoundException e) + // { + // JOptionPane.showMessageDialog (null, "Folder " + root.getAbsolutePath () + // + " has no valid disk images.", "Bad folder", JOptionPane.ERROR_MESSAGE); + // } + } + + // called after a double-click in the fileTab + public void addDiskPanel (FormattedDisk disk, String lastFileUsed, boolean activate) + { + int tabNo = 1; + for (AppleDiskTab tab : diskTabs) + { + if (tab.contains (disk)) + { + setSelectedIndex (tabNo); + return; + } + tabNo++; + } + + AppleDiskTab tab = new AppleDiskTab (disk, selector, navMan, font); + diskTabs.add (tab); + add (tab, "D" + diskTabs.size ()); + if (activate) + setSelectedIndex (diskTabs.size ()); + } + + // Called from RefreshTreeAction + public void refreshTree () + { + Tab tab = (Tab) getSelectedComponent (); + tab.refresh (); + + // Any newly created disk needs to appear in the FileSystemTab's tree + if (tab instanceof AppleDiskTab) + fileTab.replaceDisk (((AppleDiskTab) tab).disk); + } + + // Called from CloseTabAction + public void closeCurrentTab () + { + Tab tab = (Tab) getSelectedComponent (); + if (!(tab instanceof AppleDiskTab) || diskTabs.size () < 2) + return; + + int index = getSelectedIndex (); + remove (index); + diskTabs.remove (tab); + + for (int i = 1; i <= diskTabs.size (); i++) + setTitleAt (i, "D" + i); + + checkCloseTabAction (); + } + + private void checkCloseTabAction () + { + Tab tab = (Tab) getSelectedComponent (); + if (diskTabs.size () > 1 && tab instanceof AppleDiskTab) + closeTabAction.setEnabled (true); + else + closeTabAction.setEnabled (false); + } + + @Override + public void quit (Preferences prefs) + { + if (fileTab == null) + { + prefs.put (prefsRootDirectory, ""); + prefs.put (prefsLastDiskUsed, ""); + prefs.putInt (prefsLastDosUsed, -1); + prefs.put (prefsLastFileUsed, ""); + prefs.put (prefsLastSectorsUsed, ""); + } + else + { + prefs.put (prefsRootDirectory, fileTab.rootFolder.getAbsolutePath ()); + + if (diskTabs.size () == 0) + { + RedoEvent redoEvent = fileTab.redoData.getCurrentEvent (); + if (redoEvent != null) + { + DiskSelectedEvent event = (DiskSelectedEvent) redoEvent.value; + prefs.put (prefsLastDiskUsed, event.getFormattedDisk ().getAbsolutePath ()); + } + prefs.put (prefsLastFileUsed, ""); + prefs.put (prefsLastSectorsUsed, ""); + } + else + { + AbstractTab selectedTab = (AbstractTab) getSelectedComponent (); + if (selectedTab instanceof FileSystemTab) + selectedTab = diskTabs.get (diskTabs.size () - 1); + + FormattedDisk fd = ((AppleDiskTab) selectedTab).disk; + prefs.put (prefsLastDiskUsed, fd.getAbsolutePath ()); + if (fd instanceof DualDosDisk) + prefs.putInt (prefsLastDosUsed, ((DualDosDisk) fd).getCurrentDiskNo ()); + else + prefs.putInt (prefsLastDosUsed, -1); + + RedoEvent redoEvent = selectedTab.redoData.getCurrentEvent (); + if (redoEvent != null) + { + EventObject event = redoEvent.value; + + if (event instanceof FileSelectedEvent) + { + AppleFileSource afs = ((FileSelectedEvent) event).file; + prefs.put (prefsLastFileUsed, afs == null ? "" : afs.getUniqueName ()); + prefs.put (prefsLastSectorsUsed, ""); + } + else if (event instanceof SectorSelectedEvent) + { + prefs.put (prefsLastFileUsed, ""); + prefs.put (prefsLastSectorsUsed, ((SectorSelectedEvent) event).toText ()); + } + } + } + } + } + + // Pass through to DiskSelector + public void addDiskSelectionListener (DiskSelectionListener listener) + { + selector.addDiskSelectionListener (listener); + } + + // Pass through to DiskSelector + public void addFileSelectionListener (FileSelectionListener listener) + { + selector.addFileSelectionListener (listener); + } + + // Pass through to DiskSelector + public void addFileNodeSelectionListener (FileNodeSelectionListener listener) + { + selector.addFileNodeSelectionListener (listener); + } + + private class TabChangeListener implements ChangeListener + { + @Override + public void stateChanged (ChangeEvent e) + { + Tab tab = (Tab) getSelectedComponent (); + if (tab != null) + { + tab.activate (); + checkCloseTabAction (); + } + } + } + + private class MouseListener extends MouseAdapter + { + @Override + public void mousePressed (MouseEvent e) + { + JTree tree = (JTree) e.getSource (); + int selRow = tree.getRowForLocation (e.getX (), e.getY ()); + if (selRow < 0) + return; + + TreePath tp = tree.getPathForLocation (e.getX (), e.getY ()); + DefaultMutableTreeNode selectedNode = + (DefaultMutableTreeNode) tp.getLastPathComponent (); + FileNode node = (FileNode) selectedNode.getUserObject (); + if (node.file.isDirectory ()) + lister.catalogLister.setNode (selectedNode); + else if (e.getClickCount () == 2) + addDiskPanel (node.getFormattedDisk (), null, true); + } + } + + @Override + public void redo (RedoEvent event) + { + Tab tab = (Tab) getSelectedComponent (); + selector.redo = true; + + if (event.type.equals ("DiskEvent")) + { + if (tab instanceof FileSystemTab) + ((FileSystemTab) tab).redoEvent (event); + } + else if (event.type.equals ("FileEvent")) + { + if (tab instanceof AppleDiskTab) + ((AppleDiskTab) tab).redoEvent (event); + } + else if (event.type.equals ("FileNodeEvent")) + { + if (tab instanceof FileSystemTab) + ((FileSystemTab) tab).redoEvent (event); + } + else if (event.type.equals ("SectorEvent")) + { + // don't care + } + else + System.out.println ("Unknown event type : " + event.type); + + selector.redo = false; + } + + @Override + public void sectorSelected (SectorSelectedEvent event) + { + // user has clicked in the DiskLayoutPanel, so turn off any current file selection + Tab tab = (Tab) getSelectedComponent (); + if (tab instanceof AppleDiskTab) + ((AppleDiskTab) tab).tree.setSelectionPath (null); + } + + // @Override + // public void preferenceChange (PreferenceChangeEvent evt) + // { + // if (evt.getKey ().equals (PreferencesDialog.prefsCatalogFont)) + // font = new Font (evt.getNewValue (), Font.PLAIN, font.getSize ()); + // if (evt.getKey ().equals (PreferencesDialog.prefsCatalogFontSize)) + // font = new Font (font.getFontName (), + // Font.PLAIN, Integer.parseInt (evt.getNewValue ())); + // if (fileTab != null) + // fileTab.setTreeFont (font); + // for (AppleDiskTab tab : diskTabs) + // tab.setTreeFont (font); + // } + + @Override + public void restore (Preferences preferences) + { + } + + @Override + public void changeFont (FontChangeEvent fontChangeEvent) + { + font = fontChangeEvent.font; + if (fileTab != null) + fileTab.setTreeFont (font); + for (AppleDiskTab tab : diskTabs) + tab.setTreeFont (font); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/CloseTabAction.java b/src/com/bytezone/diskbrowser/gui/CloseTabAction.java new file mode 100644 index 0000000..896f42a --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/CloseTabAction.java @@ -0,0 +1,31 @@ +package com.bytezone.diskbrowser.gui; + +import java.awt.Toolkit; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.KeyStroke; + +public class CloseTabAction extends AbstractAction +{ + CatalogPanel catalogPanel; + + public CloseTabAction (CatalogPanel catalogPanel) + { + super ("Close Tab"); + putValue (Action.SHORT_DESCRIPTION, "Close the current disk tab"); + // putValue (Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke ("ctrl W")); + int mask = Toolkit.getDefaultToolkit ().getMenuShortcutKeyMask (); + putValue (Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke (KeyEvent.VK_W, mask)); + // putValue (Action.MNEMONIC_KEY, KeyEvent.VK_W); + this.catalogPanel = catalogPanel; + } + + @Override + public void actionPerformed (ActionEvent e) + { + catalogPanel.closeCurrentTab (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/CreateDatabaseAction.java b/src/com/bytezone/diskbrowser/gui/CreateDatabaseAction.java new file mode 100644 index 0000000..380f929 --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/CreateDatabaseAction.java @@ -0,0 +1,23 @@ +package com.bytezone.diskbrowser.gui; + +import java.awt.event.ActionEvent; + +import javax.swing.JOptionPane; + +import com.bytezone.common.DefaultAction; + +class CreateDatabaseAction extends DefaultAction +{ + public CreateDatabaseAction () + { + super ("Create Database", "Not working yet", null); + // putValue (Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke ("alt A")); + // putValue (Action.MNEMONIC_KEY, KeyEvent.VK_A); + } + + public void actionPerformed (ActionEvent e) + { + JOptionPane.showMessageDialog (null, "Coming soon...", "Database", + JOptionPane.INFORMATION_MESSAGE); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/DataPanel.java b/src/com/bytezone/diskbrowser/gui/DataPanel.java new file mode 100755 index 0000000..1745aff --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/DataPanel.java @@ -0,0 +1,335 @@ +package com.bytezone.diskbrowser.gui; + +import java.awt.*; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.util.List; +import java.util.prefs.Preferences; + +import javax.swing.*; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import com.bytezone.common.FontAction.FontChangeEvent; +import com.bytezone.common.FontAction.FontChangeListener; +import com.bytezone.diskbrowser.disk.DiskAddress; +import com.bytezone.diskbrowser.disk.SectorList; + +class DataPanel extends JTabbedPane implements DiskSelectionListener, FileSelectionListener, + SectorSelectionListener, + // PreferenceChangeListener, + FileNodeSelectionListener, FontChangeListener +{ + private static final int TEXT_WIDTH = 65; + + JTextArea hexText; + JTextArea disassemblyText; + + // these two panes are interchangeable + JScrollPane formattedPane; + JScrollPane imagePane; + + JTextArea formattedText; + ImagePanel imagePanel; // internal class + + boolean imageVisible = false; + + // used to determine whether the text has been set + boolean formattedTextValid; + boolean hexTextValid; + boolean assemblerTextValid; + DataSource currentDataSource; + + // private Font font; + final MenuHandler menuHandler; + + public DataPanel (MenuHandler mh, Preferences prefs) + { + // String dataFontName = + // prefs.get (PreferencesDialog.prefsDataFont, PreferencesDialog.defaultFontName); + // System.out.println (dataFontName); + // int dataFontSize = + // prefs.getInt (PreferencesDialog.prefsDataFontSize, PreferencesDialog.defaultFontSize); + // font = new Font (dataFontName, Font.PLAIN, dataFontSize); + + this.menuHandler = mh; + setTabPlacement (SwingConstants.BOTTOM); + + formattedText = new JTextArea (10, TEXT_WIDTH); + formattedPane = setPanel (formattedText, "Formatted"); + formattedText.setLineWrap (mh.lineWrapItem.isSelected ()); + formattedText + .setText ("Please use the 'File->Set root folder' command to " + + "\ntell DiskBrowser where your Apple disks are located." + + "\n\nTo see the contents of a disk in more detail, double-click" + + "\nthe disk. You will then be able to select individual files to view completely."); + + hexText = new JTextArea (10, TEXT_WIDTH); + setPanel (hexText, "Hex dump"); + + disassemblyText = new JTextArea (10, TEXT_WIDTH); + setPanel (disassemblyText, "Disassembly"); + + imagePanel = new ImagePanel (); + imagePane = + new JScrollPane (imagePanel, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, + ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + // imagePane.getVerticalScrollBar ().setUnitIncrement (font.getSize ()); + + // setTabsFont (font); + // this.setMinimumSize (new Dimension (800, 200)); + + addChangeListener (new ChangeListener () + { + @Override + public void stateChanged (ChangeEvent e) + { + switch (getSelectedIndex ()) + { + case 0: + if (!formattedTextValid) + { + if (currentDataSource == null) + formattedText.setText (""); + else + setText (formattedText, currentDataSource.getText ()); + formattedTextValid = true; + } + break; + case 1: + if (!hexTextValid) + { + if (currentDataSource == null) + hexText.setText (""); + else + setText (hexText, currentDataSource.getHexDump ()); + hexTextValid = true; + } + break; + case 2: + if (!assemblerTextValid) + { + if (currentDataSource == null) + disassemblyText.setText (""); + else + setText (disassemblyText, currentDataSource.getAssembler ()); + assemblerTextValid = true; + } + break; + default: + System.out.println ("Invalid index selected in DataPanel"); + } + } + }); + + mh.lineWrapItem.setAction (new LineWrapAction (formattedText)); + } + + private void setTabsFont (Font font) + { + formattedText.setFont (font); + hexText.setFont (font); + disassemblyText.setFont (font); + imagePane.getVerticalScrollBar ().setUnitIncrement (font.getSize ()); + } + + public String getCurrentText () + { + int index = getSelectedIndex (); + return index == 0 ? formattedText.getText () : index == 1 ? hexText.getText () + : disassemblyText.getText (); + } + + private JScrollPane setPanel (JTextArea outputPanel, String tabName) + { + outputPanel.setEditable (false); + outputPanel.setMargin (new Insets (5, 5, 5, 5)); + + JScrollPane outputScrollPane = + new JScrollPane (outputPanel, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, + ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); + outputScrollPane.setBorder (null); // remove the ugly default border + add (outputScrollPane, tabName); + return outputScrollPane; + } + + private void setDataSource (DataSource dataSource) + { + currentDataSource = dataSource; + if (dataSource == null) + { + formattedText.setText (""); + hexText.setText (""); + disassemblyText.setText (""); + checkImage (); + return; + } + + switch (getSelectedIndex ()) + { + case 0: + try + { + setText (formattedText, dataSource.getText ()); + } + catch (Exception e) + { + setText (formattedText, e.toString ()); + e.printStackTrace (); + } + hexTextValid = false; + assemblerTextValid = false; + break; + case 1: + setText (hexText, dataSource.getHexDump ()); + formattedTextValid = false; + assemblerTextValid = false; + break; + case 2: + setText (disassemblyText, dataSource.getAssembler ()); + hexTextValid = false; + formattedTextValid = false; + break; + default: + System.out.println ("Invalid index selected in DataPanel"); + } + + BufferedImage image = dataSource.getImage (); + if (image == null) + { + checkImage (); + } + else + { + imagePanel.setImage (image); + imagePane.setViewportView (imagePanel); + if (!imageVisible) + { + int selected = getSelectedIndex (); + remove (formattedPane); + add (imagePane, "Formatted", 0); + setSelectedIndex (selected); + imageVisible = true; + } + } + } + + private void checkImage () + { + if (imageVisible) + { + int selected = getSelectedIndex (); + remove (imagePane); + add (formattedPane, "Formatted", 0); + setSelectedIndex (selected); + imageVisible = false; + } + } + + private void setText (JTextArea textArea, String text) + { + textArea.setText (text); + textArea.setCaretPosition (0); + } + + private class ImagePanel extends JPanel + { + private BufferedImage image; + private int scale = 1; + + public ImagePanel () + { + this.setBackground (Color.gray); + } + + private void setImage (BufferedImage image) + { + this.image = image; + + if (image != null) + { + Graphics2D g2 = image.createGraphics (); + g2.setRenderingHint (RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + } + + int width = image.getWidth (); + int height = image.getHeight (); + + if (width < 400) + scale = (400 - 1) / width + 1; + if (scale > 4) + scale = 4; + + setPreferredSize (new Dimension (width * scale, height * scale)); + repaint (); + } + + @Override + public void paintComponent (Graphics g) + { + super.paintComponent (g); + + if (image != null) + { + Graphics2D g2 = ((Graphics2D) g); + g2.transform (AffineTransform.getScaleInstance (scale, scale)); + g2.drawImage (image, (getWidth () - image.getWidth () * scale) / 2 / scale, 4, this); + } + } + } + + @Override + public void diskSelected (DiskSelectedEvent event) + { + setSelectedIndex (0); + setDataSource (null); + if (event.getFormattedDisk () != null) + setDataSource (event.getFormattedDisk ().getCatalog ().getDataSource ()); + else + System.out.println ("bollocks in diskSelected()"); + } + + @Override + public void fileSelected (FileSelectedEvent event) + { + setDataSource (event.file.getDataSource ()); + } + + @Override + public void sectorSelected (SectorSelectedEvent event) + { + List sectors = event.getSectors (); + if (sectors == null || sectors.size () == 0) + return; + + if (sectors.size () == 1) + setDataSource (event.getFormattedDisk ().getFormattedSector (sectors.get (0))); + else + setDataSource (new SectorList (event.getFormattedDisk (), sectors)); + } + + // @Override + // public void preferenceChange (PreferenceChangeEvent evt) + // { + // if (evt.getKey ().equals (PreferencesDialog.prefsDataFont)) + // font = new Font (evt.getNewValue (), Font.PLAIN, font.getSize ()); + // if (evt.getKey ().equals (PreferencesDialog.prefsDataFontSize)) + // font = new Font (font.getFontName (), Font.PLAIN, Integer.parseInt (evt.getNewValue ())); + // setTabsFont (font); + // } + + @Override + public void fileNodeSelected (FileNodeSelectedEvent event) + { + setSelectedIndex (0); + setDataSource (event.getFileNode ()); + // FileNode node = event.getFileNode (); + } + + @Override + public void changeFont (FontChangeEvent fontChangeEvent) + { + setTabsFont (fontChangeEvent.font); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/DataSource.java b/src/com/bytezone/diskbrowser/gui/DataSource.java new file mode 100755 index 0000000..ff9ad7b --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/DataSource.java @@ -0,0 +1,18 @@ +package com.bytezone.diskbrowser.gui; + +import java.awt.image.BufferedImage; + +import javax.swing.JComponent; + +public interface DataSource +{ + public String getText (); + + public String getAssembler (); + + public String getHexDump (); + + public BufferedImage getImage (); + + public JComponent getComponent (); +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/DiskAndFileSelector.java b/src/com/bytezone/diskbrowser/gui/DiskAndFileSelector.java new file mode 100755 index 0000000..49a65a6 --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/DiskAndFileSelector.java @@ -0,0 +1,138 @@ +package com.bytezone.diskbrowser.gui; + +import javax.swing.JOptionPane; +import javax.swing.event.EventListenerList; + +import com.bytezone.diskbrowser.applefile.AppleFileSource; +import com.bytezone.diskbrowser.disk.FormattedDisk; +import com.bytezone.diskbrowser.gui.TreeBuilder.FileNode; + +class DiskAndFileSelector +{ + EventListenerList listenerList = new EventListenerList (); + FormattedDisk currentDisk; + boolean redo; + + /* + * Apple DiskSelection routines + */ + public void addDiskSelectionListener (DiskSelectionListener listener) + { + listenerList.add (DiskSelectionListener.class, listener); + } + + public void removeDiskSelectionListener (DiskSelectionListener listener) + { + listenerList.remove (DiskSelectionListener.class, listener); + } + + public void addFileNodeSelectionListener (FileNodeSelectionListener listener) + { + listenerList.add (FileNodeSelectionListener.class, listener); + } + + public void removeFileNodeSelectionListener (FileNodeSelectionListener listener) + { + listenerList.remove (FileNodeSelectionListener.class, listener); + } + + // public void fireDiskSelectionEvent (File file) + // { + // if (file.isDirectory ()) + // { + // System.out.println ("Directory received : " + file.getAbsolutePath ()); + // return; + // } + // + // if (currentDisk != null) // will this screw up the refresh command? + // { + // System.out.println (currentDisk.getDisk ().getFile ().getAbsolutePath ()); + // System.out.println (file.getAbsolutePath ()); + // } + // if (currentDisk != null + // && currentDisk.getDisk ().getFile ().getAbsolutePath ().equals (file.getAbsolutePath ())) + // fireDiskSelectionEvent (currentDisk); + // else + // { + // System.out.println (" creating disk from a file"); + // fireDiskSelectionEvent (DiskFactory.createDisk (file.getAbsolutePath ())); + // } + // } + + public void fireDiskSelectionEvent (FileNode node) + { + if (node.file.isDirectory ()) + { + fireFileNodeSelectionEvent (node); + currentDisk = null; + } + else + { + FormattedDisk fd = node.getFormattedDisk (); + if (fd == null) + JOptionPane.showMessageDialog (null, "Incorrect file format", "Format error", + JOptionPane.ERROR_MESSAGE); + else + fireDiskSelectionEvent (fd); + } + } + + public void fireFileNodeSelectionEvent (FileNode node) + { + FileNodeSelectedEvent e = new FileNodeSelectedEvent (this, node); + e.redo = redo; + FileNodeSelectionListener[] listeners = + (listenerList.getListeners (FileNodeSelectionListener.class)); + for (FileNodeSelectionListener listener : listeners) + listener.fileNodeSelected (e); + } + + public void fireDiskSelectionEvent (FormattedDisk disk) + { + if (disk == currentDisk) + { + System.out.println ("Disk event duplicated"); + return; + } + + if (disk == null) + { + System.out.println ("Null disk in fireDiskSelectionEvent()"); + return; + } + + DiskSelectedEvent e = new DiskSelectedEvent (this, disk); + e.redo = redo; + DiskSelectionListener[] listeners = + (listenerList.getListeners (DiskSelectionListener.class)); + for (DiskSelectionListener listener : listeners) + listener.diskSelected (e); + currentDisk = disk; + } + + /* + * Apple FileSelection routines + */ + + public void addFileSelectionListener (FileSelectionListener listener) + { + listenerList.add (FileSelectionListener.class, listener); + } + + public void removeFileSelectionListener (FileSelectionListener listener) + { + listenerList.remove (FileSelectionListener.class, listener); + } + + public void fireFileSelectionEvent (AppleFileSource file) + { + assert file != null; + currentDisk = null; + FileSelectedEvent e = new FileSelectedEvent (this, file); + e.redo = redo; + FileSelectionListener[] listeners = + (listenerList.getListeners (FileSelectionListener.class)); + for (FileSelectionListener listener : listeners) + listener.fileSelected (e); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/DiskBrowser.java b/src/com/bytezone/diskbrowser/gui/DiskBrowser.java new file mode 100755 index 0000000..4911745 --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/DiskBrowser.java @@ -0,0 +1,196 @@ +package com.bytezone.diskbrowser.gui; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.EventQueue; +import java.util.prefs.Preferences; + +import javax.swing.*; + +import com.bytezone.common.Platform; +import com.bytezone.common.QuitAction; +import com.bytezone.common.QuitAction.QuitListener; +import com.bytezone.common.State; + +public class DiskBrowser extends JFrame implements DiskSelectionListener, QuitListener +{ + private static final String windowTitle = "Apple ][ Disk Browser"; + private static final String PREFS_FULL_SCREEN = "full screen"; + Preferences prefs = Preferences.userNodeForPackage (this.getClass ()); + + public DiskBrowser () + { + super (windowTitle); + + State state = new State (prefs); + + if (false) + state.clear (); + + JToolBar toolBar = new JToolBar ("Toolbar", JToolBar.HORIZONTAL); + MenuHandler menuHandler = new MenuHandler (prefs); + + setJMenuBar (menuHandler.menuBar); + setLayout (new BorderLayout ()); + add (toolBar, BorderLayout.NORTH); + + RedoHandler redoHandler = new RedoHandler (getRootPane (), toolBar); // add nav buttons + toolBar.addSeparator (); + + // create and add the left-hand catalog panel + CatalogPanel catalogPanel = new CatalogPanel (menuHandler, redoHandler, prefs); + JPanel catalogBorderPanel = addPanel (catalogPanel, "Catalog", BorderLayout.WEST); + + // create and add the centre output panel + DataPanel dataPanel = new DataPanel (menuHandler, prefs); + addPanel (dataPanel, "Output", BorderLayout.CENTER); + + // create and add the right-hand disk layout panel + DiskLayoutPanel diskLayoutPanel = new DiskLayoutPanel (); + JPanel layoutBorderPanel = addPanel (diskLayoutPanel, "Disk layout", BorderLayout.EAST); + + // create actions + RootDirectoryAction rootDirectoryAction = new RootDirectoryAction (null, catalogPanel); + RefreshTreeAction refreshTreeAction = new RefreshTreeAction (catalogPanel); + // PreferencesAction preferencesAction = new PreferencesAction (this, prefs); + AbstractAction print = new PrintAction (dataPanel); + AboutAction aboutAction = new AboutAction (); + HideCatalogAction hideCatalogAction = new HideCatalogAction (this, catalogBorderPanel); + HideLayoutAction hideLayoutAction = new HideLayoutAction (this, layoutBorderPanel); + ShowFreeSectorsAction showFreeAction = + new ShowFreeSectorsAction (menuHandler, diskLayoutPanel); + DuplicateAction duplicateAction = new DuplicateAction (); + CloseTabAction closeTabAction = new CloseTabAction (catalogPanel); + + // add action buttons to toolbar + toolBar.add (rootDirectoryAction); + toolBar.add (refreshTreeAction); + // toolBar.add (preferencesAction); + toolBar.add (duplicateAction); + toolBar.add (print); + toolBar.add (aboutAction); + + // set the listeners + catalogPanel.addDiskSelectionListener (this); + catalogPanel.addDiskSelectionListener (dataPanel); + catalogPanel.addDiskSelectionListener (diskLayoutPanel); + catalogPanel.addDiskSelectionListener (redoHandler); + catalogPanel.addDiskSelectionListener (menuHandler); + + catalogPanel.addFileSelectionListener (dataPanel); + catalogPanel.addFileSelectionListener (diskLayoutPanel); + catalogPanel.addFileSelectionListener (redoHandler); + catalogPanel.addFileSelectionListener (menuHandler); + + catalogPanel.addFileNodeSelectionListener (dataPanel); + catalogPanel.addFileNodeSelectionListener (redoHandler); + + diskLayoutPanel.addSectorSelectionListener (dataPanel); + diskLayoutPanel.addSectorSelectionListener (redoHandler); + diskLayoutPanel.addSectorSelectionListener (catalogPanel); + + redoHandler.addRedoListener (catalogPanel); + redoHandler.addRedoListener (diskLayoutPanel); + + menuHandler.fontAction.addFontChangeListener (dataPanel); + menuHandler.fontAction.addFontChangeListener (catalogPanel); + menuHandler.fontAction.addFontChangeListener (diskLayoutPanel); + + // set the MenuItem Actions + menuHandler.printItem.setAction (print); + // menuHandler.addHelpMenuAction (preferencesAction, "prefs"); + menuHandler.addHelpMenuAction (aboutAction, "about"); + menuHandler.refreshTreeItem.setAction (refreshTreeAction); + menuHandler.rootItem.setAction (rootDirectoryAction); + menuHandler.showCatalogItem.setAction (hideCatalogAction); + menuHandler.showLayoutItem.setAction (hideLayoutAction); + menuHandler.showFreeSectorsItem.setAction (showFreeAction); + menuHandler.duplicateItem.setAction (duplicateAction); + menuHandler.closeTabItem.setAction (closeTabAction); + + final QuitAction quitAction = Platform.setQuit (this, prefs, menuHandler.fileMenu); + + quitAction.addQuitListener (menuHandler); + quitAction.addQuitListener (menuHandler.fontAction); + quitAction.addQuitListener (catalogPanel); + quitAction.addQuitListener (this); + + catalogPanel.setDuplicateAction (duplicateAction); + catalogPanel.setCloseTabAction (closeTabAction); + + pack (); + + // prefs.addPreferenceChangeListener (catalogPanel); + // prefs.addPreferenceChangeListener (dataPanel); + + // Remove the two optional panels if they were previously hidden + if (!menuHandler.showLayoutItem.isSelected ()) + hideLayoutAction.set (false); + if (!menuHandler.showCatalogItem.isSelected ()) + hideCatalogAction.set (false); + + // activate the highest panel now that the listeners are ready + catalogPanel.activate (); + + quitAction.restore (); + } + + private JPanel addPanel (JComponent pane, String title, String location) + { + JPanel panel = new JPanel (new BorderLayout ()); + panel.setBackground (Color.WHITE); + // panel.setOpaque (true); + panel.setBorder (BorderFactory.createTitledBorder (title)); + panel.add (pane); + add (panel, location); + return panel; + } + + @Override + public void diskSelected (DiskSelectedEvent e) + { + setTitle (windowTitle + e.getFormattedDisk () == null ? "" : e.getFormattedDisk () + .getName ()); + } + + public static void main (String[] args) + { + EventQueue.invokeLater (new Runnable () + { + @Override + public void run () + { + Platform.setLookAndFeel (); + new DiskBrowser ().setVisible (true); + } + }); + } + + @Override + public void quit (Preferences preferences) + { + prefs.putBoolean (PREFS_FULL_SCREEN, getExtendedState () == MAXIMIZED_BOTH); + } + + @Override + public void restore (Preferences preferences) + { + if (true) + { + setLocationRelativeTo (null); // centre + + // if we are on a smallish screen, just go fullscreen width + if (Platform.toolkit.getScreenSize ().width <= 1280) + setExtendedState (MAXIMIZED_HORIZ); + + // restore window if it was previously at full screen + if (prefs.getBoolean (PREFS_FULL_SCREEN, false)) + setExtendedState (MAXIMIZED_BOTH); + } + else + { + setLocation (10, 10); + setSize (1200, 812); + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/DiskDetails.java b/src/com/bytezone/diskbrowser/gui/DiskDetails.java new file mode 100644 index 0000000..2ee6bf4 --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/DiskDetails.java @@ -0,0 +1,41 @@ +package com.bytezone.diskbrowser.gui; + +import java.io.File; + +import com.bytezone.common.ComputeCRC32; + +class DiskDetails +{ + private final File file; + private long checksum = -1; + boolean duplicate; + + public DiskDetails (File file) + { + this.file = file; + duplicate = false; + } + + public String getAbsolutePath () + { + return file.getAbsolutePath (); + } + + public long getChecksum () + { + if (checksum < 0) + checksum = ComputeCRC32.getChecksumValue (file); + return checksum; + } + + public boolean delete () + { + return file.delete (); + } + + @Override + public String toString () + { + return String.format ("%s (%s)", file.getAbsolutePath (), duplicate ? "duplicate" : "OK"); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/DiskLayoutImage.java b/src/com/bytezone/diskbrowser/gui/DiskLayoutImage.java new file mode 100644 index 0000000..6244d6e --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/DiskLayoutImage.java @@ -0,0 +1,273 @@ +package com.bytezone.diskbrowser.gui; + +import java.awt.*; +import java.awt.event.InputEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.List; + +import javax.swing.JComponent; +import javax.swing.Scrollable; +import javax.swing.SwingConstants; + +import com.bytezone.diskbrowser.disk.Disk; +import com.bytezone.diskbrowser.disk.DiskAddress; +import com.bytezone.diskbrowser.disk.FormattedDisk; +import com.bytezone.diskbrowser.disk.SectorType; +import com.bytezone.diskbrowser.gui.DiskLayoutPanel.LayoutDetails; +import com.bytezone.diskbrowser.gui.RedoHandler.RedoEvent; + +class DiskLayoutImage extends JComponent implements Scrollable +{ + static final Cursor crosshairCursor = new Cursor (Cursor.CROSSHAIR_CURSOR); + FormattedDisk disk; + LayoutDetails layoutDetails; + private boolean showFreeSectors; + DiskLayoutSelection selectionHandler = new DiskLayoutSelection (); + boolean redo; + + // set defaults (used until a real disk is set) + int bw = 30; + int bh = 15; + int gw = 8; + int gh = 35; + + public DiskLayoutImage () + { + setPreferredSize (new Dimension (240 + 1, 525 + 1)); + addMouseListener (new MyMouseListener ()); + setBackground (Color.WHITE); + setOpaque (true); + } + + public void setDisk (FormattedDisk disk, LayoutDetails details) + { + this.disk = disk; + layoutDetails = details; + + // System.out.println (details); + // new Exception ().printStackTrace (); + + bw = layoutDetails.block.width; + bh = layoutDetails.block.height; + gw = layoutDetails.grid.width; + gh = layoutDetails.grid.height; + + setPreferredSize (new Dimension (gw * bw + 1, gh * bh + 1)); + selectionHandler.setSelection (null); + + repaint (); + } + + public void setShowFreeSectors (boolean showFree) + { + showFreeSectors = showFree; + repaint (); + } + + void setSelection (List sectors) + { + selectionHandler.setSelection (sectors); + if (sectors != null && sectors.size () > 0) + { + DiskAddress da = sectors.size () == 1 ? sectors.get (0) : sectors.get (1); + scrollRectToVisible (layoutDetails.getLocation (da)); + } + repaint (); + } + + @Override + protected void paintComponent (Graphics g) + { + super.paintComponent (g); + + // why doesn't linux do this? + g.setColor (Color.WHITE); + g.fillRect (0, 0, getWidth (), getHeight ()); + + if (disk == null) + return; + + Rectangle clipRect = g.getClipBounds (); + + Point p1 = new Point (clipRect.x / bw * bw, clipRect.y / bh * bh); + Point p2 = + new Point ((clipRect.x + clipRect.width - 1) / bw * bw, (clipRect.y + + clipRect.height - 1) + / bh * bh); + + // System.out.printf ("gw=%d, gh=%d, bw=%d, bh=%d%n", gw, gh, bw, bh); + // int totalBlocks = 0; + int maxBlock = gw * gh; + // System.out.printf ("Max blocks: %d%n", maxBlock); + Disk d = disk.getDisk (); + List selectedBlocks = selectionHandler.getHighlights (); + + // this stops an index error when using alt-5 to switch to 512-byte blocks + if (maxBlock > d.getTotalBlocks ()) + maxBlock = d.getTotalBlocks (); + + for (int y = p1.y; y <= p2.y; y += bh) + for (int x = p1.x; x <= p2.x; x += bw) + { + int blockNo = y / bh * gw + x / bw; + if (blockNo < maxBlock) + { + DiskAddress da = d.getDiskAddress (blockNo); + boolean flag = showFreeSectors && disk.isSectorFree (da); + boolean selected = selectedBlocks.contains (da); + drawBlock ((Graphics2D) g, blockNo, x, y, flag, selected); + // totalBlocks++; + } + } + } + + private void drawBlock (Graphics2D g, int blockNo, int x, int y, boolean flagFree, + boolean selected) + { + SectorType type = disk.getSectorType (blockNo); + int offset = (bw - 4) / 2 + 1; + + // Rectangle rect = new Rectangle (x, y, bw, bh); + Rectangle rect = new Rectangle (x, y, bw, bh); + // System.out.printf ("Rect: %4d %4d %4d %4d%n", x, y, bw, bh); + + // draw frame + if (true) // this needs to draw the outside rectangle, and show less white space + // between blocks + { + g.setColor (Color.GRAY); + g.drawRect (rect.x, rect.y, rect.width, rect.height); + } + + // draw coloured block + if (type.colour != Color.WHITE) + { + g.setColor (type.colour); + g.fillRect (rect.x + 2, rect.y + 2, rect.width - 3, rect.height - 3); + } + + // draw an indicator in free blocks + if (flagFree) + { + g.setColor (getContrastColor (type)); + g.drawOval (rect.x + offset - 2, rect.y + 4, 7, 7); + } + + // draw an indicator in selected blocks + if (selected) + { + g.setColor (getContrastColor (type)); + g.fillOval (rect.x + offset, rect.y + 6, 3, 3); + } + } + + private Color getContrastColor (SectorType type) + { + if (type.colour == Color.WHITE || type.colour == Color.YELLOW || type.colour == Color.PINK + || type.colour == Color.CYAN || type.colour == Color.ORANGE) + return Color.BLACK; + return Color.WHITE; + } + + @Override + public Dimension getPreferredScrollableViewportSize () + { + return new Dimension (240 + 1, 525 + 1); // floppy disk size + } + + @Override + public int + getScrollableUnitIncrement (Rectangle visibleRect, int orientation, int direction) + { + return orientation == SwingConstants.HORIZONTAL ? bw : bh; + } + + @Override + public int + getScrollableBlockIncrement (Rectangle visibleRect, int orientation, int direction) + { + return orientation == SwingConstants.HORIZONTAL ? bw * 4 : bh * 10; + } + + @Override + public boolean getScrollableTracksViewportHeight () + { + return false; + } + + @Override + public boolean getScrollableTracksViewportWidth () + { + return false; + } + + void redoEvent (RedoEvent redoEvent) + { + redo = true; + SectorSelectedEvent event = (SectorSelectedEvent) redoEvent.value; + setSelection (event.getSectors ()); + fireSectorSelectionEvent (event); + redo = false; + } + + private void fireSectorSelectionEvent () + { + SectorSelectedEvent event = + new SectorSelectedEvent (this, selectionHandler.getHighlights (), disk); + fireSectorSelectionEvent (event); + } + + private void fireSectorSelectionEvent (SectorSelectedEvent event) + { + event.redo = redo; + SectorSelectionListener[] listeners = + (listenerList.getListeners (SectorSelectionListener.class)); + for (SectorSelectionListener listener : listeners) + listener.sectorSelected (event); + } + + public void addSectorSelectionListener (SectorSelectionListener listener) + { + listenerList.add (SectorSelectionListener.class, listener); + } + + public void removeSectorSelectionListener (SectorSelectionListener listener) + { + listenerList.remove (SectorSelectionListener.class, listener); + } + + class MyMouseListener extends MouseAdapter + { + private Cursor currentCursor; + + @Override + public void mouseClicked (MouseEvent e) + { + int x = e.getX () / bw; + int y = e.getY () / bh; + int blockNo = y * gw + x; + DiskAddress da = disk.getDisk ().getDiskAddress (blockNo); + + boolean extend = ((e.getModifiersEx () & InputEvent.SHIFT_DOWN_MASK) > 0); + boolean append = ((e.getModifiersEx () & InputEvent.CTRL_DOWN_MASK) > 0); + + selectionHandler.doClick (disk.getDisk (), da, extend, append); + fireSectorSelectionEvent (); + repaint (); + } + + @Override + public void mouseEntered (MouseEvent e) + { + currentCursor = getCursor (); + setCursor (crosshairCursor); + } + + @Override + public void mouseExited (MouseEvent e) + { + setCursor (currentCursor); + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/DiskLayoutPanel.java b/src/com/bytezone/diskbrowser/gui/DiskLayoutPanel.java new file mode 100644 index 0000000..35a6bc9 --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/DiskLayoutPanel.java @@ -0,0 +1,245 @@ +package com.bytezone.diskbrowser.gui; + +import static javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS; +import static javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS; + +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +import javax.swing.JComponent; +import javax.swing.JPanel; +import javax.swing.JScrollPane; + +import com.bytezone.common.FontAction.FontChangeEvent; +import com.bytezone.common.FontAction.FontChangeListener; +import com.bytezone.diskbrowser.disk.Disk; +import com.bytezone.diskbrowser.disk.DiskAddress; +import com.bytezone.diskbrowser.disk.DualDosDisk; +import com.bytezone.diskbrowser.disk.FormattedDisk; +import com.bytezone.diskbrowser.gui.RedoHandler.RedoEvent; +import com.bytezone.diskbrowser.gui.RedoHandler.RedoListener; + +class DiskLayoutPanel extends JPanel implements DiskSelectionListener, FileSelectionListener, + RedoListener, FontChangeListener +{ + private static final int SIZE = 15; // basic unit of a display block + + private final DiskLayoutImage image; + private final ScrollRuler verticalRuler; + private final ScrollRuler horizontalRuler; + private final DiskLegendPanel legendPanel; + private final JScrollPane sp; + private LayoutDetails layout; + + public DiskLayoutPanel () + { + super (new BorderLayout ()); + + image = new DiskLayoutImage (); + verticalRuler = new ScrollRuler (image, ScrollRuler.VERTICAL); + horizontalRuler = new ScrollRuler (image, ScrollRuler.HORIZONTAL); + legendPanel = new DiskLegendPanel (); + + this.setBackground (Color.WHITE); + this.setOpaque (true); + + sp = new JScrollPane (image, VERTICAL_SCROLLBAR_ALWAYS, HORIZONTAL_SCROLLBAR_ALWAYS); + sp.setBackground (Color.WHITE); + sp.setOpaque (true); + sp.setColumnHeaderView (horizontalRuler); + sp.setRowHeaderView (verticalRuler); + sp.setBorder (null); + sp.setCorner (JScrollPane.UPPER_LEFT_CORNER, new Corner (true)); + sp.setCorner (JScrollPane.LOWER_LEFT_CORNER, new Corner (false)); + sp.setCorner (JScrollPane.UPPER_RIGHT_CORNER, new Corner (false)); + sp.setCorner (JScrollPane.LOWER_RIGHT_CORNER, new Corner (false)); + + // this is just so the pack is correct + add (sp, BorderLayout.CENTER); + add (legendPanel, BorderLayout.SOUTH); + } + + public DiskLayoutPanel (FormattedDisk disk) + { + this (); + setDisk (disk); + } + + public void setDisk (final FormattedDisk disk) + { + layout = new LayoutDetails (disk); + image.setDisk (disk, layout); + verticalRuler.setLayout (layout); + horizontalRuler.setLayout (layout); + legendPanel.setDisk (disk, layout); + sp.setViewportView (image); // this is the only way I know of to force a refresh + + setLayout (new BorderLayout ()); + if (disk.getGridLayout ().height == 35) + { + add (sp, BorderLayout.NORTH); + add (legendPanel, BorderLayout.CENTER); + } + else + { + add (sp, BorderLayout.CENTER); + add (legendPanel, BorderLayout.SOUTH); + } + + // Allow the disk to notify us if the interleave or blocksize is changed + disk.getDisk ().addActionListener (new ActionListener () + { + @Override + public void actionPerformed (ActionEvent e) + { + LayoutDetails layout = new LayoutDetails (disk); + image.setDisk (disk, layout); + + legendPanel.layoutDetails = layout; + legendPanel.repaint (); + + verticalRuler.setLayout (layout); + horizontalRuler.setLayout (layout); + } + }); + + repaint (); + } + + public void setHex (boolean hex) + { + verticalRuler.setHex (hex); + horizontalRuler.setHex (hex); + } + + public void setBlock (boolean block) + { + verticalRuler.setTrackMode (block); + horizontalRuler.setTrackMode (block); + } + + public void setFree (boolean free) + { + image.setShowFreeSectors (free); + } + + public void addSectorSelectionListener (SectorSelectionListener listener) + { + image.addSectorSelectionListener (listener); + } + + public void removeSectorSelectionListener (SectorSelectionListener listener) + { + image.removeSectorSelectionListener (listener); + } + + @Override + public void diskSelected (DiskSelectedEvent event) + { + setDisk (event.getFormattedDisk ()); + } + + @Override + public void fileSelected (FileSelectedEvent event) + { + // This can happen if a file is selected from a dual-dos disk + checkCorrectDisk (event.file.getFormattedDisk ()); + + // This may need to allow for sparse text files with null DiskAddresses + image.setSelection (event.file.getSectors ()); + } + + class LayoutDetails + { + Dimension block; + Dimension grid; + + public LayoutDetails (FormattedDisk formattedDisk) + { + Disk disk = formattedDisk.getDisk (); + block = new Dimension (disk.getBlockSize () == 256 ? SIZE : SIZE * 2, SIZE); + grid = formattedDisk.getGridLayout (); + } + + public Rectangle getLocation (DiskAddress da) + { + int y = da.getBlock () / grid.width; + int x = da.getBlock () % grid.width; + Rectangle r = + new Rectangle (x * block.width, y * block.height, block.width, block.height); + return r; + } + + @Override + public String toString () + { + StringBuilder text = new StringBuilder (); + text.append ("Block " + block + "\n"); + text.append ("Grid " + grid); + return text.toString (); + } + } + + class Corner extends JComponent + { + Color backgroundColor = Color.WHITE; + boolean showHex = true; + + public Corner (boolean click) + { + if (click) + addMouseListener (new MouseAdapter () + { + @Override + public void mouseClicked (MouseEvent e) + { + showHex = !showHex; + verticalRuler.setHex (showHex); + horizontalRuler.setHex (showHex); + } + }); + } + + @Override + protected void paintComponent (Graphics g) + { + g.setColor (backgroundColor); + g.fillRect (0, 0, getWidth (), getHeight ()); + } + } + + @Override + public void redo (RedoEvent event) + { + if (!event.type.equals ("SectorEvent")) + return; + + // This can happen if sectors are selected from a dual-dos disk + checkCorrectDisk (((SectorSelectedEvent) event.value).getFormattedDisk ()); + + image.redoEvent (event); + } + + private void checkCorrectDisk (FormattedDisk newDisk) + { + if (newDisk instanceof DualDosDisk) + newDisk = ((DualDosDisk) newDisk).getCurrentDisk (); // never set to a Dual-dos disk + if (newDisk != image.disk) + { + LayoutDetails layout = new LayoutDetails (newDisk); + image.setDisk (newDisk, layout); + legendPanel.setDisk (newDisk, layout); + } + } + + @Override + public void changeFont (FontChangeEvent e) + { + verticalRuler.changeFont (e.font); + horizontalRuler.changeFont (e.font); + legendPanel.changeFont (e.font); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/DiskLayoutSelection.java b/src/com/bytezone/diskbrowser/gui/DiskLayoutSelection.java new file mode 100755 index 0000000..5513ea6 --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/DiskLayoutSelection.java @@ -0,0 +1,125 @@ +package com.bytezone.diskbrowser.gui; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import com.bytezone.diskbrowser.disk.Disk; +import com.bytezone.diskbrowser.disk.DiskAddress; + +class DiskLayoutSelection implements Iterable +{ + private final List highlights; + + public DiskLayoutSelection () + { + highlights = new ArrayList (); + } + + public void doClick (Disk disk, DiskAddress da, boolean extend, boolean append) + { + /* + * Single click without modifiers - just replace previous highlights with the new + * sector. If there are no current highlights then even modifiers have the same + * effect. + */ + if ((!extend && !append) || highlights.size () == 0) + { + highlights.clear (); + highlights.add (da); + return; + } + + /* + * If the click was on an existing highlight, just remove it (regardless of modifiers) + */ + for (DiskAddress setDA : highlights) + if (da.compareTo (setDA) == 0) + { + highlights.remove (setDA); + return; + } + + /* + * Appending - just add the sector to the existing highlights + */ + if (append) + { + highlights.add (da); + Collections.sort (highlights); + return; + } + + /* + * Extending - if the existing selection is contiguous then just extend it. If not + * then things get a bit trickier. + */ + if (checkContiguous ()) + extendHighlights (disk, da); + else + adjustHighlights (disk, da); + + Collections.sort (highlights); + } + + public Iterator iterator () + { + return highlights.iterator (); + } + + // This must return a copy, or the redo function will get very confused + public List getHighlights () + { + return new ArrayList (highlights); + } + + public void setSelection (List list) + { + highlights.clear (); + if (list != null) + highlights.addAll (list); + } + + private boolean checkContiguous () + { + int range = + highlights.get (highlights.size () - 1).getBlock () - highlights.get (0).getBlock () + 1; + return (range == highlights.size ()); + } + + private void extendHighlights (Disk disk, DiskAddress da) + { + int lo, hi; + + // Are we extending in front of the current block? + if (highlights.get (0).getBlock () > da.getBlock ()) + { + lo = da.getBlock (); + hi = highlights.get (0).getBlock () - 1; + } + else + // No, must be extending at the end + { + lo = highlights.get (highlights.size () - 1).getBlock () + 1; + hi = da.getBlock (); + } + + for (int i = lo; i <= hi; i++) + highlights.add (disk.getDiskAddress (i)); + } + + private void adjustHighlights (Disk disk, DiskAddress da) + { + // If we are outside the discontiguous range, just extend as usual + if (da.getBlock () < highlights.get (0).getBlock () + || da.getBlock () > highlights.get (highlights.size () - 1).getBlock ()) + { + extendHighlights (disk, da); + return; + } + + // just treat it like a ctrl-click (hack!!) + highlights.add (da); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/DiskLegendPanel.java b/src/com/bytezone/diskbrowser/gui/DiskLegendPanel.java new file mode 100644 index 0000000..f0533be --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/DiskLegendPanel.java @@ -0,0 +1,96 @@ +package com.bytezone.diskbrowser.gui; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Graphics; + +import javax.swing.JPanel; + +import com.bytezone.common.Platform; +import com.bytezone.common.Platform.FontSize; +import com.bytezone.common.Platform.FontType; +import com.bytezone.diskbrowser.disk.FormattedDisk; +import com.bytezone.diskbrowser.disk.SectorType; +import com.bytezone.diskbrowser.gui.DiskLayoutPanel.LayoutDetails; + +class DiskLegendPanel extends JPanel +{ + static final int LEFT = 10; + static final int TOP = 10; + + FormattedDisk disk; + LayoutDetails layoutDetails; + Font font; + + public DiskLegendPanel () + { + // font = new Font ("SansSerif", Font.PLAIN, 12); + font = Platform.getFont (FontType.SANS_SERIF, FontSize.BASE); + setBackground (Color.WHITE); + } + + public void setDisk (FormattedDisk disk, LayoutDetails details) + { + this.disk = disk; + layoutDetails = details; + repaint (); + } + + void changeFont (Font font) + { + this.font = font; + repaint (); + } + + @Override + public Dimension getPreferredSize () + { + return new Dimension (0, 160); // width/height + } + + @Override + protected void paintComponent (Graphics g) + { + super.paintComponent (g); + + if (disk == null) + return; + + g.setFont (font); + + int count = 0; + int lineHeight = 20; + + for (SectorType type : disk.getSectorTypeList ()) + { + int x = LEFT + (count % 2 == 0 ? 0 : 145); + int y = TOP + count++ / 2 * lineHeight; + + // draw border + g.setColor (Color.GRAY); + g.drawRect (x, y, layoutDetails.block.width, layoutDetails.block.height); + + // draw the colour + g.setColor (type.colour); + g.fillRect (x + 2, y + 2, layoutDetails.block.width - 3, layoutDetails.block.height - 3); + + // draw the text + g.setColor (Color.BLACK); + g.drawString (type.name, x + layoutDetails.block.width + 4, y + 12); + } + + int y = ++count / 2 * lineHeight + TOP * 2 + 5; + int val = disk.falseNegativeBlocks (); + if (val > 0) + { + g.drawString (val + " empty sector" + (val == 1 ? "" : "s") + " marked as unavailable", + 10, y); + y += lineHeight; + } + val = disk.falsePositiveBlocks (); + if (val > 0) + g.drawString (val + " used sector" + (val == 1 ? "" : "s") + " marked as available", 10, + y); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/DiskSelectedEvent.java b/src/com/bytezone/diskbrowser/gui/DiskSelectedEvent.java new file mode 100755 index 0000000..8b2f2f7 --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/DiskSelectedEvent.java @@ -0,0 +1,40 @@ +package com.bytezone.diskbrowser.gui; + +import java.util.EventObject; + +import com.bytezone.diskbrowser.disk.DiskFactory; +import com.bytezone.diskbrowser.disk.FormattedDisk; + +public class DiskSelectedEvent extends EventObject +{ + private final FormattedDisk owner; + boolean redo; + + public DiskSelectedEvent (Object source, FormattedDisk disk) + { + super (source); + this.owner = disk; + } + + public FormattedDisk getFormattedDisk () + { + return owner; + } + + @Override + public String toString () + { + return owner.getDisk ().getFile ().getAbsolutePath (); + } + + public String toText () + { + return owner.getAbsolutePath (); + } + + public static DiskSelectedEvent create (Object source, String path) + { + FormattedDisk formattedDisk = DiskFactory.createDisk (path); + return formattedDisk == null ? null : new DiskSelectedEvent (source, formattedDisk); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/DiskSelectionListener.java b/src/com/bytezone/diskbrowser/gui/DiskSelectionListener.java new file mode 100755 index 0000000..7d59730 --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/DiskSelectionListener.java @@ -0,0 +1,8 @@ +package com.bytezone.diskbrowser.gui; + +import java.util.EventListener; + +public interface DiskSelectionListener extends EventListener +{ + public void diskSelected (DiskSelectedEvent event); +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/DuplicateAction.java b/src/com/bytezone/diskbrowser/gui/DuplicateAction.java new file mode 100644 index 0000000..582e866 --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/DuplicateAction.java @@ -0,0 +1,314 @@ +package com.bytezone.diskbrowser.gui; + +import static javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER; +import static javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import javax.swing.*; + +import com.bytezone.common.DefaultAction; +import com.bytezone.input.SpringUtilities; + +public class DuplicateAction extends DefaultAction +{ + Map> duplicateDisks; + int rootFolderLength; + File rootFolder; + DuplicateWindow window; + + public DuplicateAction () + { + super ("Check for duplicates...", "Check for duplicate disks", + "/com/bytezone/diskbrowser/icons/"); + + setIcon (Action.SMALL_ICON, "save_delete_16.png"); + setIcon (Action.LARGE_ICON_KEY, "save_delete_32.png"); + } + + public void setDuplicates (File rootFolder, Map> duplicateDisks) + { + this.duplicateDisks = duplicateDisks; + this.rootFolderLength = rootFolder.getAbsolutePath ().length (); + this.rootFolder = rootFolder; + setEnabled (duplicateDisks.size () > 0); + } + + @Override + public void actionPerformed (ActionEvent arg0) + { + if (duplicateDisks == null) + { + System.out.println ("No duplicate disks found"); + return; + } + + if (window != null) + { + window.setVisible (true); + return; + } + window = new DuplicateWindow (); + for (List diskList : duplicateDisks.values ()) + new DuplicateWorker (diskList, window).execute (); + } + + class DuplicateWindow extends JFrame + { + int unfinishedWorkers; + int folderNameLength; + + JButton buttonDelete = new JButton ("Delete selected"); + JButton buttonCancel = new JButton ("Cancel"); + JButton buttonAll = new JButton ("Select all duplicates"); + JButton buttonClear = new JButton ("Clear all"); + JPanel mainPanel = new JPanel (); + + List disksSelected = new ArrayList (); + List duplicatePanels = new ArrayList (); + + public DuplicateWindow () + { + super ("Duplicate Disk Detection - " + rootFolder.getAbsolutePath ()); + unfinishedWorkers = duplicateDisks.size (); + folderNameLength = rootFolder.getAbsolutePath ().length (); + + mainPanel.setLayout (new BoxLayout (mainPanel, BoxLayout.PAGE_AXIS)); + + JScrollPane sp = + new JScrollPane (mainPanel, VERTICAL_SCROLLBAR_ALWAYS, HORIZONTAL_SCROLLBAR_NEVER); + sp.getVerticalScrollBar ().setUnitIncrement (100); + add (sp, BorderLayout.CENTER); + + JPanel panel = new JPanel (); + panel.add (buttonClear); + panel.add (buttonAll); + panel.add (buttonDelete); + panel.add (buttonCancel); + add (panel, BorderLayout.SOUTH); + + buttonClear.setEnabled (false); + buttonAll.setEnabled (false); + buttonDelete.setEnabled (false); + buttonCancel.setEnabled (false); + + buttonAll.addActionListener (new ActionListener () + { + @Override + public void actionPerformed (ActionEvent e) + { + for (DuplicatePanel dp : duplicatePanels) + { + int count = 0; + for (JCheckBox cb : dp.checkBoxes) + { + if (count > 0 && dp.duplicateDisks.get (count).duplicate) + if (!cb.isSelected ()) + { + cb.setSelected (true); // doesn't fire the actionListener! + disksSelected.add (dp.duplicateDisks.get (count)); + } + ++count; + } + } + buttonDelete.setEnabled (disksSelected.size () > 0); + buttonClear.setEnabled (disksSelected.size () > 0); + } + }); + + buttonClear.addActionListener (new ActionListener () + { + @Override + public void actionPerformed (ActionEvent e) + { + for (DuplicatePanel dp : duplicatePanels) + for (JCheckBox cb : dp.checkBoxes) + cb.setSelected (false); // doesn't fire the actionListener! + disksSelected.clear (); + buttonDelete.setEnabled (false); + buttonClear.setEnabled (false); + } + }); + + buttonCancel.addActionListener (new ActionListener () + { + @Override + public void actionPerformed (ActionEvent e) + { + DuplicateWindow.this.setVisible (false); + } + }); + + buttonDelete.addActionListener (new ActionListener () + { + @Override + public void actionPerformed (ActionEvent e) + { + int totalDeleted = 0; + int totalFailed = 0; + for (DuplicatePanel dp : duplicatePanels) + { + int count = 0; + for (JCheckBox cb : dp.checkBoxes) + { + if (cb.isSelected () && false) + { + DiskDetails dd = dp.duplicateDisks.get (count); + if (dd.delete ()) + { + ++totalDeleted; + System.out.println ("Deleted : " + dd); + } + else + { + ++totalFailed; + System.out.println ("Failed : " + dd); + } + } + ++count; + } + } + System.out.printf ("Deleted : %d, Failed : %d%n", totalDeleted, totalFailed); + } + }); + + setSize (600, 700); + setLocationRelativeTo (null); + setDefaultCloseOperation (HIDE_ON_CLOSE); + setVisible (true); + } + + // create a DuplicatePanel based on the updated DiskDetails + public synchronized void addResult (List duplicateDisks) + { + // create panel and add it to the window + DuplicatePanel dp = + new DuplicatePanel (duplicateDisks, folderNameLength, disksSelected, buttonDelete, + buttonClear); + mainPanel.add (dp); + duplicatePanels.add (dp); + + validate (); + + if (--unfinishedWorkers == 0) + { + buttonAll.setEnabled (true); + buttonCancel.setEnabled (true); + } + else + mainPanel.add (Box.createRigidArea (new Dimension (0, 20))); + } + } + + class DuplicatePanel extends JPanel + { + List checkBoxes = new ArrayList (); + List duplicateDisks; + + public DuplicatePanel (List duplicateDisks, int folderNameLength, + List disksSelected, JButton deleteButton, JButton clearButton) + { + this.duplicateDisks = duplicateDisks; + setLayout (new SpringLayout ()); + setAlignmentX (LEFT_ALIGNMENT); + + int count = 0; + for (DiskDetails dd : duplicateDisks) + { + JCheckBox cb = new JCheckBox (); + checkBoxes.add (cb); + + cb.addActionListener (new CheckBoxActionListener (dd, disksSelected, deleteButton, + clearButton)); + add (cb); + if (++count == 1) + add (new JLabel ("Source disk")); + else + { + String text = dd.duplicate ? "Duplicate" : "OK"; + add (new JLabel (text)); + } + String checksum = + dd.duplicate || count == 1 ? "" : " (checksum = " + dd.getChecksum () + ")"; + add (new JLabel (dd.getAbsolutePath ().substring (folderNameLength) + checksum)); + } + SpringUtilities.makeCompactGrid (this, duplicateDisks.size (), 3, //rows, cols + 10, 0, //initX, initY + 10, 0); //xPad, yPad + } + } + + class CheckBoxActionListener implements ActionListener + { + DiskDetails diskDetails; + List disksSelected; + JButton deleteButton; + JButton clearButton; + + public CheckBoxActionListener (DiskDetails diskDetails, List disksSelected, + JButton deleteButton, JButton clearButton) + { + this.diskDetails = diskDetails; + this.disksSelected = disksSelected; + this.deleteButton = deleteButton; + this.clearButton = clearButton; + } + + @Override + public void actionPerformed (ActionEvent e) + { + if (((JCheckBox) e.getSource ()).isSelected ()) + disksSelected.add (diskDetails); + else + disksSelected.remove (diskDetails); + deleteButton.setEnabled (disksSelected.size () > 0); + clearButton.setEnabled (disksSelected.size () > 0); + } + } + + class DuplicateWorker extends SwingWorker, Void> + { + List duplicateDisks; + DuplicateWindow owner; + + public DuplicateWorker (List duplicateDisks, DuplicateWindow owner) + { + this.duplicateDisks = duplicateDisks; + this.owner = owner; + } + + @Override + protected void done () + { + try + { + owner.addResult (get ()); + } + catch (Exception e) + { + e.printStackTrace (); + } + } + + @Override + protected List doInBackground () throws Exception + { + long firstChecksum = -1; + for (DiskDetails dd : duplicateDisks) + { + if (firstChecksum < 0) + firstChecksum = dd.getChecksum (); + else + dd.duplicate = (dd.getChecksum () == firstChecksum); + } + return duplicateDisks; + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/ExecuteDiskAction.java b/src/com/bytezone/diskbrowser/gui/ExecuteDiskAction.java new file mode 100755 index 0000000..2c490ee --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/ExecuteDiskAction.java @@ -0,0 +1,38 @@ +package com.bytezone.diskbrowser.gui; + +import java.awt.Desktop; +import java.awt.event.ActionEvent; +import java.io.IOException; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.JOptionPane; +import javax.swing.KeyStroke; + +class ExecuteDiskAction extends AbstractAction +{ + // should replace this by making the action a listener + MenuHandler owner; + + public ExecuteDiskAction (MenuHandler owner) + { + super ("Run current disk"); + putValue (Action.SHORT_DESCRIPTION, "Same as double-clicking on the disk"); + putValue (Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke ("alt X")); + this.owner = owner; + } + + public void actionPerformed (ActionEvent e) + { + try + { + Desktop.getDesktop ().open (owner.currentDisk.getDisk ().getFile ()); + } + catch (IOException e1) + { + e1.printStackTrace (); + JOptionPane.showMessageDialog (null, "Error opening disk : " + + owner.currentDisk.getDisk ().getFile (), "Bugger", JOptionPane.INFORMATION_MESSAGE); + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/FileNodeSelectedEvent.java b/src/com/bytezone/diskbrowser/gui/FileNodeSelectedEvent.java new file mode 100644 index 0000000..d8e3c4a --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/FileNodeSelectedEvent.java @@ -0,0 +1,33 @@ +package com.bytezone.diskbrowser.gui; + +import java.util.EventObject; + +import com.bytezone.diskbrowser.gui.TreeBuilder.FileNode; + +public class FileNodeSelectedEvent extends EventObject +{ + private final FileNode node; + boolean redo; + + public FileNodeSelectedEvent (Object source, FileNode node) + { + super (source); + this.node = node; + } + + public FileNode getFileNode () + { + return node; + } + + @Override + public String toString () + { + return node.file.getAbsolutePath (); + } + + public String toText () + { + return node.file.getAbsolutePath (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/FileNodeSelectionListener.java b/src/com/bytezone/diskbrowser/gui/FileNodeSelectionListener.java new file mode 100644 index 0000000..47167dd --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/FileNodeSelectionListener.java @@ -0,0 +1,8 @@ +package com.bytezone.diskbrowser.gui; + +import java.util.EventListener; + +public interface FileNodeSelectionListener extends EventListener +{ + public void fileNodeSelected (FileNodeSelectedEvent event); +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/FileSelectedEvent.java b/src/com/bytezone/diskbrowser/gui/FileSelectedEvent.java new file mode 100755 index 0000000..f186374 --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/FileSelectedEvent.java @@ -0,0 +1,40 @@ +package com.bytezone.diskbrowser.gui; + +import java.util.EventObject; + +import com.bytezone.diskbrowser.applefile.AppleFileSource; +import com.bytezone.diskbrowser.disk.DualDosDisk; + +public class FileSelectedEvent extends EventObject +{ + public final AppleFileSource file; + boolean redo; + + public FileSelectedEvent (Object source, AppleFileSource file) + { + super (source); + this.file = file; + + // If a file is selected from a disk which is contained in a Dual-dos disk, then the DDS + // must be told so that it can ensure its internal currentDisk is set correctly + DualDosDisk ddd = (DualDosDisk) file.getFormattedDisk ().getParent (); + if (ddd != null) + ddd.setCurrentDisk (file); + } + + @Override + public String toString () + { + return file.getUniqueName (); + } + + public String toText () + { + return file.getUniqueName (); + } + + public static FileSelectedEvent create (Object source, AppleFileSource afs) + { + return new FileSelectedEvent (source, afs); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/FileSelectionListener.java b/src/com/bytezone/diskbrowser/gui/FileSelectionListener.java new file mode 100755 index 0000000..a47dcd9 --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/FileSelectionListener.java @@ -0,0 +1,8 @@ +package com.bytezone.diskbrowser.gui; + +import java.util.EventListener; + +public interface FileSelectionListener extends EventListener +{ + public void fileSelected (FileSelectedEvent event); +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/FileSystemTab.java b/src/com/bytezone/diskbrowser/gui/FileSystemTab.java new file mode 100755 index 0000000..ea88fbd --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/FileSystemTab.java @@ -0,0 +1,241 @@ +package com.bytezone.diskbrowser.gui; + +/*********************************************************************************************** + * JPanel which displays a scrolling JTree containing details of all disks in the user's + * root directory. The JTree consists entirely of FileNode objects (which are simply + * wrappers for File objects). There will always be exactly one instance contained in + * Catalog Panel, along with any number of AppleDiskTab instances. + ***********************************************************************************************/ + +import java.awt.Font; +import java.io.File; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; + +import javax.swing.JTree; +import javax.swing.event.TreeExpansionEvent; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; +import javax.swing.event.TreeWillExpandListener; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.ExpandVetoException; +import javax.swing.tree.TreePath; + +import com.bytezone.diskbrowser.disk.DiskFactory; +import com.bytezone.diskbrowser.disk.FormattedDisk; +import com.bytezone.diskbrowser.gui.RedoHandler.RedoEvent; +import com.bytezone.diskbrowser.gui.TreeBuilder.FileNode; + +class FileSystemTab extends AbstractTab +{ + File rootFolder; + Map> duplicateDisks; + + public FileSystemTab (File folder, DiskAndFileSelector selector, RedoHandler navMan, + Font font, DiskSelectedEvent diskEvent) // throws NoDisksFoundException + { + super (navMan, selector, font); + this.rootFolder = folder; + + TreeBuilder tb = new TreeBuilder (folder); + // if (tb.totalDisks == 0) + // throw new NoDisksFoundException (); + + duplicateDisks = tb.duplicateDisks; + setTree (tb.tree); + setSelectionListener (tree); + + if (diskEvent == null) + { + DefaultMutableTreeNode node = findFirstLeafNode (); + if (node != null) + { + FileNode fn = (FileNode) node.getUserObject (); + diskEvent = new DiskSelectedEvent (this, DiskFactory.createDisk (fn.file)); + } + } + + if (diskEvent != null) + navMan.diskSelected (diskEvent); + else + System.out.println ("No disk event"); + + // temporary code while I sort out the DOS checksum feature + if (tb.dosMap.keySet ().size () > 0) + { + System.out.printf ("Unique DOSs : %4d%n", tb.dosMap.keySet ().size ()); + long lastKey = -1; + int beginIndex = rootFolder.getAbsolutePath ().length (); + for (Long key : tb.dosMap.keySet ()) + { + if (key != lastKey) + { + lastKey = key; + System.out.printf ("%,14d (%d)%n", key, tb.dosMap.get (key).size ()); + } + for (File file : tb.dosMap.get (key)) + System.out.printf (" %s%n", + file.getAbsolutePath ().substring (beginIndex)); + } + } + } + + public FileSystemTab (File folder, DiskAndFileSelector selector, RedoHandler navMan, + Font font) + // throws NoDisksFoundException + { + this (folder, selector, navMan, font, null); // default to first available disk + } + + @Override + public void activate () + { + tree.setSelectionPath (null); // turn off any current selection to force an event + navMan.setCurrentData (redoData); + } + + // connected to RefreshTreeAction + @Override + public void refresh () + { + String currentDiskName = ((FileNode) getSelectedObject ()).file.getAbsolutePath (); + TreeBuilder tb = new TreeBuilder (rootFolder); + setTree (tb.tree); + if (currentDiskName != null) + showNode (findNode (currentDiskName)); + setSelectionListener (tree); + } + + void redoEvent (RedoEvent event) + { + DefaultMutableTreeNode node = null; + if (event.type.equals ("FileNodeEvent")) + { + FileNode fn = ((FileNodeSelectedEvent) event.value).getFileNode (); + node = fn.parentNode; + } + else + { + FormattedDisk disk = ((DiskSelectedEvent) event.value).getFormattedDisk (); + node = findNode (disk.getAbsolutePath ()); + } + if (node == null) + node = findNode (2); + if (node != null) + showNode (node); + else + System.out.println ("Disk node not found"); + } + + private DefaultMutableTreeNode findNode (String absolutePath) + { + DefaultMutableTreeNode rootNode = getRootNode (); + + if (true) + return search (rootNode, absolutePath); + + // old code + Enumeration children = rootNode.breadthFirstEnumeration (); + while (children.hasMoreElements ()) + { + DefaultMutableTreeNode node = children.nextElement (); + FileNode fn = (FileNode) node.getUserObject (); + System.out.println ("Comparing : " + fn.file.getAbsolutePath ()); + + if (absolutePath.startsWith (fn.file.getAbsolutePath ())) + { + System.out.println ("promising"); + fn.readFiles (); + } + + if (fn.file.getAbsolutePath ().equals (absolutePath)) + return node; + } + System.out.println ("Node not found : " + absolutePath); + return null; + } + + private DefaultMutableTreeNode search (DefaultMutableTreeNode node, String absolutePath) + { + FileNode fn = (FileNode) node.getUserObject (); + + int children = node.getChildCount (); + if (children == 0) + { + fn.readFiles (); + children = node.getChildCount (); + } + + for (int i = 0; i < children; i++) + { + DefaultMutableTreeNode childNode = (DefaultMutableTreeNode) node.getChildAt (i); + FileNode fn2 = (FileNode) childNode.getUserObject (); + + String path = fn2.file.getAbsolutePath (); + if (absolutePath.equals (path)) + return childNode; + + if (fn2.file.isDirectory () && absolutePath.startsWith (path) + && absolutePath.charAt (path.length ()) == '/') + { + DefaultMutableTreeNode node2 = search (childNode, absolutePath); + if (node2 != null) + return node2; + } + } + + return null; + } + + public void replaceDisk (FormattedDisk disk) + { + // first check currently selected disk + FileNode fn = (FileNode) getSelectedObject (); + if (fn != null && fn.replaceDisk (disk)) + return; + + // find the old disk and replace it + DefaultMutableTreeNode rootNode = getRootNode (); + Enumeration children = rootNode.breadthFirstEnumeration (); + while (children.hasMoreElements ()) + { + DefaultMutableTreeNode node = children.nextElement (); + fn = (FileNode) node.getUserObject (); + if (fn.replaceDisk (disk)) + break; + } + } + + private void setSelectionListener (JTree tree) + { + tree.addTreeSelectionListener (new TreeSelectionListener () + { + @Override + public void valueChanged (TreeSelectionEvent e) + { + FileNode fn = (FileNode) getSelectedObject (); + if (fn != null) + eventHandler.fireDiskSelectionEvent (fn); + } + }); + + tree.addTreeWillExpandListener (new TreeWillExpandListener () + { + @Override + public void treeWillCollapse (TreeExpansionEvent e) throws ExpandVetoException + { + } + + @Override + public void treeWillExpand (TreeExpansionEvent e) throws ExpandVetoException + { + TreePath path = e.getPath (); + DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent (); + FileNode fn = (FileNode) node.getUserObject (); + if (node.getChildCount () == 0) + fn.readFiles (); + } + }); + } +} diff --git a/src/com/bytezone/diskbrowser/gui/HideCatalogAction.java b/src/com/bytezone/diskbrowser/gui/HideCatalogAction.java new file mode 100755 index 0000000..e599b29 --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/HideCatalogAction.java @@ -0,0 +1,47 @@ +package com.bytezone.diskbrowser.gui; + +import java.awt.BorderLayout; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.JFrame; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.KeyStroke; + +class HideCatalogAction extends AbstractAction +{ + JFrame owner; + JPanel catalogPanel; + + public HideCatalogAction (JFrame owner, JPanel catalogPanel) + { + super ("Show catalog panel"); + putValue (Action.SHORT_DESCRIPTION, "Show/hide the catalog panel"); + putValue (Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke ("alt C")); + putValue (Action.MNEMONIC_KEY, KeyEvent.VK_C); + this.owner = owner; + this.catalogPanel = catalogPanel; + } + + public void actionPerformed (ActionEvent e) + { + set (((JMenuItem) e.getSource ()).isSelected ()); + } + + public void set (boolean show) + { + if (show) + { + owner.add (catalogPanel, BorderLayout.WEST); + owner.validate (); + } + else + { + owner.remove (catalogPanel); + owner.validate (); + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/HideLayoutAction.java b/src/com/bytezone/diskbrowser/gui/HideLayoutAction.java new file mode 100755 index 0000000..ab175fc --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/HideLayoutAction.java @@ -0,0 +1,47 @@ +package com.bytezone.diskbrowser.gui; + +import java.awt.BorderLayout; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.JFrame; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.KeyStroke; + +class HideLayoutAction extends AbstractAction +{ + JFrame owner; + JPanel layoutPanel; + + public HideLayoutAction (JFrame owner, JPanel layoutPanel) + { + super ("Show disk layout panel"); + putValue (Action.SHORT_DESCRIPTION, "Show/hide the disk layout panel"); + putValue (Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke ("alt D")); + putValue (Action.MNEMONIC_KEY, KeyEvent.VK_D); + this.owner = owner; + this.layoutPanel = layoutPanel; + } + + public void actionPerformed (ActionEvent e) + { + set (((JMenuItem) e.getSource ()).isSelected ()); + } + + public void set (boolean show) + { + if (show) + { + owner.add (layoutPanel, BorderLayout.EAST); + owner.validate (); + } + else + { + owner.remove (layoutPanel); + owner.validate (); + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/InterleaveAction.java b/src/com/bytezone/diskbrowser/gui/InterleaveAction.java new file mode 100755 index 0000000..7622402 --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/InterleaveAction.java @@ -0,0 +1,34 @@ +package com.bytezone.diskbrowser.gui; + +import java.awt.event.ActionEvent; + +import javax.swing.Action; +import javax.swing.KeyStroke; + +import com.bytezone.common.DefaultAction; +import com.bytezone.diskbrowser.disk.FormattedDisk; + +public class InterleaveAction extends DefaultAction +{ + int interleave; + FormattedDisk currentDisk; + static String[] names = { "DOS", "Prodos", "Infocom", "CPM" }; + + public InterleaveAction (int interleave) + { + super (names[interleave] + " interleave", "Alter interleave"); + putValue (Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke ("alt " + interleave)); + this.interleave = interleave; + } + + public void setDisk (FormattedDisk disk) + { + currentDisk = disk; + } + + @Override + public void actionPerformed (ActionEvent e) + { + currentDisk.getDisk ().setInterleave (interleave); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/LineWrapAction.java b/src/com/bytezone/diskbrowser/gui/LineWrapAction.java new file mode 100755 index 0000000..141ed92 --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/LineWrapAction.java @@ -0,0 +1,29 @@ +package com.bytezone.diskbrowser.gui; + +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.JMenuItem; +import javax.swing.JTextArea; +import javax.swing.KeyStroke; + +class LineWrapAction extends AbstractAction +{ + JTextArea owner; + + public LineWrapAction (JTextArea owner) + { + super ("Line wrap"); + putValue (Action.SHORT_DESCRIPTION, "Print the contents of the output panel"); + putValue (Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke ("alt W")); + putValue (Action.MNEMONIC_KEY, KeyEvent.VK_W); + this.owner = owner; + } + + public void actionPerformed (ActionEvent e) + { + owner.setLineWrap (((JMenuItem) e.getSource ()).isSelected ()); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/MenuHandler.java b/src/com/bytezone/diskbrowser/gui/MenuHandler.java new file mode 100755 index 0000000..aed2e04 --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/MenuHandler.java @@ -0,0 +1,253 @@ +package com.bytezone.diskbrowser.gui; + +import java.awt.Desktop; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.prefs.Preferences; + +import javax.swing.*; + +import com.bytezone.common.*; +import com.bytezone.common.QuitAction.QuitListener; +import com.bytezone.diskbrowser.disk.DataDisk; +import com.bytezone.diskbrowser.disk.FormattedDisk; + +public class MenuHandler implements DiskSelectionListener, FileSelectionListener, QuitListener +{ + private static final String PREFS_LINE_WRAP = "line wrap"; + private static final String PREFS_SHOW_CATALOG = "show catalog"; + private static final String PREFS_SHOW_LAYOUT = "show layout"; + private static final String PREFS_SHOW_FREE_SECTORS = "show free sectors"; + + FormattedDisk currentDisk; + + JMenuBar menuBar = new JMenuBar (); + JMenu fileMenu = new JMenu ("File"); + JMenu formatMenu = new JMenu ("Format"); + JMenu helpMenu = new JMenu ("Help"); + + // File menu items + JMenuItem openItem = new JMenuItem ("Open disk..."); + JMenuItem rootItem = new JMenuItem ("Set root folder..."); + JMenuItem refreshTreeItem = new JMenuItem ("Refresh current tree"); + JMenuItem executeDiskItem; + JMenuItem printItem = new JMenuItem ("Print output panel..."); + public final JMenuItem createCatalogFileItem = new JMenuItem ("Create catalog file..."); + public final JMenuItem createDiskFileItem = new JMenuItem ("Create disk file..."); + JMenuItem dbItem = new JMenuItem (new CreateDatabaseAction ()); + JMenuItem closeTabItem = new JMenuItem (); + JMenuItem duplicateItem = new JMenuItem (); + FontAction fontAction; + + // Format menu items + JMenuItem lineWrapItem = new JCheckBoxMenuItem ("Line wrap"); + JMenuItem showLayoutItem = new JCheckBoxMenuItem ("Show layout panel"); + JMenuItem showCatalogItem = new JCheckBoxMenuItem ("Show catalog panel"); + JMenuItem showFreeSectorsItem = new JCheckBoxMenuItem ("Show free sectors"); + + JMenuItem sector256Item = new JRadioButtonMenuItem ("256 byte sectors"); + JMenuItem sector512Item = new JRadioButtonMenuItem ("512 byte blocks"); + JMenuItem interleave0Item = new JRadioButtonMenuItem (new InterleaveAction (0)); + JMenuItem interleave1Item = new JRadioButtonMenuItem (new InterleaveAction (1)); + JMenuItem interleave2Item = new JRadioButtonMenuItem (new InterleaveAction (2)); + JMenuItem interleave3Item = new JRadioButtonMenuItem (new InterleaveAction (3)); + + public MenuHandler (Preferences prefs) + { + menuBar.add (fileMenu); + menuBar.add (formatMenu); + menuBar.add (helpMenu); + + if (false) + fileMenu.add (openItem); + + fileMenu.add (rootItem); + fileMenu.addSeparator (); + fileMenu.add (refreshTreeItem); + addLauncherMenu (); + fileMenu.add (printItem); + fileMenu.addSeparator (); + fileMenu.add (closeTabItem); + + fontAction = new FontAction (); + JMenuItem fontItem = new JMenuItem (fontAction); + fileMenu.add (fontItem); + fontAction.setSampleText ("120 FOR Z = 14 TO 24:\n" + " VTAB 5:\n" + " HTAB Z:\n" + + " PRINT AB$:\n" + " FOR TI = 1 TO 50:\n" + " NEXT :\n" + " POKE 0,Z + 40:\n" + + " POKE 1,9:\n" + " CALL MU:\n" + " VTAB 5:\n" + " HTAB Z:\n" + + " PRINT SPC(12):\n" + "NEXT :\n" + "VTAB 5:\n" + "HTAB 24:\n" + "PRINT AB$\n"); + + if (false) + { + fileMenu.add (createCatalogFileItem); + fileMenu.add (createDiskFileItem); + fileMenu.add (dbItem); + } + fileMenu.add (duplicateItem); + + formatMenu.add (lineWrapItem); + formatMenu.add (showCatalogItem); + formatMenu.add (showLayoutItem); + formatMenu.add (showFreeSectorsItem); + formatMenu.addSeparator (); + formatMenu.add (interleave0Item); + formatMenu.add (interleave1Item); + formatMenu.add (interleave2Item); + formatMenu.add (interleave3Item); + formatMenu.addSeparator (); + formatMenu.add (sector256Item); + formatMenu.add (sector512Item); + + helpMenu.add (new JMenuItem (new EnvironmentAction ())); + + sector256Item.setActionCommand ("256"); + sector256Item.setAccelerator (KeyStroke.getKeyStroke ("alt 4")); + sector512Item.setActionCommand ("512"); + sector512Item.setAccelerator (KeyStroke.getKeyStroke ("alt 5")); + + lineWrapItem.setAccelerator (KeyStroke.getKeyStroke ("alt W")); + printItem.setAccelerator (KeyStroke.getKeyStroke ("control P")); + + // ButtonGroup dosGroup = new ButtonGroup (); + ButtonGroup sectorGroup = new ButtonGroup (); + ButtonGroup interleaveGroup = new ButtonGroup (); + + sectorGroup.add (sector256Item); + sectorGroup.add (sector512Item); + interleaveGroup.add (interleave0Item); + interleaveGroup.add (interleave1Item); + interleaveGroup.add (interleave2Item); + interleaveGroup.add (interleave3Item); + + dbItem.setEnabled (false); + + // preferences + lineWrapItem.setSelected (prefs.getBoolean (PREFS_LINE_WRAP, true)); + showLayoutItem.setSelected (prefs.getBoolean (PREFS_SHOW_LAYOUT, true)); + showCatalogItem.setSelected (prefs.getBoolean (PREFS_SHOW_CATALOG, true)); + showFreeSectorsItem.setSelected (prefs.getBoolean (PREFS_SHOW_FREE_SECTORS, false)); + } + + void addHelpMenuAction (Action action, String functionName) + { + if (Platform.MAC) + { + try + { + if (functionName.equals ("about")) + OSXAdapter.setAboutHandler (action, + action.getClass ().getDeclaredMethod (functionName, + (Class[]) null)); + else if (functionName.equals ("prefs")) + OSXAdapter.setPreferencesHandler (action, + action.getClass () + .getDeclaredMethod (functionName, + (Class[]) null)); + } + catch (Exception e) + { + e.printStackTrace (); + } + } + else + { + helpMenu.add (new JMenuItem (action)); + } + } + + private void addLauncherMenu () + { + if (!Desktop.isDesktopSupported ()) + return; + + boolean openSupported = false; + for (Desktop.Action action : Desktop.Action.values ()) + if (action.toString ().equals ("OPEN")) + { + openSupported = true; + break; + } + if (!openSupported) + return; + + executeDiskItem = new JMenuItem (new ExecuteDiskAction (this)); + fileMenu.add (executeDiskItem); + fileMenu.addSeparator (); + } + + @Override + public void quit (Preferences prefs) + { + prefs.putBoolean (PREFS_LINE_WRAP, lineWrapItem.isSelected ()); + prefs.putBoolean (PREFS_SHOW_LAYOUT, showLayoutItem.isSelected ()); + prefs.putBoolean (PREFS_SHOW_CATALOG, showCatalogItem.isSelected ()); + prefs.putBoolean (PREFS_SHOW_FREE_SECTORS, showFreeSectorsItem.isSelected ()); + } + + @Override + public void diskSelected (DiskSelectedEvent event) + { + currentDisk = event.getFormattedDisk (); + adjustMenus (currentDisk); + } + + @Override + public void fileSelected (FileSelectedEvent event) + { + // This can happen if a file is selected from a dual-dos disk + if (event.file.getFormattedDisk () != currentDisk) + { + currentDisk = event.file.getFormattedDisk (); + adjustMenus (currentDisk); + } + } + + private void adjustMenus (final FormattedDisk disk) + { + if (disk != null) + { + sector256Item.setSelected (disk.getDisk ().getBlockSize () == 256); + sector512Item.setSelected (disk.getDisk ().getBlockSize () == 512); + interleave0Item.setSelected (disk.getDisk ().getInterleave () == 0); + interleave1Item.setSelected (disk.getDisk ().getInterleave () == 1); + interleave2Item.setSelected (disk.getDisk ().getInterleave () == 2); + interleave3Item.setSelected (disk.getDisk ().getInterleave () == 3); + } + + boolean isDataDisk = (disk instanceof DataDisk); + + sector256Item.setEnabled (isDataDisk); + sector512Item.setEnabled (isDataDisk); + interleave0Item.setEnabled (isDataDisk); + interleave1Item.setEnabled (isDataDisk); + interleave2Item.setEnabled (isDataDisk); + interleave3Item.setEnabled (isDataDisk); + + if (isDataDisk) + { + // make this an action too + ActionListener sectorListener = new ActionListener () + { + @Override + public void actionPerformed (ActionEvent e) + { + int size = Integer.parseInt (e.getActionCommand ()); + disk.getDisk ().setBlockSize (size); + } + }; + + sector256Item.addActionListener (sectorListener); + sector512Item.addActionListener (sectorListener); + + ((InterleaveAction) interleave0Item.getAction ()).setDisk (currentDisk); + ((InterleaveAction) interleave1Item.getAction ()).setDisk (currentDisk); + ((InterleaveAction) interleave2Item.getAction ()).setDisk (currentDisk); + ((InterleaveAction) interleave3Item.getAction ()).setDisk (currentDisk); + } + } + + @Override + public void restore (Preferences preferences) + { + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/NoDisksFoundException.java b/src/com/bytezone/diskbrowser/gui/NoDisksFoundException.java new file mode 100644 index 0000000..50b156b --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/NoDisksFoundException.java @@ -0,0 +1,6 @@ +package com.bytezone.diskbrowser.gui; + +class NoDisksFoundException extends Exception +{ + +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/OpenFileAction.java b/src/com/bytezone/diskbrowser/gui/OpenFileAction.java new file mode 100755 index 0000000..83c4cfb --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/OpenFileAction.java @@ -0,0 +1,48 @@ +package com.bytezone.diskbrowser.gui; + +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.io.File; + +import javax.swing.Action; +import javax.swing.JFileChooser; +import javax.swing.KeyStroke; +import javax.swing.filechooser.FileNameExtensionFilter; + +import com.bytezone.common.DefaultAction; +import com.bytezone.diskbrowser.disk.DiskFactory; + +// I don't think this is needed anymore +class OpenFileAction extends DefaultAction +{ + // DiskBrowser owner; + CatalogPanel catalogPanel; + + public OpenFileAction (DiskBrowser owner, CatalogPanel catalogPanel) + { + super ("Open disk...", "Opens a single disk image", "/com/bytezone/diskbrowser/icons/"); + putValue (Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke ("control O")); + putValue (Action.MNEMONIC_KEY, KeyEvent.VK_O); + // this.owner = owner; + this.catalogPanel = catalogPanel; + + setIcon (Action.SMALL_ICON, "Open16.gif"); + } + + public void actionPerformed (ActionEvent e) + { + JFileChooser chooser = new JFileChooser ("C:/"); + chooser.setDialogTitle ("Select disk image"); + FileNameExtensionFilter filter = new FileNameExtensionFilter ("DSK & PO Images", "dsk", "po"); + chooser.setFileFilter (filter); + // if (owner.selectedDisk != null) + // chooser.setSelectedFile (owner.selectedDisk.getDisk ().getFile ()); + int result = chooser.showOpenDialog (null); + if (result == JFileChooser.APPROVE_OPTION) + { + File file = chooser.getSelectedFile (); + if (file != null) + catalogPanel.addDiskPanel (DiskFactory.createDisk (file.getAbsolutePath ()), null, true); + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/PreferencesAction.java b/src/com/bytezone/diskbrowser/gui/PreferencesAction.java new file mode 100644 index 0000000..394d923 --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/PreferencesAction.java @@ -0,0 +1,39 @@ +package com.bytezone.diskbrowser.gui; + +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.util.prefs.Preferences; + +import javax.swing.Action; +import javax.swing.JFrame; +import javax.swing.KeyStroke; + +import com.bytezone.common.DefaultAction; + +public class PreferencesAction extends DefaultAction +{ + JFrame owner; + Preferences prefs; + + public PreferencesAction (JFrame owner, Preferences prefs) + { + super ("Preferences...", "Set preferences", "/com/bytezone/diskbrowser/icons/"); + putValue (Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke ("alt P")); + putValue (Action.MNEMONIC_KEY, KeyEvent.VK_P); + + setIcon (Action.LARGE_ICON_KEY, "script_gear_32.png"); + this.owner = owner; + this.prefs = prefs; + } + + @Override + public void actionPerformed (ActionEvent e) + { + prefs (); + } + + public void prefs () + { + new PreferencesDialog (owner, prefs); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/PreferencesDialog.java b/src/com/bytezone/diskbrowser/gui/PreferencesDialog.java new file mode 100644 index 0000000..00f8848 --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/PreferencesDialog.java @@ -0,0 +1,203 @@ +package com.bytezone.diskbrowser.gui; + +import java.awt.BorderLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.util.prefs.Preferences; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; + +import com.bytezone.common.FontTester; +import com.bytezone.input.SpringUtilities; + +class PreferencesDialog extends JDialog +{ + static final String prefsCatalogFont = "CatalogFont"; + static final String prefsDataFont = "DataFont"; + static final String prefsCatalogFontSize = "CatalogFontSize"; + static final String prefsDataFontSize = "DataFontSize"; + + static final String defaultFontName = "Lucida Sans Typewriter"; + static final int defaultFontSize = 12; + static final String[] monoFonts = new FontTester ().getMonospacedFontList (); + + private final JComboBox catalogFontList = new JComboBox (monoFonts); + private final JComboBox dataFontList = new JComboBox (monoFonts); + private final String[] sizes = { "8", "9", "10", "11", "12", "13", "14", "15", "16" }; + private final JComboBox catalogFontSizes = new JComboBox (sizes); + private final JComboBox dataFontSizes = new JComboBox (sizes); + private final Preferences prefs; + + private final JButton apply = new JButton ("Apply"); + + private String catalogFontName; + private String dataFontName; + private int catalogFontSize; + private int dataFontSize; + + public PreferencesDialog (JFrame owner, Preferences prefs) + { + super (owner, "Set Preferences", false); + + this.prefs = prefs; + + catalogFontName = prefs.get (prefsCatalogFont, defaultFontName); + dataFontName = prefs.get (prefsDataFont, defaultFontName); + catalogFontSize = prefs.getInt (prefsCatalogFontSize, defaultFontSize); + dataFontSize = prefs.getInt (prefsDataFontSize, defaultFontSize); + + catalogFontList.setSelectedItem (catalogFontName); + dataFontList.setSelectedItem (dataFontName); + catalogFontSizes.setSelectedItem (catalogFontSize + ""); + dataFontSizes.setSelectedItem (dataFontSize + ""); + + catalogFontList.setMaximumRowCount (30); + dataFontList.setMaximumRowCount (30); + catalogFontSizes.setMaximumRowCount (sizes.length); + dataFontSizes.setMaximumRowCount (sizes.length); + + Listener listener = new Listener (); + catalogFontList.addActionListener (listener); + dataFontList.addActionListener (listener); + catalogFontSizes.addActionListener (listener); + dataFontSizes.addActionListener (listener); + + setDefaultCloseOperation (DISPOSE_ON_CLOSE); + setResizable (false); + addCancelByEscapeKey (); // doesn't seem to work + + JPanel layoutPanel = new JPanel (); + layoutPanel.setBorder (new EmptyBorder (10, 20, 0, 20)); // T/L/B/R + layoutPanel.setLayout (new SpringLayout ()); + + layoutPanel.add (new JLabel ("Catalog panel font", JLabel.TRAILING)); + layoutPanel.add (catalogFontList); + layoutPanel.add (catalogFontSizes); + + layoutPanel.add (new JLabel ("Output panel font", JLabel.TRAILING)); + layoutPanel.add (dataFontList); + layoutPanel.add (dataFontSizes); + + SpringUtilities.makeCompactGrid (layoutPanel, 2, 3, //rows, cols + 10, 5, //initX, initY + 10, 5); //xPad, yPad + + JPanel panel = new JPanel (new BorderLayout ()); + panel.add (layoutPanel, BorderLayout.CENTER); + panel.add (getCommandPanel (), BorderLayout.SOUTH); + getContentPane ().add (panel); + + pack (); + setLocationRelativeTo (owner); + setVisible (true); + } + + private JComponent getCommandPanel () + { + JButton cancel = new JButton ("Cancel"); + cancel.addActionListener (new ActionListener () + { + @Override + public void actionPerformed (ActionEvent event) + { + closeDialog (); + } + }); + + apply.setEnabled (false); + apply.addActionListener (new ActionListener () + { + @Override + public void actionPerformed (ActionEvent event) + { + updatePreferences (); + apply.setEnabled (false); + } + }); + + JButton ok = new JButton ("OK"); + getRootPane ().setDefaultButton (ok); + ok.addActionListener (new ActionListener () + { + @Override + public void actionPerformed (ActionEvent event) + { + updatePreferences (); + closeDialog (); + } + }); + + JPanel commandPanel = new JPanel (); + commandPanel.add (cancel); + commandPanel.add (apply); + commandPanel.add (ok); + + return commandPanel; + } + + private void updatePreferences () + { + String newFontName = (String) catalogFontList.getSelectedItem (); + if (!newFontName.equals (catalogFontName)) + { + prefs.put (prefsCatalogFont, newFontName); + catalogFontName = newFontName; + } + + newFontName = (String) dataFontList.getSelectedItem (); + if (!newFontName.equals (dataFontName)) + { + prefs.put (prefsDataFont, newFontName); + dataFontName = newFontName; + } + + int newFontSize = Integer.parseInt ((String) catalogFontSizes.getSelectedItem ()); + if (newFontSize != catalogFontSize) + { + prefs.putInt (prefsCatalogFontSize, newFontSize); + catalogFontSize = newFontSize; + } + + newFontSize = Integer.parseInt ((String) dataFontSizes.getSelectedItem ()); + if (newFontSize != dataFontSize) + { + prefs.putInt (prefsDataFontSize, newFontSize); + dataFontSize = newFontSize; + } + } + + private void addCancelByEscapeKey () + { + String CANCEL_ACTION_KEY = "CANCEL_ACTION_KEY"; + int noModifiers = 0; + KeyStroke escapeKey = KeyStroke.getKeyStroke (KeyEvent.VK_ESCAPE, noModifiers, false); + InputMap inputMap = + getRootPane ().getInputMap (JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); + inputMap.put (escapeKey, CANCEL_ACTION_KEY); + AbstractAction cancelAction = new AbstractAction () + { + @Override + public void actionPerformed (ActionEvent e) + { + closeDialog (); + } + }; + getRootPane ().getActionMap ().put (CANCEL_ACTION_KEY, cancelAction); + } + + private void closeDialog () + { + dispose (); + } + + class Listener implements ActionListener + { + @Override + public void actionPerformed (ActionEvent e) + { + apply.setEnabled (true); + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/PrintAction.java b/src/com/bytezone/diskbrowser/gui/PrintAction.java new file mode 100755 index 0000000..4a036f4 --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/PrintAction.java @@ -0,0 +1,52 @@ +package com.bytezone.diskbrowser.gui; + +import java.awt.EventQueue; +import java.awt.Toolkit; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.awt.print.PrinterException; +import java.awt.print.PrinterJob; + +import javax.swing.Action; +import javax.swing.KeyStroke; + +import com.bytezone.common.DefaultAction; + +class PrintAction extends DefaultAction +{ + DataPanel owner; + + public PrintAction (DataPanel owner) + { + super ("Print...", "Print the contents of the output panel", "/com/bytezone/diskbrowser/icons/"); + int mask = Toolkit.getDefaultToolkit ().getMenuShortcutKeyMask (); + putValue (Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke (KeyEvent.VK_P, mask)); + putValue (Action.MNEMONIC_KEY, KeyEvent.VK_P); + this.owner = owner; + + setIcon (Action.SMALL_ICON, "printer_16.png"); + setIcon (Action.LARGE_ICON_KEY, "printer_32.png"); + } + + public void actionPerformed (ActionEvent e) + { + Runnable runner = new Runnable () + { + public void run () + { + try + { + PrinterJob job = PrinterJob.getPrinterJob (); + job.setPrintable (new PrintDocument (owner.getCurrentText ())); + if (job.printDialog ()) + job.print (); + } + catch (PrinterException e) + { + System.out.println ("printer error"); + } + } + }; + EventQueue.invokeLater (runner); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/PrintDocument.java b/src/com/bytezone/diskbrowser/gui/PrintDocument.java new file mode 100755 index 0000000..5179c37 --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/PrintDocument.java @@ -0,0 +1,129 @@ +package com.bytezone.diskbrowser.gui; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Component; +import java.awt.Font; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.font.LineMetrics; +import java.awt.print.PageFormat; +import java.awt.print.Printable; +import java.util.Enumeration; +import java.util.Vector; + +class PrintDocument extends Component implements Printable +{ + String lines[]; + int lineHeight; + int pages; + Font font = new Font ("Lucida Sans Typewriter", Font.PLAIN, 7); + int linesPerPage; + int x = 50; + int y = 20; + + public PrintDocument (String text) + { + lines = wrapText (text, 112); + } + + public int print (Graphics g, PageFormat pageFormat, int page) + { + Graphics2D g2 = (Graphics2D) g; + if (lineHeight == 0) + { + LineMetrics lm = font.getLineMetrics ("0", g2.getFontRenderContext ()); + lineHeight = (int) lm.getHeight (); + linesPerPage = (int) pageFormat.getImageableHeight () / lineHeight - 5; + pages = (lines.length - 1) / linesPerPage; + } + + if (pages < page) + return Printable.NO_SUCH_PAGE; + + g2.translate (pageFormat.getImageableX (), pageFormat.getImageableY ()); + g2.setPaint (Color.black); + g2.setStroke (new BasicStroke (2)); + + g2.setFont (font); + + int first = page * linesPerPage; + int last = first + linesPerPage; + if (last > lines.length) + last = lines.length; + + for (int line = first; line < last; line++) + g2.drawString (lines[line], x, y + (line % linesPerPage + 2) * lineHeight); + + return (PAGE_EXISTS); + } + + // Routine copied from http://progcookbook.blogspot.com/2006/02/text-wrapping-function-for-java.html + static String[] wrapText (String text, int len) + { + // return empty array for null text + if (text == null) + return new String[] {}; + + // return text if len is zero or less + if (len <= 0) + return new String[] { text }; + + // return text if less than length + if (text.length () <= len) + return new String[] { text }; + + char[] chars = text.toCharArray (); + Vector lines = new Vector (); + StringBuilder line = new StringBuilder (); + StringBuilder word = new StringBuilder (); + + for (int i = 0; i < chars.length; i++) + { + if (chars[i] == 10) + { + line.append (word); + word.delete (0, word.length ()); + lines.add (line.toString ()); + line.delete (0, line.length ()); + continue; + } + + word.append (chars[i]); + + if (chars[i] == ' ') + { + if ((line.length () + word.length ()) > len) + { + lines.add (line.toString ()); + line.delete (0, line.length ()); + } + + line.append (word); + word.delete (0, word.length ()); + } + } + + // handle any extra chars in current word + if (word.length () > 0) + { + if ((line.length () + word.length ()) > len) + { + lines.add (line.toString ()); + line.delete (0, line.length ()); + } + line.append (word); + } + + // handle extra line + if (line.length () > 0) + lines.add (line.toString ()); + + String[] ret = new String[lines.size ()]; + int c = 0; // counter + for (Enumeration e = lines.elements (); e.hasMoreElements (); c++) + ret[c] = e.nextElement (); + + return ret; + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/RedoHandler.java b/src/com/bytezone/diskbrowser/gui/RedoHandler.java new file mode 100644 index 0000000..95fa672 --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/RedoHandler.java @@ -0,0 +1,235 @@ +package com.bytezone.diskbrowser.gui; + +import java.awt.Event; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.net.URL; +import java.util.ArrayList; +import java.util.EventListener; +import java.util.EventObject; +import java.util.List; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.ActionMap; +import javax.swing.ImageIcon; +import javax.swing.InputMap; +import javax.swing.JComponent; +import javax.swing.JRootPane; +import javax.swing.JToolBar; +import javax.swing.KeyStroke; +import javax.swing.event.EventListenerList; + +class RedoHandler implements FileSelectionListener, DiskSelectionListener, SectorSelectionListener, + FileNodeSelectionListener +{ + private static final String base = "/com/bytezone/diskbrowser/icons/"; + EventListenerList listenerList = new EventListenerList (); + Action leftAction = new LeftAction (); + Action rightAction = new RightAction (); + RedoData redoData = new RedoData (leftAction, rightAction); + static int id = 0; + + public RedoHandler (JRootPane jRootPane, JToolBar toolBar) + { + // This code works as long as the toolBar arrows have focus first + InputMap im = jRootPane.getInputMap (JComponent.WHEN_IN_FOCUSED_WINDOW); + ActionMap am = jRootPane.getActionMap (); + + im.put (KeyStroke.getKeyStroke (KeyEvent.VK_LEFT, Event.ALT_MASK), "LeftAction"); + am.put ("LeftAction", leftAction); + im.put (KeyStroke.getKeyStroke (KeyEvent.VK_RIGHT, Event.ALT_MASK), "RightAction"); + am.put ("RightAction", rightAction); + + toolBar.add (leftAction); + toolBar.add (rightAction); + } + + public RedoData createData () + { + RedoData data = new RedoData (leftAction, rightAction); + this.redoData = data; // doesn't fire an event this way + return data; + } + + public void setCurrentData (RedoData data) + { + this.redoData = data; + RedoEvent event = redoData.getCurrentEvent (); + if (event != null) + fireRedoEvent (event); + } + + private void fireRedoEvent (RedoEvent event) + { + RedoListener[] listeners = (listenerList.getListeners (RedoListener.class)); + for (RedoListener listener : listeners) + listener.redo (event); + } + + public void addRedoListener (RedoListener listener) + { + listenerList.add (RedoListener.class, listener); + } + + public void removeRedoListener (RedoListener listener) + { + listenerList.remove (RedoListener.class, listener); + } + + @Override + public void diskSelected (DiskSelectedEvent event) + { + if (!event.redo) // it's an event we just caused + addEvent (new RedoEvent ("DiskEvent", event)); + } + + @Override + public void fileNodeSelected (FileNodeSelectedEvent event) + { + if (!event.redo) // it's an event we just caused + addEvent (new RedoEvent ("FileNodeEvent", event)); + } + + @Override + public void fileSelected (FileSelectedEvent event) + { + if (!event.redo) // it's an event we just caused + addEvent (new RedoEvent ("FileEvent", event)); + } + + @Override + public void sectorSelected (SectorSelectedEvent event) + { + if (!event.redo) // it's an event we just caused + addEvent (new RedoEvent ("SectorEvent", event)); + } + + private void addEvent (RedoEvent event) + { + redoData.addEvent (event); + } + + public class RedoEvent extends EventObject + { + String type; + EventObject value; + + public RedoEvent (String type, EventObject value) + { + super (RedoHandler.this); + this.type = type; + this.value = value; + } + + @Override + public String toString () + { + return ("[type=" + type + ", value=" + value + "]"); + } + } + + public interface RedoListener extends EventListener + { + void redo (RedoEvent event); + } + + class LeftAction extends AbstractAction + { + public LeftAction () + { + super ("Back"); + putValue (Action.SHORT_DESCRIPTION, "Undo selection"); + URL url = getClass ().getResource (base + "Symbol-Left-32.png"); + if (url != null) + putValue (Action.LARGE_ICON_KEY, new ImageIcon (url)); + } + + @Override + public void actionPerformed (ActionEvent e) + { + fireRedoEvent (redoData.getPreviousEvent ()); + } + } + + class RightAction extends AbstractAction + { + public RightAction () + { + super ("Forward"); + putValue (Action.SHORT_DESCRIPTION, "Redo selection"); + URL url = getClass ().getResource (base + "Symbol-Right-32.png"); + if (url != null) + putValue (Action.LARGE_ICON_KEY, new ImageIcon (url)); + } + + @Override + public void actionPerformed (ActionEvent e) + { + fireRedoEvent (redoData.getNextEvent ()); + } + } + + class RedoData + { + List events = new ArrayList (); + int currentEvent = -1; + Action leftAction; + Action rightAction; + final int seq = id++; + + public RedoData (Action left, Action right) + { + leftAction = left; + rightAction = right; + setArrows (); + } + + RedoEvent getCurrentEvent () + { + if (currentEvent < 0) + return null; + setArrows (); + return events.get (currentEvent); + } + + RedoEvent getNextEvent () + { + RedoEvent event = events.get (++currentEvent); + setArrows (); + return event; + } + + RedoEvent getPreviousEvent () + { + RedoEvent event = events.get (--currentEvent); + setArrows (); + return event; + } + + void addEvent (RedoEvent event) + { + while (currentEvent < events.size () - 1) + events.remove (events.size () - 1); + ++currentEvent; + events.add (event); + setArrows (); + } + + private void setArrows () + { + rightAction.setEnabled (currentEvent < events.size () - 1); + leftAction.setEnabled (currentEvent > 0); + } + + @Override + public String toString () + { + StringBuilder text = new StringBuilder (); + text.append ("Current event (" + seq + ") : " + currentEvent + "\n"); + for (RedoEvent event : events) + text.append (" - " + event + "\n"); + return text.toString (); + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/RefreshTreeAction.java b/src/com/bytezone/diskbrowser/gui/RefreshTreeAction.java new file mode 100755 index 0000000..52d5b9d --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/RefreshTreeAction.java @@ -0,0 +1,34 @@ +package com.bytezone.diskbrowser.gui; + +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; + +import javax.swing.Action; +import javax.swing.KeyStroke; + +import com.bytezone.common.DefaultAction; + +class RefreshTreeAction extends DefaultAction +{ + CatalogPanel owner; + + public RefreshTreeAction (CatalogPanel owner) + { + super ("Refresh current tree", "Makes newly added/modified disks available", + "/com/bytezone/diskbrowser/icons/"); +// putValue (Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke ("alt R")); + putValue (Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke (KeyEvent.VK_F5, 0)); +// putValue (Action.MNEMONIC_KEY, KeyEvent.VK_R); + putValue (Action.MNEMONIC_KEY, KeyEvent.VK_F5); + this.owner = owner; + + setIcon (Action.SMALL_ICON, "arrow_refresh.png"); + setIcon (Action.LARGE_ICON_KEY, "arrow_refresh_32.png"); + } + + @Override + public void actionPerformed (ActionEvent e) + { + owner.refreshTree (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/RootDirectoryAction.java b/src/com/bytezone/diskbrowser/gui/RootDirectoryAction.java new file mode 100755 index 0000000..830813a --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/RootDirectoryAction.java @@ -0,0 +1,53 @@ +package com.bytezone.diskbrowser.gui; + +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.io.File; + +import javax.swing.Action; +import javax.swing.JFileChooser; +import javax.swing.KeyStroke; + +import com.bytezone.common.DefaultAction; +import com.bytezone.common.Platform; + +class RootDirectoryAction extends DefaultAction +{ + File rootDirectory; + CatalogPanel catalogPanel; + + public RootDirectoryAction (File rootDirectory, CatalogPanel catalogPanel) + { + super ("Set HOME folder...", "Defines root folder where the disk images are kept", + "/com/bytezone/diskbrowser/icons/"); + putValue (Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke ("alt H")); + putValue (Action.MNEMONIC_KEY, KeyEvent.VK_H); + this.rootDirectory = rootDirectory; + this.catalogPanel = catalogPanel; + + setIcon (Action.SMALL_ICON, "folder_explore_16.png"); + setIcon (Action.LARGE_ICON_KEY, "folder_explore_32.png"); + } + + @Override + public void actionPerformed (ActionEvent e) + { + JFileChooser chooser = new JFileChooser (Platform.userHome); + chooser.setDialogTitle ("Select FOLDER containing disk images"); + chooser.setFileSelectionMode (JFileChooser.DIRECTORIES_ONLY); + if (rootDirectory != null) + chooser.setSelectedFile (rootDirectory); + int result = chooser.showDialog (null, "Accept"); + if (result == JFileChooser.APPROVE_OPTION) + { + File file = chooser.getSelectedFile (); + if (!file.isDirectory ()) + file = file.getParentFile (); + if (file != null) + { + rootDirectory = file; + catalogPanel.changeRootPanel (file); + } + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/ScrollRuler.java b/src/com/bytezone/diskbrowser/gui/ScrollRuler.java new file mode 100644 index 0000000..09c6eec --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/ScrollRuler.java @@ -0,0 +1,129 @@ +package com.bytezone.diskbrowser.gui; + +import java.awt.*; + +import javax.swing.JComponent; + +import com.bytezone.common.Platform; +import com.bytezone.common.Platform.FontSize; +import com.bytezone.common.Platform.FontType; +import com.bytezone.diskbrowser.gui.DiskLayoutPanel.LayoutDetails; + +class ScrollRuler extends JComponent +{ + // dimensions of the ruler + public static final int HEIGHT = 20; + public static final int WIDTH = 40; + + public static final int HORIZONTAL = 0; + public static final int VERTICAL = 1; + Font font = Platform.getFont (FontType.SANS_SERIF, FontSize.BASE); + + int orientation; + boolean isHex = true; + boolean isTrackMode = true; + LayoutDetails layoutDetails; + JComponent image; + + public ScrollRuler (JComponent image, int orientation) + { + this.orientation = orientation; + this.image = image; + + // set defaults until setLayout is called + if (orientation == HORIZONTAL) + setPreferredSize (new Dimension (0, HEIGHT)); // width/height + else + setPreferredSize (new Dimension (WIDTH, 0)); + } + + public void setLayout (LayoutDetails layout) + { + this.layoutDetails = layout; + + // Must match the preferred size of DiskLayoutImage + if (orientation == HORIZONTAL) + setPreferredSize (new Dimension (layout.block.width * layout.grid.width + 1, HEIGHT)); // width/height + else + setPreferredSize (new Dimension (WIDTH, layout.block.height * layout.grid.height + 1)); + + setTrackMode (layout.grid.width == 16 || layout.grid.width == 13); // will call repaint () + } + + public void changeFont (Font font) + { + this.font = font; + repaint (); + } + + public void setHex (boolean hex) + { + isHex = hex; + repaint (); + } + + public void setTrackMode (boolean trackMode) + { + isTrackMode = trackMode; + repaint (); + } + + @Override + protected void paintComponent (Graphics g) + { + Rectangle clipRect = g.getClipBounds (); + // g.setColor (new Color (240, 240, 240)); + g.setColor (Color.WHITE); + g.fillRect (clipRect.x, clipRect.y, clipRect.width, clipRect.height); + + if (layoutDetails == null) + return; + + g.setFont (font); // how do I do this in the constructor? + g.setColor (Color.black); + + if (orientation == HORIZONTAL) + drawHorizontal (g, clipRect, layoutDetails.block.width); + else + drawVertical (g, clipRect, layoutDetails.block.height); + } + + private void drawHorizontal (Graphics g, Rectangle clipRect, int width) + { + int start = (clipRect.x / width); + int end = start + clipRect.width / width; + end = Math.min (end, image.getWidth () / width - 1); + + String format; + int offset; + + if (layoutDetails.block.width <= 15) + { + format = isHex ? "%1X" : "%1d"; + offset = isHex ? 4 : 0; + } + else + { + format = isHex ? "%02X" : "%02d"; + offset = 7; + } + + for (int i = start; i <= end; i++) + g.drawString (String.format (format, i), i * width + offset, 15); + } + + private void drawVertical (Graphics g, Rectangle clipRect, int height) + { + int start = (clipRect.y / height); + int end = start + clipRect.height / height; + end = Math.min (end, image.getHeight () / height - 1); + + String format = isHex ? "%04X" : "%04d"; + + for (int i = start; i <= end; i++) + { + int value = isTrackMode ? i : i * layoutDetails.grid.width; + g.drawString (String.format (format, value), 4, i * height + 13); + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/SectorSelectedEvent.java b/src/com/bytezone/diskbrowser/gui/SectorSelectedEvent.java new file mode 100755 index 0000000..0cee8e5 --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/SectorSelectedEvent.java @@ -0,0 +1,50 @@ +package com.bytezone.diskbrowser.gui; + +import java.util.EventObject; +import java.util.List; + +import com.bytezone.diskbrowser.disk.DiskAddress; +import com.bytezone.diskbrowser.disk.FormattedDisk; +import com.bytezone.diskbrowser.disk.SectorListConverter; + +public class SectorSelectedEvent extends EventObject +{ + private final List sectors; + private final FormattedDisk owner; + boolean redo; + + public SectorSelectedEvent (Object source, List sectors, FormattedDisk owner) + { + super (source); + this.sectors = sectors; + // always store the parent if this disk is part of a dual-dos disk + this.owner = owner.getParent () == null ? owner : owner.getParent (); + } + + public List getSectors () + { + return sectors; + } + + public FormattedDisk getFormattedDisk () + { + return owner; + } + + public String toText () + { + StringBuilder text = new StringBuilder (); + SectorListConverter slc = new SectorListConverter (sectors); + text.append (slc.sectorText); + return text.toString (); + } + + public static SectorSelectedEvent create (Object source, FormattedDisk owner, String sectorsText) + { + if (sectorsText.startsWith ("$")) + sectorsText = sectorsText.substring (3); // only for old records + + SectorListConverter slc = new SectorListConverter (sectorsText, owner.getDisk ()); + return new SectorSelectedEvent (source, slc.sectors, owner); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/SectorSelectionListener.java b/src/com/bytezone/diskbrowser/gui/SectorSelectionListener.java new file mode 100755 index 0000000..2d6aa99 --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/SectorSelectionListener.java @@ -0,0 +1,8 @@ +package com.bytezone.diskbrowser.gui; + +import java.util.EventListener; + +public interface SectorSelectionListener extends EventListener +{ + public void sectorSelected (SectorSelectedEvent event); +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/ShowFreeSectorsAction.java b/src/com/bytezone/diskbrowser/gui/ShowFreeSectorsAction.java new file mode 100755 index 0000000..be7f448 --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/ShowFreeSectorsAction.java @@ -0,0 +1,31 @@ +package com.bytezone.diskbrowser.gui; + +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.KeyStroke; + +class ShowFreeSectorsAction extends AbstractAction +{ + DiskLayoutPanel panel; + MenuHandler mh; + + public ShowFreeSectorsAction (MenuHandler mh, DiskLayoutPanel panel) + { + super ("Show free sectors"); + putValue (Action.SHORT_DESCRIPTION, + "Display which sectors are marked free in the disk layout panel"); + putValue (Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke ("alt F")); + putValue (Action.MNEMONIC_KEY, KeyEvent.VK_F); + this.panel = panel; + this.mh = mh; + panel.setFree (mh.showFreeSectorsItem.isSelected ()); // set initial state + } + + public void actionPerformed (ActionEvent e) + { + panel.setFree (mh.showFreeSectorsItem.isSelected ()); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/Tab.java b/src/com/bytezone/diskbrowser/gui/Tab.java new file mode 100755 index 0000000..64c324f --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/Tab.java @@ -0,0 +1,20 @@ +package com.bytezone.diskbrowser.gui; + +/*********************************************************************************************** + * Interface implemented by AbstractTab, and in turn FileSystemTab and AppleDiskTab. + * + * + ***********************************************************************************************/ + +import javax.swing.tree.DefaultMutableTreeNode; + +interface Tab +{ + public void refresh (); + + public void activate (); + + public DefaultMutableTreeNode getRootNode (); +} + +// public void addMouseListener (MouseAdapter ma) \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/gui/TreeBuilder.java b/src/com/bytezone/diskbrowser/gui/TreeBuilder.java new file mode 100755 index 0000000..647e184 --- /dev/null +++ b/src/com/bytezone/diskbrowser/gui/TreeBuilder.java @@ -0,0 +1,352 @@ +package com.bytezone.diskbrowser.gui; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.net.URL; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.zip.CRC32; +import java.util.zip.Checksum; + +import javax.swing.ImageIcon; +import javax.swing.JComponent; +import javax.swing.JTree; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeCellRenderer; +import javax.swing.tree.DefaultTreeModel; + +import com.bytezone.diskbrowser.FileFormatException; +import com.bytezone.diskbrowser.disk.AppleDisk; +import com.bytezone.diskbrowser.disk.Disk; +import com.bytezone.diskbrowser.disk.DiskFactory; +import com.bytezone.diskbrowser.disk.FormattedDisk; + +public class TreeBuilder +{ + private static SimpleDateFormat sdf = new SimpleDateFormat ("dd MMM yyyy"); + private static final boolean FULL_TREE = false; + private static final List suffixes = Arrays.asList ("po", "dsk", "do", "hdv", "2mg", + "d13", "sdk", "gz"); + + FileComparator fc = new FileComparator (); + JTree tree; + int totalDisks; + int totalFolders; + + Map totalFiles = new TreeMap (); + + Map> duplicateDisks = new TreeMap> (); + Map diskNames = new HashMap (); + Map> dosMap = new TreeMap> (); + + public TreeBuilder (File folder) + { + assert (folder.exists ()); + assert (folder.isDirectory ()); + + long start = System.currentTimeMillis (); + + FileNode fn = new FileNode (folder); + DefaultMutableTreeNode root = new DefaultMutableTreeNode (fn); + fn.setTreeNode (root); + addFiles (root, folder); + DefaultTreeModel treeModel = new DefaultTreeModel (root); + tree = new JTree (treeModel); + + long duration = System.currentTimeMillis () - start; + System.out + .printf ("Tree building took %,d milliseconds for %,d disk%s and %,d folder%s%n", + duration, totalDisks, (totalDisks == 1 ? "" : "s"), totalFolders, + (totalFolders == 1 ? "" : "s")); + + treeModel.setAsksAllowsChildren (true); // allows empty nodes to appear as folders + setDiskIcon ("/com/bytezone/diskbrowser/icons/disk.png"); + ((FileNode) root.getUserObject ()).disks = totalDisks; + + if (FULL_TREE) + { + System.out.printf ("%nFolders ..... %,5d%n", totalFolders); + System.out.printf ("Disks ....... %,5d%n%n", totalDisks); + + int tf = 0; + for (String key : totalFiles.keySet ()) + { + int t = totalFiles.get (key); + tf += t; + System.out.printf ("%13.13s %,5d%n", key + " ...........", t); + } + System.out.printf ("%nTotal ...... %,6d%n%n", tf); + } + } + + private void addFiles (DefaultMutableTreeNode node, File directory) + { + File[] files = directory.listFiles (); + if (files == null || files.length == 0) + { + System.out.println ("Empty folder : " + directory.getAbsolutePath ()); + return; + } + + FileNode parentNode = (FileNode) node.getUserObject (); + Arrays.sort (files, fc); + for (File file : files) + { + if (file.isDirectory ()) + { + FileNode fn = new FileNode (file); + DefaultMutableTreeNode newNode = new DefaultMutableTreeNode (fn); + fn.setTreeNode (newNode); + newNode.setAllowsChildren (true); + node.add (newNode); + totalFolders++; + + if (FULL_TREE) + addFiles (newNode, file); // recursion! + continue; + } + + if (FULL_TREE) + { + int pos = file.getName ().lastIndexOf ('.'); + if (pos > 0) + { + String type = file.getName ().substring (pos + 1).toLowerCase (); + if (totalFiles.containsKey (type)) + { + int t = totalFiles.get (type); + totalFiles.put (type, ++t); + } + else + totalFiles.put (type, 1); + } + } + + if (file.length () != 143360 && file.length () != 116480 && file.length () != 819264 + && file.length () < 200000) + { + String name = file.getName ().toLowerCase (); + if (!name.endsWith (".sdk") && !name.endsWith (".dsk.gz")) + continue; + } + + parentNode.disks++; + String filename = file.getAbsolutePath (); + if (validFileType (filename)) + { + FileNode fn = new FileNode (file); + DefaultMutableTreeNode newNode = new DefaultMutableTreeNode (fn); + fn.setTreeNode (newNode); + newNode.setAllowsChildren (false); + node.add (newNode); + + if (false) + checkDuplicates (file); + totalDisks++; + + if (false) + checksumDos (file); + } + } + } + + private void checksumDos (File file) + { + if (file.length () != 143360 || file.getAbsolutePath ().contains ("/ZDisks/")) + return; + + Disk disk = new AppleDisk (file, 35, 16); + byte[] buffer = disk.readSector (0, 0); + + Checksum checksum = new CRC32 (); + checksum.update (buffer, 0, buffer.length); + long cs = checksum.getValue (); + List files = dosMap.get (cs); + if (files == null) + { + files = new ArrayList (); + dosMap.put (cs, files); + } + files.add (file); + } + + private void checkDuplicates (File file) + { + if (diskNames.containsKey (file.getName ())) + { + List diskList = duplicateDisks.get (file.getName ()); + if (diskList == null) + { + diskList = new ArrayList (); + duplicateDisks.put (file.getName (), diskList); + diskList.add (new DiskDetails (diskNames.get (file.getName ()))); // add the original + } + diskList.add (new DiskDetails (file)); // add the duplicate + } + else + diskNames.put (file.getName (), file); + } + + private boolean validFileType (String filename) + { + int dotPos = filename.lastIndexOf ('.'); + if (dotPos < 0) + return false; + + String suffix = filename.substring (dotPos + 1).toLowerCase (); + return suffixes.contains (suffix); + } + + private void setDiskIcon (String iconName) + { + URL url = this.getClass ().getResource (iconName); + if (url != null) + { + ImageIcon icon = new ImageIcon (url); + DefaultTreeCellRenderer renderer = (DefaultTreeCellRenderer) tree.getCellRenderer (); + renderer.setLeafIcon (icon); + tree.setCellRenderer (renderer); + tree.setRowHeight (18); + } + else + System.out.println ("Failed to set the disk icon : " + iconName); + } + + /* + * Class used to control the text displayed by the JTree. + */ + public class FileNode implements DataSource + { + DefaultMutableTreeNode parentNode; + public final File file; + private static final int MAX_NAME_LENGTH = 36; + private static final int SUFFIX_LENGTH = 12; + private static final int PREFIX_LENGTH = MAX_NAME_LENGTH - SUFFIX_LENGTH - 3; + private FormattedDisk formattedDisk; + int disks; + boolean showDisks; + + public FileNode (File file) + { + this.file = file; + } + + public void setTreeNode (DefaultMutableTreeNode node) + { + this.parentNode = node; + } + + public void readFiles () + { + addFiles (parentNode, file); + } + + public FormattedDisk getFormattedDisk () + { + if (formattedDisk == null) + try + { + formattedDisk = DiskFactory.createDisk (file); + } + catch (FileFormatException e) + { + System.out.println ("Swallowing a FileFormatException in TreeBuilder"); + System.out.println (e.getMessage ()); + return null; + } + return formattedDisk; + } + + public boolean replaceDisk (FormattedDisk disk) + { + String path = disk.getDisk ().getFile ().getAbsolutePath (); + if (formattedDisk != null && path.equals (file.getAbsolutePath ())) + { + formattedDisk = disk; + return true; + } + return false; + } + + @Override + public String toString () + { + String name = file.getName (); + if (name.length () > MAX_NAME_LENGTH) + name = + name.substring (0, PREFIX_LENGTH) + "..." + + name.substring (name.length () - SUFFIX_LENGTH); + if (showDisks && disks > 0) + return String.format ("%s (%,d)", name, disks); + return name; + } + + @Override + public String getText () + { + StringBuilder text = new StringBuilder (); + + text.append ("Directory : " + file.getAbsolutePath () + "\n\n"); + text.append ("D File names " + + " Date Size Type\n"); + text.append ("- ----------------------------------------" + + " ----------- -------------- ---------\n"); + for (File f : file.listFiles ()) + { + String name = f.getName (); + if (name.startsWith (".")) + continue; + + Date d = new Date (f.lastModified ()); + int pos = name.lastIndexOf ('.'); + String type = pos > 0 && !f.isDirectory () ? name.substring (pos) : ""; + String size = f.isDirectory () ? "" : String.format ("%,14d", f.length ()); + text.append (String.format ("%s %-40.40s %s %-14s %s%n", f.isDirectory () ? "D" + : " ", name, sdf.format (d), size, type)); + } + if (text.length () > 0) + text.deleteCharAt (text.length () - 1); + return text.toString (); + } + + @Override + public String getAssembler () + { + return null; + } + + @Override + public String getHexDump () + { + return null; + } + + @Override + public BufferedImage getImage () + { + return null; + } + + @Override + public JComponent getComponent () + { + return null; + } + } + + private class FileComparator implements Comparator + { + @Override + public int compare (File filea, File fileb) + { + boolean fileaIsDirectory = filea.isDirectory (); + boolean filebIsDirectory = fileb.isDirectory (); + + if (fileaIsDirectory && !filebIsDirectory) + return -1; + if (!fileaIsDirectory && filebIsDirectory) + return 1; + return filea.getName ().compareToIgnoreCase (fileb.getName ()); + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/icons/Symbol-Left-32.png b/src/com/bytezone/diskbrowser/icons/Symbol-Left-32.png new file mode 100644 index 0000000000000000000000000000000000000000..901e58bba6a43d4f1dc929178b1f1845292b980e GIT binary patch literal 3770 zcmV;r4n^^aP)4Tx0C)kNmUmPX*B8g%%xo{TU6vwc>AklFq%OTkl_mFQv@x1^BM1TV}0C2duqR=S6Xn?LjUp6xrb&~O43j*Nv zEr418u3H3zGns$s|L;SQD-ufpfWpxLJ03rmi*g~#S@{x?OrJ!Vo{}kJ7$ajbnjp%m zGEV!%=70KpVow?KvV}a4moSaFCQKV= zXBIPnpP$8-NG!rR+)R#`$7JVZi#Wn10DSspSrkx`)s~4C+0n+?(b2-z5-tDd^^cpM zz5W?wz5V3zGUCskL5!X++LzcbT23thtSPiMTfS&1I{|204}j|3FPi>70OSh+Xzlyz zdl<5LNtZ}OE>>3g`T3RtKG#xK(9i3CI(+v0d-&=+OWAp!Ysd8Ar*foO5~i%E+?=c& zshF87;&Ay)i~kOm zCIB-Z!^JGdti+UJsxgN!t(Y#%b<8kk67vyD#cE*9urAm@Y#cTXn~yERR$}Y1E!Yd# zo7hq8Ya9;8z!~A3Z~?e@Tn26#t`xT$*Ni)h>&K1Yrto;Y8r}@=h7ZGY@Dh9xekcA2 z{tSKqKZ<`tAQQ9+wgf*y0zpVvOQ<9qCY&Y=5XJ~ILHOG0j2XwBQ%7jM`P2tv~{#P+6CGu9Y;5!2hua>CG_v;z4S?CC1rc%807-x z8s$^ULkxsr$OvR)G0GUn7`GVjR5Vq*RQM{JRGL%DRgX~5SKp(4L49HleU9rK?wsN|$L8GCfHh1tA~lw29MI^|n9|hJ z^w$(=?$kW5IibbS^3=-Es?a*EHLgw5cGnhYS7@Kne#%s4dNH$@Rm?8tq>hG8fR0pW zzfP~tjINRHeBHIW&AJctNO~;2RJ{tlPQ6KeZT(RF<@$~KcMXUJEQ54|9R}S7(}qTd zv4$HA+YFx=sTu_uEj4O1x^GN1_Ap*-Tx)#81ZToB$u!w*a?KPrbudjgtugI0gUuYx z1ZKO<`pvQC&gMe%TJu2*iiMX&o<*a@uqDGX#B!}=o8@yWeX9hktybMuAFUm%v#jf^ z@7XBX1lg>$>9G0T*3_13TVs2}j%w#;x5}>F?uEUXJ>Pzh{cQ)DL#V?BhfaqNj!uqZ z$0o;dCw-@6r(I5iEIKQkRm!^LjCJ;QUgdn!`K^nii^S!a%Wtk0u9>cfU7yS~n#-SC zH+RHM*Nx-0-)+d9>7MMq&wa>4$AjZh>+#4_&y(j_?>XjW;+5fb#Ot}YwYS*2#e16V z!d}5X>x20C`xN{1`YQR(_pSDQ=%?$K=GW*q>F?mb%>QfvHXt})YrtTjW*|4PA#gIt zDQHDdS1=_wD!4lMQHW`XIHV&K4h;(37J7f4!93x-wlEMD7`83!LAX));_x3Ma1r4V zH4%>^Z6cRPc1O{olA;bry^i*dE{nc5-*~=serJq)Okzw!%yg_zYWi`#ol25V;v^kU#wN!mA5MPH z3FFjqrcwe^cBM>m+1wr6XFN|{1#g`1#xLiOrMjh-r#?w@OWT$Wgg6&&5F%x&L(6hXP*!%2{VOVIa)adIsGCtQITk9vCHD^izmgw;`&@D zcVTY3gpU49^+=7S>!rha?s+wNZ}MaEj~6Hw2n%|am@e70WNfM5(r=exmT{MLF4tMU zX8G_6uNC`OLMu~NcCOM}Rk&(&wg2ivYe;J{*Zj2BdTsgISLt?eJQu}$~QLORDCnMIdyYynPb_W zEx0YhEw{FMY&}%2SiZD;WLxOA)(U1tamB0cN!u@1+E?z~LE0hRF;o>&)xJ}I=a!xC ztJAA*)_B)6@6y<{Y1i~_-tK`to_m`1YVIxB`);3L-|hYW`&(-bYby`n4&)tpTo+T< z{VnU;hI;k-lKKw^g$IWYMIP#EaB65ctZ}%k5pI+=jvq-pa_u{x@7kLzn)Wv{noEv? zqtc^Kzfb=D*0JDYoyS?nn|?6(VOI;SrMMMpUD7()mfkkh9^c-7BIrbChiga6kCs0k zJgIZC=9KcOveTr~g{NoFEIl)IR&;jaT-v#j&ZN$J=i|=b=!)p-y%2oi(nY_E=exbS z&s=i5bn>#xz3Ke>~2=f&N;yEFGz-^boBexUH6@}b7V+Mi8+ZXR+R zIyLMw-18{v(Y+Dw$g^K^e|bMz_?Y^*a!h-y;fd{&ljDBl*PbqTI{HlXY-Xb9SH)j< zJvV;-!*8Cy^-RW1j=m7TnEk!cvIsm zzR80Rl(5lfDYpr#6OVXF# zIef1YSj|ANo+fEi8@p;!*p08EVI!+ zrj4O*CO(V>C0%lApC=Y`Vg|Qw{wINf&+_Y=qJeWSZ^^vYu`bGV(`MXum`$-_dPj)N z9A$z&(cNJf=r&H=g%0Ufyg|buW7tccJp4mpbkPzVzLf9V_C(~P-c3qNEE;5!y5)ky z387j9ticlj!KnpcLdSOp@6e;~uC7a}{7yO+y=5qiazJALAh3Vrw{TpFz2DoZoP4@1 z6)aNJs?LQ0qJ$$D0~pb9a#|)}yV=0ICEi^fOX!dth{4vwksL|jl?fai8Qa#LRL;G$ zCG%W58Df(qs|@4=x&$av0R~Vii$_S5$8%nG<~jY_9@%jir&L8LdXNbo`f?%>E13tL zX^o%mZcC*(RcNA!bo!3Shz#HeK%AWVQ341uoP&&b_eoUP0E^ni)uGJIQzUUYAhH;g zW{k~DmIj|(7f;`#u`;Eb9;n-NoRm!hHvveF<}FK{xSJ%KASGm14>BA;SLh&1*wDdA z1a;G5GtEh6bNV?R-rN3jX(rNp<;G-oTAgJIwKtI@64h@m`hkdu6TJc1+a5s>0trq^ zI8jb4F^bO*G!@V9+c5ZT?ot2kJDR#S%vgw0EBnEh-G1@v?)QM#8#;~gp2j3AP1~c35^%XO_WFg~i%+eIWIJ0D zT`JX1Gq`9dnNryt3OM5rj2Vm>L?fo5$OLAddq-w`83OtWuR@nGWn48sUy=Zbyz=p# zhQ>4>+R>WmH-a*|TbyU9aDW-phIaLOp*xufsUgcO&lA{Gv#f|L**swWF99Fy)#2Q+ z?X9s7GDz*t@3;+NC07*qoM6N<$f?)hJHvj+t literal 0 HcmV?d00001 diff --git a/src/com/bytezone/diskbrowser/icons/Symbol-Right-32.png b/src/com/bytezone/diskbrowser/icons/Symbol-Right-32.png new file mode 100644 index 0000000000000000000000000000000000000000..05c5890f420590b97d5a9054956e0882ee1af2e7 GIT binary patch literal 3854 zcmV+p5ApDcP)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z000CvNklRdw&o>}1!?B%2r)L=Z%bh=dpw4|*|p@FGG$At*>T zv#u`$87aj-!F4a0*tY&oPYO}LtQ|YDcj}9MA68~n` zh5tVUMx{?8!cf7w*EWw7cWfRxShV^@$NBP#0fYd~hkK${>Zqljt&gl>*K?ygHbn_`CF3o_0lpAvsWKQ1RNH|@;wTmjZ*LoY;-@pUL%*Dx z+ox9d8xgl`PQ7t(;)k8v*1sqU0YL##5Cj241eI)*JPB16%Lc5{@wp7JDwHB+@_L)^ zPu0GiZl&Xq(Mz{VLaQp&Q%5y%7!gFk2&ia27gPlm!2qJtmmAH%&<&YfKvfU|bKSt` zNP*X%wy*v;QQI;x-FXM$yPHDbJk&a#>#elEp%KBTM+J;1nt!#?Or`>=p|5XyI@(*C zd8h3Hu~D{fu58*kRsZJav$g#}^aIeO55R{&y_0gI)x{dYkQtgc=of4}FS8m@6>M(M zNM0R%&wx>AJ5M7GJT_Lg4?Zxk_webNXRkEU@gW=kC4luFs_m3ZjV`g#d?Upw`DVEh zBlB~WTqgr&jqiKMYFNA`*Kegzj3Zuta{UV@rs_u~uD9QF$d|U`JilLSa_sUPrO04K zh*Ysgu&P9=L`JYG#3tisyxv@hj9H*#uqs5Rw@(Jts$xV~Te6I;D-E5#(j4!2Ic@{s zJ)|y>N&p4v7hXUlAR((c%|{6tPjX(5B!FAO@97y}ctDw%p5xc^a|eRzyM;&_mb}(Q zmXPKncTZXC4n)RKiDB|WZTj@p_MWZ_Uk?wOED3v!$A>$o(4%F}3?HB}V2EAHkyEpW zt~S!$!-LTo-*)tFf>s2Iu`*mVlx#14i*q2(Ix=Yd8nWk$zR8*cBD%Y}Z*Xw-q-A@4tf~261EHA_%fJbC(U181Tt``@; zg+MtnRIKp#x!JQ*wPcqVJ#3fh=eFK*JN}$XBO&)nxT)XY^tM1RZFha;m8U8v3Tq3) z^~AHGU@4@IY^he;^bcsBqBV$+|;0ln5tKaOJgv=rNBvalRh@-eHwW~%`1H-LWz0Po`I%v50*ng9R* Q07*qoM6N<$f-^WZFvDRxbN~PV literal 0 HcmV?d00001 diff --git a/src/com/bytezone/diskbrowser/icons/arrow_refresh.png b/src/com/bytezone/diskbrowser/icons/arrow_refresh.png new file mode 100644 index 0000000000000000000000000000000000000000..257cfee396d5cbebd839de85ce07de888f4ea206 GIT binary patch literal 674 zcmV;T0$u%yP)c5r#tc9wCFE4rIa-!1q-0q!CnXT4+IPh@KIlp_)|DJ;of^@6O&$Qb|%0Qj}Sozsbo8Q|>Mbn=U zP2E7#wB?O`Ep!GZny!_X7GPuz^XAJUAIBgXg^BNwVUi{61V^rojQRaE_qNj4NLdoVFk+|dMdJ*; zhruC&vq5gXzRrV0P}bF)f!c~)+<0;`kvy2BK~0+`3U)UG7hM{IuyBhaR6s z^E4C0gyZR%rDXZ6T~Z#euCIvb`VE|rHoo@?;#gG;J%Lwr*M*c6rP9`TeALEboY+=M z_9iFvJX#TP`AWOxTOW5H#;`tQNBIm9*+5DUdmdObp`;j!>}EcEvauMR>P~t_*#n}! zx{k1Mn;#1yWC@t0h{~O4*tM2I<{~!k28MaoAd1baJIiXe5uVjC=3!%jS3*P_5Qj*e zMuH)F`}<^aT#vEUo#>M7I&k@5J6}Uhj82Wy|5@Hbx@RKrQvzahya-(6+;Z6|hDkxh z+p$h@q{FG4u2oi&6#bWJbgAj{v=B}^Y5y2!b=uZ+`M(4h0H#Ie$NWKnJpcdz07*qo IM6N<$g2$IKwEzGB literal 0 HcmV?d00001 diff --git a/src/com/bytezone/diskbrowser/icons/arrow_refresh_32.png b/src/com/bytezone/diskbrowser/icons/arrow_refresh_32.png new file mode 100644 index 0000000000000000000000000000000000000000..fdf20e26cd11305fd5cb59a633da930067ee30eb GIT binary patch literal 1664 zcmV-`27md9P)+8^?#X=0li+or^58?{(eDn76k ztkFc+79S{&738tJ7FhN<({pF;D&j7?HKdbVc9%PI&v(9a&UelLV|#_HJqRWDOO(=g zDVPUzV+GQ%;I0Lfez_kY^IIXzM9Q2n1!pk3H zJt5yvkhu|dT|G=~-%vbh!$2PC1cV!6U=WQIei1so4!!m|)xgM0Ci>k@emJ3Sd#mf*8;X$3{2SToS7#f?=a7u1dQeG_W}r^ zWHL;e+Cv#{rDV_m%#O;$u`7qr--eT9`~ltfhQrY^(;DGYC{Ovr}0=NjtFN7V#p zeWk~yGU8zR6U;myvf+Xa86GW1!UIvidM9Sh-Gm#Pm*v427KtWH8Ec@>k-OwP9#SE@ z2k=KQ^MJ^P3N~D@A*13!5q#-6Gn%s>%bYfNNV*Hj$h(`Cufft;FXB$?kFa^H5cO&V zRJ!kX0d2Q%m@z4hYMlD`OLLH)k+ek}GT5;3%09IF>H%!FnmuzeVxuFmDj^fb&@kL> zFM-8&NA;XN!+L|Mg+O@V{QDHDnWjj7X(!_;^N^pFRDfH}N3c5KD}4RtT8g1WnJPTh z`(#IwT6walz~!-_YoHGITPomK82%}J@=$_OzGwNN;ct060zd?1~jmsB`@D|--_Qyb&K-tcQ2sTWxS-@*l z4wNv%lY$;nv!00=sY&@wqTYorGR`Erla>R1CRStIa;%UdI%bZa;L!wM&|<7(U_?L5 zJEVx}$OtM~$n;ztb&xAHlx+j3Dc^(Fx#zuJe|!Ou8Y)pO$724(7ce7wqDCz{n6+7I z#gGg>R)|(o?6Z@OzemzmRZH1!fy-Ql&#oQ9L2kTz_*^LX=?{@io>OPzaCR4R z0!XvM&tQF|kFS0nb0ygK0?%*|o*Ii5um=_Np)EO0000< KMNUMnLSTZocMvN8 literal 0 HcmV?d00001 diff --git a/src/com/bytezone/diskbrowser/icons/disk.jpg b/src/com/bytezone/diskbrowser/icons/disk.jpg new file mode 100755 index 0000000000000000000000000000000000000000..cfe1d43e18c8670b6d442573b446f65a45594cde GIT binary patch literal 724 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<ECr+Na zbot8FYu9hwy!G(W<0ns_J%91?)yGetzkL1n{m0K=Ab&A3Fhjfr_ZgbM1cClyVqsxs zVF&q(k*OSrnFU!`6%E;h90S=C3x$=88aYIqCNA7~kW<+>=!0ld(M2vX6_bamA32hj7k>2H{9)Pq!_x~YvJYFWown|_-Fcf&hAxL!<}@3Z#vWey`p5Oz hSG#{KY!2MncF*64<7sK{C%&gF4Mi3<0qp;80sy=j0-OK< literal 0 HcmV?d00001 diff --git a/src/com/bytezone/diskbrowser/icons/disk.png b/src/com/bytezone/diskbrowser/icons/disk.png new file mode 100644 index 0000000000000000000000000000000000000000..d0d400ec6459b70b39a5b97890f9073cfc8a080a GIT binary patch literal 730 zcmV<00ww*4P)RCwBqQ%y)!K^Q%A@7(vE_o>eo zlKmUjqT1WW7A-1i7qkhB2%>1?wjw3+iP)l*e~T7|B(_j=6BfRL5W&ZoFDpqK1Oy*Tyl3hv#|jpA*=#wa#ImpKF3} zB_k{w1p2TvbNac(lHV`ebnNI8oA&vFdixEE#m z(B9EaRfRY{aB08u>TN=G5U%fm>Iu~fDZNdbmHOU z9h4?Uk@4Z?tu@3Rb{yeqNn?ff=Q+xEe7!WAFH^3DIaxi8Xw-#WK@;g0^443+IR+@JOS6zC#@SX4~FY8I^GtMwe&FdGe T&}aG&w2Q&h)z4*}Q$iB}-w;Ve literal 0 HcmV?d00001 diff --git a/src/com/bytezone/diskbrowser/icons/folder_explore_16.png b/src/com/bytezone/diskbrowser/icons/folder_explore_16.png new file mode 100644 index 0000000000000000000000000000000000000000..c830338d89ee2befb52c0cb2a6fd72df21b55218 GIT binary patch literal 815 zcmV+~1JL}5P)RCwBqQ%g(}Q4~Gzz3I>vfg&G6 zfP#obq6UA&Pjq2KapOYW04A_78dgSKx-@ZNh%qrl2^vMCQ8u_Bae*er#28ToiW;;c z0c@!)ls}~{GxNMNmHOLwGczx9?>*<;FB@n%Fl}gYmo#*k!ngnvik~rFO@Aq-3M;~_ zM*(BqD{93AS2W%MmbHYF5a?~L%O{c#jQzqUIp_ke*d`cPaxDUB7zBVuR;;eU7k+N{ZyIVmZl0GC0UCpaq{KojFCo1Os0og8^1@j?5B+ zW3%z{A{XUjdg>7hi_&ExnKEaRbb!C_GiH6E1e}0|tRWW#WqB$6RJX?o0z@Iy6w8oI zTNvnjJ%i_8_9Dtt$x9+5-Hs#sOHo*wYt`3ttY;QwDwSnS-BFsg!ZiR7e+s t410PEEa90aQ2+0`{hw_Ac@F*%U;ua(2cjIGj_Uvb002ovPDHLkV1gnTWOsyvF!PjEOO7VvN^68Wp@tA_!iwAcAos3_)PJ zA=?;(Ze{J-wOiYB&gb);o^Bm>0`Yc|H~r4nukZW3&wKl-2qEx)J_<$O{?f&tXfT!v z%i^mjcuY}Mn9$eFzkA~{ZW#X*0G;BM#^>Jt2GvrpP{x6&;pbOxX_DtD@qZG)1lw~O zPw#t*{%elK{Ktf+@+KImVeEKeVN6k!lcQPHBzx=0-qpvlr;k{(3<%ZDHy0((fQ*PR zVWq;Df7iQz#&%rmRG_=a-pxx3v8Rt3A~2P&iawSBd#NH5-URdoPtv;J&rP@k4_sxv%9>GJ@|J#W#zIZpp@wv}TxTujQ>ZncwgGz21J$61l0X<5b!giU(T z`C}{b8mOFC39J2CnW9-1l>Ew}mlVrVV2xn2p!q9-Q?5WXp5Dd{x1L|?Dj85U7dn30 zi|~O%)K|zW6_YE=Q9ZAk>kPWWMfk3DBKC9_<%L4UnNtUGL2VS3r>0<4^07VbYAV7?C85 z&?jS)k{>EVpy>+M|4@RS#B@Ax?{ci&6T^y?2aqx?hF3+YPsg&ePs79a--Z`oc@yil z2{hHm7}_Z?(9Z4)*+=z|*4ZLNDcei5G?xSBw`kfY+bInAwJA{cdX4_yC zcCV~qZA4^}LZ|m_OtLd(V$ClDXsj+lZz6;Kl!dq$6TG82?T<>^qj&wp<+aej|xXoZfSq0Z2+xtKD z0J~Ajx=wR;8F153BxxX)p>cXHji=e9d~Q`9Lpp8IO~EoWdCgD|?QF-^eUXnH0AjQi z^UJ}*Jt&>ZCPA-+jH0R+zL>C$T4oxt!32sXXb2??o)#DsSg0}VX=K<5;oLUV#ckjdEs#(|>N|A2n2#xz) zHolP|CHPY3O<~vGy{Pu};81vIgKZ%1QljQU5bfwfaqTQ7kL=KXim$MEd}z9)6ki6n zVo`-@H}ioMX(pj$8tIHcVwgEW@v#bKSy6ob%|;AOnvZXH^{vXMKSyeL%d3~RYluh8U_?a{26;)2?i6s7Iit8XmXX zb}f}k_S^o zzp$acu5Rw_KCibf_a8%sG#-!tG`Xy7LDkFvT;5VNOpBs+Rte*^1zSGbi3=atB=3jA z%9ts0;sAtmRw|i9DWeo+Q`8x)*+la`^ym{`H3cGARLr?jaRCsxp0~mvn!jj4{k=ht zo~O{vHHVXUGEPsA$k~puHv*YV#m_zccw@s8i#MZ&st<3^&XzgX!?bqDbAA4zAoFvK_#s;NO2FGv$=3_f|A*R+uNM zV&U2qpD*Cv@BamWd=OJ8Y$k{~>>T~C0OUaUovOaqKv+A#2cXLa3Pg>M#E__HjG#v4=_3lLL<0te1c!kr#0Ah0Q9uM0)ad3w z7=k$Mwr-5B>)LK>yT0!AKerte1WL&8CV$$T|D6AP=l{R|JNI(vn}?sWeAj$Q&~DOe z1Q>K`{8LwBuC09W9R8N&gyU`wqLMw0xLIf{ripVeCNC1Lman;H@Z^<`jq#5v?%@3_gfQGEz6+v#6 z0j4q2j5iEGj&3v$e*qYKP9(ilLfvjfp! zfHEK`S?KUf7)YHUsP*HM@!)y@XvF@ePiI?;P}3Yl2m+i4{SLINIsv}OQ3kA6PPBL1{k2v%qhQG7OoWlZ+&_D^SOhv zMh!7#qRAssssTrHGmdUqv5fqVKu7D|9>0&efLBTpipbQ8sU%3+@mEvCYXzX?wT9Id zrDJiTEleci&`^H{9{Z_}DFe;q@91i^?f1Gnh=hV-Jg0&&Eu8?QUX_T~0zjCuY}Yo+ z*aC&w-C-G#a1du3ZLaE#3*RHZm&{jJwfcbD<&{Jg-2n-4!!X2oe&l;j3*wKT%*Y6r*hMD$n~juRR8Vaj#FZeNP_wx2<6g^*Mga1stl zJy`n9>-YtTFS`Z+nyIht@ywesNefLX+I>-_&xN%67>y*iF_DQ? zO+LSrQ~n*Vn*}-WB^Fk!g}w7v)Sderj&8p)!_+SYqX*7Gc7F>#{AxMQIom7uEyI%x z#1#SLmd?u>anIw92g^#~4yg$)kLv0gw6!^u)c0;4)0$(*OnVN`-TM}Pvag4us~N!v z@P2tL)6ag%(Je_pQnO*nS%h`FU&T3F_tfv-#o;TFL(HA_@XpaA@)6;6ka-zR4UKS7 z-4CyvQzk{ip@g}rsW0P|@|lm69BbVIm%o|X5C|n4ja7<`x%_qZ{;d- z-N9d$cnj7|G8l~3aia=|^?=9iMqS-WoUB?smp1>C=A1~ddTlyN%$aGhcbx*YKZ3;6 zkaQs+`_IOh&))S1%$X^mE@iK5y31uitvBUwzkTvJxT%C9k?YjyGiW<;XoaKha4q>; z$$|;3Ll#r2f(omsPB4nZ*X8{2BOg@Mik^? z=nxb9RAr`kX{jj~Q&b4=AoFJqmy0c8tn0a`W)#ia(*pV&z=}g0!j$eFBH`MZv6C;_ zM+!kZvCE6iVRN}S0tO&OmSxPoyBHB#6rt`6lWF7X&Dzd7@g`C>VB^-k$~V9z0OYVA zwXP0~%gT(MFe(9jLN@diOE1QJbYX%L3bux=WdKCB$%ZXoD=C+IMM_STDo)V=K@e~m zSq-)PhPD0DvDKVK8;h|+XfAq#p-oh=%{hwuw+&!x5O0<=vb^|jvd2wki~pV{E!m)H zFJWEQP|TVJuSolFdhH7nljLU-QT@~g*0+jhz-L>)=t)z#9Z;Y(Znub z*6<^0Gm4Po?8I;#9p6YFM|y3nldL5TE6O$#)>GBj=UrUjYUH0kCXG&7JCK00000NkvXXu0mjfhFE9+ literal 0 HcmV?d00001 diff --git a/src/com/bytezone/diskbrowser/icons/font_delete_32.png b/src/com/bytezone/diskbrowser/icons/font_delete_32.png new file mode 100644 index 0000000000000000000000000000000000000000..eeb7206dfcedb874bf123cdd0e8dc96939dedcde GIT binary patch literal 1816 zcmV+z2j}>SP)0Z~yAX;z6@Sv5psMB|24;|KT=6Gh@5qY;&;sIV>u7R<(#Y(rR3 zi*X6HyQ|gGTIdHYb*3}ZJ8fsCGjs1;-*bB*6|t?Pc#}7ine(1=p8LGd`ZWZ9NS#Yws(bu&7@gMO!J@yrw59nQkn%Q_Cy?G6TPLgZ7h zxlprl->%#IGto1o;=N-Erh8q0Xsw|!@nQquu6l55^$cHb-O7c<_^?UAk?uIWE*X;D zeFKeKE;ay*#ap=huIpyw(1~G$5*lQILlP2r$y!0<(!~TI)I7Ck^L1C2`n=gLeAW?% zEDEqvw1-s`<++eQcbWU50VwpZ$+0`#kKI~56ZJpDkkL7aJO#%g7*8XI$}pq)4~r+- z<3a&QGnTB~dwcCHbVYS^Dk`Zh1BvJtDf(j>Ov`aYa%3->NQf5#fLpX-`>L`+Uu|WX z4@bHZNDW4yB@@JeV6YI0XK*QXf?%;%O{9Yh0brG~^8Z%nuRvo@5-AAi@9scycnE^X zLm&V=tvaGh5UjTOQw>0<-L&_OnmHvlSZrvEsECF8p{9}}2?8L}&H`yb1P9sf0Cd=< zvIN|`yI;86>B_oy*}}`v)R%%pvrl)lj|hJz7+dH)nc&F1nQrQ4Iuodhp*J&pu7#fkdRQek2~FF5rz)q*R?+(Mg6RSB>Y0 zlLa92vTMt&we#?8Fij%k(AnMw;d zfUxB0k9XC~D>IZmoYtYHlQ`Yg5k9tU!z(l%rHD1}*>W&Epkz3UjEv0=&0#|-%Ylr` zGp-Q1iG+9cLtAU|N@g{zUFpa1uts(RgnPR%5DtOkcoO>PG8uYnRkUESoJ3juRunY; z6FRZLiz0OTeMFKED-br-9X);&FsDrdfOYBf`;|2-7Rj(W5sGO>pI3NAVwk3KoVhum zT7jImUV!(|PWWzIjjYNmAvheyxSB{H((*k{9X^Pm!NGS|eATj^2DFI)2(=scynI<{ z$z#iF7NU#ncGP4>tEQV%GXx?@603Z31IkW*fr>vrj%54y7&+PiwZG5E5p#WzuUQJI zyb{fCzX2r>+`i&JEe|jdzY3tJc1>RS&G!fXxUv=lDGR~naqL(lg28}6J=>pwNC_6q z2%o^luU|#QBTpgz>EDs25|J$eCRk*saGDBfdNJhHcjEtVzK+&lXz9A99~yp598%G; zKkch5FNMn6sFpgqJG&63x;JdTZDmGHr^e)pn`Y%bS8(5h)ydE6p#@Kam*unNfyoyM z0a~CJsl%V5=*C+xxckGGXkfW`Ug52OZIR36_Ai)IhQKhO3=E*T`8%}ieff6Uyp?i> z$K*RbPG_~-n}gWrZQwKwdMY(mXJ_Tckx;c0t;n9g7*?KVS2o-E09fq#r5|0pcmW3D zL~Ow(t(y;`%_y2EPRY&Wpz;&NE1J5$n2RQ!| zyQ-Ev^N;=WX3w1F&URDjbVQXQ1cLpvORBBfXf3ZG!X|5L<(N|tpgyQM>h^JsUUbfSLxfL@JE%5CQgu`qSGnSE`vrDb0P?x*h z7+$*^x~ftI&M5)qaO4RrhvklHWYnP?IwjZLv=D5bO5K^JkWKeyZRfxG3P@z8+jiC) z{{hYcpbx5Alad%&;LW1Ntuokp=aB&INo`Es5mKUHb6`jl>;U8O){cJ}lSL{n zGdw_G9{)>QO&tg-ujbk1oq1j_IiE!L!_4^z06$7Qy88OT=CHYJ`D}_3^K+tyiiZyK z^Q>qo3jSJE{#v=mwYqA~Y_^lZf^MQ_&H8S|g-HojqXy`O&F5sBV9o`gR zj3GbgJaIDb`e4E3TgpAoTgr+H;UzZchBBH#wGD+rXz%Q%&4>1{Yiirbh@cqaFg1_S zffUp3Da`Y)_IcLlh{D{lRSc`zH=p!Hw(aZ=9-*(j<|(R86@W7!7?U0fdVO4C{s!~l zVdfQLcL$S2Z8E3fZ@#jdrL>u)9plAD*%D?M&r0+s0R{ka*?3Z61^Cu(v^Re+QC2dv>+ zS85PT*?2b-#mvedY;HLS_$7`VEJS_vUX=JG9u7`nHNK^aZ~?&ubn4vQehQ!F6L>bh zgt^rux?8HS^tc{2srbE?MBl`+!kNc>Pr1%BcU-K**snOggySgm>quuD2$7x6XG!ca zrTWjfX$mm^i4*FjP^WMl=X&{p0KP6Hh-koP=*sTBp2x{MAK^E3Smg6BK-{Dx3nX)I(&_xHP}RoAokKf{z-79-zqTri*W7#Fn_5wN^~FL{ z!wD1>S=gpe*i;=rb8T?v%C-;8s$_$TBobSA^L85R&o3XLixk!g4`&wNca4t8o=XkK zQA|Zf=Ms1|9aEQxnxoin&=>+6lsr221I*_aR|Kt0000k7RCwB)S8Z@q`L{>?WIR zLb5R*d>IqI3RYpTwqr-Jno@97P<|*<+Zp?#?XR{o^=I23b~;o0quLH-S_iP884C=> zsg+KP;(&-i0+K}XLEe`@}mh^^+d! zP5{W&&m8)_th&Dc%;W2^c|$Y2E}+~Ccs=ATYqcC9`_K&_F>4~4$RI{?{>m3PdAf&z z{Bh#V9Y3JgjsFLL8r=ET(5jW;`WK$O9}P7Msw#jppT%+J@;EN{PhujTw!@B`d!b4X z+MB9yZ)+{qEU&`w4Gld*as1;{4@M%f(dfY^o9VUe?Ep|Kb{rmj`my!lEf1_fLkPGQ zN#VC|UqF01167tF$%5`%%pbPwXjt}D=!S_3pBsBR+EHKS!^OTBKJFU9*-!f;lW#uV zQVc@A)$_yGpL%S4JsWSS2ll_$kA26l*jBtQ1*#%LkqCs8Pd))z680K{b19My;&k^Y zvbu?fHrz#5B*By<|8(u6-%E8J{vFvdO97CpckF+?y|wY{J3H24Syo8{Il@4P#-}lrOylAEnxM^D7@wRiw_3gxPIY%4 z&nIRcfT;ZD%dKj#`AjnPkIKKJ-$}>dvROtfM=3 zV`xmnzs`@4=ZYOtR+<;az<2%J=J0&z_&2ZdUg5h8$m5dn@zrs}rqgI^38H;v7;^2} zPPRD@pzNNGkgR&zHh!fEHI(gq=RWe>MR%SLZC+Gaw*MDDg?okKCcf~JBS%m7V|lm& z4dF^is;kXMHuC^vPusRV8`n0H$H2vbB$0}4d#)6CGcOccFL`zF%;>yv{>^K=Czm+G zkx0+!7#K^SHt0oNZ2)Dfw(nuv3jJ|eYWc2~#wz%IGEQBZBGe9gQKCpCN*xdFzxvN% z{QBMN_L`dvFaSjsCF@#-vwkfSM{8pUJ|8ie%fE}3e!~DfmgH({Y6wy?0Ry9PqFKfE zrugBs)hX+Biz^|Ql$^nw8|*!UUXXJI#$qWvxYmm@;t3(U+t@Y(P?%0t3I=L+zXsz@BMDpts_JF^8vQoIt;CEFfxT`Jd36!ejAW1NehZ1%O!|N zUi6GCB-B)TF--j-mC8^#X?<*)0q8VB9s6Q*3Xy0E%jyG8SxI8SgjzHLCB$yugn3D@ z9KFLa%%!u?%XHMlU4rwKWYkcZs5_kenfa5=knNWX_&c6naTG z;cul`^4w>mNF+3fOsYRaHrckEBRJWbOI(Sm_w4cqe7L)*(ncPinJe(xnS!`d4t=-v z768wFwFXBn#i&BQY{`6;9Lf6XatH$W&cA!1C1#O4`}%X)*uXV9rYX7eDed$3yS(k& zj-0*V+vX-Fd~{PIbg~win0Cf5;UIOBH@o-u*+Zz6<1=Rq_xy0nLOSb#G zM|Ff75w9sZ??o<8YAVXfcsbrU(S;imNir}ydE>1Yc9Sh`rRn04jM>3Vb-#TgRrm1r ztSOa^C+4uGC4~A=ITAA(QMKhHXO5_pn-WvZM~Y{FrrJu-Cj;I%-c81*q0c0vV+VF_ zB^$#e$^2X>7d5*KPpzrTo$t&YGnDdTIX{1oWy5+TY7IR{mmT41<^EJfX5OyR2 z)MjhR>rlW;2{(q$lRcP8%)r!UVq*t)eus1qlEk?D79GoZ%qM9MZGH7+CD`0iPpvvs z6|hOFg?OT&#z)~+>^rt{9a78c7>p$8TOf+7Lov+K2&ShqFyceUCyxH`7o=~9Bw9R{ zFZf|X5+tefv^=odzv-o4N~*slP~nA}CuADeI5(KnIZ^p)OloEsqFIypT&E$_%1#d^ zKYH!w+Te%Xq-Ts|>UKX&3P<)bl4_C~B@kW`xc|kiO7-232$yf^%}Ru=$;!+OWTU0000 z1C181;z23I3x$IR4OTBE{tX)c0TY^%*u;~G(W`%f3B@J`JxJ?CQ4ZF46%svQ#YClx zbbmf)9tBy#!AT}>GV^`k&YL$Y?pNlpoG8FjJ_JI9VN~_}uoy%Ups-^CkcI)cHRu9TMbsYihSPBY4!Opd zKp7(H=4BX$)gTCp6ys>B7s9k)QJhO7W-b$`#u(T0wYHRULCnt1s&cukLAP$-fhA3l zAc8Q82}sjm0{kGv<3|s5u2d><&k@?r7FJi+FfcHPM8c+_i@gWHzlWflg6n!{e*A=7 z?lhRIXJA_vawDhE-``IH3A&rWVWt!dxEgCqYo{}wM7!TDED=8 zXb7I?X^s{!sNnlPdXq;`t3AVyjqfN-PU)9W-BI#T`335YB@7Inz|725&0((WbUKad z{L@KRPbLhkW^drZ&1rmf;)^t#0-mf5A{)KNi^bvbAwR$CSMI%}O;c)%-03>U zoa5SlEOSMrQqf{IP)i6-=GvL#C+JC#Q3Hw}ou5&?3hQ89UA^1{fFH2uB(o2+LwamQlo{nway!=H(|DE@ zfTc{>3pRz*$_5}bYmE*oPXtowa{!SMK(xi0*%E+)lP?8515j3M(zmgB96JEms3RAE z2(jgYL~coRbQ96#xm!K=#ox;K%qj3?w!v%SXjouftTRgl`d3$Lz#fqnVU&@GL=t%- zqtz!ht&>Kx6973iHFZ6TqRGhgy|e{9DX?g}X&dGhK|1#C*@dafxmplL29w}1Lv~Lv zfC3w@R;z6bPoMrAC3y{>e)0`Mc0U&tTPW>%6-z;Y3m3j^dtIqiCX@2=m5R`4HnF&P z7Zz2m@B1ij8-(Y17#!Y#UWC{fjYb1^@7+heUf1(d=>XU!;&Oqen6t`xR}_^L$*r`n^uU%K*UMA3S)9u5k(+$9)k%7>2lY`&S)u&vQ|$)ii*Y zkpc#A`*S zZM6a5ClqvQD9^HM@BFSQrL~ zhy|5lvnb|(=K(NMnm970N(M7KJBvHN|3(oI`Wv-HHndF9FxkBM*4rqT%eoa4s*f+K zJq5gotDeOnAV1B`;m-&6G0@kisd#93xWnJR3kQ>|Q5P{4vyNLo-^7O#6GTKo8^9Dm z?6HS|ex+YJQet`OA^zBV5_4xi=&21SzPgG2r5h;r51>WG$5Bi=A_1V**VpGSUHtB7 z5+X*vZNTT=)bkq;^KHqF?brRSbzQl9G5)C;huJ27#sKQm)6<{Pb?1itfiqF1A|Tn} z<=+!pv?n@KIbWvR)^ne*XDe)*4X)F8MB~W@8{2s?%aEUohf)9}bh5zH4X^8LTN*Ua zIvpH@QM0`e@9(ojAfXPU~6FG4Bu?~0YGug!n_<&Q^`BU*Ojz@;j(q!3lK=n!AY({UO#lFTB>(_`g8%^e{{R4h=>PzA zFaQARU;qF*m;eA5Z<1fdMgRZ;K}keGRCwB?l1)fdVHAd+@7}rNjE#d$kycn%i+-rg zRFV)OY#BmC1yQaFYSE%1#9Fi{qAW2f4EmuEGAM?jMYITsC?YCX;vX)`L`Nx0y>qYL z->-$t1zj{j2hQRwo|pH$=Ljh!|5Kr+m(bPmpanPuME{YS1@6{096u+j4&5&fN22M< z`hzsA?7}5fN%&vN{L~*Boc|7 z0bmrnms8Sk zlyowM7F)thZy#G)n(4WIi}Cbi{rn0@Jl73c__G*>!Hi`Qt*K$;aR+OU9YHH7VC(g( z47IiMURllMy%*jo+3#>IE9i`#A*1V8R4cN8^rLP|L;eQ$j6?a#bz xEuD!JIFELpjDgUKr@jvz?(Q7DSl7Pi&j6~85jy-Db9(>)002ovPDHLkV1j<4GNb?i literal 0 HcmV?d00001 diff --git a/src/com/bytezone/diskbrowser/icons/save_delete_32.png b/src/com/bytezone/diskbrowser/icons/save_delete_32.png new file mode 100644 index 0000000000000000000000000000000000000000..92d5d2109b538218ec4b70ff40a34ffb6db91efa GIT binary patch literal 1869 zcmV-T2eSByP)z@;j(q!3lK=n!AY({UO#lFTB>(_`g8%^e{{R4h=>PzA zFaQARU;qF*m;eA5Z<1fdMgRZ?!%0LzRCwCNms^OOXBo$T?{}P?*_qwVo^o(EHHRkF zT63%xi&9G|g%;5oF;Z_t=v4{DlBzeN^v0^CVs9)`1Y6jO1SwI1?Z#$JOjI^$G&YH; zNwdf7Iy*Bv=Wo9EeIGAoGTE37RCb{+41CP#`91&V|NOrhB_e#7$KgXh-u?TYZC^`r z=d)Y4tl!QuqGVFOKQxd`t0m8AZT(`U-VZ1RB$Ht<*x!}@-_OR{%6y^GP$2clyfHTV z+Cjhoc&+!S_ofqjUVr(~=Wso=X%c*TlfqpEjfyqlj~6Vy2R$7;JvPesf#OmCFZ5;j z%+&^$<|AU=1W47U2wb~jfNa(0jnlovl`^}x_X?1?4sg9c*HIG87aH-=kr&>4=k$3l z&uavQ<2WmR)U`vUoS!@Yx3NpbYHXJ@e&fQ5YILOoZ{Ihcd1#?*xjYx+c^;m(@-0!0 zM_*s>;P!oA8cYUS!wr|U0hGcmRgId;LnRVo1IqRB{gnf_u1mILsK>&_ZY#?II7)(Y z%`&lIF|DzSS{(p`L%w3EM8ZJwHUMgwObCL2FkJZ-C|6ytfzie+3$WY?f`A}cg|P7i zFen9X6Xt^f_`XjNs8x`_AQA%#`~N7Q#l!bko$)o! zKq;)XDC^)Tv4D*1?Y$Miip7dW>5HFVhmuXK$vuxBT3hflIE!JX$W zFg=;&rP>})kTE5G)jLZ6*%6#-4$(GH!_~X?-Ff%lZ{7l9Fj}|1Uv6xIXq1Ui5(7<- zfJW&C&u323zvCX#P1 z_SX+zxia4VO=u0FSlb){V8bMm82~?lid`zzDoV`FQLQy7#k(fM{j&6NggR`{uI*Qaa@_>YH)KfODcUWuM0Lt<3!VrQGR|gap7ndz= z3k6>K^DFfE3v~N)?RkqRCgNdYe1`lhC`=1Pld=EmB;N4VGdn-o<+UQl6>B`Pw(S8Y z@bH5U@I%spN2!>*Mfy07+53y1v$5kUuBx}4Zp2}twvHpRo}B7ne_gY-a}uRYD_tyU ze==?ALyviY5o;UhnT2|_Dp*m1MKvloFQbg(On(s{{5-J zoqMc_Z)>lY^d*h~qZHavj)~%iOioNLe6KpcW3Q)&2D@thwOmTS zZJl#J3cBC&5*g$8p|U!5drMjGWhs86yE95vYkvJo-Tv+iv)8_(KJ(>+*KY+WpbzM5 z{c$V2r3}3takyv%&a=YPH z2Jd<%qQIl9;V!}h^=R(0Bi7^x=%v}T9wIh0Qy<+VG?`jb}{Sk1K^`Kcm`AePTgoh?Q65lwSZGwYG;n^ z3jk+avNhe~awoapqn{gO1}NHr&Tbd`SFI^lKTFcp7XU%wgwPq|N2(K99$NabGyXb3 zeJ-u<@L1jak2LEG0E}T%P5$~G0{)9)#S;l`ciJH;uEuU}dEQt4D~5pdv}hx`DE_Wq z^dD>pbVRu7_AYX%Z1Glj`UD^=oG~OoPXv@9_mh7E0TRdq=Wlg@>ufG$(exF7;MPeA zqk|>Xll$Z$grN5c20&2UD(ig)5Tw_kCk5r9Uo)C}s@utk=IRn(0dOHp-cOx8+Nhr^HzMLOlU5{|`+XrE&-36(sj8fwi%*_F zbwxS5{qRC?B)$Y3-T?640Fcq9$)k*_^|BA5LKJi;{_%T!cW|FtwyVl7Vb!V}tjx(q zGFsHIZrlE8^M~6$IojKB!3zLCX~vSR3sPc6P7Dt~tF>Dt6yu>cQA-_J{>RDV@aNr# zj*7&I+(Q@_Hx2SccDqdt(}jzd=nVfTGnB+4 zdQCw=7ZplX&`ufPW-jIih&}yHd**o*?EIL>Cn6#u9M>wYqQ1Ts2Av-5ZEYAIACIQy z7L=5f;GNAIXL|U{Kac@WzdVMycC=AKyWQE7(F(i^`vxQ2@qFZc_5sF@jl-zuC|tXG z9aUA8Sh;*Lc3NU_>FNzsR#vKOV`5{mVdLh~H#fcg74!B30H{_(*lK+&L6;3+n3Y@Z`cQ1cyc8I!q|Y{dz@BO$|CNotQJnj5VuQ5r5os zJq?LI>fIk4T~1kLuY+9fB-eB6zJ=NXaRU?^QIGYqjHae0*c=Y3{UQ{%j1kmoRCN-Y zyLRtJOzaqBW@aEE zAp!eou1hl`!B*3nnVGpaK7IlWk^zNo z(a-b+JIf$Jt}DM%Y?`v{;jlm$$#r%D!7{)XanN2H)wiPD14v0pLfp9V6harm!y^zM zKM|9Xk`ZbOrSQrK3Jiuop`gr}T734*R?=-{9^^!|YfJHOcR;)<0y1zA~H>Kao{@8*-`2OSQ_l|8$6t?@J` zT(3L9peYxJ$VJvchiK(pUc7YK=aS?%jV(|WEZV}KAA!-J2+H(}i zNNufEQ|i)!YxCTU!wJ9}7)hJkdn^54jlTsL0D)5&ae|tUSpWb407*qoM6N<$g0!&x AL;wH) literal 0 HcmV?d00001 diff --git a/src/com/bytezone/diskbrowser/infocom/Abbreviations.java b/src/com/bytezone/diskbrowser/infocom/Abbreviations.java new file mode 100755 index 0000000..3fcf555 --- /dev/null +++ b/src/com/bytezone/diskbrowser/infocom/Abbreviations.java @@ -0,0 +1,74 @@ +package com.bytezone.diskbrowser.infocom; + +import java.util.ArrayList; +import java.util.List; + +import com.bytezone.diskbrowser.applefile.AbstractFile; + +class Abbreviations extends AbstractFile +{ + List list; + Header header; + int dataPtr; + int dataSize; + int tablePtr; + int tableSize; + + public Abbreviations (Header header) + { + super ("Abbreviations", header.buffer); + this.header = header; + + dataPtr = header.getWord (header.abbreviationsTable) * 2; + dataSize = header.abbreviationsTable - dataPtr; + tablePtr = header.abbreviationsTable; + tableSize = header.objectTable - header.abbreviationsTable; + + // prepare hex dump + hexBlocks.add (new HexBlock (dataPtr, dataSize, "Abbreviations data:")); + hexBlocks.add (new HexBlock (tablePtr, tableSize, "Abbreviations table:")); + } + + private void populate () + { + System.out.println ("populating abbreviations"); + list = new ArrayList (); + + for (int i = header.abbreviationsTable; i < header.objectTable; i += 2) + { + int j = header.getWord (i) * 2; + ZString zs = new ZString (buffer, j, header); + list.add (zs); + } + } + + public String getAbbreviation (int abbreviationNumber) + { + if (list == null) + populate (); + return list.get (abbreviationNumber).value; + } + + @Override + public String getText () + { + if (list == null) + populate (); + + StringBuilder text = new StringBuilder (); + +// text.append (String.format ("Data address....%04X %d%n", dataPtr, dataPtr)); +// text.append (String.format ("Data size.......%04X %d%n", dataSize, dataSize)); +// text.append (String.format ("Table address...%04X %d%n", tablePtr, tablePtr)); +// text.append (String.format ("Table size......%04X %d (%d words)%n%n", tableSize, tableSize, +// (tableSize / 2))); + + int count = 0; + for (ZString word : list) + text.append (String.format ("%3d %s%n", count++, word.value)); + if (list.size () > 0) + text.deleteCharAt (text.length () - 1); + + return text.toString (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/infocom/AttributeManager.java b/src/com/bytezone/diskbrowser/infocom/AttributeManager.java new file mode 100644 index 0000000..889e88a --- /dev/null +++ b/src/com/bytezone/diskbrowser/infocom/AttributeManager.java @@ -0,0 +1,85 @@ +package com.bytezone.diskbrowser.infocom; + +import java.util.ArrayList; +import java.util.List; + +import javax.swing.tree.DefaultMutableTreeNode; + +import com.bytezone.diskbrowser.applefile.AbstractFile; +import com.bytezone.diskbrowser.disk.DefaultAppleFileSource; +import com.bytezone.diskbrowser.disk.FormattedDisk; + +class AttributeManager extends AbstractFile +{ + List list = new ArrayList (); + Header header; + + public AttributeManager (String name, byte[] buffer, Header header) + { + super (name, buffer); + this.header = header; + + for (int attrNo = 0; attrNo < 32; attrNo++) + list.add (new Statistic (attrNo)); + } + + public void addNodes (DefaultMutableTreeNode node, FormattedDisk disk) + { + node.setAllowsChildren (true); + + int count = 0; + for (Statistic stat : list) + { + DefaultMutableTreeNode child = + new DefaultMutableTreeNode (new DefaultAppleFileSource (("Attribute " + count++), stat + .getText (), disk)); + node.add (child); + child.setAllowsChildren (false); + } + } + + @Override + public String getText () + { + StringBuilder text = new StringBuilder ("Attribute Frequency\n"); + text.append ("--------- ---------\n"); + + for (Statistic stat : list) + text.append (String.format ("%s%n", stat)); + if (text.length () > 0) + text.deleteCharAt (text.length () - 1); + return text.toString (); + } + + private class Statistic + { + int id; + List list = new ArrayList (); + + public Statistic (int id) + { + this.id = id; + for (ZObject o : header.objectManager) + if (o.attributes.get (id)) + list.add (o); + } + + String getText () + { + StringBuilder text = new StringBuilder ("Objects with attribute " + id + " set:\n\n"); + for (ZObject o : list) + { + text.append (String.format ("%3d %-28s%n", o.id, o.name)); + } + if (text.length () > 0) + text.deleteCharAt (text.length () - 1); + return text.toString (); + } + + @Override + public String toString () + { + return String.format (" %2d %3d", id, list.size ()); + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/infocom/CodeManager.java b/src/com/bytezone/diskbrowser/infocom/CodeManager.java new file mode 100644 index 0000000..91db800 --- /dev/null +++ b/src/com/bytezone/diskbrowser/infocom/CodeManager.java @@ -0,0 +1,167 @@ +package com.bytezone.diskbrowser.infocom; + +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import javax.swing.tree.DefaultMutableTreeNode; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.applefile.AbstractFile; +import com.bytezone.diskbrowser.disk.DefaultAppleFileSource; +import com.bytezone.diskbrowser.disk.FormattedDisk; + +class CodeManager extends AbstractFile +{ + Header header; + int codeSize; + Map routines = new TreeMap (); + + public CodeManager (Header header) + { + super ("Code", header.buffer); + this.header = header; + } + + public void addNodes (DefaultMutableTreeNode root, FormattedDisk disk) + { + root.setAllowsChildren (true); + + codeSize = header.stringPointer - header.highMemory; // should be set by now - do this better! + + int count = 0; + for (Routine routine : routines.values ()) + { + DefaultMutableTreeNode node = + new DefaultMutableTreeNode (new DefaultAppleFileSource (String.format ("%3d %s (%04X)", + ++count, routine.name, routine.startPtr / 2), routine, disk)); + node.setAllowsChildren (false); + root.add (node); + } + } + + public void addMissingRoutines () + { + System.out.printf ("%nWalking the code block%n%n"); + int total = 0; + int ptr = header.highMemory; + while (ptr < header.stringPointer) + { + if (ptr % 2 == 1) // routine must start on a word boundary + ptr++; + + if (containsRoutineAt (ptr)) + { + ptr += getRoutine (ptr).length; + continue; + } + + Routine routine = addRoutine (ptr, 0); + if (routine == null) + { + System.out.printf ("Invalid routine found : %05X%n", ptr); + ptr = findNextRoutine (ptr + 1); + System.out.printf ("skipping to %05X%n", ptr); + if (ptr == 0) + break; + } + else + { + total++; + ptr += routine.length; + } + } + System.out.printf ("%n%d new routines found by walking the code block%n%n", total); + } + + private int findNextRoutine (int address) + { + for (Routine routine : routines.values ()) + if (routine.startPtr > address) + return routine.startPtr; + return 0; + } + + @Override + public String getText () + { + StringBuilder text = new StringBuilder (); + int count = 0; + int nextAddress = header.highMemory; + text.append (" # Address Size Lines Strings Called Calls Gap Pack\n"); + text.append ("--- ------- ---- ----- ------- ------ ----- --- ----\n"); + for (Routine r : routines.values ()) + { + int gap = r.startPtr - nextAddress; + text.append (String.format ( + "%3d %05X %5d %3d %2d %3d %3d %4d %04X%n", ++count, + r.startPtr, r.length, r.instructions.size (), r.strings, r.calledBy.size (), r.calls + .size (), gap, r.startPtr / 2)); + + nextAddress = r.startPtr + r.length; + } + text.deleteCharAt (text.length () - 1); + return text.toString (); + } + + public boolean containsRoutineAt (int address) + { + return (routines.containsKey (address)); + } + + public void addCodeRoutines () + { + List routines = header.objectManager.getCodeRoutines (); + System.out.println ("Adding " + routines.size () + " code routines"); + for (Integer address : routines) + addRoutine (address, 0); + } + + public void addActionRoutines () + { + List routines = header.grammar.getActionRoutines (); + System.out.println ("Adding " + routines.size () + " action routines"); + for (Integer address : routines) + addRoutine (address, 0); + } + + public Routine addRoutine (int address, int caller) + { + if (address == 0) // stack-based call + return null; + if (address > header.fileLength) + return null; + + // check whether we already have this routine + if (routines.containsKey (address)) + { + Routine routine = routines.get (address); + routine.addCaller (caller); + return routine; + } + + // try to create a new Routine + Routine r = new Routine (address, header, caller); + if (r.length == 0) // invalid routine + return null; + + // recursively add all routines called by this one + routines.put (address, r); + for (int ptr : r.calls) + addRoutine (ptr, address); + + return r; + } + + public Routine getRoutine (int address) + { + return routines.get (address); + } + + @Override + public String getHexDump () + { + // this depends on codeSize being set after the strings have been processed + return HexFormatter.format (buffer, header.highMemory, codeSize); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/infocom/Dictionary.java b/src/com/bytezone/diskbrowser/infocom/Dictionary.java new file mode 100755 index 0000000..2dce904 --- /dev/null +++ b/src/com/bytezone/diskbrowser/infocom/Dictionary.java @@ -0,0 +1,241 @@ +package com.bytezone.diskbrowser.infocom; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.applefile.AbstractFile; + +class Dictionary extends AbstractFile +{ + Map dictionary; + int totalEntries; + int totalSeparators; + int dictionaryPtr, dictionarySize; + int entryLength; + Header header; + + // this could be a Google Multimap + Map> synonymList = new TreeMap> (); + + public Dictionary (Header header) + { + super ("Dictionary", header.buffer); + this.header = header; + + dictionaryPtr = header.dictionaryOffset; + dictionary = new TreeMap (); + + totalSeparators = buffer[dictionaryPtr] & 0xFF; + int ptr = dictionaryPtr + totalSeparators + 1; + entryLength = buffer[ptr++] & 0xFF; + + totalEntries = header.getWord (ptr); + + ptr += 2; + int count = 0; + for (int i = 0; i < totalEntries; i++) + { + ZString string = new ZString (buffer, ptr, header); + dictionary.put (ptr, string); + WordEntry wordEntry = new WordEntry (string, count++); + + // add the WordEntry to the appropriate list + List wordEntryList = synonymList.get (wordEntry.key); + if (wordEntryList == null) + { + wordEntryList = new ArrayList (); + synonymList.put (wordEntry.key, wordEntryList); + } + wordEntryList.add (wordEntry); + + // check for words with the property flag + if ((buffer[ptr + 4] & 0x10) != 0) + { + int b1 = buffer[ptr + 5] & 0xFF; + int property = (b1 >= 1 && b1 <= 31) ? b1 : buffer[ptr + 6] & 0xFF; + if (header.propertyNames[property] == null + || header.propertyNames[property].length () > string.value.length ()) + header.propertyNames[property] = string.value; + } + ptr += entryLength; + } + + dictionarySize = totalSeparators + 3 + entryLength * totalEntries; + + for (int i = 1; i < header.propertyNames.length; i++) + if (header.propertyNames[i] == null) + header.propertyNames[i] = i + ""; + } + + public boolean containsWordAt (int address) + { + return dictionary.containsKey (address); + } + + public String wordAt (int address) + { + if (dictionary.containsKey (address)) + return dictionary.get (address).value; + return "dictionary can't find word @ " + address; + } + + public List getVerbs (int value) + { + List words = new ArrayList (); + int ptr = dictionaryPtr + totalSeparators + 4; + + for (ZString word : dictionary.values ()) + { + int b1 = buffer[ptr + 4] & 0xFF; + int b2 = buffer[ptr + 5] & 0xFF; + int b3 = buffer[ptr + 6] & 0xFF; + // mask seems to be 0x40 + if ((b1 == 0x41 && b2 == value) || ((b1 == 0x62 || b1 == 0xC0 || b1 == 0x44) && b3 == value)) + words.add (word.value); + ptr += entryLength; + } + return words; + } + + public List getPrepositions (int value) + { + List words = new ArrayList (); + int ptr = dictionaryPtr + totalSeparators + 4; + + for (ZString word : dictionary.values ()) + { + int b1 = buffer[ptr + 4] & 0xFF; + int b2 = buffer[ptr + 5] & 0xFF; + int b3 = buffer[ptr + 6] & 0xFF; + // mask seems to be 0x08 + if (((b1 == 0x08 || b1 == 0x18 || b1 == 0x48) && b2 == value) + || ((b1 == 0x1B || b1 == 0x0C || b1 == 0x2A) && b3 == value)) + words.add (word.value); + ptr += entryLength; + } + return words; + } + + @Override + public String getHexDump () + { + StringBuilder text = new StringBuilder (); + text.append (HexFormatter.format (buffer, dictionaryPtr, dictionarySize)); + return text.toString (); + } + + @Override + public String getText () + { + StringBuilder text = new StringBuilder (); + +// text.append (String.format ("Total entries : %,6d%n", totalEntries)); +// text.append (String.format ("Separators : %,6d%n", totalSeparators)); +// text.append (String.format ("Offset : %,6d %04X%n%n", dictionaryPtr, dictionaryPtr)); + + int count = 0; + int ptr = dictionaryPtr + totalSeparators + 4; + + for (ZString word : dictionary.values ()) + { + text.append (String.format ("%04X %3d %-6s %s", ptr, count++, word.value, HexFormatter + .getHexString (buffer, ptr + 4, entryLength - 4))); + int b1 = buffer[ptr + 4] & 0xFF; + int b2 = buffer[ptr + 5] & 0xFF; + int b3 = buffer[ptr + 6] & 0xFF; + if (b1 == 65) + text.append (String.format (" %3d%n", b2)); + else if (b1 == 98 || b1 == 0xC0 || b1 == 0x44) + text.append (String.format (" %3d%n", b3)); + else + text.append ("\n"); + ptr += entryLength; + } + + if (true) + { + int lastValue = 0; + for (List list : synonymList.values ()) + { + WordEntry wordEntry = list.get (0); + if (wordEntry.value != lastValue) + { + lastValue = wordEntry.value; + text.append ("\n"); + } + + if (wordEntry.value == 0x80) // nouns are all in one entry + { + for (WordEntry we : list) + text.append (we + "\n"); + text.deleteCharAt (text.length () - 1); + } + else + text.append (wordEntry); + if ((buffer[wordEntry.word.startPtr + 4] & 0x10) != 0) + text.append (" property"); + text.append ("\n"); + } + } + + text.deleteCharAt (text.length () - 1); + return text.toString (); + } + + private class WordEntry implements Comparable + { + ZString word; + int seq; + int value; + int key; + String bits; + + public WordEntry (ZString word, int seq) + { + this.word = word; + this.seq = seq; + + // build key from 3 bytes following the word characters + int b1 = buffer[word.startPtr + 4] & 0xFF; + int b2 = buffer[word.startPtr + 5] & 0xFF; + int b3 = buffer[word.startPtr + 6] & 0xFF; + + this.key = (b1 << 16) | (b2 << 8) | b3; + this.value = b1; + this.bits = Integer.toBinaryString (b1); + if (bits.length () < 8) + bits = "00000000".substring (bits.length ()) + bits; + } + + @Override + public int compareTo (WordEntry o) + { + return this.value - o.value; + } + + @Override + public String toString () + { + StringBuilder list = new StringBuilder ("["); + if ((key & 0x0800000) == 0) + { + for (WordEntry we : synonymList.get (key)) + list.append (we.word.value + ", "); + list.deleteCharAt (list.length () - 1); + list.deleteCharAt (list.length () - 1); + } + else + list.append (word.value); + list.append ("]"); + + StringBuilder text = new StringBuilder (); + text.append (String.format ("%04X %3d %-6s %s %s %s", word.startPtr, seq, + word.value, bits, HexFormatter + .getHexString (buffer, word.startPtr + 4, entryLength - 4), list.toString ())); + return text.toString (); + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/infocom/Globals.java b/src/com/bytezone/diskbrowser/infocom/Globals.java new file mode 100644 index 0000000..f69d814 --- /dev/null +++ b/src/com/bytezone/diskbrowser/infocom/Globals.java @@ -0,0 +1,44 @@ +package com.bytezone.diskbrowser.infocom; + +import com.bytezone.diskbrowser.applefile.AbstractFile; + +class Globals extends AbstractFile +{ + static final int TOTAL_GLOBALS = 240; + Header header; + int globalsPtr, globalsSize; + int arrayPtr, arraySize; + + public Globals (Header header) + { + super ("Globals", header.buffer); + this.header = header; + + globalsPtr = header.globalsOffset; + globalsSize = TOTAL_GLOBALS * 2; + arrayPtr = globalsPtr + globalsSize; + arraySize = header.staticMemory - arrayPtr; + + // add entries for AbstractFile.getHexDump () + hexBlocks.add (new HexBlock (globalsPtr, globalsSize, "Globals:")); + hexBlocks.add (new HexBlock (arrayPtr, arraySize, "Arrays:")); + } + + @Override + public String getText () + { + StringBuilder text = new StringBuilder (); + for (int i = 1; i <= TOTAL_GLOBALS; i++) + { + int value = header.getWord (globalsPtr + i * 2); + text.append (String.format ("G%03d %04X ", i, value)); + int address = value * 2; + if (address >= header.stringPointer && address < header.fileLength) + text.append (header.stringManager.stringAt (address) + "\n"); + else + text.append (String.format ("%,6d%n", value)); + } + text.deleteCharAt (text.length () - 1); + return text.toString (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/infocom/Grammar.java b/src/com/bytezone/diskbrowser/infocom/Grammar.java new file mode 100644 index 0000000..8bcf191 --- /dev/null +++ b/src/com/bytezone/diskbrowser/infocom/Grammar.java @@ -0,0 +1,331 @@ +package com.bytezone.diskbrowser.infocom; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.applefile.AbstractFile; + +class Grammar extends AbstractFile +{ + private static final int SENTENCE_LENGTH = 8; + Header header; + int indexPtr, indexSize; + int tablePtr, tableSize; + int actionPtr, actionSize; + int preActionPtr, preActionSize; + int prepositionPtr, prepositionSize; + int indexEntries; + int totalPrepositions; + int padding; + + List sentenceGroups = new ArrayList (); + Map> actionList = new TreeMap> (); + + List actionRoutines = new ArrayList (); + List preActionRoutines = new ArrayList (); + + public Grammar (String name, byte[] buffer, Header header) + { + super (name, buffer); + this.header = header; + + indexPtr = header.staticMemory; // start of the index + tablePtr = header.getWord (indexPtr); // start of the data (end of the index) + indexSize = tablePtr - indexPtr; + indexEntries = indexSize / 2; + + padding = getPadding (); + + int lastEntry = header.getWord (tablePtr - 2); // address of the last data entry + tableSize = lastEntry + getRecordLength (lastEntry) - tablePtr; // uses padding + actionPtr = tablePtr + tableSize; // start of the action routines + actionSize = getTotalActions () * 2; // uses padding + + preActionSize = actionSize; + preActionPtr = actionPtr + actionSize; + prepositionPtr = preActionPtr + preActionSize; + + totalPrepositions = header.getWord (prepositionPtr); + prepositionSize = totalPrepositions * 4 + 2; + + if (false) + { + System.out.printf ("indexPtr %,8d %4X%n", indexPtr, indexPtr); + System.out.printf ("indexSize %,8d%n", indexSize); + System.out.printf ("indexEntries %,8d%n", indexEntries); + System.out.printf ("tablePtr %,8d %4X%n", tablePtr, tablePtr); + System.out.printf ("tableSize %,8d%n", tableSize); + System.out.printf ("actionPtr %,8d %4X%n", actionPtr, actionPtr); + System.out.printf ("actionSize %,8d%n", actionSize); + System.out.printf ("actionEntries %,8d%n", actionSize / 2); + System.out.printf ("preActionPtr %,8d %4X%n", preActionPtr, preActionPtr); + System.out.printf ("preActionSize %,8d%n", preActionSize); + System.out.printf ("prepPtr %,8d %4X%n", prepositionPtr, prepositionPtr); + System.out.printf ("prepSize %,8d%n", prepositionSize); + System.out.printf ("totPreps %,8d%n", totalPrepositions); + } + + // add entries for AbstractFile.getHexDump () + hexBlocks.add (new HexBlock (indexPtr, indexSize, "Index:")); + hexBlocks.add (new HexBlock (tablePtr, tableSize, "Grammar data:")); + hexBlocks.add (new HexBlock (actionPtr, actionSize, "Action routines:")); + hexBlocks.add (new HexBlock (preActionPtr, preActionSize, "Pre-action routines:")); + hexBlocks.add (new HexBlock (prepositionPtr, prepositionSize, "Preposition table:")); + + // create SentenceGroup and Sentence objects and action lists + int count = 255; + for (int i = 0; i < indexEntries; i++) + { + int offset = header.getWord (indexPtr + i * 2); + SentenceGroup sg = new SentenceGroup (count--, offset); + sentenceGroups.add (sg); + for (Sentence sentence : sg) + { + // add to hashmap + if (!actionList.containsKey (sentence.actionId)) + actionList.put (sentence.actionId, new ArrayList ()); + actionList.get (sentence.actionId).add (sentence); + // add to pre-action routine list + if (sentence.preActionRoutine > 0 + && !preActionRoutines.contains (sentence.preActionRoutine)) + preActionRoutines.add (sentence.preActionRoutine); + // add to action routine list + if (sentence.actionRoutine > 0 && !actionRoutines.contains (sentence.actionRoutine)) + actionRoutines.add (sentence.actionRoutine); + } + } + Collections.sort (actionRoutines); + Collections.sort (preActionRoutines); + } + + private int getPadding () + { + // calculate record padding size (Zork has 1 byte padding, Planetfall has 0) + int r1 = header.getWord (indexPtr); + int r2 = header.getWord (indexPtr + 2); + int sentences = header.getByte (r1); + return r2 - r1 - (sentences * SENTENCE_LENGTH) - 1; + } + + private int getRecordLength (int recordPtr) + { + return (buffer[recordPtr] & 0xFF) * SENTENCE_LENGTH + padding + 1; + } + + private int getTotalActions () + { + // loop through each record in each index entry, and find the highest action number + int ptr = tablePtr; + int highest = 0; + for (int i = 0; i < indexEntries; i++) + { + int totSentences = buffer[ptr++]; + for (int j = 0; j < totSentences; j++) + { + int val = buffer[ptr + 7] & 0xFF; + if (val > highest) + highest = val; + ptr += SENTENCE_LENGTH; + } + ptr += padding; // could be zero or one + } + return highest + 1; // zero-based, so increment it + } + + public List getActionRoutines () + { + List routines = new ArrayList (); + routines.addAll (actionRoutines); + routines.addAll (preActionRoutines); + return routines; + } + + @Override + public String getText () + { + String line = + "-----------------------------------------------------" + + "-----------------------------------------------------------\n"; + StringBuilder text = + new StringBuilder (sentenceGroups.size () + " Grammar tables\n==================\n\n"); + + // add the sentences in their original SentenceGroup sequence + for (SentenceGroup sg : sentenceGroups) + text.append (sg + "\n" + line); + + text.append ("\n" + actionList.size () + " Action groups\n=================\n\n"); + + // add the sentences in their actionId sequence + for (List list : actionList.values ()) + { + for (Sentence sentence : list) + text.append (sentence + "\n"); + text.append (line); + } + + text.append ("\n" + preActionRoutines.size () + + " Pre-action routines\n======================\n\n"); + + // add sentences in pre-action routine sequence + for (Integer routine : preActionRoutines) + { + for (Sentence sentence : getSentences (routine)) + text.append (sentence + "\n"); + text.append (line); + } + + text.append ("\n" + actionRoutines.size () + " Action routines\n===================\n\n"); + + // add sentences in action routine sequence + for (Integer routine : actionRoutines) + { + for (Sentence sentence : getSentences (routine)) + text.append (sentence + "\n"); + text.append (line); + } + + text.append ("\n" + totalPrepositions + " Prepositions\n===============\n\n"); + text.append (HexFormatter.getHexString (buffer, prepositionPtr, 2) + "\n"); + for (int i = 0, ptr = prepositionPtr + 2; i < totalPrepositions; i++, ptr += 4) + { + text.append (HexFormatter.getHexString (buffer, ptr, 4) + " "); + int id = header.getByte (ptr + 3); + List preps = header.dictionary.getPrepositions (id); + String prepString = makeWordBlock (preps); + text.append (prepString + "\n"); + } + + text.deleteCharAt (text.length () - 1); + return text.toString (); + } + + private List getSentences (int routine) + { + List sentences = new ArrayList (); + + for (SentenceGroup sg : sentenceGroups) + for (Sentence s : sg.sentences) + if (s.actionRoutine == routine || s.preActionRoutine == routine) + sentences.add (s); + + return sentences; + } + + private String makeWordBlock (List words) + { + StringBuilder text = new StringBuilder ("["); + if (words.size () > 0) + { + for (String word : words) + text.append (word + ", "); + text.deleteCharAt (text.length () - 1); + text.deleteCharAt (text.length () - 1); + } + else + text.append ("** not found **"); + text.append ("]"); + return text.toString (); + } + + private class SentenceGroup implements Iterable + { + int startPtr; + int id; + List sentences = new ArrayList (); + String verbString; // list of synonyms inside [] + + public SentenceGroup (int id, int ptr) + { + this.startPtr = ptr; + this.id = id; + + int records = buffer[ptr] & 0xFF; + verbString = makeWordBlock (header.dictionary.getVerbs (id)); + + for (int j = 0, offset = startPtr + 1; j < records; j++, offset += SENTENCE_LENGTH) + sentences.add (new Sentence (offset, this)); + } + + @Override + public String toString () + { + StringBuilder text = new StringBuilder (); + for (Sentence sentence : sentences) + text.append (sentence + "\n"); + text.deleteCharAt (text.length () - 1); + return text.toString (); + } + + @Override + public Iterator iterator () + { + return sentences.iterator (); + } + } + + private class Sentence + { + int startPtr; + SentenceGroup parent; + int actionId; + int actionRoutine; // mandatory + int preActionRoutine; // optional + String sentenceText; + + public Sentence (int ptr, SentenceGroup parent) + { + this.startPtr = ptr; + this.parent = parent; + + // byte 0 contains the number of objects in the sentence + int totObjects = buffer[ptr++] & 0xFF; // 0-2 + + // build the sentence text from bytes 1-2 + StringBuilder sentence = new StringBuilder (); + for (int k = 0; k < totObjects; k++) + { + int b = buffer[ptr++] & 0xFF; + if (b > 0) + sentence.append (" " + getPrep (b)); + sentence.append (" OBJ"); + } + sentenceText = sentence.toString (); + + // do something with bytes 3-6 + // ... what that is I have no idea + + // get action pointer from byte 7 + actionId = buffer[startPtr + 7] & 0xFF; + int targetOffset = actionId * 2; // index into the action and pre-action blocks + actionRoutine = header.getWord (actionPtr + targetOffset) * 2; + preActionRoutine = header.getWord (preActionPtr + targetOffset) * 2; + } + + private String getPrep (int value) + { + int offset = prepositionPtr + 2 + (totalPrepositions - (255 - value) - 1) * 4; + int address = header.getWord (offset); + return header.dictionary.wordAt (address); + } + + private String getText () + { + return parent.verbString + " " + sentenceText; + } + + @Override + public String toString () + { + StringBuilder text = new StringBuilder (String.format ("%3d %04X ", parent.id, startPtr)); + text.append (HexFormatter.getHexString (buffer, startPtr, SENTENCE_LENGTH)); + String r1 = preActionRoutine == 0 ? "" : String.format ("R:%05X", preActionRoutine); + text.append (String.format (" %-7s R:%05X %s", r1, actionRoutine, getText ())); + return text.toString (); + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/infocom/Header.java b/src/com/bytezone/diskbrowser/infocom/Header.java new file mode 100755 index 0000000..ababa29 --- /dev/null +++ b/src/com/bytezone/diskbrowser/infocom/Header.java @@ -0,0 +1,136 @@ +package com.bytezone.diskbrowser.infocom; + +import java.io.File; + +import com.bytezone.diskbrowser.applefile.AbstractFile; + +class Header extends AbstractFile +{ + final String[] propertyNames = new String[32]; + + File file; + int version; + int highMemory; + int programCounter; + int dictionaryOffset; + int objectTable; + int globalsOffset; + int staticMemory; + int abbreviationsTable; + int fileLength; + int checksum; + int stringPointer; + + Abbreviations abbreviations; + Dictionary dictionary; + ObjectManager objectManager; + StringManager stringManager; + CodeManager codeManager; + Globals globals; + Grammar grammar; + + public Header (String name, byte[] buffer, File file) + { + super (name, buffer); + this.file = file; + + version = getByte (0); + highMemory = getWord (4); + programCounter = getWord (6); + dictionaryOffset = getWord (8); + objectTable = getWord (10); + globalsOffset = getWord (12); + staticMemory = getWord (14); + abbreviationsTable = getWord (24); + checksum = getWord (28); + fileLength = getWord (26) * 2; + + if (fileLength == 0) + fileLength = buffer.length; + + // do the basic managers + abbreviations = new Abbreviations (this); + dictionary = new Dictionary (this); + globals = new Globals (this); // may display ZStrings + + // set up an empty object to store Routines in + codeManager = new CodeManager (this); + + grammar = new Grammar ("Grammar", buffer, this); + + // add all the ZObjects, and analyse them to find stringPtr, DICT etc. + objectManager = new ObjectManager (this); + + // add all the ZStrings + stringManager = new StringManager ("Strings", buffer, this); + + codeManager.addRoutine (programCounter - 1, 0); + codeManager.addActionRoutines (); // obtained from Grammar + codeManager.addCodeRoutines (); // obtained from Object properties + codeManager.addMissingRoutines (); // requires stringPtr to be set + + // add entries for AbstractFile.getHexDump () + hexBlocks.add (new HexBlock (0, 64, "Header data:")); + } + + public String getAbbreviation (int index) + { + return abbreviations.getAbbreviation (index); + } + + public boolean containsWordAt (int address) + { + return dictionary.containsWordAt (address); + } + + public String wordAt (int address) + { + return dictionary.wordAt (address); + } + + @Override + public String getText () + { + StringBuilder text = new StringBuilder (); + + text.append (String.format ("Disk name %s%n", file.getName ())); + text.append (String.format ("Version %d%n", version)); + text.append ("\nDynamic memory:\n"); + text.append (String.format (" Abbreviation table %04X %,6d%n", abbreviationsTable, + abbreviationsTable)); + text.append (String.format (" Objects table %04X %,6d%n", objectTable, objectTable)); + text.append (String.format (" Global variables %04X %,6d%n", globalsOffset, + globalsOffset)); + + text.append ("\nStatic memory:\n"); + text.append (String + .format (" Grammar table etc %04X %,6d%n", staticMemory, staticMemory)); + text.append (String.format (" Dictionary %04X %,6d%n", dictionaryOffset, + dictionaryOffset)); + text.append ("\nHigh memory:\n"); + text.append (String.format (" ZCode %04X %,6d%n", highMemory, highMemory)); + text.append (String.format (" Program counter %04X %,6d%n", programCounter, + programCounter)); + text.append (String.format ("\nFile length %05X %,6d%n", fileLength, fileLength)); + text.append (String.format ("Checksum %04X %,6d%n", checksum, checksum)); + text.append (String.format ("%nZString offset %05X %,6d%n", stringPointer, + stringPointer)); + + text.append (String.format ("Total strings %d%n", stringManager.strings + .size ())); + text.append (String.format ("Total objects %d%n", objectManager.list + .size ())); + + return text.toString (); + } + + int getByte (int offset) + { + return buffer[offset] & 0xFF; + } + + int getWord (int offset) + { + return ((buffer[offset] << 8) & 0xFF00) | ((buffer[offset + 1]) & 0xFF); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/infocom/InfocomDisk.java b/src/com/bytezone/diskbrowser/infocom/InfocomDisk.java new file mode 100755 index 0000000..74f6654 --- /dev/null +++ b/src/com/bytezone/diskbrowser/infocom/InfocomDisk.java @@ -0,0 +1,265 @@ +package com.bytezone.diskbrowser.infocom; + +import static java.lang.System.out; + +import java.awt.Color; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.List; + +import javax.swing.JOptionPane; +import javax.swing.tree.DefaultMutableTreeNode; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.applefile.AppleFileSource; +import com.bytezone.diskbrowser.disk.AbstractFormattedDisk; +import com.bytezone.diskbrowser.disk.AppleDisk; +import com.bytezone.diskbrowser.disk.DefaultAppleFileSource; +import com.bytezone.diskbrowser.disk.Disk; +import com.bytezone.diskbrowser.disk.DiskAddress; +import com.bytezone.diskbrowser.disk.SectorType; +import com.bytezone.diskbrowser.gui.DataSource; + +public class InfocomDisk extends AbstractFormattedDisk +{ + private static final int BLOCK_SIZE = 256; + private static final boolean TYPE_NODE = true; + private static final boolean TYPE_LEAF = false; + private byte[] data; + int version; + Header header; + + Color green = new Color (0, 200, 0); + + SectorType bootSector = new SectorType ("Boot code", Color.lightGray); + SectorType stringsSector = new SectorType ("Strings", Color.magenta); + SectorType objectsSector = new SectorType ("Objects", green); + SectorType dictionarySector = new SectorType ("Dictionary", Color.blue); + SectorType abbreviationsSector = new SectorType ("Abbreviations", Color.red); + SectorType codeSector = new SectorType ("Code", Color.orange); + SectorType headerSector = new SectorType ("Header", Color.cyan); + SectorType globalsSector = new SectorType ("Globals", Color.darkGray); + SectorType grammarSector = new SectorType ("Grammar", Color.gray); + + public InfocomDisk (Disk disk) + { + super (disk); + + setSectorTypes (); + + data = disk.readSector (3, 0); // read first sector to get file size + data = getBuffer (getWord (26) * 2); // read entire file into data buffer + + if (false) + createStoryFile ("Zork1.sf"); + + DefaultMutableTreeNode root = getCatalogTreeRoot (); + DefaultMutableTreeNode codeNode = null; + DefaultMutableTreeNode objectNode = null; + + header = new Header ("Header", data, disk.getFile ()); + + addToTree (root, "Header", header, TYPE_LEAF); + addToTree (root, "Abbreviations", header.abbreviations, TYPE_LEAF); + + objectNode = addToTree (root, "Objects", header.objectManager, TYPE_NODE); + header.objectManager.addNodes (objectNode, this); + addToTree (root, "Globals", header.globals, TYPE_LEAF); + addToTree (root, "Grammar", header.grammar, TYPE_LEAF); + addToTree (root, "Dictionary", header.dictionary, TYPE_LEAF); + codeNode = addToTree (root, "Code", header.codeManager, TYPE_NODE); + header.codeManager.addNodes (codeNode, this); + + addToTree (root, "Strings", header.stringManager, TYPE_LEAF); + + PropertyManager pm = new PropertyManager ("Properties", data, header); + pm.addNodes (addToTree (objectNode, "Properties", pm, TYPE_NODE), this); + + AttributeManager am = new AttributeManager ("Attributes", data, header); + am.addNodes (addToTree (objectNode, "Attributes", am, TYPE_NODE), this); + + sectorTypes[48] = headerSector; + + setSectors (header.abbreviationsTable, header.objectTable, abbreviationsSector); + setSectors (header.objectTable, header.globalsOffset, objectsSector); + setSectors (header.globalsOffset, header.staticMemory, globalsSector); + setSectors (header.staticMemory, header.dictionaryOffset, grammarSector); + setSectors (header.dictionaryOffset, header.highMemory, dictionarySector); + setSectors (header.highMemory, header.stringPointer, codeSector); + setSectors (header.stringPointer, header.fileLength, stringsSector); + } + + private void setSectorTypes () + { + sectorTypesList.add (bootSector); + sectorTypesList.add (headerSector); + sectorTypesList.add (abbreviationsSector); + sectorTypesList.add (objectsSector); + sectorTypesList.add (globalsSector); + sectorTypesList.add (grammarSector); + sectorTypesList.add (dictionarySector); + sectorTypesList.add (codeSector); + sectorTypesList.add (stringsSector); + + for (int track = 0; track < 3; track++) + for (int sector = 0; sector < 16; sector++) + if (!disk.isSectorEmpty (track, sector)) + sectorTypes[track * 16 + sector] = bootSector; + } + + private void setSectors (int sectorFrom, int sectorTo, SectorType type) + { + int blockNo = sectorFrom / disk.getBlockSize () + 48; + int blockTo = sectorTo / disk.getBlockSize () + 48; + while (blockNo <= blockTo) + { + if (!disk.isSectorEmpty (blockNo)) + sectorTypes[blockNo] = type; + blockNo++; + } + } + + private int getFileSize () + { + byte[] buffer = null; + int startBlock = getWord (4) / 256 + 48; + int fileSize = 0; + for (DiskAddress da : disk) + { + if (da.getBlock () > startBlock && disk.isSectorEmpty (da)) + { + out.println ("Empty : " + da); + buffer = disk.readSector (da.getBlock () - 1); + fileSize = (da.getBlock () - 48) * disk.getBlockSize (); + break; + } + } + int ptr = 255; + while (buffer[ptr--] == 0) + fileSize--; + return fileSize; + } + + private byte[] getBuffer (int fileSize) + { + if (fileSize == 0) + fileSize = getFileSize (); + System.out.println ("File size : " + fileSize); + data = new byte[fileSize]; + + for (int track = 3, ptr = 0; track < 35; track++) + for (int sector = 0; sector < 16; sector++, ptr += BLOCK_SIZE) + { + byte[] temp = disk.readSector (track, sector); + int spaceLeft = fileSize - ptr; + if (spaceLeft <= BLOCK_SIZE) + { + System.arraycopy (temp, 0, data, ptr, spaceLeft); + return data; + } + System.arraycopy (temp, 0, data, ptr, BLOCK_SIZE); + } + return data; + } + + private DefaultMutableTreeNode addToTree (DefaultMutableTreeNode root, String title, + DataSource af, boolean allowsChildren) + { + DefaultMutableTreeNode node = + new DefaultMutableTreeNode (new DefaultAppleFileSource (title, af, this)); + node.setAllowsChildren (allowsChildren); + root.add (node); + return node; + } + + public List getFileSectors (int fileNo) + { + return null; + } + + @Override + public AppleFileSource getCatalog () + { + return new DefaultAppleFileSource (header.getText (), this); + } + + public static boolean isCorrectFormat (AppleDisk disk) + { + disk.setInterleave (2); + return checkFormat (disk); + } + + public static boolean checkFormat (AppleDisk disk) + { + byte[] buffer = disk.readSector (3, 0); + + int version = buffer[0] & 0xFF; + int highMemory = HexFormatter.intValue (buffer[5], buffer[4]); + int programCounter = HexFormatter.intValue (buffer[7], buffer[6]); + int dictionary = HexFormatter.intValue (buffer[9], buffer[8]); + int objectTable = HexFormatter.intValue (buffer[11], buffer[10]); + int globals = HexFormatter.intValue (buffer[13], buffer[12]); + int staticMemory = HexFormatter.intValue (buffer[15], buffer[14]); + int abbreviationsTable = HexFormatter.intValue (buffer[25], buffer[24]); + int fileLength = HexFormatter.intValue (buffer[27], buffer[26]); + + if (false) + { + System.out.printf ("Version %,6d%n", version); + System.out.printf ("Abbreviations %,6d%n", abbreviationsTable); + System.out.printf ("Objects %,6d%n", objectTable); + System.out.printf ("Globals %,6d%n", globals); + System.out.printf ("Static memory %,6d%n", staticMemory); + System.out.printf ("Dictionary %,6d%n", dictionary); + System.out.printf ("High memory %,6d%n", highMemory); + System.out.printf ("Program counter %,6d%n", programCounter); + System.out.printf ("File length %,6d%n", fileLength); + } + + if (abbreviationsTable >= objectTable) + return false; + if (objectTable >= globals) + return false; + if (globals >= staticMemory) + return false; + if (staticMemory >= dictionary) + return false; + if (dictionary >= highMemory) + return false; +// if (highMemory > programCounter) +// return false; + + if (version < 2 || version > 3) + { + System.out.println ("Incorrect format : " + version); + JOptionPane + .showMessageDialog (null, "This appears to be an Infocom disk," + " but version " + + version + " is not supported", "Unknown disk format", + JOptionPane.INFORMATION_MESSAGE); + return false; + } + + return true; + } + + private int getWord (int offset) + { + return (((data[offset] << 8) & 0xFF00) | ((data[offset + 1]) & 0xFF)); + } + + private void createStoryFile (String fileName) + { + File f = new File (fileName); + try + { + FileOutputStream fos = new FileOutputStream (f); + fos.write (data); + fos.close (); + } + catch (IOException e) + { + e.printStackTrace (); + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/infocom/Instruction.java b/src/com/bytezone/diskbrowser/infocom/Instruction.java new file mode 100755 index 0000000..f4eacac --- /dev/null +++ b/src/com/bytezone/diskbrowser/infocom/Instruction.java @@ -0,0 +1,493 @@ +package com.bytezone.diskbrowser.infocom; + +import java.util.ArrayList; +import java.util.List; + +import com.bytezone.diskbrowser.HexFormatter; + +class Instruction +{ + Opcode opcode; + int startPtr; + byte[] buffer; + // List abbreviations; + Header header; + + static final String[] name2OP = + { "*bad*", "je", "jl", "jg", "dec_chk", "inc_chk", "jin", "test", "or", "and", "test_attr", + "set_attr", "clear_attr", "store", "insert_obj", "loadw", "loadb", "get_prop", + "get_prop_addr", "get_next_prop", "add", "sub", "mul", "div", "mod", "call_2s", + "call_2n", "set_colour", "throw", "*bad*", "*bad*", "*bad*" }; + static final String[] name1OP = + { "jz", "get_sibling", "get_child", "get_parent", "get_prop_len", "inc", "dec", + "print_addr", "call_ls", "remove_obj", "print_obj", "ret", "jump", "print_paddr", "load", + "not" }; + static final String[] name0OP = + { "rtrue", "rfalse", "print", "print_ret", "nop", "save", "restore", "restart", + "ret_popped", "pop", "quit", "new_line", "show_status", "verify", "", "piracy" }; + static final String[] nameVAR = + { "call", "storew", "storeb", "put_prop", "sread", "print_char", "print_num", "random", + "push", "pull", "split_window", "set_window", "call_vs2", "erase_window", "erase_line", + "set_cursor", "get_cursor", "set_text_style", "buffer_mode", "output_stream", + "input_stream", "sound_effect", "read_char", "scan_table", "not", "call_vn", "call_vn2", + "tokenise", "encode_text", "copy_table", "print_table", "check_arg" }; + + public Instruction (byte[] buffer, int ptr, Header header) + { + this.buffer = buffer; + this.startPtr = ptr; + this.header = header; + byte b1 = buffer[ptr]; + + // long + if ((b1 & 0x80) == 0) + opcode = new Opcode2OPLong (buffer, ptr); + // short + else if ((b1 & 0x40) == 0) + { + if ((b1 & 0x30) == 0x30) + opcode = new Opcode0OP (buffer, ptr); + else + opcode = new Opcode1OP (buffer, ptr); + } + // variable + else + { + if ((b1 & 0x20) == 0) + opcode = new Opcode2OPVar (buffer, ptr); + else + opcode = new OpcodeVar (buffer, ptr); + } + } + + public int length () + { + return opcode.length (); + } + + public boolean isReturn () + { + return opcode.isReturn; + } + + public boolean isPrint () + { + return opcode.string != null; + } + + public boolean isCall () + { + return opcode.isCall; + } + + public boolean isJump () + { + // could use jumpTarget != 0 + return (opcode instanceof Opcode1OP && opcode.opcodeNumber == 12); + } + + public boolean isBranch () + { + return opcode.branch != null; + } + + public boolean isStore () + { + return opcode.store != null; + } + + public int target () + { + return isBranch () ? opcode.branch.target : 0; + } + + @Override + public String toString () + { + int max = opcode.length (); + String extra = ""; + if (max > 9) + { + max = 9; + extra = ".."; + } + String hex = HexFormatter.getHexString (buffer, startPtr, max); + return String.format ("%-26s%2s %s", hex, extra, opcode.toString ()); + } + + abstract class Opcode + { + int opcodeNumber; + int opcodeLength; + List operands; + int totalOperandLength; + ArgumentBranch branch; + ArgumentString string; + OperandVariable store; + boolean isReturn, isCall, isExit; + int jumpTarget; + int callTarget; + + public Opcode () + { + operands = new ArrayList (); + } + + @Override + public String toString () + { + StringBuilder text = new StringBuilder (); + if (false) + text.append (HexFormatter.formatNoHeader (buffer, startPtr, length ()) + "\n"); + text.append (String.format ("%05X : %-12s", startPtr, opcodeName ())); + if (jumpTarget != 0) + text.append (String.format (" L:%05X", jumpTarget)); + else if (isCall) + { + text.append (String.format (" R:%05X (", callTarget)); + int count = 0; + for (Operand op : operands) + if (count++ > 0) + text.append (op + ", "); + if (operands.size () > 1) + text.delete (text.length () - 2, text.length ()); + text.append (") --> " + store); + } + else + { + for (Operand op : operands) + text.append (" " + op); + if (branch != null) + text.append (branch); + if (store != null) + text.append (" --> " + store); + if (string != null) + text.append (" \"" + string + "\""); + } + return text.toString (); + } + + public int length () + { + int length = totalOperandLength + opcodeLength; + if (branch != null) + length += branch.length; + if (store != null) + length += store.length; + if (string != null) + length += string.length; + return length; + } + + public abstract String opcodeName (); + + private void addOperand (Operand operand) + { + operands.add (operand); + totalOperandLength += operand.length; + } + + protected void addOperand (byte[] buffer, int ptr, boolean bit1, boolean bit2) + { + int offset = ptr + totalOperandLength; + if (bit1) + { + if (!bit2) + addOperand (new OperandVariable (buffer[offset])); // %10 + } + else if (bit2) + addOperand (new OperandByte (buffer[offset])); // %01 + else + addOperand (new OperandWord (header.getWord (offset))); // %00 + } + + protected void addOperand (byte[] buffer, int ptr, boolean bit) + { + int address = ptr + totalOperandLength; + if (address >= buffer.length) + { + System.out.println ("Illegal byte address : " + address); + return; + } + if (bit) + addOperand (new OperandVariable (buffer[address])); + else + addOperand (new OperandByte (buffer[address])); + } + + protected void setVariableOperands (int ptr) + { + int value = buffer[ptr + 1] & 0xFF; + for (int i = 0; i < 4; i++) + { + boolean bit1 = ((value & 0x80) == 0x80); + boolean bit2 = ((value & 0x40) == 0x40); + if (bit1 && bit2) + break; + addOperand (buffer, ptr + 2, bit1, bit2); + value <<= 2; + } + } + + protected void setStore (byte[] buffer) + { + store = new OperandVariable (buffer[startPtr + totalOperandLength + opcodeLength]); + } + + protected void setBranch (byte[] buffer) + { + int offset = startPtr + totalOperandLength + (store == null ? 0 : 1) + opcodeLength; + if ((buffer[offset] & 0x40) == 0x40) + branch = new ArgumentBranch (buffer[offset], offset); + else + branch = new ArgumentBranch (header.getWord (offset), offset); + } + + protected void setZString (byte[] buffer) + { + int offset = startPtr + totalOperandLength + opcodeLength; + string = new ArgumentString (buffer, offset); + } + } + + class Opcode0OP extends Opcode + { + public Opcode0OP (byte[] buffer, int ptr) + { + opcodeNumber = buffer[ptr] & 0x0F; + opcodeLength = 1; + + if (opcodeNumber == 5 || opcodeNumber == 6 || opcodeNumber == 13) + setBranch (buffer); + + if (opcodeNumber == 0 || opcodeNumber == 1 || opcodeNumber == 3 || opcodeNumber == 8) + isReturn = true; + + if (opcodeNumber == 2 || opcodeNumber == 3) + setZString (buffer); + + if (opcodeNumber == 7 || opcodeNumber == 10) + isExit = true; + } + + @Override + public String opcodeName () + { + return name0OP[opcodeNumber]; + } + } + + class Opcode1OP extends Opcode + { + public Opcode1OP (byte[] buffer, int ptr) + { + opcodeNumber = buffer[ptr] & 0x0F; + opcodeLength = 1; + + boolean bit1 = ((buffer[ptr] & 0x20) == 0x20); + boolean bit2 = ((buffer[ptr] & 0x10) == 0x10); + addOperand (buffer, ptr + 1, bit1, bit2); + + if ((opcodeNumber >= 1 && opcodeNumber <= 4) || opcodeNumber == 8 || opcodeNumber == 14 + || opcodeNumber == 15) + setStore (buffer); + if (opcodeNumber <= 2) + setBranch (buffer); + if (opcodeNumber == 12) + jumpTarget = (short) operands.get (0).value + startPtr - 2 + length (); + if (opcodeNumber == 11) + isReturn = true; + } + + @Override + public String opcodeName () + { + return name1OP[opcodeNumber]; + } + } + + abstract class Opcode2OP extends Opcode + { + public Opcode2OP () + { + opcodeLength = 1; + } + + public void setArguments (byte[] buffer) + { + if ((opcodeNumber >= 1 && opcodeNumber <= 7) || opcodeNumber == 10) + setBranch (buffer); + else if ((opcodeNumber >= 15 && opcodeNumber <= 25) || opcodeNumber == 8 || opcodeNumber == 9) + setStore (buffer); + } + + @Override + public String opcodeName () + { + return name2OP[opcodeNumber]; + } + } + + class Opcode2OPLong extends Opcode2OP + { + public Opcode2OPLong (byte[] buffer, int ptr) + { + opcodeNumber = buffer[ptr] & 0x1F; + boolean bit1 = ((buffer[ptr] & 0x40) == 0x40); + boolean bit2 = ((buffer[ptr] & 0x20) == 0x20); + addOperand (buffer, ptr + 1, bit1); + addOperand (buffer, ptr + 1, bit2); + + setArguments (buffer); + } + } + + class Opcode2OPVar extends Opcode2OP + { + public Opcode2OPVar (byte[] buffer, int ptr) + { + opcodeNumber = buffer[ptr] & 0x1F; + opcodeLength = 2; + setVariableOperands (ptr); + setArguments (buffer); + } + } + + class OpcodeVar extends Opcode + { + public OpcodeVar (byte[] buffer, int ptr) + { + opcodeNumber = buffer[ptr] & 0x1F; + opcodeLength = 2; + setVariableOperands (ptr); + + if (opcodeNumber == 0 || opcodeNumber == 7) + setStore (buffer); + if (opcodeNumber == 0) + { + isCall = true; + callTarget = operands.get (0).value * 2; + } + } + + @Override + public String opcodeName () + { + return nameVAR[opcodeNumber]; + } + } + + abstract class Operand + { + int length; + int value; + } + + class OperandWord extends Operand + { + public OperandWord (int value) + { + this.value = value; + length = 2; + } + + @Override + public String toString () + { + return String.format ("#%05d", value); + } + } + + class OperandByte extends Operand + { + public OperandByte (byte value) + { + this.value = value & 0xFF; + length = 1; + } + + @Override + public String toString () + { + return String.format ("#%03d", value); + } + } + + class OperandVariable extends Operand + { + public OperandVariable (byte value) + { + this.value = value & 0xFF; + length = 1; + } + + @Override + public String toString () + { + if (value == 0) + return ("ToS"); + if (value <= 15) + return (String.format ("L%02d", value)); + return String.format ("G%03d", (value - 15)); + } + } + + class ArgumentBranch extends Operand + { + int target; + boolean branchOnTrue; + + public ArgumentBranch (byte value, int offset) + { + branchOnTrue = (value & 0x80) == 0x80; + int val = value & 0x3F; // unsigned + if (val <= 1) + target = val; + else + target = val + offset - 1; + length = 1; + } + + public ArgumentBranch (int value, int offset) + { + branchOnTrue = (value & 0x8000) == 0x8000; + int val = value & 0x3FFF; // signed + if (val > 8191) + val -= 16384; + target = val + offset; + length = 2; + } + + @Override + public String toString () + { + StringBuilder text = new StringBuilder (); + text.append (" [" + (branchOnTrue ? "true" : "false") + "] "); + if (target == 0 || target == 1) + text.append (target == 0 ? "RFALSE" : "RTRUE"); + else + text.append (String.format ("%05X", target)); + return text.toString (); + } + } + + class ArgumentString extends Operand + { + ZString text; + int startPtr; + byte[] buffer; + + public ArgumentString (byte[] buffer, int offset) + { + this.buffer = buffer; + text = new ZString (buffer, offset, header); + length = text.length; + } + + @Override + public String toString () + { + return text.value; + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/infocom/ObjectAnalyser.java b/src/com/bytezone/diskbrowser/infocom/ObjectAnalyser.java new file mode 100644 index 0000000..4a03663 --- /dev/null +++ b/src/com/bytezone/diskbrowser/infocom/ObjectAnalyser.java @@ -0,0 +1,217 @@ +package com.bytezone.diskbrowser.infocom; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.bytezone.diskbrowser.infocom.ZObject.Property; + +class ObjectAnalyser +{ + Header header; + ObjectManager parent; + List list = new ArrayList (); + List routines = new ArrayList (); + + public ObjectAnalyser (Header header, ObjectManager parent) + { + this.header = header; + this.parent = parent; + + // assign the DICT property for each object + setDictionary (); + + // find the point where code ends and strings begin + setStringPointer (); + + // add routines called from object properties (requires stringPointer) + createPropertyLinks (); + + // assumes that all properties with exactly three bytes are routine addresses +// checkThreeByteProperties (); + } + + public void setStringPointer () + { + PropertyTester pt = new PropertyTester (parent.list); + pt.addTest (new LengthTwoCondition ()); + HighMemoryCondition hmc = new HighMemoryCondition (); + pt.addTest (hmc); + pt.doTests (); + + System.out.println ("\nSetting the string pointer\n"); + + for (Integer propertyNo : pt) + // list of all properties that passed all tests + list.add (hmc.statistics[propertyNo]); + Collections.sort (list); + + // Calculate lowest string pointer + int lo = list.get (0).lo; + for (Statistics s : list) + { + System.out.println (s); + if (s.hi > lo && s.lo < lo) + lo = s.lo; + if (s.hi < lo) + { + header.stringPointer = lo; + break; + } + } + } + + public void createPropertyLinks () + { + int sCount = 0; + int rCount = 0; + int totStrings = 0; + int totRoutines = 0; + + for (Statistics s : list) + { + if (header.propertyNames[s.propertyNumber].charAt (0) >= 'a') + continue; + if (s.lo >= header.stringPointer) + { + header.propertyNames[s.propertyNumber] = "STR" + ++sCount; + totStrings += s.offsets.size (); + } + else + { + header.propertyNames[s.propertyNumber] = "CODE" + ++rCount; + routines.addAll (s.offsets); + totRoutines += s.offsets.size (); + } + } + System.out.println ("Strings found : " + totStrings); + System.out.println ("Routines found : " + totRoutines); + } + + public void checkThreeByteProperties () + { + for (ZObject object : parent.list) + { + for (Property property : object.properties) + { + if (header.propertyNames[property.propertyNumber].charAt (0) < 'a' && property.length == 3) + { + int address = header.getWord (property.ptr + 1) * 2; + System.out.println ("checking " + address); + header.codeManager.addRoutine (address, 0); + } + } + } + } + + // find the property with only dictionary entries + public void setDictionary () + { + PropertyTester pt = new PropertyTester (parent.list); + pt.addTest (new LengthEvenCondition ()); + pt.addTest (new ValidDictionaryCondition ()); + pt.doTests (); + + for (Integer i : pt) + // should only be one + header.propertyNames[i] = "DICT"; + } + + class Statistics implements Comparable + { + int propertyNumber; + int lo; + int hi; + List offsets = new ArrayList (); + + public Statistics (int propertyNumber) + { + this.propertyNumber = propertyNumber; + } + + public void increment (Property property) + { + offsets.add (property.offset); + if (property.offset > hi) + hi = property.offset; + if (property.offset < lo || lo == 0) + lo = property.offset; + } + + @Override + public String toString () + { + return String.format ("%2d %3d %,7d %,7d", propertyNumber, offsets.size (), lo, hi); + } + + @Override + public int compareTo (Statistics o) + { + return o.hi - hi; + } + } + + class LengthTwoCondition extends Condition + { + @Override + boolean test (Property property) + { + return property.length == 2; + } + } + + class LengthThreeCondition extends Condition + { + @Override + boolean test (Property property) + { + return property.length == 3; + } + } + + class LengthEvenCondition extends Condition + { + @Override + boolean test (Property property) + { + return (property.length % 2) == 0; + } + } + + class HighMemoryCondition extends Condition + { + int lo, hi; + Statistics[] statistics = new Statistics[32]; // note there is no property #0 + + public HighMemoryCondition () + { + lo = header.highMemory; + hi = header.fileLength; + for (int i = 1; i < statistics.length; i++) + statistics[i] = new Statistics (i); + } + + @Override + boolean test (Property property) + { + statistics[property.propertyNumber].increment (property); + int address = header.getWord (property.ptr + 1) * 2; + return (address >= lo && address < hi) || address == 0; + } + } + + class ValidDictionaryCondition extends Condition + { + @Override + boolean test (Property property) + { + for (int i = 1; i <= property.length; i += 2) + { + int address = header.getWord (property.ptr + i); + if (!header.containsWordAt (address)) + return false; + } + return true; + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/infocom/ObjectManager.java b/src/com/bytezone/diskbrowser/infocom/ObjectManager.java new file mode 100755 index 0000000..7b5b5e2 --- /dev/null +++ b/src/com/bytezone/diskbrowser/infocom/ObjectManager.java @@ -0,0 +1,96 @@ +package com.bytezone.diskbrowser.infocom; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import javax.swing.tree.DefaultMutableTreeNode; + +import com.bytezone.diskbrowser.applefile.AbstractFile; +import com.bytezone.diskbrowser.disk.DefaultAppleFileSource; +import com.bytezone.diskbrowser.disk.FormattedDisk; + +class ObjectManager extends AbstractFile implements Iterable +{ + Header header; + List list; + int defaultsPtr, defaultsSize; + int tablePtr, tableSize; + int propertyPtr, propertySize; + ObjectAnalyser analyser; + + public ObjectManager (Header header) + { + super ("Objects", header.buffer); + this.header = header; + + defaultsPtr = header.objectTable; + defaultsSize = 62; + tablePtr = header.objectTable + 62; + propertyPtr = header.getWord (tablePtr + 7); + propertySize = header.globalsOffset - propertyPtr; + tableSize = (propertyPtr - tablePtr); + int totalObjects = tableSize / ZObject.HEADER_SIZE; + list = new ArrayList (tableSize); + + for (int objectNo = 0; objectNo < totalObjects; objectNo++) + list.add (new ZObject (null, buffer, tablePtr + objectNo * ZObject.HEADER_SIZE, objectNo + 1, + header)); + + // analyse objects - set stringPtr etc. + analyser = new ObjectAnalyser (header, this); + + // add entries for AbstractFile.getHexDump () + hexBlocks.add (new HexBlock (defaultsPtr, defaultsSize, "Property defaults:")); + hexBlocks.add (new HexBlock (tablePtr, tableSize, "Objects table:")); + hexBlocks.add (new HexBlock (propertyPtr, propertySize, "Properties:")); + } + + public void addNodes (DefaultMutableTreeNode root, FormattedDisk disk) + { + root.setAllowsChildren (true); + + for (ZObject zo : list) + if (zo.parent == 0) + buildObjectTree (zo, root, disk); + } + + private void buildObjectTree (ZObject object, DefaultMutableTreeNode parentNode, + FormattedDisk disk) + { + DefaultMutableTreeNode child = + new DefaultMutableTreeNode (new DefaultAppleFileSource (object.name, object, disk)); + parentNode.add (child); + if (object.sibling > 0) + buildObjectTree (header.objectManager.list.get (object.sibling - 1), parentNode, disk); + if (object.child > 0) + buildObjectTree (header.objectManager.list.get (object.child - 1), child, disk); + else + child.setAllowsChildren (false); + } + + public List getCodeRoutines () + { + return analyser.routines; + } + + @Override + public String getText () + { + StringBuilder text = + new StringBuilder (" # Attributes Pr Sb Ch Prop Title\n--- -----------" + + " -- -- -- ----- -----------------------------\n"); + + int objectNumber = 0; + for (ZObject zo : list) + text.append (String.format ("%3d %s%n", ++objectNumber, zo)); + text.deleteCharAt (text.length () - 1); + return text.toString (); + } + + @Override + public Iterator iterator () + { + return list.iterator (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/infocom/PropertyManager.java b/src/com/bytezone/diskbrowser/infocom/PropertyManager.java new file mode 100644 index 0000000..5baab58 --- /dev/null +++ b/src/com/bytezone/diskbrowser/infocom/PropertyManager.java @@ -0,0 +1,92 @@ +package com.bytezone.diskbrowser.infocom; + +import java.util.ArrayList; +import java.util.List; + +import javax.swing.tree.DefaultMutableTreeNode; + +import com.bytezone.diskbrowser.applefile.AbstractFile; +import com.bytezone.diskbrowser.disk.DefaultAppleFileSource; +import com.bytezone.diskbrowser.disk.FormattedDisk; + +class PropertyManager extends AbstractFile +{ + List list = new ArrayList (); + Header header; + + public PropertyManager (String name, byte[] buffer, Header header) + { + super (name, buffer); + this.header = header; + + for (int propertyNo = 1; propertyNo <= 31; propertyNo++) + { + Statistic statistic = new Statistic (propertyNo); + list.add (statistic); + } + } + + public void addNodes (DefaultMutableTreeNode node, FormattedDisk disk) + { + node.setAllowsChildren (true); + + for (Statistic stat : list) + if (stat.list.size () > 0) + { + String title = "Property " + header.propertyNames[stat.id].trim (); + DefaultMutableTreeNode child = + new DefaultMutableTreeNode (new DefaultAppleFileSource (title, stat.getText (), disk)); + node.add (child); + child.setAllowsChildren (false); + } + } + + @Override + public String getText () + { + StringBuilder text = new StringBuilder ("Property Type Frequency\n"); + text.append ("-------- ----- ---------\n"); + + for (Statistic stat : list) + text.append (String.format ("%s%n", stat)); + if (text.length () > 0) + text.deleteCharAt (text.length () - 1); + return text.toString (); + } + + private class Statistic + { + int id; + List list = new ArrayList (); + + public Statistic (int id) + { + this.id = id; + for (ZObject o : header.objectManager) + { + ZObject.Property p = o.getProperty (id); + if (p != null) + list.add (o); + } + } + + String getText () + { + StringBuilder text = new StringBuilder ("Objects with property " + id + " set:\n\n"); + for (ZObject o : list) + { + ZObject.Property p = o.getProperty (id); + text.append (String.format ("%3d %-29s%s%n", o.id, o.name, p.toString ().substring (7))); + } + if (text.length () > 0) + text.deleteCharAt (text.length () - 1); + return text.toString (); + } + + @Override + public String toString () + { + return String.format (" %2d %-6s %3d", id, header.propertyNames[id], list.size ()); + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/infocom/PropertyTester.java b/src/com/bytezone/diskbrowser/infocom/PropertyTester.java new file mode 100644 index 0000000..09644cb --- /dev/null +++ b/src/com/bytezone/diskbrowser/infocom/PropertyTester.java @@ -0,0 +1,65 @@ +package com.bytezone.diskbrowser.infocom; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import com.bytezone.diskbrowser.infocom.ZObject.Property; + +class PropertyTester implements Iterable +{ + List objects; + List conditions = new ArrayList (); + List matchedProperties; + + public PropertyTester (List objects) + { + this.objects = objects; + } + + public void addTest (Condition test) + { + conditions.add (test); + } + + public void doTests () + { + boolean[] propFail = new boolean[32]; + int[] propTestCount = new int[32]; + matchedProperties = new ArrayList (); + + for (ZObject object : objects) + propertyLoop: for (Property property : object.properties) + { + if (propFail[property.propertyNumber] || property.length == 0) + continue; + for (Condition condition : conditions) + if (!condition.test (property)) + { + propFail[property.propertyNumber] = true; + continue propertyLoop; + } + ++propTestCount[property.propertyNumber]; + } + + for (int i = 1; i < propFail.length; i++) + if (!propFail[i] && propTestCount[i] > 0) + matchedProperties.add (i); + } + + @Override + public Iterator iterator () + { + return matchedProperties.iterator (); + } + + public int totalSuccessfulProperties () + { + return matchedProperties.size (); + } +} + +abstract class Condition +{ + abstract boolean test (Property property); +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/infocom/Routine.java b/src/com/bytezone/diskbrowser/infocom/Routine.java new file mode 100644 index 0000000..2fa53ec --- /dev/null +++ b/src/com/bytezone/diskbrowser/infocom/Routine.java @@ -0,0 +1,153 @@ +package com.bytezone.diskbrowser.infocom; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import com.bytezone.diskbrowser.applefile.AbstractFile; + +class Routine extends AbstractFile implements Iterable, Comparable +{ + int startPtr, length, strings, locals; + Header header; + + List parameters = new ArrayList (); + List instructions = new ArrayList (); + List calls = new ArrayList (); + List calledBy = new ArrayList (); + List actions = new ArrayList (); // not used yet + + private static final String padding = " "; + + public Routine (int ptr, Header header, int caller) + { + super (String.format ("Routine %05X", ptr), header.buffer); + this.header = header; + + locals = buffer[ptr] & 0xFF; + if (locals > 15) + return; + + startPtr = ptr++; // also used to flag a valid routine + calledBy.add (caller); + + for (int i = 1; i <= locals; i++) + { + parameters.add (new Parameter (i, header.getWord (ptr))); // default values + ptr += 2; + } + + while (true) + { + if (buffer[ptr] == 0 || buffer[ptr] == 0x20 || buffer[ptr] == 0x40) + { + System.out.println ("Bad instruction found : " + ptr); + return; + } + Instruction instruction = new Instruction (buffer, ptr, header); + instructions.add (instruction); + + if (instruction.isCall () && instruction.opcode.callTarget > 0) // not stack-based + calls.add (instruction.opcode.callTarget); + + if (instruction.isPrint ()) + strings++; + + ptr += instruction.length (); + + // is it a backwards jump? + if (instruction.isJump () && instruction.target () < ptr && !moreCode (ptr)) + break; + + // is it an unconditional return? + if (instruction.isReturn () && !moreCode (ptr)) + break; + } + length = ptr - startPtr; + + hexBlocks.add (new HexBlock (startPtr, length, null)); + + // check for branches outside this routine + if (true) + { + int endPtr = startPtr + length; + for (Instruction ins : instructions) + { + int target = + ins.target () > 256 ? ins.target () + : ins.opcode.jumpTarget > 256 ? ins.opcode.jumpTarget : 0; + if (target == 0) + continue; + if (ins.isBranch () && (target > endPtr || target < startPtr)) + System.out.println (ins); + if (ins.isJump () && (target > endPtr || target < startPtr)) + System.out.println (ins); + } + } + } + + // test whether the routine contains any instructions pointing to this address + private boolean moreCode (int ptr) + { + for (Instruction ins : instructions) + { + if (ins.isBranch () && ins.target () == ptr) + return true; + // should this be calling ins.target () ? + if (ins.isJump () && ins.opcode.jumpTarget == ptr) + return true; + } + return false; + } + + public void addCaller (int caller) + { + calledBy.add (caller); + } + + @Override + public String getText () + { + StringBuilder text = new StringBuilder (); + text.append (String.format ("Called by : %3d%n", calledBy.size ())); + text.append (String.format ("Calls : %3d%n%n", calls.size ())); + text.append (String.format ("%s%05X : %d%n", padding, startPtr, locals)); + for (Parameter parameter : parameters) + text.append (padding + parameter.toString () + "\n"); + text.append ("\n"); + for (Instruction instruction : instructions) + text.append (instruction + "\n"); + return text.toString (); + } + + class Parameter + { + int value; + int sequence; + + public Parameter (int sequence, int value) + { + this.value = value; + this.sequence = sequence; + } + + @Override + public String toString () + { + return String.format ("%05X : L%02d : %d", (startPtr + (sequence - 1) * 2 + 1), sequence, + value); + } + } + + @Override + public Iterator iterator () + { + return instructions.iterator (); + } + + @Override + public int compareTo (Routine o) + { + return startPtr - o.startPtr; + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/infocom/StringManager.java b/src/com/bytezone/diskbrowser/infocom/StringManager.java new file mode 100644 index 0000000..39661a5 --- /dev/null +++ b/src/com/bytezone/diskbrowser/infocom/StringManager.java @@ -0,0 +1,70 @@ +package com.bytezone.diskbrowser.infocom; + +import java.util.Map; +import java.util.TreeMap; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.applefile.AbstractFile; + +class StringManager extends AbstractFile +{ + Header header; + Map strings = new TreeMap (); + + public StringManager (String name, byte[] buffer, Header header) + { + super (name, buffer); + this.header = header; + + int ptr = header.stringPointer; + int max = header.fileLength; + while (ptr < max) + { + ZString zs = new ZString (buffer, ptr, header); + if (zs.value == null) + break; // used when eof not known or correct - fix!! + strings.put (ptr, zs); + ptr += zs.length; + } + } + + public boolean containsStringAt (int address) + { + return strings.containsKey (address); + } + + public String stringAt (int address) + { + if (strings.containsKey (address)) + return strings.get (address).value; + return "String not found at : " + address; + } + + @Override + public String getText () + { + StringBuilder text = new StringBuilder (); + int count = 0; + text.append (" # Start String\n"); + text.append ("--- ----- --------------------------------------------------------" + + "-------------------\n"); + + for (ZString s : strings.values ()) + { + String s2 = s.value.replace ("\n", "\n "); + text.append (String.format ("%3d %05X \"%s\"%n", ++count, s.startPtr, s2)); + } + + if (text.length () > 0) + text.deleteCharAt (text.length () - 1); + + return text.toString (); + } + + @Override + public String getHexDump () + { + int size = header.fileLength - header.stringPointer; + return HexFormatter.format (buffer, header.stringPointer, size); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/infocom/ZObject.java b/src/com/bytezone/diskbrowser/infocom/ZObject.java new file mode 100755 index 0000000..6e9048e --- /dev/null +++ b/src/com/bytezone/diskbrowser/infocom/ZObject.java @@ -0,0 +1,203 @@ +package com.bytezone.diskbrowser.infocom; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.List; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.applefile.AbstractFile; + +class ZObject extends AbstractFile +{ + static final int HEADER_SIZE = 9; + + int id; + int startPtr; + int propertyTablePtr; + int propertyTableLength; + int parent, sibling, child; + List properties = new ArrayList (); + Header header; + BitSet attributes = new BitSet (32); + + public ZObject (String name, byte[] buffer, int offset, int seq, Header header) + { + super (name, buffer); + this.header = header; + + startPtr = offset; + id = seq; + + // attributes + int bitIndex = 0; + for (int i = 0; i < 4; i++) + { + byte b = buffer[offset + i]; + for (int j = 0; j < 8; j++) + { + if ((b & 0x80) == 0x80) + attributes.set (bitIndex); + b <<= 1; + ++bitIndex; + } + } + + // object's relatives + parent = header.getByte (offset + 4); + sibling = header.getByte (offset + 5); + child = header.getByte (offset + 6); + + // the property header contains the object's short name + propertyTablePtr = header.getWord (offset + 7); + int ptr = propertyTablePtr; + int nameLength = header.getByte (ptr) * 2; + this.name = nameLength == 0 ? "<>" : new ZString (buffer, ++ptr, header).value; + ptr += nameLength; + + // read each property + while (buffer[ptr] != 0) + { + Property p = new Property (buffer, ptr); + properties.add (p); + ptr += p.length + 1; + } + propertyTableLength = ptr - propertyTablePtr; + } + + @Override + public String getText () + { + StringBuilder text = new StringBuilder (); + + text.append (String.format ("ID : %3d %s%n%nAttributes : ", id, name)); + text.append (HexFormatter.getHexString (buffer, startPtr, 4)); + text.append (" " + attributes.toString ()); + + String obj1 = parent == 0 ? "" : header.objectManager.list.get (parent - 1).name; + String obj2 = sibling == 0 ? "" : header.objectManager.list.get (sibling - 1).name; + String obj3 = child == 0 ? "" : header.objectManager.list.get (child - 1).name; + + text.append (String.format ("%n%nParent : %02X (%3d) %s%n", parent, parent, obj1)); + text.append (String.format ("Sibling : %02X (%3d) %s%n", sibling, sibling, obj2)); + text.append (String.format ("Child : %02X (%3d) %s%n%n", child, child, obj3)); + + for (Property prop : properties) + text.append (prop + "\n"); + + return text.toString (); + } + + @Override + public String getHexDump () + { + StringBuilder text = new StringBuilder ("Header :\n\n"); + text.append (HexFormatter.formatNoHeader (buffer, startPtr, HEADER_SIZE)); + text.append ("\n\nProperty table:\n\n"); + text.append (HexFormatter.formatNoHeader (buffer, propertyTablePtr, propertyTableLength)); + return text.toString (); + } + + Property getProperty (int id) + { + for (Property p : properties) + if (p.propertyNumber == id) + return p; + return null; + } + + @Override + public String toString () + { + return HexFormatter.getHexString (buffer, startPtr, HEADER_SIZE) + " " + name; + } + + class Property + { + int propertyNumber; + int ptr; + int length; + + int offset; // only used if length == 2 + + public Property (byte[] buffer, int ptr) + { + this.ptr = ptr; + length = header.getByte (ptr) / 32 + 1; + propertyNumber = header.getByte (ptr) % 32; + + if (length == 2) + offset = header.getWord (ptr + 1) * 2; + } + + private ZObject getObject () + { + return header.objectManager.list.get ((buffer[ptr + 1] & 0xFF) - 1); + } + + @Override + public String toString () + { + StringBuilder text = + new StringBuilder (String.format ("%5s : ", header.propertyNames[propertyNumber])); + + String propertyType = header.propertyNames[propertyNumber]; + + if (!propertyType.equals ("DICT") && !propertyType.contains ("STR")) + text.append (String.format ("%-20s", HexFormatter.getHexString (buffer, ptr + 1, length))); + + if (propertyType.charAt (0) >= 'a') // directions are in lowercase + { + switch (length) + { + case 1: + text.append (getObject ().name); + break; + case 2: + text.append ("\"" + header.stringManager.stringAt (offset) + "\""); + break; + case 3: + int address = header.getWord (ptr + 1) * 2; + text.append (String.format ("R:%05X", address)); + break; + case 4: + address = header.getWord (ptr + 3) * 2; + if (address > 0) + text.append ("\"" + header.stringManager.stringAt (address) + "\""); + break; + default: + break; + } + } + else if (propertyType.equals ("DICT")) + { + for (int i = 1; i <= length; i += 2) + { + int address = header.getWord (ptr + i); + text.append (header.wordAt (address) + ", "); + } + text.deleteCharAt (text.length () - 1); + text.deleteCharAt (text.length () - 1); + } + else if (propertyType.startsWith ("CODE")) + { + if (offset > 0) // cretin contains 00 00 + { + Routine r = header.codeManager.getRoutine (offset); + if (r != null) + text.append ("\n\n" + r.getText ()); + else + // this can happen if the property is mislabelled as code + text.append ("\n\n****** null routine\n"); + } + } + else if (propertyType.startsWith ("STR")) + { + text + .append (String.format ("(%4X) \"%s\"", offset, header.stringManager + .stringAt (offset))); + } + + return text.toString (); + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/infocom/ZString.java b/src/com/bytezone/diskbrowser/infocom/ZString.java new file mode 100755 index 0000000..39674fb --- /dev/null +++ b/src/com/bytezone/diskbrowser/infocom/ZString.java @@ -0,0 +1,156 @@ +package com.bytezone.diskbrowser.infocom; + +class ZString +{ + private static String[] letters = + { " abcdefghijklmnopqrstuvwxyz", " ABCDEFGHIJKLMNOPQRSTUVWXYZ", + " 0123456789.,!?_#\'\"/\\-:()" }; + String value; + Header header; + int startPtr; + int length; + + public ZString (byte[] buffer, int offset, Header header) + { + ZStringBuilder text = new ZStringBuilder (); + this.header = header; + this.startPtr = offset; + + while (true) + { + if (offset >= header.buffer.length - 1) + { + System.out.println ("********" + text.toString ()); + break; + } + + // get the next two bytes + int val = header.getWord (offset); + + // process each zChar as a 5-bit value + text.processZChar ((byte) ((val >> 10) & 0x1F)); + text.processZChar ((byte) ((val >> 5) & 0x1F)); + text.processZChar ((byte) (val & 0x1F)); + + if ((val & 0x8000) > 0) // bit 15 = finished flag + { + length = offset - startPtr + 2; + value = text.toString (); + break; + } + offset += 2; + } + } + + @Override + public String toString () + { + return value; + } + + private class ZStringBuilder + { + int alphabet; + boolean shift; + int shiftAlphabet; + int synonym; + int buildingLevel; + int builtLetter; + StringBuilder text = new StringBuilder (); + + private void processZChar (byte zchar) + { + // A flag to indicate that we are building a character not in the alphabet. The + // value indicates which half of the character the current zchar goes into. Once + // both halves are full, we use the ascii value in builtLetter. + if (buildingLevel > 0) + { + builtLetter = (short) ((builtLetter << 5) | zchar); + if (++buildingLevel == 3) + { + text.append ((char) builtLetter); + buildingLevel = 0; + } + return; + } + + // A flag to indicate that we need to insert an abbreviation. The synonym value + // (1-3) indicates which abbreviation block to use, and the current zchar is the offset + // within that block. + if (synonym > 0) + { + text.append (header.getAbbreviation ((synonym - 1) * 32 + zchar)); + synonym = 0; + return; + } + + // this should be in the switch + if ((shift && shiftAlphabet == 2) || (!shift && alphabet == 2)) + { + if (zchar == 6) + { + buildingLevel = 1; + builtLetter = 0; + shift = false; + return; + } + if (zchar == 7) + { + text.append ("\n"); + shift = false; + return; + } + } + + // zChar values 0-5 have special meanings, and 6-7 are special only in alphabet #2. + // Otherwise it's just a straight lookup into the current alphabet. + switch (zchar) + { + case 0: + text.append (" "); + shift = false; + return; + case 1: + synonym = zchar; + return; + case 2: + case 3: + if (header.version >= 3) + { + synonym = zchar; + return; + } + // version 1 or 2 + shiftAlphabet = (alphabet + zchar - 1) % 3; + shift = true; + return; + case 4: + case 5: + if (header.version >= 3) // shift key + { + shiftAlphabet = zchar - 3; + shift = true; + } + else + // shift lock key + alphabet = (alphabet + zchar - 3) % 3; + return; + default: + if (shift) + { + text.append (letters[shiftAlphabet].charAt (zchar)); + shift = false; + } + else + text.append (letters[alphabet].charAt (zchar)); + return; + } + } + + @Override + public String toString () + { + return text.toString (); + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/pascal/PascalCatalogSector.java b/src/com/bytezone/diskbrowser/pascal/PascalCatalogSector.java new file mode 100644 index 0000000..c914ebd --- /dev/null +++ b/src/com/bytezone/diskbrowser/pascal/PascalCatalogSector.java @@ -0,0 +1,75 @@ +package com.bytezone.diskbrowser.pascal; + +import java.text.DateFormat; +import java.util.GregorianCalendar; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.disk.AbstractSector; +import com.bytezone.diskbrowser.disk.Disk; + +class PascalCatalogSector extends AbstractSector +{ + private static DateFormat df = DateFormat.getDateInstance (DateFormat.SHORT); + private static String[] fileTypes = { "Volume", "Bad", "Code", "Text", "Info", "Data", "Graf", + "Foto", "SecureDir" }; + + public PascalCatalogSector (Disk disk, byte[] buffer) + { + super (disk, buffer); + } + + @Override + public String createText () + { + StringBuilder text = getHeader ("Pascal Catalog Sectors"); + + addTextAndDecimal (text, buffer, 0, 2, "First directory block"); + addTextAndDecimal (text, buffer, 2, 2, "Last directory block"); + + addText (text, buffer, 4, 2, "File type : " + fileTypes[buffer[5]]); + + String name = HexFormatter.getPascalString (buffer, 6); + addText (text, buffer, 6, 4, ""); + addText (text, buffer, 10, 4, "Volume name : " + name); + addTextAndDecimal (text, buffer, 14, 2, "Blocks on disk"); + addTextAndDecimal (text, buffer, 16, 2, "Files on disk"); + addTextAndDecimal (text, buffer, 18, 2, "First block of volume"); + + GregorianCalendar calendar = HexFormatter.getPascalDate (buffer, 20); + String date = calendar == null ? "--" : df.format (calendar.getTime ()); + addText (text, buffer, 20, 2, "Most recent date setting : " + date); + addTextAndDecimal (text, buffer, 22, 4, "Reserved"); + + int ptr = PascalDisk.CATALOG_ENTRY_SIZE; + int totalFiles = HexFormatter.intValue (buffer[16], buffer[17]); + + while (ptr < buffer.length && totalFiles > 0) + { + if (buffer[ptr + 6] == 0) + break; + text.append ("\n"); + addTextAndDecimal (text, buffer, ptr + 0, 2, "File's first block"); + addTextAndDecimal (text, buffer, ptr + 2, 2, "File's last block"); + int type = buffer[ptr + 4] & 0x0F; + if (type < fileTypes.length) + addText (text, buffer, ptr + 4, 2, "File type : " + fileTypes[type]); + int wildcard = buffer[ptr + 4] & 0xC0; + addText (text, buffer, ptr + 4, 2, "Wildcard : " + wildcard); + name = HexFormatter.getPascalString (buffer, ptr + 6); + addText (text, buffer, ptr + 6, 4, ""); + addText (text, buffer, ptr + 10, 4, ""); + addText (text, buffer, ptr + 14, 4, ""); + addText (text, buffer, ptr + 18, 4, "File name : " + name); + addTextAndDecimal (text, buffer, ptr + 22, 2, "Bytes in file's last block"); + + calendar = HexFormatter.getPascalDate (buffer, ptr + 24); + date = calendar == null ? "--" : df.format (calendar.getTime ()); + addText (text, buffer, ptr + 24, 2, "Date : " + date); + + ptr += PascalDisk.CATALOG_ENTRY_SIZE; + --totalFiles; // what if there are deleted files? + } + + return text.toString (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/pascal/PascalDisk.java b/src/com/bytezone/diskbrowser/pascal/PascalDisk.java new file mode 100755 index 0000000..24d756f --- /dev/null +++ b/src/com/bytezone/diskbrowser/pascal/PascalDisk.java @@ -0,0 +1,497 @@ +package com.bytezone.diskbrowser.pascal; + +import java.awt.Color; +import java.text.DateFormat; +import java.util.ArrayList; +import java.util.GregorianCalendar; +import java.util.List; + +import javax.swing.tree.DefaultMutableTreeNode; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.applefile.*; +import com.bytezone.diskbrowser.disk.*; +import com.bytezone.diskbrowser.gui.DataSource; + +public class PascalDisk extends AbstractFormattedDisk +{ + static final int CATALOG_ENTRY_SIZE = 26; + private static DateFormat df = DateFormat.getDateInstance (DateFormat.SHORT); + private final VolumeEntry volume; + private final PascalCatalogSector diskCatalogSector; + + private final String[] fileTypes = { "Volume", "Xdsk", "Code", "Text", "Info", "Data", + "Graf", "Foto", "SecureDir" }; + + SectorType diskBootSector = new SectorType ("Boot", Color.lightGray); + SectorType catalogSector = new SectorType ("Catalog", Color.magenta); + SectorType dataSector = new SectorType ("Data", new Color (0, 200, 0)); // green + SectorType codeSector = new SectorType ("Code", Color.red); + SectorType textSector = new SectorType ("Text", Color.blue); + SectorType infoSector = new SectorType ("Info", Color.orange); + SectorType grafSector = new SectorType ("Graf", Color.cyan); + SectorType fotoSector = new SectorType ("Foto", Color.gray); + + public PascalDisk (Disk disk) + { + super (disk); + + sectorTypesList.add (diskBootSector); + sectorTypesList.add (catalogSector); + sectorTypesList.add (dataSector); + sectorTypesList.add (codeSector); + sectorTypesList.add (textSector); + sectorTypesList.add (infoSector); + sectorTypesList.add (grafSector); + sectorTypesList.add (fotoSector); + + List blocks = disk.getDiskAddressList (0, 1); + byte[] buffer = disk.readSectors (blocks); + this.bootSector = new BootSector (disk, buffer, "Pascal"); + + buffer = disk.readSector (2); + byte[] data = new byte[CATALOG_ENTRY_SIZE]; + System.arraycopy (buffer, 0, data, 0, CATALOG_ENTRY_SIZE); + + volume = new VolumeEntry (data); + + for (int i = 0; i < 2; i++) + if (!disk.isSectorEmpty (i)) + { + sectorTypes[i] = diskBootSector; + freeBlocks.set (i, false); + } + + for (int i = 2; i < 280; i++) + freeBlocks.set (i, true); + + List sectors = new ArrayList (); + for (int i = 2; i < volume.lastBlock; i++) + { + DiskAddress da = disk.getDiskAddress (i); + if (!disk.isSectorEmpty (da)) + sectorTypes[i] = catalogSector; + sectors.add (da); + freeBlocks.set (i, false); + } + buffer = disk.readSectors (sectors); + diskCatalogSector = new PascalCatalogSector (disk, buffer); // uses all 4 sectors + + DefaultMutableTreeNode root = getCatalogTreeRoot (); + DefaultMutableTreeNode volumeNode = new DefaultMutableTreeNode (volume); + root.add (volumeNode); + + // read the catalog + List addresses = new ArrayList (); + for (int i = 2; i < volume.lastBlock; i++) + addresses.add (disk.getDiskAddress (i)); + buffer = disk.readSectors (addresses); + + // loop through each catalog entry (what if there are deleted files?) + for (int i = 1; i <= volume.totalFiles; i++) + { + int ptr = i * CATALOG_ENTRY_SIZE; + data = new byte[CATALOG_ENTRY_SIZE]; + System.arraycopy (buffer, ptr, data, 0, CATALOG_ENTRY_SIZE); + FileEntry fe = new FileEntry (data); + fileEntries.add (fe); + DefaultMutableTreeNode node = new DefaultMutableTreeNode (fe); + if (fe.fileType == 2) // PascalCode + { + node.setAllowsChildren (true); + PascalCode pc = (PascalCode) fe.getDataSource (); + for (PascalSegment ps : pc) + { + // List blocks = new ArrayList (); + DefaultMutableTreeNode segmentNode = + new DefaultMutableTreeNode (new PascalCodeObject (ps, fe.firstBlock)); + node.add (segmentNode); + segmentNode.setAllowsChildren (false); + } + } + else + node.setAllowsChildren (false); + volumeNode.add (node); + for (int j = fe.firstBlock; j < fe.lastBlock; j++) + freeBlocks.set (j, false); + } + + volumeNode.setUserObject (getCatalog ()); + makeNodeVisible (volumeNode.getFirstLeaf ()); + } + + public static boolean isCorrectFormat (AppleDisk disk, boolean debug) + { + disk.setInterleave (1); + if (checkFormat (disk, debug)) + return true; + disk.setInterleave (0); + if (checkFormat (disk, debug)) + return true; + disk.setInterleave (3); + return checkFormat (disk, debug); + } + + public static boolean checkFormat (AppleDisk disk, boolean debug) + { + byte[] buffer = disk.readSector (2); + int nameLength = HexFormatter.intValue (buffer[6]); + if (nameLength < 1 || nameLength > 7) + { + if (debug) + System.out.println ("bad name length : " + nameLength); + return false; + } + if (debug) + { + String name = HexFormatter.getPascalString (buffer, 6); + System.out.println ("Name ok : " + name); + } + + int from = HexFormatter.intValue (buffer[0], buffer[1]); + int to = HexFormatter.intValue (buffer[2], buffer[3]); + if (from != 0 || to != 6) + return false; // will only work for floppies! + + List addresses = new ArrayList (); + for (int i = 2; i < to; i++) + addresses.add (disk.getDiskAddress (i)); + buffer = disk.readSectors (addresses); + + int blocks = HexFormatter.intValue (buffer[14], buffer[15]); + if (blocks > 280) + return false; + int files = HexFormatter.intValue (buffer[16], buffer[17]); + if (files < 0 || files > 77) + return false; + + if (debug) + System.out.println ("Files found : " + files); + + for (int i = 1; i <= files; i++) + { + int ptr = i * 26; + int a = HexFormatter.intValue (buffer[ptr], buffer[ptr + 1]); + int b = HexFormatter.intValue (buffer[ptr + 2], buffer[ptr + 3]); + int c = HexFormatter.intValue (buffer[ptr + 4], buffer[ptr + 5]); + if (b < a) + return false; + if (c == 0) + return false; + nameLength = HexFormatter.intValue (buffer[ptr + 6]); + if (nameLength < 1 || nameLength > 15) + return false; + } + + return true; + } + + @Override + public DataSource getFormattedSector (DiskAddress da) + { + SectorType st = sectorTypes[da.getBlock ()]; + if (st == diskBootSector) + return bootSector; + if (st == catalogSector) + return diskCatalogSector; + String name = getSectorFilename (da); + if (name != null) + return new DefaultSector (name, disk, disk.readSector (da)); + return super.getFormattedSector (da); + } + + @Override + public String getSectorFilename (DiskAddress da) + { + for (AppleFileSource ce : fileEntries) + if (((CatalogEntry) ce).contains (da)) + return ((CatalogEntry) ce).name; + return null; + } + + @Override + public List getFileSectors (int fileNo) + { + if (fileNo < 0 || fileNo >= fileEntries.size ()) + return null; + return fileEntries.get (fileNo).getSectors (); + } + + public DataSource getFile (int fileNo) + { + if (fileNo < 0 || fileNo >= fileEntries.size ()) + return null; + return fileEntries.get (fileNo).getDataSource (); + } + + @Override + public AppleFileSource getCatalog () + { + String newLine = String.format ("%n"); + String newLine2 = newLine + newLine; + String line = "---- --------------- ---- -------- ------- ---- ----" + newLine; + String date = volume.date == null ? "--" : df.format (volume.date.getTime ()); + StringBuilder text = new StringBuilder (); + text.append ("Disk : " + disk.getFile ().getAbsolutePath () + newLine2); + text.append ("Volume : " + volume.name + newLine); + text.append ("Date : " + date + newLine2); + text.append ("Blks Name Type Date Length Frst Last" + newLine); + text.append (line); + + int usedBlocks = 6; + for (AppleFileSource fe : fileEntries) + { + FileEntry ce = (FileEntry) fe; + int size = ce.lastBlock - ce.firstBlock; + usedBlocks += size; + date = ce.date == null ? "--" : df.format (ce.date.getTime ()); + int bytes = (size - 1) * 512 + ce.bytesUsedInLastBlock; + text.append (String.format (" %3d %-15s %s %8s %,8d $%03X $%03X%n", size, + ce.name, fileTypes[ce.fileType], date, bytes, ce.firstBlock, + ce.lastBlock)); + } + text.append (line); + text.append (String.format ("Blocks free : %3d Blocks used : %3d Total blocks : %3d%n", + (volume.totalBlocks - usedBlocks), usedBlocks, + volume.totalBlocks)); + return new DefaultAppleFileSource (volume.name, text.toString (), this); + } + + private abstract class CatalogEntry implements AppleFileSource + { + String name; + int firstBlock; + int lastBlock; // block AFTER last used block + int fileType; + GregorianCalendar date; + List blocks = new ArrayList (); + AbstractFile file; + + public CatalogEntry (byte[] buffer) + { + firstBlock = HexFormatter.intValue (buffer[0], buffer[1]); + lastBlock = HexFormatter.intValue (buffer[2], buffer[3]); + // fileType = HexFormatter.intValue (buffer[4], buffer[5]); + fileType = buffer[4] & 0x0F; + name = HexFormatter.getPascalString (buffer, 6); + + for (int i = firstBlock; i < lastBlock; i++) + blocks.add (disk.getDiskAddress (i)); + } + + private boolean contains (DiskAddress da) + { + for (DiskAddress sector : blocks) + if (sector.compareTo (da) == 0) + return true; + return false; + } + + @Override + public String toString () + { + int size = lastBlock - firstBlock; + return String.format ("%03d %s %-15s", size, fileTypes[fileType], name); + } + + @Override + public List getSectors () + { + List sectors = new ArrayList (blocks); + return sectors; + } + + @Override + public FormattedDisk getFormattedDisk () + { + return PascalDisk.this; + } + + @Override + public String getUniqueName () + { + return name; + } + } + + private class VolumeEntry extends CatalogEntry + { + int totalFiles; + int totalBlocks; + + public VolumeEntry (byte[] buffer) + { + super (buffer); + totalBlocks = HexFormatter.intValue (buffer[14], buffer[15]); + totalFiles = HexFormatter.intValue (buffer[16], buffer[17]); + firstBlock = HexFormatter.intValue (buffer[18], buffer[19]); + date = HexFormatter.getPascalDate (buffer, 20); + + // for (int i = firstBlock; i < lastBlock; i++) + // sectorType[i] = catalogSector; + } + + @Override + public AbstractFile getDataSource () + { + System.out.println ("in Volume Entry **********************"); + if (file != null) + return file; + + byte[] buffer = disk.readSectors (blocks); + file = new DefaultAppleFile (name, buffer); + return file; + } + } + + private class FileEntry extends CatalogEntry + { + int bytesUsedInLastBlock; + + public FileEntry (byte[] buffer) + { + super (buffer); + bytesUsedInLastBlock = HexFormatter.intValue (buffer[22], buffer[23]); + date = HexFormatter.getPascalDate (buffer, 24); + + for (int i = firstBlock; i < lastBlock; i++) + switch (fileType) + { + case 2: + sectorTypes[i] = codeSector; + break; + case 3: + sectorTypes[i] = textSector; + break; + case 4: + sectorTypes[i] = infoSector; + break; + case 5: + sectorTypes[i] = dataSector; + break; + case 6: + sectorTypes[i] = grafSector; + break; + case 7: + sectorTypes[i] = fotoSector; + break; + default: + System.out.println ("Unknown pascal file type : " + fileType); + sectorTypes[i] = dataSector; + break; + } + } + + @Override + public AbstractFile getDataSource () + { + if (file != null) + return file; + + byte[] buffer = getExactBuffer (); + + // try + { + switch (fileType) + { + case 3: + file = new PascalText (name, buffer); + break; + case 2: + file = new PascalCode (name, buffer); + break; + case 4: + file = new PascalInfo (name, buffer); + break; + case 0: + // volume + break; + case 5: + // data + if (name.equals ("SYSTEM.CHARSET")) + { + file = new Charset (name, buffer); + break; + } + if (name.equals ("WT")) // only testing + { + file = new WizardryTitle (name, buffer); + break; + } + // intentional fall-through + default: + // unknown + file = new DefaultAppleFile (name, buffer); + } + } + // catch (Exception e) + // { + // file = new ErrorMessageFile (name, buffer, e); + // e.printStackTrace (); + // } + return file; + } + + private byte[] getExactBuffer () + { + byte[] buffer = disk.readSectors (blocks); + byte[] exactBuffer; + if (bytesUsedInLastBlock < 512) + { + int exactLength = buffer.length - 512 + bytesUsedInLastBlock; + exactBuffer = new byte[exactLength]; + System.arraycopy (buffer, 0, exactBuffer, 0, exactLength); + } + else + exactBuffer = buffer; + return exactBuffer; + } + } + + class PascalCodeObject implements AppleFileSource + { + private final AbstractFile segment; + private final List blocks; + + public PascalCodeObject (PascalSegment segment, int firstBlock) + { + this.segment = segment; + this.blocks = new ArrayList (); + + int lo = firstBlock + segment.blockNo; + int hi = lo + (segment.size - 1) / 512; + for (int i = lo; i <= hi; i++) + blocks.add (disk.getDiskAddress (i)); + } + + @Override + public DataSource getDataSource () + { + return segment; + } + + @Override + public FormattedDisk getFormattedDisk () + { + return PascalDisk.this; + } + + @Override + public List getSectors () + { + return blocks; + } + + @Override + public String getUniqueName () + { + return segment.name; // this should be fileName/segmentName + } + + @Override + public String toString () + { + return segment.name; + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/prodos/CatalogEntry.java b/src/com/bytezone/diskbrowser/prodos/CatalogEntry.java new file mode 100755 index 0000000..94064c6 --- /dev/null +++ b/src/com/bytezone/diskbrowser/prodos/CatalogEntry.java @@ -0,0 +1,49 @@ +package com.bytezone.diskbrowser.prodos; + +import java.util.ArrayList; +import java.util.GregorianCalendar; +import java.util.List; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.applefile.AppleFileSource; +import com.bytezone.diskbrowser.disk.Disk; +import com.bytezone.diskbrowser.disk.DiskAddress; +import com.bytezone.diskbrowser.disk.FormattedDisk; + +abstract class CatalogEntry implements AppleFileSource +{ + FormattedDisk parentDisk; + DirectoryHeader parentDirectory; + String name; + int storageType; + GregorianCalendar created; + int version; + int minVersion; + int access; + List dataBlocks = new ArrayList (); + Disk disk; + + public CatalogEntry (ProdosDisk parentDisk, byte[] entryBuffer) + { + this.parentDisk = parentDisk; + this.disk = parentDisk.getDisk (); + name = HexFormatter.getString (entryBuffer, 1, entryBuffer[0] & 0x0F); + storageType = (entryBuffer[0] & 0xF0) >> 4; + created = HexFormatter.getAppleDate (entryBuffer, 24); + version = HexFormatter.intValue (entryBuffer[28]); + minVersion = HexFormatter.intValue (entryBuffer[29]); + access = HexFormatter.intValue (entryBuffer[30]); + } + + public String getUniqueName () + { + if (parentDirectory == null) + return name; + return parentDirectory.getUniqueName () + "/" + name; + } + + public FormattedDisk getFormattedDisk () + { + return parentDisk; + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/prodos/DirectoryHeader.java b/src/com/bytezone/diskbrowser/prodos/DirectoryHeader.java new file mode 100755 index 0000000..08d093e --- /dev/null +++ b/src/com/bytezone/diskbrowser/prodos/DirectoryHeader.java @@ -0,0 +1,19 @@ +package com.bytezone.diskbrowser.prodos; + +import com.bytezone.diskbrowser.HexFormatter; + +abstract class DirectoryHeader extends CatalogEntry +{ + int entryLength; + int entriesPerBlock; + int fileCount; + + public DirectoryHeader (ProdosDisk parentDisk, byte[] entryBuffer) + { + super (parentDisk, entryBuffer); + + entryLength = HexFormatter.intValue (entryBuffer[31]); + entriesPerBlock = HexFormatter.intValue (entryBuffer[32]); + fileCount = HexFormatter.intValue (entryBuffer[33], entryBuffer[34]); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/prodos/FileEntry.java b/src/com/bytezone/diskbrowser/prodos/FileEntry.java new file mode 100755 index 0000000..e31528f --- /dev/null +++ b/src/com/bytezone/diskbrowser/prodos/FileEntry.java @@ -0,0 +1,560 @@ +package com.bytezone.diskbrowser.prodos; + +import java.util.ArrayList; +import java.util.GregorianCalendar; +import java.util.List; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.applefile.*; +import com.bytezone.diskbrowser.appleworks.AppleworksADBFile; +import com.bytezone.diskbrowser.appleworks.AppleworksSSFile; +import com.bytezone.diskbrowser.appleworks.AppleworksWPFile; +import com.bytezone.diskbrowser.disk.DiskAddress; +import com.bytezone.diskbrowser.gui.DataSource; + +// - Set sector types for each used sector +// - Populate dataBlocks, indexBlocks, catalogBlock and masterIndexBlock +// - Provide getDataSource () + +class FileEntry extends CatalogEntry implements ProdosConstants +{ + private final int fileType; + final int keyPtr; + private final int blocksUsed; + private final int endOfFile; + private final int auxType; + private final GregorianCalendar modified; + private final int headerPointer; + private DataSource file; + private final DiskAddress catalogBlock; + private DiskAddress masterIndexBlock; + private final List indexBlocks = new ArrayList (); + private boolean invalid; + + public FileEntry (ProdosDisk fDisk, byte[] entryBuffer, DirectoryHeader parent, + int parentBlock) + { + super (fDisk, entryBuffer); + this.parentDirectory = parent; + this.catalogBlock = this.disk.getDiskAddress (parentBlock); + + fileType = entryBuffer[16] & 0xFF; + keyPtr = HexFormatter.intValue (entryBuffer[17], entryBuffer[18]); + blocksUsed = HexFormatter.intValue (entryBuffer[19], entryBuffer[20]); + endOfFile = HexFormatter.intValue (entryBuffer[21], entryBuffer[22], entryBuffer[23]); + + auxType = HexFormatter.intValue (entryBuffer[31], entryBuffer[32]); + modified = HexFormatter.getAppleDate (entryBuffer, 33); + headerPointer = HexFormatter.intValue (entryBuffer[37], entryBuffer[38]); + + switch (storageType) + { + case TYPE_SEEDLING: + parentDisk.setSectorType (keyPtr, fDisk.dataSector); + DiskAddress da = disk.getDiskAddress (keyPtr); + if (da != null) + dataBlocks.add (da); + else + invalid = true; + break; + + case TYPE_SAPLING: + if (isGEOSFile ()) + traverseGEOSIndex (keyPtr); + else + traverseIndex (keyPtr); + break; + + case TYPE_TREE: + parentDisk.setSectorType (keyPtr, fDisk.masterIndexSector); + masterIndexBlock = disk.getDiskAddress (keyPtr); + if (isGEOSFile ()) + traverseGEOSMasterIndex (keyPtr); + else + traverseMasterIndex (keyPtr); + break; + + case TYPE_GSOS_EXTENDED_FILE: + parentDisk.setSectorType (keyPtr, fDisk.extendedKeySector); + indexBlocks.add (disk.getDiskAddress (keyPtr)); + byte[] buffer2 = disk.readSector (keyPtr); + // data fork and resource fork + for (int i = 0; i < 512; i += 256) + { + int storageType = buffer2[i] & 0x0F; + int keyBlock = HexFormatter.intValue (buffer2[i + 1], buffer2[i + 2]); + switch (storageType) + { + case ProdosConstants.TYPE_SEEDLING: + parentDisk.setSectorType (keyBlock, fDisk.dataSector); + dataBlocks.add (disk.getDiskAddress (keyBlock)); + break; + case ProdosConstants.TYPE_SAPLING: + traverseIndex (keyBlock); + break; + default: + System.out.println ("fork not a sapling or seedling!!!"); + } + } + break; + + case TYPE_SUBDIRECTORY: + int block = keyPtr; + do + { + dataBlocks.add (disk.getDiskAddress (block)); + byte[] buffer = disk.readSector (block); + block = HexFormatter.intValue (buffer[2], buffer[3]); + } while (block > 0); + break; + + default: + System.out.println ("Unknown storage type: " + storageType); + } + } + + private boolean isGEOSFile () + { + return ((fileType & 0xF0) == 0x80); + } + + private void removeEmptyBlocks () + { + while (dataBlocks.size () > 0) + { + DiskAddress da = dataBlocks.get (dataBlocks.size () - 1); + if (da.getBlock () == 0) + dataBlocks.remove (dataBlocks.size () - 1); + else + break; + } + } + + private void traverseMasterIndex (int keyPtr) + { + byte[] buffer = disk.readSector (keyPtr); // master index + // find the last used index block + // get the file size from the catalog and only check those blocks + int highestBlock = 0; + // A master index block can never be more than half full + for (int i = 127; i >= 0; i--) + { + int block = HexFormatter.intValue (buffer[i], buffer[i + 256]); + if (block > 0) + { + highestBlock = i; + break; + } + } + for (int i = 0; i <= highestBlock; i++) + { + int block = HexFormatter.intValue (buffer[i], buffer[i + 256]); // index + if (block != 0) + traverseIndex (block); + else + // add 256 empty data blocks + { + DiskAddress da = disk.getDiskAddress (0); + for (int j = 0; j < 256; j++) + dataBlocks.add (da); + } + } + removeEmptyBlocks (); + } + + private void traverseIndex (int keyBlock) + { + parentDisk.setSectorType (keyBlock, ((ProdosDisk) parentDisk).indexSector); + indexBlocks.add (disk.getDiskAddress (keyBlock)); + byte[] buffer = disk.readSector (keyBlock); + for (int i = 0; i < 256; i++) + { + int block = HexFormatter.intValue (buffer[i], buffer[i + 256]); + if (!disk.isValidAddress (block)) + { + System.out.println ("Invalid block in " + name + " : " + block); + invalid = true; + break; + } + // System.out.printf ("%4d %02X %02X%n", block, fileType, auxType); + // should we break if block == 0 and it's not a text file? + // if (block == 0 && !(fileType == ProdosConstants.FILE_TYPE_TEXT && auxType > 0)) + // if (block == 0 && fileType != 4) + // break; + if (block != 0) + { + parentDisk.setSectorType (block, ((ProdosDisk) parentDisk).dataSector); + dataBlocks.add (disk.getDiskAddress (block)); + } + } + } + + private void traverseGEOSMasterIndex (int keyPtr) + { + byte[] buffer = disk.readSector (keyPtr); // master index + // int length = HexFormatter.intValue (buffer[0xFF], buffer[0x1FF]); + for (int i = 0; i < 0x80; i++) + { + int block = HexFormatter.intValue (buffer[i], buffer[i + 256]); + if (block == 0) + break; + if (block == 0xFFFF) + continue; + traverseGEOSIndex (block); + } + } + + private void traverseGEOSIndex (int keyPtr) + { + parentDisk.setSectorType (keyPtr, ((ProdosDisk) parentDisk).indexSector); + indexBlocks.add (disk.getDiskAddress (keyPtr)); + byte[] buffer = disk.readSector (keyPtr); + // int length = HexFormatter.intValue (buffer[0xFF], buffer[0x1FF]); + for (int i = 0; i < 0x80; i++) + { + int block = HexFormatter.intValue (buffer[i], buffer[i + 256]); + if (block == 0) + break; + if (block == 0xFFFF) + continue; + parentDisk.setSectorType (block, ((ProdosDisk) parentDisk).dataSector); + dataBlocks.add (disk.getDiskAddress (block)); + } + } + + @Override + public DataSource getDataSource () + { + if (file != null) + return file; + if (invalid) + { + file = new DefaultAppleFile (name, null); + return file; + } + /* + * Text files with reclen > 0 are random access, possibly with gaps between + * records, so they need to be handled separately. + */ + if (fileType == FILE_TYPE_TEXT && auxType > 0) + { + switch (storageType) + { + case TYPE_TREE: + return getTreeTextFile (); + case TYPE_SAPLING: + return getSaplingTextFile (); + case TYPE_SEEDLING: + return getSeedlingTextFile (); + } + } + + byte[] buffer = isGEOSFile () ? getGEOSBuffer () : getBuffer (); + byte[] exactBuffer = getExactBuffer (buffer); + + try + { + switch (fileType) + { + case FILE_TYPE_BINARY: + case FILE_TYPE_RELOCATABLE: + case FILE_TYPE_SYS: + if (ShapeTable.isShapeTable (exactBuffer)) + file = new ShapeTable (name, exactBuffer); + else if (SimpleText.isHTML (exactBuffer)) + file = new SimpleText (name, exactBuffer); + else if (HiResImage.isGif (exactBuffer)) + file = new HiResImage (name, exactBuffer); + else if ((endOfFile == 0x1FF8 || endOfFile == 0x1FFF || endOfFile == 0x2000 || endOfFile == 0x4000) + && (auxType == 0x1FFF || auxType == 0x2000 || auxType == 0x4000)) + file = new HiResImage (name, exactBuffer); + else if (endOfFile == 38400 && name.startsWith ("LVL.")) + file = new LodeRunner (name, exactBuffer); + else + file = new AssemblerProgram (name, exactBuffer, auxType); + break; + case FILE_TYPE_TEXT: + assert auxType == 0; // auxType > 0 handled above + if (name.endsWith (".S")) + file = new MerlinSource (name, exactBuffer, auxType, endOfFile); + else + file = new TextFile (name, exactBuffer, auxType, endOfFile); + break; + case FILE_TYPE_APPLESOFT_BASIC: + file = new BasicProgram (name, exactBuffer); + break; + case FILE_TYPE_INTEGER_BASIC: + file = new IntegerBasicProgram (name, exactBuffer); + break; + case FILE_TYPE_DIRECTORY: + VolumeDirectoryHeader vdh = ((ProdosDisk) parentDisk).vdh; + file = + new ProdosDirectory (parentDisk, name, buffer, vdh.totalBlocks, + vdh.freeBlocks, vdh.usedBlocks); + break; + case FILE_TYPE_APPLESOFT_BASIC_VARS: + if (endOfFile == 0) + { + System.out.println ("Stored Variables EOF = 0"); + file = new StoredVariables (name, buffer); + } + else + file = new StoredVariables (name, exactBuffer); + break; + case FILE_TYPE_APPLETALK: + file = new DefaultAppleFile (name + " (Appletalk file)", buffer); + break; + case FILE_TYPE_AWP: + file = new AppleworksWPFile (name + " (Appleworks Word Processor)", buffer); + break; + case FILE_TYPE_ADB: + file = new AppleworksADBFile (name + " (Appleworks Database File)", buffer); + break; + case FILE_TYPE_ASP: + file = new AppleworksSSFile (name + " (Appleworks Spreadsheet File)", buffer); + break; + case FILE_TYPE_ASM_SOURCE: + file = new SimpleText (name, exactBuffer); + break; + case FILE_TYPE_ICN: + file = new IconFile (name, exactBuffer); + break; + case FILE_TYPE_PNT: + file = new HiResImage (name, exactBuffer, fileType, auxType); + break; + case FILE_TYPE_PIC: + file = new HiResImage (name, exactBuffer, fileType, auxType); + break; + default: + System.out.format ("Unknown file type : %02X%n", fileType); + if (fileType == 0xB3) + file = + new DefaultAppleFile (name, exactBuffer, + "S16 Apple IIgs Application Program"); + else + file = new DefaultAppleFile (name, exactBuffer); + } + } + catch (Exception e) + { + file = new ErrorMessageFile (name, buffer, e); + e.printStackTrace (); + } + return file; + } + + private byte[] getExactBuffer (byte[] buffer) + { + byte[] exactBuffer; + if (buffer.length < endOfFile) + { + exactBuffer = new byte[buffer.length]; + System.arraycopy (buffer, 0, exactBuffer, 0, buffer.length); + } + else if (buffer.length == endOfFile) + exactBuffer = buffer; + else + { + exactBuffer = new byte[endOfFile]; + System.arraycopy (buffer, 0, exactBuffer, 0, endOfFile); + } + return exactBuffer; + } + + private DataSource getTreeTextFile () + { + List buffers = new ArrayList (); + List addresses = new ArrayList (); + int logicalBlock = 0; + + byte[] mainIndexBuffer = disk.readSector (keyPtr); + for (int i = 0; i < 256; i++) + { + int indexBlock = HexFormatter.intValue (mainIndexBuffer[i], mainIndexBuffer[i + 256]); + if (indexBlock > 0) + logicalBlock = readIndexBlock (indexBlock, addresses, buffers, logicalBlock); + else + { + if (addresses.size () > 0) + { + byte[] tempBuffer = disk.readSectors (addresses); + buffers.add (new TextBuffer (tempBuffer, auxType, logicalBlock - addresses.size ())); + addresses.clear (); + } + logicalBlock += 256; + } + } + if (buffers.size () == 1 && name.endsWith (".S")) + return new MerlinSource (name, buffers.get (0).buffer, auxType, endOfFile); + return new TextFile (name, buffers, auxType, endOfFile); + } + + private DataSource getSaplingTextFile () + { + List buffers = new ArrayList (); + List addresses = new ArrayList (); + readIndexBlock (keyPtr, addresses, buffers, 0); + if (buffers.size () == 1 && name.endsWith (".S")) + return new MerlinSource (name, buffers.get (0).buffer, auxType, endOfFile); + return new TextFile (name, buffers, auxType, endOfFile); + } + + private DataSource getSeedlingTextFile () + { + byte[] buffer = getBuffer (); + if (endOfFile < buffer.length) + { + byte[] exactBuffer = new byte[endOfFile]; + System.arraycopy (buffer, 0, exactBuffer, 0, endOfFile); + buffer = exactBuffer; + } + if (name.endsWith (".S")) + return new MerlinSource (name, buffer, auxType, endOfFile); + return new TextFile (name, buffer, auxType, endOfFile); + } + + private byte[] getBuffer () + { + switch (storageType) + { + case TYPE_SEEDLING: + case TYPE_SAPLING: + case TYPE_TREE: + return disk.readSectors (dataBlocks); + case TYPE_GSOS_EXTENDED_FILE: + // this will return the data fork and the resource fork concatenated + return disk.readSectors (dataBlocks); + case TYPE_SUBDIRECTORY: + byte[] fullBuffer = new byte[dataBlocks.size () * BLOCK_ENTRY_SIZE]; // 39 * 13 = 507 + int offset = 0; + for (DiskAddress da : dataBlocks) + { + byte[] buffer = disk.readSector (da); + System.arraycopy (buffer, 4, fullBuffer, offset, BLOCK_ENTRY_SIZE); + offset += BLOCK_ENTRY_SIZE; + } + return fullBuffer; + default: + System.out.println ("Unknown storage type in getBuffer : " + storageType); + return new byte[512]; + } + } + + private byte[] getGEOSBuffer () + { + switch (storageType) + { + case TYPE_SEEDLING: + System.out.println ("Seedling GEOS file : " + name); // not sure if this is possible + return disk.readSectors (dataBlocks); + case TYPE_SAPLING: + return getIndexFile (keyPtr); + case TYPE_TREE: + return getMasterIndexFile (keyPtr); + default: + System.out.println ("Unknown storage type for GEOS file : " + storageType); + return new byte[512]; + } + } + + private byte[] getMasterIndexFile (int keyPtr) + { + byte[] buffer = disk.readSector (keyPtr); + int length = HexFormatter.intValue (buffer[0xFF], buffer[0x1FF]); + byte[] fileBuffer = new byte[length]; + int ptr = 0; + for (int i = 0; i < 0x80; i++) + { + int block = HexFormatter.intValue (buffer[i], buffer[i + 256]); + if (block == 0) + break; + if (block == 0xFFFF) // should this insert 131,072 zeroes? + continue; + byte[] temp = getIndexFile (block); + System.arraycopy (temp, 0, fileBuffer, ptr, temp.length); + ptr += temp.length; + } + return fileBuffer; + } + + private byte[] getIndexFile (int keyPtr) + { + byte[] buffer = disk.readSector (keyPtr); + int length = HexFormatter.intValue (buffer[0xFF], buffer[0x1FF]); + byte[] fileBuffer = new byte[length]; + for (int i = 0; i < 0x80; i++) + { + int block = HexFormatter.intValue (buffer[i], buffer[i + 256]); + if (block == 0) + break; + if (block == 0xFFFF) // should this insert 512 zeroes? + continue; + byte[] temp = disk.readSector (block); + System.arraycopy (temp, 0, fileBuffer, i * 512, length > 512 ? 512 : length); + length -= 512; + } + return fileBuffer; + } + + private int readIndexBlock (int indexBlock, List addresses, + List buffers, int logicalBlock) + { + byte[] indexBuffer = disk.readSector (indexBlock); + for (int j = 0; j < 256; j++) + { + int block = HexFormatter.intValue (indexBuffer[j], indexBuffer[j + 256]); + if (block > 0) + addresses.add (disk.getDiskAddress (block)); + else if (addresses.size () > 0) + { + byte[] tempBuffer = disk.readSectors (addresses); + buffers.add (new TextBuffer (tempBuffer, auxType, logicalBlock - addresses.size ())); + addresses.clear (); + } + logicalBlock++; + } + return logicalBlock; + } + + @Override + public List getSectors () + { + List sectors = new ArrayList (); + sectors.add (catalogBlock); + if (masterIndexBlock != null) + sectors.add (masterIndexBlock); + sectors.addAll (indexBlocks); + sectors.addAll (dataBlocks); + return sectors; + } + + public boolean contains (DiskAddress da) + { + if (da.equals (masterIndexBlock)) + return true; + for (DiskAddress block : indexBlocks) + if (block.compareTo (da) == 0) + return true; + for (DiskAddress block : dataBlocks) + if (block.compareTo (da) == 0) + return true; + return false; + } + + @Override + public String toString () + { + if (ProdosConstants.fileTypes[fileType].equals ("DIR")) + return name; + // String locked = (access == 0x01) ? "*" : " "; + String locked = (access == 0x00) ? "*" : " "; + if (true) + return String.format ("%s %03d %s", ProdosConstants.fileTypes[fileType], blocksUsed, + locked) + name; + String timeC = created == null ? "" : ProdosDisk.df.format (created.getTime ()); + String timeF = modified == null ? "" : ProdosDisk.df.format (modified.getTime ()); + return String.format ("%s %s%-30s %3d %,10d %15s %15s", + ProdosConstants.fileTypes[fileType], locked, parentDirectory.name + + "/" + name, blocksUsed, endOfFile, timeC, timeF); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/prodos/ProdosBitMapSector.java b/src/com/bytezone/diskbrowser/prodos/ProdosBitMapSector.java new file mode 100755 index 0000000..8c858ec --- /dev/null +++ b/src/com/bytezone/diskbrowser/prodos/ProdosBitMapSector.java @@ -0,0 +1,72 @@ +package com.bytezone.diskbrowser.prodos; + +import java.awt.Dimension; + +import com.bytezone.diskbrowser.disk.AbstractSector; +import com.bytezone.diskbrowser.disk.Disk; +import com.bytezone.diskbrowser.disk.DiskAddress; + +class ProdosBitMapSector extends AbstractSector +{ + DiskAddress da; + ProdosDisk parent; + + ProdosBitMapSector (ProdosDisk parent, Disk disk, byte[] buffer, DiskAddress da) + { + super (disk, buffer); + this.da = da; + this.parent = parent; + } + + @Override + public String createText () + { + Dimension grid = parent.getGridLayout (); + + // check range of bits for current block - so far I don't have a disk that needs + // more than a single block + int relativeBlock = da.getBlock () - parent.vdh.bitMapBlock; +// System.out.println ("rel " + relativeBlock); +// System.out.println ("width : " + grid.width); + int startBit = relativeBlock * 4096; + int endBit = startBit + 4096; + if (startBit >= grid.width * grid.height) + return "This sector is not used - the physical file size makes it unnecessary"; + + int width = (grid.width - 1) / 8 + 1; // must be 1-4 + + StringBuilder text = getHeader ("Prodos Bit Map Sector"); + + if (false) + { + int block = 0; + for (int row = 0; row < grid.height; row++) + { + int offset = block / 8; + StringBuilder details = new StringBuilder (); + for (int col = 0; col < grid.width; col++) + details.append (parent.isSectorFree (block++) ? ". " : "X "); + addText (text, buffer, offset, width, details.toString ()); + } + } + else + { + int startRow = relativeBlock * 512 / width; + int endRow = startRow + (512 / width); + int block = startBit; + int byteNo = 0; +// System.out.printf ("Start %d, end %d%n", startRow, endRow); + for (int row = startRow; row < endRow; row++) + { + StringBuilder details = new StringBuilder (); + for (int col = 0; col < grid.width; col++) + details.append (parent.isSectorFree (block++) ? ". " : "X "); + addText (text, buffer, byteNo, width, details.toString ()); + byteNo += width; + } + } + + text.deleteCharAt (text.length () - 1); + return text.toString (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/prodos/ProdosCatalogSector.java b/src/com/bytezone/diskbrowser/prodos/ProdosCatalogSector.java new file mode 100755 index 0000000..3a62fa1 --- /dev/null +++ b/src/com/bytezone/diskbrowser/prodos/ProdosCatalogSector.java @@ -0,0 +1,196 @@ +package com.bytezone.diskbrowser.prodos; + +import static com.bytezone.diskbrowser.prodos.ProdosConstants.ENTRY_SIZE; +import static com.bytezone.diskbrowser.prodos.ProdosConstants.TYPE_DIRECTORY_HEADER; +import static com.bytezone.diskbrowser.prodos.ProdosConstants.TYPE_FREE; +import static com.bytezone.diskbrowser.prodos.ProdosConstants.TYPE_GSOS_EXTENDED_FILE; +import static com.bytezone.diskbrowser.prodos.ProdosConstants.TYPE_PASCAL_ON_PROFILE; +import static com.bytezone.diskbrowser.prodos.ProdosConstants.TYPE_SAPLING; +import static com.bytezone.diskbrowser.prodos.ProdosConstants.TYPE_SEEDLING; +import static com.bytezone.diskbrowser.prodos.ProdosConstants.TYPE_SUBDIRECTORY; +import static com.bytezone.diskbrowser.prodos.ProdosConstants.TYPE_SUBDIRECTORY_HEADER; +import static com.bytezone.diskbrowser.prodos.ProdosConstants.TYPE_TREE; + +import java.util.GregorianCalendar; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.disk.AbstractSector; +import com.bytezone.diskbrowser.disk.Disk; + +class ProdosCatalogSector extends AbstractSector +{ + ProdosCatalogSector (Disk disk, byte[] buffer) + { + super (disk, buffer); + } + + @Override + public String createText () + { + StringBuilder text = getHeader ("Prodos Catalog Sector"); + + addTextAndDecimal (text, buffer, 0, 2, "Previous block"); + addTextAndDecimal (text, buffer, 2, 2, "Next block"); + + for (int i = 4, max = buffer.length - ENTRY_SIZE; i <= max; i += ENTRY_SIZE) + { + if (buffer[i] == 0 && buffer[i + 1] == 0) + break; + text.append ("\n"); + + // first byte contains the file type (left nybble) and name length (right nybble) + int fileType = (buffer[i] & 0xF0) >> 4; + int nameLength = buffer[i] & 0x0F; + + // deleted files set file type and name length to zero, but the file name is still valid + String typeText = fileType + " = " + getType (buffer[i]); + if (fileType == 0) + addText (text, buffer, i, 1, typeText + " : " + getDeletedName (i + 1)); + else + addText (text, buffer, i, 1, typeText + ", " + nameLength + " = Name length"); + + addText (text, buffer, i + 1, 4, HexFormatter.getString (buffer, i + 1, nameLength)); + + switch (fileType) + { + case TYPE_FREE: + case TYPE_SEEDLING: + case TYPE_SAPLING: + case TYPE_TREE: + case TYPE_PASCAL_ON_PROFILE: + case TYPE_GSOS_EXTENDED_FILE: + case TYPE_SUBDIRECTORY: + text.append (doFileDescription (i)); + break; + case TYPE_SUBDIRECTORY_HEADER: + text.append (doSubdirectoryHeader (i)); + break; + case TYPE_DIRECTORY_HEADER: + text.append (doVolumeDirectoryHeader (i)); + break; + default: + text.append ("Unknown\n"); + } + } + if (text.length () > 0) + text.deleteCharAt (text.length () - 1); + return text.toString (); + } + + private String doFileDescription (int offset) + { + StringBuilder text = new StringBuilder (); + int fileType = HexFormatter.intValue (buffer[offset + 16]); + addText (text, buffer, offset + 16, 1, "File type (" + ProdosConstants.fileTypes[fileType] + + ")"); + addTextAndDecimal (text, buffer, offset + 17, 2, "Key pointer"); + addTextAndDecimal (text, buffer, offset + 19, 2, "Blocks used"); + addTextAndDecimal (text, buffer, offset + 21, 3, "EOF"); + GregorianCalendar created = HexFormatter.getAppleDate (buffer, offset + 24); + String dateC = created == null ? "" : ProdosDisk.df.format (created.getTime ()); + addText (text, buffer, offset + 24, 4, "Creation date : " + dateC); + addTextAndDecimal (text, buffer, offset + 28, 1, "Version"); + addText (text, buffer, offset + 29, 1, "Minimum version"); + addText (text, buffer, offset + 30, 1, "Access"); + addTextAndDecimal (text, buffer, offset + 31, 2, "Auxilliary type - " + + getAuxilliaryText (fileType)); + GregorianCalendar modified = HexFormatter.getAppleDate (buffer, offset + 33); + String dateM = modified == null ? "" : ProdosDisk.df.format (modified.getTime ()); + addText (text, buffer, offset + 33, 4, "Modification date : " + dateM); + addTextAndDecimal (text, buffer, offset + 37, 2, "Header pointer"); + return text.toString (); + } + + private String doVolumeDirectoryHeader (int offset) + { + StringBuilder text = new StringBuilder (); + addText (text, buffer, offset + 16, 4, "Not used"); + text.append (getCommonHeader (offset)); + addTextAndDecimal (text, buffer, offset + 35, 2, "Bit map pointer"); + addTextAndDecimal (text, buffer, offset + 37, 2, "Total blocks"); + return text.toString (); + } + + private String doSubdirectoryHeader (int offset) + { + StringBuilder text = new StringBuilder (); + addText (text, buffer, offset + 16, 1, "Hex $75"); + addText (text, buffer, offset + 17, 3, "Not used"); + text.append (getCommonHeader (offset)); + addTextAndDecimal (text, buffer, offset + 35, 2, "Parent block"); + addTextAndDecimal (text, buffer, offset + 37, 1, "Parent entry number"); + addTextAndDecimal (text, buffer, offset + 38, 1, "Parent entry length"); + return text.toString (); + } + + private String getCommonHeader (int offset) + { + StringBuilder text = new StringBuilder (); + addText (text, buffer, offset + 20, 4, "Not used"); + GregorianCalendar created = HexFormatter.getAppleDate (buffer, offset + 24); + String dateC = created == null ? "" : ProdosDisk.df.format (created.getTime ()); + addText (text, buffer, offset + 24, 4, "Creation date : " + dateC); + addText (text, buffer, offset + 28, 1, "Prodos version"); + addText (text, buffer, offset + 29, 1, "Minimum version"); + addText (text, buffer, offset + 30, 1, "Access"); + addTextAndDecimal (text, buffer, offset + 31, 1, "Entry length"); + addTextAndDecimal (text, buffer, offset + 32, 1, "Entries per block"); + addTextAndDecimal (text, buffer, offset + 33, 2, "File count"); + return text.toString (); + } + + private String getAuxilliaryText (int fileType) + { + switch (fileType) + { + case 0x04: + return "record length"; + case 0xFD: + return "address of stored variables"; + case 0x06: + case 0xFC: + case 0xFF: + return "load address"; + case 0xB3: + return "forked"; + default: + return "???"; + } + } + + private String getType (byte flag) + { + switch ((flag & 0xF0) >> 4) + { + case TYPE_FREE: + return "Deleted"; + case TYPE_SEEDLING: + return "Seedling"; + case TYPE_SAPLING: + return "Sapling"; + case TYPE_TREE: + return "Tree"; + case TYPE_PASCAL_ON_PROFILE: + return "Pascal area on a Profile HD"; + case TYPE_GSOS_EXTENDED_FILE: + return "GS/OS extended file"; + case TYPE_SUBDIRECTORY: + return "Subdirectory"; + case TYPE_SUBDIRECTORY_HEADER: + return "Subdirectory Header"; + case TYPE_DIRECTORY_HEADER: + return "Volume Directory Header"; + default: + return "???"; + } + } + + // Deleted files leave the name intact, but set the name length to zero + private String getDeletedName (int offset) + { + StringBuilder text = new StringBuilder (); + for (int i = offset, max = offset + 15; i < max && buffer[i] != 0; i++) + text.append ((char) HexFormatter.intValue (buffer[i])); + return text.toString (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/prodos/ProdosConstants.java b/src/com/bytezone/diskbrowser/prodos/ProdosConstants.java new file mode 100755 index 0000000..8d0ecc7 --- /dev/null +++ b/src/com/bytezone/diskbrowser/prodos/ProdosConstants.java @@ -0,0 +1,195 @@ +package com.bytezone.diskbrowser.prodos; + +interface ProdosConstants +{ + int FILE_TYPE_TEXT = 0x04; + int FILE_TYPE_BINARY = 0x06; + int FILE_TYPE_PICT = 0x08; + int FILE_TYPE_DIRECTORY = 0x0F; + int FILE_TYPE_ADB = 0x19; + int FILE_TYPE_AWP = 0x1A; + int FILE_TYPE_ASP = 0x1B; + int FILE_TYPE_ASM_SOURCE = 0xB0; + int FILE_TYPE_ASM_OBJECT = 0xB1; + int FILE_TYPE_FORKED_FILE = 0xB3; // S16 + int FILE_TYPE_PNT = 0xC0; + int FILE_TYPE_PIC = 0xC1; + int FILE_TYPE_ICN = 0xCA; + int FILE_TYPE_INTEGER_BASIC = 0xFA; + int FILE_TYPE_INTEGER_BASIC_VARS = 0xFB; + int FILE_TYPE_APPLESOFT_BASIC = 0xFC; + int FILE_TYPE_APPLESOFT_BASIC_VARS = 0xFD; + int FILE_TYPE_RELOCATABLE = 0xFE; + int FILE_TYPE_SYS = 0xFF; + int FILE_TYPE_APPLETALK = 0xE2; + + int TYPE_DIRECTORY_HEADER = 15; + int TYPE_SUBDIRECTORY_HEADER = 14; + int TYPE_SUBDIRECTORY = 13; + int TYPE_GSOS_EXTENDED_FILE = 5; + int TYPE_PASCAL_ON_PROFILE = 4; + int TYPE_TREE = 3; + int TYPE_SAPLING = 2; + int TYPE_SEEDLING = 1; + int TYPE_FREE = 0; + + String[] fileTypes = { "NON", "BAD", "PCD", "PTX", "TXT", "PDA", "BIN", "FNT", "FOT", "BA3", + "DA3", "WPF", "SOS", "$0D", "$0E", "DIR", "RPD", "RPI", "AFD", "AFM", + "AFR", "SCL", "PFS", "$17", "$18", "ADB", "AWP", "ASP", "$1C", "$1D", + "$1E", "$1F", "TDM", "$21", "$22", "$23", "$24", "$25", "$26", "$27", + "$28", "$29", "8SC", "8OB", "8IC", "8LD", "P8C", "$2F", "$30", "$31", + "$32", "$33", "$34", "$35", "$36", "$37", "$38", "$39", "$3A", "$3B", + "$3C", "$3D", "$3E", "$3F", "DIC", "OCR", "FTD", "$43", "$44", "$45", + "$46", "$47", "$48", "$49", "$4A", "$4B", "$4C", "$4D", "$4E", "$4F", + "GWP", "GSS", "GDB", "DRW", "GDP", "HMD", "EDU", "STN", "HLP", "COM", + "CFG", "ANM", "MUM", "ENT", "DVU", "FIN", "$60", "$61", "$62", "$63", + "$64", "$65", "$66", "$67", "$68", "$69", "$6A", "BIO", "$6C", "TDR", + "PRE", "HDV", "$70", "$71", "$72", "$73", "$74", "$75", "$76", "$77", + "$78", "$79", "$7A", "$7B", "$7C", "$7D", "$7E", "$7F", "GES", "GEA", + "GEO", "GED", "GEF", "GEP", "GEI", "GEX", "$88", "GEV", "$8A", "GEC", + "GEK", "GEW", "$8E", "$8F", "$90", "$91", "$92", "$93", "$94", "$95", + "$96", "$97", "$98", "$99", "$9A", "$9B", "$9C", "$9D", "$9E", "$9F", + "WP ", "$A1", "$A2", "$A3", "$A4", "$A5", "$A6", "$A7", "$A8", "$A9", + "$AA", "GSB", "TDF", "BDF", "$AE", "$AF", "SRC", "OBJ", "LIB", "S16", + "RTL", "EXE", "PIF", "TIF", "NDA", "CDA", "TOL", "DVR", "LDF", "FST", + "$BE", "DOC", "PNT", "PIC", "ANI", "PAL", "$C4", "OOG", "SCR", "CDV", + "FON", "FND", "ICN", "$CB", "$CC", "$CD", "$CE", "$CF", "$D0", "$D1", + "$D2", "$D3", "$D4", "MUS", "INS", "MDI", "SND", "$D9", "$DA", "DBM", + "$DC", "DDD", "$DE", "$DF", "LBR", "$E1", "ATK", "$E3", "$E4", "$E5", + "$E6", "$E7", "$E8", "$E9", "$EA", "$EB", "$EC", "$ED", "R16", "PAS", + "CMD", "$F1", "$F2", "$F3", "$F4", "$F5", "$F6", "$F7", "$F8", "OS ", + "INT", "IVR", "BAS", "VAR", "REL", "SYS" }; + + int ENTRY_SIZE = 39; + int ENTRIES_PER_BLOCK = 13; + int BLOCK_ENTRY_SIZE = ENTRY_SIZE * ENTRIES_PER_BLOCK; +} + +/* http://www.kreativekorp.com/miscpages/a2info/filetypes.shtml + * + * $00 UNK Unknown + * $01 BAD Bad Block File + * $02 PCD Pascal Code + * $03 PTX Pascal Text + * $04 TXT ASCII Text + * $05 PDA Pascal Data + * $06 BIN Binary File + * $07 FNT Apple III Font + * $08 FOT HiRes/Double HiRes File + * $09 BA3 Apple III BASIC Program + * $0A DA3 Apple III BASIC Data + * $0B WPF Generic Word Processor File + * $0C SOS SOS System File + * $0F DIR ProDOS Directory + * $10 RPD RPS Data + * $11 RPI RPS Index + * $12 AFD AppleFile Discard + * $13 AFM AppleFile Model + * $14 AFR AppleFile Report + * $15 SCL Screen Library + * $16 PFS PFS Document + * $19 ADB AppleWorks Database + * $1A AWP AppleWorks Word Processor + * $1B ASP AppleWorks Spreadsheet + * $20 TDM Desktop Manager File + * $21 IPS Instant Pascal Source + * $22 UPV UCSD Pascal Volume + * $29 3SD SOS Directory + * $2A 8SC Source Code + * $2B 8OB Object Code + * $2C 8IC Interpretted Code + * $2D 8LD Language Data + * $2E P8C ProDOS 8 Code Module + * $41 OCR Optical Character Recognition File + * $50 GWP Apple IIgs Word Processor File + * $51 GSS Apple IIgs Spreadsheet File + * $52 GDB Apple IIgs Database File + * $53 DRW Object Oriented Graphics File + * $54 GDP Apple IIgs Desktop Publishing File + * $55 HMD HyperMedia + * $56 EDU Educational Program Data + * $57 STN Stationery + * $58 HLP Help File + * $59 COM Communications File + * $5A CFG Configuration File + * $5B ANM Animation File + * $5C MUM Multimedia File + * $5D ENT Entertainment Program File + * $5E DVU Development Utility File + * $60 PRE PC Pre-Boot + * $6B BIO PC BIOS + * $6D DVR PC Driver + * $6E PRE PC Pre-Boot + * $6F HDV PC Hard Disk Image + * $77 KES KES Software + * $7B TLB KES Software + * $7F JCP KES Software + * $80 GeOS System File + * $81 GeOS Desk Accessory + * $82 GeOS Application + * $83 GeOS Document + * $84 GeOS Font + * $85 GeOS Printer Driver + * $86 GeOS Input Driver + * $87 GeOS Auxilary Driver + * $8B GeOS Clock Driver + * $8C GeOS Interface Card Driver + * $8D GeOS Formatting Data + * $A0 WP WordPerfect File + * $A6 + * $AB GSB Apple IIgs BASIC Program + * $AC TDF Apple IIgs BASIC TDF + * $AD BDF Apple IIgs BASIC Data + * $B0 SRC Apple IIgs Source Code + * $B1 OBJ Apple IIgs Object Code + * $B2 LIB Apple IIgs Library + * $B3 S16 Apple IIgs Application Program + * $B4 RTL Apple IIgs Runtime Library + * $B5 EXE Apple IIgs Shell + * $B6 PIF Apple IIgs Permanent INIT + * $B7 TIF Apple IIgs Temporary INIT + * $B8 NDA Apple IIgs New Desk Accessory + * $B9 CDA Apple IIgs Classic Desk Accessory + * $BA TOL Apple IIgs Tool + * $BB DRV Apple IIgs Device Driver + * $BC LDF Apple IIgs Generic Load File + * $BD FST Apple IIgs File System Translator + * $BF DOC Apple IIgs Document + * $C0 PNT Apple IIgs Packed Super HiRes + * $C1 PIC Apple IIgs Super HiRes + * $C2 ANI PaintWorks Animation + * $C3 PAL PaintWorks Palette + * $C5 OOG Object-Oriented Graphics + * $C6 SCR Script + * $C7 CDV Apple IIgs Control Panel + * $C8 FON Apple IIgs Font + * $C9 FND Apple IIgs Finder Data + * $CA ICN Apple IIgs Icon File + * $D5 MUS Music File + * $D6 INS Instrument File + * $D7 MDI MIDI File + * $D8 SND Apple IIgs Sound File + * $DB DBM DB Master Document + * $E0 LBR Archive File + * $E2 ATK AppleTalk Data + * $EE R16 EDASM 816 Relocatable Code + * $EF PAR Pascal Area + * $F0 CMD ProDOS Command File + * $F1 OVL User Defined 1 + * $F2 User Defined 2 + * $F3 User Defined 3 + * $F4 User Defined 4 + * $F5 BAT User Defined 5 + * $F6 User Defined 6 + * $F7 User Defined 7 + * $F8 PRG User Defined 8 + * $F9 P16 Apple IIgs System File + * $FA INT Integer BASIC Program + * $FB IVR Integer BASIC Variables + * $FC BAS Applesoft BASIC Program + * $FD VAR Applesoft BASIC Variables + * $FE REL EDASM Relocatable Code + * $FF SYS ProDOS System File + */ + +// See also http://www.kreativekorp.com/miscpages/a2info/filetypes.shtml \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/prodos/ProdosDirectory.java b/src/com/bytezone/diskbrowser/prodos/ProdosDirectory.java new file mode 100755 index 0000000..b22d816 --- /dev/null +++ b/src/com/bytezone/diskbrowser/prodos/ProdosDirectory.java @@ -0,0 +1,126 @@ +package com.bytezone.diskbrowser.prodos; + +import java.util.GregorianCalendar; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.applefile.AbstractFile; +import com.bytezone.diskbrowser.disk.FormattedDisk; + +class ProdosDirectory extends AbstractFile +{ + private static final String NO_DATE = ""; + private static final String newLine = String.format ("%n"); + private static final String newLine2 = newLine + newLine; + + // private final Disk parent; + private final FormattedDisk parentFD; + private final int totalBlocks; + private final int freeBlocks; + private final int usedBlocks; + + public ProdosDirectory (FormattedDisk parent, String name, byte[] buffer, int totalBlocks, + int freeBlocks, int usedBlocks) + { + super (name, buffer); + this.parentFD = parent; + this.totalBlocks = totalBlocks; + this.freeBlocks = freeBlocks; + this.usedBlocks = usedBlocks; + } + + @Override + public String getText () + { + StringBuffer text = new StringBuffer (); + text.append ("Disk : " + parentFD.getAbsolutePath () + newLine2); + for (int i = 0; i < buffer.length; i += 39) + { + int storageType = (buffer[i] & 0xF0) >> 4; + if (storageType == 0) + continue; // break?? + int nameLength = buffer[i] & 0x0F; + String filename = HexFormatter.getString (buffer, i + 1, nameLength); + String subType = ""; + String locked; + + switch (storageType) + { + case ProdosConstants.TYPE_DIRECTORY_HEADER: + case ProdosConstants.TYPE_SUBDIRECTORY_HEADER: + text.append ("/" + filename + newLine2); + text.append (" NAME TYPE BLOCKS " + + "MODIFIED CREATED ENDFILE SUBTYPE" + newLine2); + break; + case ProdosConstants.TYPE_FREE: + case ProdosConstants.TYPE_SEEDLING: + case ProdosConstants.TYPE_SAPLING: + case ProdosConstants.TYPE_TREE: + case ProdosConstants.TYPE_PASCAL_ON_PROFILE: + case ProdosConstants.TYPE_GSOS_EXTENDED_FILE: + case ProdosConstants.TYPE_SUBDIRECTORY: + int type = HexFormatter.intValue (buffer[i + 16]); + int blocks = HexFormatter.intValue (buffer[i + 19], buffer[i + 20]); + + GregorianCalendar created = HexFormatter.getAppleDate (buffer, i + 24); + String dateC = + created == null ? NO_DATE : ProdosDisk.sdf.format (created.getTime ()) + .toUpperCase (); + String timeC = created == null ? "" : ProdosDisk.stf.format (created.getTime ()); + GregorianCalendar modified = HexFormatter.getAppleDate (buffer, i + 33); + String dateM = + modified == null ? NO_DATE : ProdosDisk.sdf.format (modified.getTime ()) + .toUpperCase (); + String timeM = modified == null ? "" : ProdosDisk.stf.format (modified.getTime ()); + int eof = HexFormatter.intValue (buffer[i + 21], buffer[i + 22], buffer[i + 23]); + int fileType = HexFormatter.intValue (buffer[i + 16]); + locked = (buffer[i + 30] & 0xE0) == 0xE0 ? " " : "*"; + + switch (fileType) + { + case 4: // Text file + int aux = HexFormatter.intValue (buffer[i + 31], buffer[i + 32]); + subType = String.format ("R=%5d", aux); + break; + case 6: // BIN file + aux = HexFormatter.intValue (buffer[i + 31], buffer[i + 32]); + subType = String.format ("A=$%4X", aux); + break; + case 0x1A: // AWP file + aux = HexFormatter.intValue (buffer[i + 32], buffer[i + 31]); // backwards! + if (aux != 0) + filename = convert (filename, aux); + break; + default: + subType = ""; + } + + text.append (String.format ("%s%-15s %3s %5d %9s %5s %9s %5s %8d %7s%n", locked, + filename, ProdosConstants.fileTypes[type], blocks, + dateM, timeM, dateC, timeC, eof, subType)); + break; + default: + text.append (" >>= 1) + { + if ((flags & weight) != 0) + { + if (newName[i] == '.') + newName[i] = ' '; + else if (newName[i] >= 'A' && newName[i] <= 'Z') + newName[i] += 32; + } + } + return new String (newName); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/prodos/ProdosDisk.java b/src/com/bytezone/diskbrowser/prodos/ProdosDisk.java new file mode 100755 index 0000000..7c70d7b --- /dev/null +++ b/src/com/bytezone/diskbrowser/prodos/ProdosDisk.java @@ -0,0 +1,247 @@ +package com.bytezone.diskbrowser.prodos; + +import java.awt.Color; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.tree.DefaultMutableTreeNode; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.applefile.AppleFileSource; +import com.bytezone.diskbrowser.applefile.BootSector; +import com.bytezone.diskbrowser.disk.*; +import com.bytezone.diskbrowser.gui.DataSource; + +public class ProdosDisk extends AbstractFormattedDisk +{ + final SectorType dosSector = new SectorType ("Bootstrap Loader", Color.lightGray); + final SectorType catalogSector = new SectorType ("Catalog", new Color (0, 200, 0)); // green + final SectorType volumeMapSector = new SectorType ("Volume Map", Color.blue); + final SectorType subcatalogSector = new SectorType ("Subcatalog", Color.magenta); + final SectorType masterIndexSector = new SectorType ("Master Index", Color.orange); + final SectorType indexSector = new SectorType ("Index", Color.cyan); + final SectorType dataSector = new SectorType ("Data", Color.red); + final SectorType extendedKeySector = new SectorType ("Extended key", Color.gray); + + private final List headerEntries = new ArrayList (); + VolumeDirectoryHeader vdh = null; + private static final boolean debug = false; + + static final DateFormat df = DateFormat.getInstance (); + static final SimpleDateFormat sdf = new SimpleDateFormat ("d-MMM-yy"); + static final SimpleDateFormat stf = new SimpleDateFormat ("H:mm"); + + public ProdosDisk (Disk disk) + { + super (disk); + + sectorTypesList.add (dosSector); + sectorTypesList.add (catalogSector); + sectorTypesList.add (subcatalogSector); + sectorTypesList.add (volumeMapSector); + sectorTypesList.add (masterIndexSector); + sectorTypesList.add (indexSector); + sectorTypesList.add (dataSector); + sectorTypesList.add (extendedKeySector); + + for (int block = 0; block < 2; block++) + if (!disk.isSectorEmpty (disk.getDiskAddress (block))) + sectorTypes[block] = dosSector; + + byte[] buffer = disk.readSector (0); + bootSector = new BootSector (disk, buffer, "Prodos"); + + DefaultMutableTreeNode root = getCatalogTreeRoot (); + DefaultMutableTreeNode volumeNode = new DefaultMutableTreeNode ("empty volume node"); + root.add (volumeNode); + processDirectoryBlock (2, null, volumeNode); + makeNodeVisible (volumeNode.getFirstLeaf ()); + + for (DiskAddress da2 : disk) + { + int blockNo = da2.getBlock (); + if (freeBlocks.get (blockNo)) + { + if (!stillAvailable (da2)) + falsePositives++; + } + else if (stillAvailable (da2)) + falseNegatives++; + } + } + + private void processDirectoryBlock (int block, FileEntry parent, + DefaultMutableTreeNode parentNode) + { + DirectoryHeader localHeader = null; + SectorType currentSectorType = null; + + do + { + byte[] sectorBuffer = disk.readSector (block); + sectorTypes[block] = currentSectorType; + + for (int ptr = 4, max = disk.getBlockSize () - ProdosConstants.ENTRY_SIZE; ptr < max; ptr += + ProdosConstants.ENTRY_SIZE) + { + int storageType = (sectorBuffer[ptr] & 0xF0) >> 4; + if (storageType == 0) // deleted or unused + continue; + + byte[] entry = new byte[ProdosConstants.ENTRY_SIZE]; + System.arraycopy (sectorBuffer, ptr, entry, 0, ProdosConstants.ENTRY_SIZE); + + switch (storageType) + { + case ProdosConstants.TYPE_DIRECTORY_HEADER: + assert headerEntries.size () == 0; + vdh = new VolumeDirectoryHeader (this, entry); + localHeader = vdh; + assert localHeader.entryLength == ProdosConstants.ENTRY_SIZE; + headerEntries.add (localHeader); + currentSectorType = catalogSector; + sectorTypes[block] = currentSectorType; + for (int i = 0; i < vdh.totalBitMapBlocks; i++) + sectorTypes[vdh.bitMapBlock + i] = volumeMapSector; + parentNode.setUserObject (vdh); // populate the empty volume node + break; + case ProdosConstants.TYPE_SUBDIRECTORY_HEADER: + localHeader = new SubDirectoryHeader (this, entry, parent); + headerEntries.add (localHeader); + currentSectorType = subcatalogSector; + sectorTypes[block] = currentSectorType; + break; + case ProdosConstants.TYPE_SUBDIRECTORY: + FileEntry ce = new FileEntry (this, entry, localHeader, block); + fileEntries.add (ce); + DefaultMutableTreeNode directoryNode = new DefaultMutableTreeNode (ce); + directoryNode.setAllowsChildren (true); + parentNode.add (directoryNode); + processDirectoryBlock (ce.keyPtr, ce, directoryNode); // Recursion !! + break; + case ProdosConstants.TYPE_SEEDLING: + case ProdosConstants.TYPE_SAPLING: + case ProdosConstants.TYPE_TREE: + case ProdosConstants.TYPE_PASCAL_ON_PROFILE: + case ProdosConstants.TYPE_GSOS_EXTENDED_FILE: + FileEntry fe = new FileEntry (this, entry, localHeader, block); + fileEntries.add (fe); + DefaultMutableTreeNode node = new DefaultMutableTreeNode (fe); + node.setAllowsChildren (false); + parentNode.add (node); + break; + default: + System.out.println ("Unknown storage type : " + storageType); + System.out.println (HexFormatter.format (entry, 0, entry.length)); + } + } + block = HexFormatter.intValue (sectorBuffer[2], sectorBuffer[3]); + } while (block > 0); + } + + public static boolean isCorrectFormat (AppleDisk disk) + { + disk.setInterleave (1); + if (checkFormat (disk)) + return true; + disk.setInterleave (0); + return checkFormat (disk); + } + + public static boolean checkFormat (AppleDisk disk) + { + byte[] buffer = disk.readSector (2); // Prodos KEY BLOCK + if (debug) + System.out.println (HexFormatter.format (buffer)); + + // check entry length and entries per block + if (buffer[0x23] != 0x27 || buffer[0x24] != 0x0D) + return false; + int bitMapBlock = HexFormatter.intValue (buffer[0x27], buffer[0x28]); + if (bitMapBlock != 6) + return false; + return true; + } + + public DataSource getFile (int fileNo) + { + if (fileNo == 0) + return ((VolumeDirectoryHeader) headerEntries.get (0)).getDataSource (); + return fileEntries.get (fileNo - 1).getDataSource (); + } + + @Override + public AppleFileSource getCatalog () + { + return new DefaultAppleFileSource ("Catalog", headerEntries.get (0).getDataSource (), this); + } + + @Override + public String toString () + { + StringBuffer text = new StringBuffer (); + String newLine = String.format ("%n"); + + VolumeDirectoryHeader volumeDirectory = (VolumeDirectoryHeader) headerEntries.get (0); + String timeC = + volumeDirectory.created == null ? "" : df + .format (volumeDirectory.created.getTime ()); + text.append ("Volume name : " + volumeDirectory.name + newLine); + text.append ("Creation date : " + timeC + newLine); + text.append ("ProDOS version : " + volumeDirectory.version + newLine); + text.append ("Min ProDOS version : " + volumeDirectory.minVersion + newLine); + text.append ("Access rights : " + volumeDirectory.access + newLine); + text.append ("Entry length : " + volumeDirectory.entryLength + newLine); + text.append ("Entries per block : " + volumeDirectory.entriesPerBlock + newLine); + text.append ("File count : " + volumeDirectory.fileCount + newLine); + text.append ("Bitmap block : " + volumeDirectory.bitMapBlock + newLine); + text.append ("Total blocks : " + volumeDirectory.totalBlocks + newLine); + + return text.toString (); + } + + @Override + public DataSource getFormattedSector (DiskAddress da) + { + if (da.getBlock () == 0) + return bootSector; + + byte[] buffer = disk.readSector (da); + SectorType type = sectorTypes[da.getBlock ()]; + + if (type == catalogSector || type == subcatalogSector) + return new ProdosCatalogSector (disk, buffer); + if (type == volumeMapSector) + return new ProdosBitMapSector (this, disk, buffer, da); + if (type == masterIndexSector || type == indexSector) + return new ProdosIndexSector (getSectorFilename (da), disk, buffer); + if (type == extendedKeySector) + return new ProdosExtendedKeySector (disk, buffer); + if (type == dosSector) + return new DefaultSector ("Boot sector", disk, buffer); + + String name = getSectorFilename (da); + if (name != null) + return new DefaultSector (name, disk, buffer); + return super.getFormattedSector (da); + } + + @Override + public String getSectorFilename (DiskAddress da) + { + for (AppleFileSource fe : fileEntries) + if (((FileEntry) fe).contains (da)) + return ((FileEntry) fe).getUniqueName (); + return null; + } + + @Override + public List getFileSectors (int fileNo) + { + if (fileNo == 0) + return ((VolumeDirectoryHeader) headerEntries.get (0)).getSectors (); + return fileEntries.get (fileNo - 1).getSectors (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/prodos/ProdosExtendedKeySector.java b/src/com/bytezone/diskbrowser/prodos/ProdosExtendedKeySector.java new file mode 100755 index 0000000..be0b2fc --- /dev/null +++ b/src/com/bytezone/diskbrowser/prodos/ProdosExtendedKeySector.java @@ -0,0 +1,45 @@ +package com.bytezone.diskbrowser.prodos; + +import com.bytezone.diskbrowser.disk.AbstractSector; +import com.bytezone.diskbrowser.disk.Disk; + +class ProdosExtendedKeySector extends AbstractSector +{ + + public ProdosExtendedKeySector (Disk disk, byte[] buffer) + { + super (disk, buffer); + } + + @Override + public String createText () + { + StringBuilder text = getHeader ("Prodos Extended Key Block"); + + for (int i = 0; i < 512; i += 256) + { + addText (text, buffer, i, 1, "Storage type (" + getType (buffer[i]) + ")"); + addTextAndDecimal (text, buffer, i + 1, 2, "Key block"); + addTextAndDecimal (text, buffer, i + 3, 2, "Blocks used"); + addTextAndDecimal (text, buffer, i + 5, 3, "EOF"); + text.append ("\n"); + } + + return text.toString (); + } + + private String getType (byte flag) + { + switch ((flag & 0x0F)) + { + case ProdosConstants.TYPE_SEEDLING: + return "Seedling"; + case ProdosConstants.TYPE_SAPLING: + return "Sapling"; + case ProdosConstants.TYPE_TREE: + return "Tree"; + default: + return "???"; + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/prodos/ProdosIndexSector.java b/src/com/bytezone/diskbrowser/prodos/ProdosIndexSector.java new file mode 100755 index 0000000..bb61cba --- /dev/null +++ b/src/com/bytezone/diskbrowser/prodos/ProdosIndexSector.java @@ -0,0 +1,35 @@ +package com.bytezone.diskbrowser.prodos; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.disk.AbstractSector; +import com.bytezone.diskbrowser.disk.Disk; + +class ProdosIndexSector extends AbstractSector +{ + String name; + + ProdosIndexSector (String name, Disk disk, byte[] buffer) + { + super (disk, buffer); + this.name = name; + } + + @Override + public String createText () + { + StringBuilder text = getHeader ("Prodos Index Sector : " + name); + + for (int i = 0; i < 256; i++) + { + text.append (String.format ("%02X %02X %02X", i, buffer[i], buffer[i + 256])); + if (buffer[i] != 0 || buffer[i + 256] != 0) + text.append (String.format (" %s%n", + "block " + HexFormatter.intValue (buffer[i], buffer[i + 256]))); + else + text.append ("\n"); + } + if (text.length () > 0) + text.deleteCharAt (text.length () - 1); + return text.toString (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/prodos/SubDirectoryHeader.java b/src/com/bytezone/diskbrowser/prodos/SubDirectoryHeader.java new file mode 100755 index 0000000..56dca6e --- /dev/null +++ b/src/com/bytezone/diskbrowser/prodos/SubDirectoryHeader.java @@ -0,0 +1,43 @@ +package com.bytezone.diskbrowser.prodos; + +import java.util.List; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.disk.DiskAddress; +import com.bytezone.diskbrowser.gui.DataSource; + +class SubDirectoryHeader extends DirectoryHeader +{ + int parentPointer; + int parentSequence; + int parentSize; + + public SubDirectoryHeader (ProdosDisk parentDisk, byte[] entryBuffer, FileEntry parent) + { + super (parentDisk, entryBuffer); + this.parentDirectory = parent.parentDirectory; + + parentPointer = HexFormatter.intValue (entryBuffer[35], entryBuffer[36]); + parentSequence = HexFormatter.intValue (entryBuffer[37]); + parentSize = HexFormatter.intValue (entryBuffer[38]); + } + + @Override + public String toString () + { + String locked = (access == 0x01) ? "*" : " "; + return String.format (" %s%-40s %15s", locked, "/" + name, ProdosDisk.df.format (created + .getTime ())); + } + + public DataSource getDataSource () + { + // should this return a directory listing? + return null; + } + + public List getSectors () + { + return null; + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/prodos/VolumeDirectoryHeader.java b/src/com/bytezone/diskbrowser/prodos/VolumeDirectoryHeader.java new file mode 100755 index 0000000..09903b7 --- /dev/null +++ b/src/com/bytezone/diskbrowser/prodos/VolumeDirectoryHeader.java @@ -0,0 +1,124 @@ +package com.bytezone.diskbrowser.prodos; + +import java.util.ArrayList; +import java.util.List; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.disk.DiskAddress; +import com.bytezone.diskbrowser.gui.DataSource; + +/* + * There is only one of these - it is always the first entry in the first block. + * Every other entry will be either a SubDirectoryHeader or a FileEntry. + */ +class VolumeDirectoryHeader extends DirectoryHeader +{ + int bitMapBlock; + int totalBlocks; + int freeBlocks; + int usedBlocks; + int totalBitMapBlocks; + + public VolumeDirectoryHeader (ProdosDisk parentDisk, byte[] entryBuffer) + { + super (parentDisk, entryBuffer); + + bitMapBlock = HexFormatter.intValue (entryBuffer[35], entryBuffer[36]); + totalBlocks = HexFormatter.intValue (entryBuffer[37], entryBuffer[38]); + if (totalBlocks == 0xFFFF | totalBlocks == 0x7FFF) + totalBlocks = (int) disk.getFile ().length () / 4096 * 8; // ignore extra bytes + // totalBitMapBlocks = (totalBlocks * 8 - 1) / 4096 + 1; + totalBitMapBlocks = (totalBlocks - 1) / 512 + 1; + + int block = 2; + do + { + dataBlocks.add (disk.getDiskAddress (block)); + byte[] buffer = disk.readSector (block); + block = HexFormatter.intValue (buffer[2], buffer[3]); + } while (block > 0); + + // convert the Free Sector Table + int bitMapBytes = totalBlocks / 8; // one bit per block + byte[] buffer = new byte[bitMapBytes]; + int bitMapBlocks = (bitMapBytes - 1) / disk.getSectorsPerTrack () + 1; + int lastBitMapBlock = bitMapBlock + bitMapBlocks - 1; + int ptr = 0; + + for (block = bitMapBlock; block <= lastBitMapBlock; block++) + { + byte[] temp = disk.readSector (block); + int bytesToCopy = buffer.length - ptr; + if (bytesToCopy > temp.length) + bytesToCopy = temp.length; + System.arraycopy (temp, 0, buffer, ptr, bytesToCopy); + ptr += bytesToCopy; + } + + block = 0; + int max = (totalBlocks - 1) / 8 + 1; // bytes required for sector map + // int max = (disk.getTotalBlocks () - 1) / 8 + 1; // bytes required for sector map + + for (int i = 0; i < max; i++) + { + byte b = buffer[i]; + for (int j = 0; j < 8; j++) + { + if ((b & 0x80) == 0x80) + { + freeBlocks++; + parentDisk.setSectorFree (block++, true); + } + else + { + usedBlocks++; + parentDisk.setSectorFree (block++, false); + } + b <<= 1; + } + } + } + + @Override + public DataSource getDataSource () + { + List blockList = new ArrayList (); + int block = 2; + do + { + byte[] buf = disk.readSector (block); + blockList.add (buf); + block = HexFormatter.intValue (buf[2], buf[3]); // next block + } while (block > 0); + + byte[] fullBuffer = new byte[blockList.size () * 507]; + int offset = 0; + for (byte[] bfr : blockList) + { + System.arraycopy (bfr, 4, fullBuffer, offset, 507); + offset += 507; + } + return new ProdosDirectory (parentDisk, name, fullBuffer, totalBlocks, freeBlocks, + usedBlocks); + } + + @Override + public List getSectors () + { + List sectors = new ArrayList (); + sectors.addAll (dataBlocks); + return sectors; + } + + @Override + public String toString () + { + if (false) + { + String locked = (access == 0x01) ? "*" : " "; + String timeC = created == null ? "" : ProdosDisk.df.format (created.getTime ()); + return String.format (" %s%-42s %15s", locked, "/" + name, timeC); + } + return name; + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/wizardry/AbstractImage.java b/src/com/bytezone/diskbrowser/wizardry/AbstractImage.java new file mode 100755 index 0000000..148cc49 --- /dev/null +++ b/src/com/bytezone/diskbrowser/wizardry/AbstractImage.java @@ -0,0 +1,11 @@ +package com.bytezone.diskbrowser.wizardry; + +import com.bytezone.diskbrowser.applefile.AbstractFile; + +public abstract class AbstractImage extends AbstractFile +{ + public AbstractImage (String name, byte[] buffer) + { + super (name, buffer); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/wizardry/Character.java b/src/com/bytezone/diskbrowser/wizardry/Character.java new file mode 100755 index 0000000..5ebe331 --- /dev/null +++ b/src/com/bytezone/diskbrowser/wizardry/Character.java @@ -0,0 +1,331 @@ +package com.bytezone.diskbrowser.wizardry; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.applefile.AbstractFile; + +class Character extends AbstractFile +{ + private final Attributes attributes; + private final Statistics stats; + int scenario; + + private final Collection spellBook = new ArrayList (); + private final Collection baggageList = new ArrayList (); + + static String[] races = { "No race", "Human", "Elf", "Dwarf", "Gnome", "Hobbit" }; + static String[] alignments = { "Unalign", "Good", "Neutral", "Evil" }; + static String[] types = + { "Fighter", "Mage", "Priest", "Thief", "Bishop", "Samurai", "Lord", "Ninja" }; + static String[] statuses = + { "OK", "Afraid", "Asleep", "Paralyze", "Stoned", "Dead", "Ashes", "Lost" }; + + public Character (String name, byte[] buffer, int scenario) + { + super (name, buffer); + this.scenario = scenario; + + attributes = new Attributes (); + stats = new Statistics (); + + stats.race = races[HexFormatter.intValue (buffer[34])]; + stats.typeInt = HexFormatter.intValue (buffer[36]); + stats.type = types[stats.typeInt]; + stats.ageInWeeks = HexFormatter.intValue (buffer[38], buffer[39]); + stats.statusValue = buffer[40]; + stats.status = statuses[stats.statusValue]; + stats.alignment = alignments[HexFormatter.intValue (buffer[42])]; + + stats.gold = + HexFormatter.intValue (buffer[52], buffer[53]) + + HexFormatter.intValue (buffer[54], buffer[55]) * 10000; + stats.experience = + HexFormatter.intValue (buffer[124], buffer[125]) + + HexFormatter.intValue (buffer[126], buffer[127]) * 10000; + stats.level = HexFormatter.intValue (buffer[132], buffer[133]); + + stats.hitsLeft = HexFormatter.intValue (buffer[134], buffer[135]); + stats.hitsMax = HexFormatter.intValue (buffer[136], buffer[137]); + stats.armourClass = buffer[176]; + + attributes.strength = HexFormatter.intValue (buffer[44]) % 16; + if (attributes.strength < 3) + attributes.strength += 16; + attributes.array[0] = attributes.strength; + + int i1 = HexFormatter.intValue (buffer[44]) / 16; + int i2 = HexFormatter.intValue (buffer[45]) % 4; + attributes.intelligence = i1 / 2 + i2 * 8; + attributes.array[1] = attributes.intelligence; + + attributes.piety = HexFormatter.intValue (buffer[45]) / 4; + attributes.array[2] = attributes.piety; + + attributes.vitality = HexFormatter.intValue (buffer[46]) % 16; + if (attributes.vitality < 3) + attributes.vitality += 16; + attributes.array[3] = attributes.vitality; + + int a1 = HexFormatter.intValue (buffer[46]) / 16; + int a2 = HexFormatter.intValue (buffer[47]) % 4; + attributes.agility = a1 / 2 + a2 * 8; + attributes.array[4] = attributes.agility; + + attributes.luck = HexFormatter.intValue (buffer[47]) / 4; + attributes.array[5] = attributes.luck; + } + + public void linkItems (List itemList) + { + boolean equipped; + boolean identified; + int totItems = buffer[58]; + stats.assetValue = 0; + + for (int ptr = 60; totItems > 0; ptr += 8, totItems--) + { + int itemID = HexFormatter.intValue (buffer[ptr + 6]); + if (scenario == 3) + itemID = (itemID + 24) % 256; + if (itemID >= 0 && itemID < itemList.size ()) + { + Item item = itemList.get (itemID); + equipped = (buffer[ptr] == 1); + identified = (buffer[ptr + 4] == 1); + baggageList.add (new Baggage (item, equipped, identified)); + stats.assetValue += item.getCost (); + item.partyOwns++; + } + else + System.out.println (name + " ItemID : " + itemID + " is outside range 0:" + + itemList.size ()); + } + } + + public void linkSpells (List spellList) + { + for (int i = 138; i < 145; i++) + { + for (int bit = 0; bit < 8; bit++) + { + byte b = buffer[i]; + if (((b >>> bit) & 1) == 1) + { + int index = (i - 138) * 8 + bit; + if (index > 0 && index <= spellList.size ()) + spellBook.add (spellList.get (index - 1)); + else + System.out.println (name + " SpellID : " + index + " is outside range 0:" + + spellList.size ()); + } + } + } + } + + @Override + public String getText () + { + StringBuilder text = new StringBuilder (); + + text.append ("Character name ..... " + name); + text.append ("\n\nRace ............... " + stats.race); + text.append ("\nType ............... " + stats.type); + text.append ("\nAlignment .......... " + stats.alignment); + text.append ("\nStatus ............. " + stats.status); + // text.append ("\nType ............... " + stats.typeInt); + // text.append ("\nStatus ............. " + stats.statusValue); + text.append ("\nGold ............... " + String.format ("%,d", stats.gold)); + text.append ("\nExperience ......... " + String.format ("%,d", stats.experience)); + text.append ("\nNext level ......... " + String.format ("%,d", stats.nextLevel)); + text.append ("\nLevel .............. " + stats.level); + text.append ("\nAge in weeks ....... " + + String.format ("%,d (%d)", stats.ageInWeeks, (stats.ageInWeeks / 52))); + text.append ("\nHit points left .... " + stats.hitsLeft); + text.append ("\nMaximum hits ....... " + stats.hitsMax); + text.append ("\nArmour class ....... " + stats.armourClass); + text.append ("\nAsset value ........ " + String.format ("%,d", stats.assetValue)); + text.append ("\nAwards ............. " + isWinner ()); + text.append ("\nOut ................ " + isOut ()); + text.append ("\n\nStrength ........... " + attributes.strength); + text.append ("\nIntelligence ....... " + attributes.intelligence); + text.append ("\nPiety .............. " + attributes.piety); + text.append ("\nVitality ........... " + attributes.vitality); + text.append ("\nAgility ............ " + attributes.agility); + text.append ("\nLuck ............... " + attributes.luck); + + int[] spellPoints = getMageSpellPoints (); + text.append ("\n\nMage spell points .."); + for (int i = 0; i < spellPoints.length; i++) + text.append (" " + spellPoints[i]); + + spellPoints = getPriestSpellPoints (); + text.append ("\nPriest spell points "); + for (int i = 0; i < spellPoints.length; i++) + text.append (" " + spellPoints[i]); + + text.append ("\n\nSpells :"); + for (Spell s : spellBook) + text.append ("\n" + s); + + text.append ("\n\nItems :"); + for (Baggage b : baggageList) + text.append ("\n" + b); + + return text.toString (); + } + + public void linkExperience (ExperienceLevel exp) + { + stats.nextLevel = exp.getExperiencePoints (stats.level); + } + + public int[] getMageSpellPoints () + { + int[] spells = new int[7]; + + for (int i = 0; i < 7; i++) + spells[i] = buffer[146 + i * 2]; + + return spells; + } + + public int[] getPriestSpellPoints () + { + int[] spells = new int[7]; + + for (int i = 0; i < 7; i++) + spells[i] = buffer[160 + i * 2]; + + return spells; + } + + public Long getNextLevel () + { + return stats.nextLevel; + } + + // this is temporary until I have more data + public String isWinner () + { + int v1 = buffer[206]; + int v2 = buffer[207]; + if (v1 == 0x01) + return ">"; + if (v1 == 0x00 && v2 == 0x00) + return ""; + if (v1 == 0x00 && v2 == 0x20) + return "D"; + if (v1 == 0x20 && v2 == 0x20) + return "*D"; + if (v1 == 0x21 && v2 == 0x60) + return ">*DG"; + if (v1 == 0x21 && v2 == 0x28) + return ">*KD"; + return "Unknown : " + v1 + " " + v2; + } + + public boolean isOut () + { + return (buffer[32] == 1); + } + + public String getType () + { + return stats.type; + } + + public String getRace () + { + return stats.race; + } + + public String getAlignment () + { + return stats.alignment; + } + + public Attributes getAttributes () + { + return attributes; + } + + public Statistics getStatistics () + { + return stats; + } + + public Iterator getBaggage () + { + return baggageList.iterator (); + } + + public Iterator getSpells () + { + return spellBook.iterator (); + } + + @Override + public String toString () + { + return name; + } + + public class Baggage + { + public Item item; + public boolean equipped; + public boolean identified; + + public Baggage (Item item, boolean equipped, boolean identified) + { + this.item = item; + this.equipped = equipped; + this.identified = identified; + } + + @Override + public String toString () + { + return String.format ("%s%-15s (%d)", equipped ? "*" : " ", item.name, item.getCost ()); + } + } + + public class Statistics implements Cloneable + { + public String race; + public String type; + public String alignment; + public String status; + public int typeInt; + public int statusValue; + public int gold; + public int experience; + public long nextLevel; + public int level; + public int ageInWeeks; + public int hitsLeft; + public int hitsMax; + public int armourClass; + public int assetValue; + } + + public class Attributes + { + public int strength; + public int intelligence; + public int piety; + public int vitality; + public int agility; + public int luck; + public int[] array; + + public Attributes () + { + array = new int[6]; + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/wizardry/CodedMessage.java b/src/com/bytezone/diskbrowser/wizardry/CodedMessage.java new file mode 100755 index 0000000..b0b0935 --- /dev/null +++ b/src/com/bytezone/diskbrowser/wizardry/CodedMessage.java @@ -0,0 +1,27 @@ +package com.bytezone.diskbrowser.wizardry; + +import com.bytezone.diskbrowser.HexFormatter; + +class CodedMessage extends Message +{ + public static int codeOffset = 185; + + public CodedMessage (byte[] buffer) + { + super (buffer); + } + + @Override + protected String getLine (int offset) + { + int length = HexFormatter.intValue (buffer[offset]); + byte[] translation = new byte[length]; + codeOffset--; + for (int j = 0; j < length; j++) + { + translation[j] = buffer[offset + 1 + j]; + translation[j] -= codeOffset - j * 3; + } + return HexFormatter.getString (translation, 0, length); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/wizardry/Dice.java b/src/com/bytezone/diskbrowser/wizardry/Dice.java new file mode 100755 index 0000000..67267c7 --- /dev/null +++ b/src/com/bytezone/diskbrowser/wizardry/Dice.java @@ -0,0 +1,27 @@ +package com.bytezone.diskbrowser.wizardry; + +class Dice +{ + int qty; + int sides; + int bonus; + + public Dice (byte[] buffer, int offset) + { + qty = buffer[offset]; + sides = buffer[offset + 2]; + bonus = buffer[offset + 4]; + } + + @Override + public String toString () + { + if (qty == 0) + return ""; + StringBuilder text = new StringBuilder (); + text.append (String.format ("%dd%d", qty, sides)); + if (bonus > 0) + text.append ("+" + bonus); + return text.toString (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/wizardry/ExperienceLevel.java b/src/com/bytezone/diskbrowser/wizardry/ExperienceLevel.java new file mode 100755 index 0000000..20bcfc0 --- /dev/null +++ b/src/com/bytezone/diskbrowser/wizardry/ExperienceLevel.java @@ -0,0 +1,44 @@ +package com.bytezone.diskbrowser.wizardry; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.applefile.AbstractFile; + +class ExperienceLevel extends AbstractFile +{ + private final long[] expLevels = new long[13]; + + public ExperienceLevel (String name, byte[] buffer) + { + super (name, buffer); + + int seq = 0; + + for (int ptr = 0; ptr < buffer.length; ptr += 6) + { + if (buffer[ptr] == 0) + break; + + long points = + HexFormatter.intValue (buffer[ptr], buffer[ptr + 1]) + + HexFormatter.intValue (buffer[ptr + 2], buffer[ptr + 3]) * 10000 + + HexFormatter.intValue (buffer[ptr + 4], buffer[ptr + 5]) * 100000000L; + expLevels[seq++] = points; + } + } + + public long getExperiencePoints (int level) + { + if (level < 13) + return expLevels[level]; + return (level - 12) * expLevels[0] + expLevels[12]; + } + + @Override + public String getText () + { + StringBuilder line = new StringBuilder (); + for (long exp : expLevels) + line.append (exp + "\n"); + return line.toString (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/wizardry/Header.java b/src/com/bytezone/diskbrowser/wizardry/Header.java new file mode 100755 index 0000000..8629d93 --- /dev/null +++ b/src/com/bytezone/diskbrowser/wizardry/Header.java @@ -0,0 +1,227 @@ +package com.bytezone.diskbrowser.wizardry; + +import java.util.ArrayList; +import java.util.List; + +import javax.swing.tree.DefaultMutableTreeNode; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.applefile.AppleFileSource; +import com.bytezone.diskbrowser.applefile.DefaultAppleFile; +import com.bytezone.diskbrowser.disk.DefaultAppleFileSource; +import com.bytezone.diskbrowser.disk.DiskAddress; +import com.bytezone.diskbrowser.disk.FormattedDisk; + +class Header +{ + static String[] typeText = + { "header", "maze", "monsters", "rewards", "items", "characters", "images", "char levels" }; + static String[] scenarioNames = + { "PROVING GROUNDS OF THE MAD OVERLORD!", "THE KNIGHT OF DIAMONDS", + "THE LEGACY OF LLYLGAMYN" }; + + static final int MAZE_AREA = 1; + static final int MONSTER_AREA = 2; + static final int TREASURE_TABLE_AREA = 3; + static final int ITEM_AREA = 4; + static final int CHARACTER_AREA = 5; + static final int IMAGE_AREA = 6; + static final int EXPERIENCE_AREA = 7; + + String scenarioTitle; + public int scenarioID; + List data = new ArrayList (8); + FormattedDisk owner; + + public Header (DefaultMutableTreeNode dataNode, FormattedDisk owner) + { + this.owner = owner; + + AppleFileSource afs = (AppleFileSource) dataNode.getUserObject (); + List sectors = afs.getSectors (); + DefaultAppleFile daf = (DefaultAppleFile) afs.getDataSource (); + scenarioTitle = HexFormatter.getPascalString (daf.buffer, 0); + + while (scenarioID < scenarioNames.length) + if (scenarioNames[scenarioID++].equals (scenarioTitle)) + break; + assert (scenarioID <= scenarioNames.length) : "Invalid scenario ID : " + scenarioID; + + for (int i = 0; i < 8; i++) + data.add (new ScenarioData (daf.buffer, i, sectors)); + + StringBuilder text = + new StringBuilder ("Data type Offset Size Units ???\n" + + "------------ ------ ----- ----- -----\n"); + + for (ScenarioData sd : data) + text.append (sd + "\n"); + + daf.setText (text.toString ()); + + text = new StringBuilder (scenarioTitle + "\n\n"); + + int ptr = 106; + while (daf.buffer[ptr] != -1) + { + text.append (HexFormatter.getPascalString (daf.buffer, ptr) + "\n"); + ptr += 10; + } + + DefaultAppleFileSource dafs = new DefaultAppleFileSource ("Header", text.toString (), owner); + dafs.setSectors (data.get (0).sectors); + DefaultMutableTreeNode headerNode = new DefaultMutableTreeNode (dafs); + dataNode.add (headerNode); + + int totalBlocks = data.get (0).sectors.size (); + linkText ("Text", data.get (0).sectors.get (0), headerNode); + if (scenarioID < 3) + { + linkPictures ("Alphabet", data.get (0).sectors.get (1), headerNode); + linkPictures ("Graphics", data.get (0).sectors.get (2), headerNode); + linkPictures ("Unknown", data.get (0).sectors.get (3), headerNode); + } + linkSpells ("Mage spells", data.get (0).sectors.get (totalBlocks - 2), headerNode); + linkSpells ("Priest spells", data.get (0).sectors.get (totalBlocks - 1), headerNode); + + if (false && scenarioID <= 2) + { + System.out.println (printChars (daf.buffer, 1)); + System.out.println (printChars (daf.buffer, 2)); + } + } + + private void linkText (String title, DiskAddress da, DefaultMutableTreeNode headerNode) + { + List blocks = new ArrayList (); + blocks.add (da); + + StringBuilder text = new StringBuilder (scenarioTitle + "\n\n"); + + int ptr = 106; + byte[] buffer = owner.getDisk ().readSector (da); + while (buffer[ptr] != -1) + { + text.append (HexFormatter.getPascalString (buffer, ptr) + "\n"); + ptr += 10; + } + ptr += 2; + text.append ("\n"); + while (ptr < 512) + { + int value = HexFormatter.intValue (buffer[ptr], buffer[ptr + 1]); + text.append (String.format ("%04X %,6d%n", value, value)); + ptr += 2; + } + + DefaultAppleFileSource dafs = new DefaultAppleFileSource (title, text.toString (), owner); + dafs.setSectors (blocks); + DefaultMutableTreeNode node = new DefaultMutableTreeNode (dafs); + node.setAllowsChildren (false); + headerNode.add (node); + } + + private void linkPictures (String title, DiskAddress da, DefaultMutableTreeNode headerNode) + { + List blocks = new ArrayList (); + blocks.add (da); + + byte[] buffer = owner.getDisk ().readSector (da); + String text = printChars (buffer, 0); + + DefaultAppleFileSource dafs = new DefaultAppleFileSource (title, text, owner); + dafs.setSectors (blocks); + DefaultMutableTreeNode node = new DefaultMutableTreeNode (dafs); + node.setAllowsChildren (false); + headerNode.add (node); + } + + private void linkSpells (String title, DiskAddress da, DefaultMutableTreeNode headerNode) + { + List blocks = new ArrayList (); + blocks.add (da); + int level = 1; + + StringBuilder list = new StringBuilder ("Level " + level + ":\n"); + byte[] buffer = owner.getDisk ().readSector (da); + String text = HexFormatter.getString (buffer, 0, 512); + String[] spells = text.split ("\n"); + for (String s : spells) + { + if (s.length () == 0) + break; + if (s.startsWith ("*")) + { + s = s.substring (1); + level++; + list.append ("\nLevel " + level + ":\n"); + } + list.append (" " + s + "\n"); + } + + DefaultAppleFileSource dafs = new DefaultAppleFileSource (title, list.toString (), owner); + dafs.setSectors (blocks); + DefaultMutableTreeNode node = new DefaultMutableTreeNode (dafs); + node.setAllowsChildren (false); + headerNode.add (node); + } + + private String printChars (byte[] buffer, int block) + { + StringBuilder text = new StringBuilder (); + for (int i = block * 512; i < (block + 1) * 512; i += 64) + { + for (int line = 0; line < 8; line++) + { + for (int j = 0; j < 8; j++) + { + int value = HexFormatter.intValue (buffer[i + line + j * 8]); + for (int bit = 0; bit < 7; bit++) + { + if ((value & 0x01) == 1) + text.append ("O"); + else + text.append ("."); + value >>= 1; + } + text.append (" "); + } + text.append ("\n"); + } + text.append ("\n"); + } + return text.toString (); + } + + // this could be the base factory class for all Wizardry types + class ScenarioData + { + int dunno; + int total; + int totalBlocks; + int dataOffset; + int type; + List sectors; + + public ScenarioData (byte[] buffer, int seq, List sectors) + { + int offset = 42 + seq * 2; + dunno = HexFormatter.intValue (buffer[offset]); + total = HexFormatter.intValue (buffer[offset + 16]); + totalBlocks = HexFormatter.intValue (buffer[offset + 32]); + dataOffset = HexFormatter.intValue (buffer[offset + 48]); + type = seq; + + this.sectors = new ArrayList (totalBlocks); + for (int i = dataOffset, max = dataOffset + totalBlocks; i < max; i++) + this.sectors.add (sectors.get (i)); + } + + @Override + public String toString () + { + return String.format ("%-15s %3d %3d %3d %3d", typeText[type], dataOffset, + totalBlocks, total, dunno); + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/wizardry/Image.java b/src/com/bytezone/diskbrowser/wizardry/Image.java new file mode 100755 index 0000000..24a073c --- /dev/null +++ b/src/com/bytezone/diskbrowser/wizardry/Image.java @@ -0,0 +1,59 @@ +package com.bytezone.diskbrowser.wizardry; + +import java.awt.image.BufferedImage; +import java.awt.image.DataBuffer; + +class Image extends AbstractImage +{ + public Image (String name, byte[] buffer) + { + super (name, buffer); + + if (buffer[0] == -61 && buffer[1] == -115) + fixSlime (buffer); + + image = new BufferedImage (70, 50, BufferedImage.TYPE_BYTE_GRAY); // width/height + DataBuffer db = image.getRaster ().getDataBuffer (); + int element = 0; + + for (int j = 0; j < 500; j++) + { + int bits = buffer[j] & 0xFF; + for (int m = 0; m < 7; m++) + { + if (bits == 0) + { + element += 7 - m; + break; + } + if ((bits & 1) == 1) + db.setElem (element, 255); + bits >>= 1; + element++; + } + } + } + + private void fixSlime (byte[] buffer) + { + for (int i = 0; i < 208; i++) + buffer[i] = 0; + buffer[124] = -108; + buffer[134] = -43; + buffer[135] = -128; + buffer[144] = -44; + buffer[145] = -126; + buffer[154] = -48; + buffer[155] = -118; + buffer[164] = -64; + buffer[165] = -86; + buffer[174] = -64; + buffer[175] = -86; + buffer[184] = -63; + buffer[185] = -86; + buffer[194] = -44; + buffer[195] = -86; + buffer[204] = -44; + buffer[205] = -126; + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/wizardry/ImageV2.java b/src/com/bytezone/diskbrowser/wizardry/ImageV2.java new file mode 100755 index 0000000..5546124 --- /dev/null +++ b/src/com/bytezone/diskbrowser/wizardry/ImageV2.java @@ -0,0 +1,32 @@ +package com.bytezone.diskbrowser.wizardry; + +import java.awt.image.BufferedImage; +import java.awt.image.DataBuffer; + +class ImageV2 extends AbstractImage +{ + public ImageV2 (String name, byte[] buffer) + { + super (name, buffer); + + image = new BufferedImage (70, 48, BufferedImage.TYPE_BYTE_GRAY); // width/height + DataBuffer db = image.getRaster ().getDataBuffer (); + int offset = 0; + int size = 7; + + for (int i = 0; i < 6; i++) + for (int j = 0; j < 10; j++) + for (int k = 7; k >= 0; k--) + { + int element = i * 560 + j * 7 + k * 70; + int bits = buffer[offset++] & 0xFF; + for (int m = size - 1; m >= 0; m--) + { + if ((bits & 1) == 1) + db.setElem (element, 255); + bits >>= 1; + element++; + } + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/wizardry/Item.java b/src/com/bytezone/diskbrowser/wizardry/Item.java new file mode 100755 index 0000000..4029438 --- /dev/null +++ b/src/com/bytezone/diskbrowser/wizardry/Item.java @@ -0,0 +1,141 @@ +package com.bytezone.diskbrowser.wizardry; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.applefile.AbstractFile; + +class Item extends AbstractFile implements Comparable +{ + public final int itemID; + private final int type; + private final long cost; + public int partyOwns; + String genericName; + static int counter = 0; + public final Dice damage; + public final int armourClass; + public final int speed; + + public Item (String name, byte[] buffer) + { + super (name, buffer); + itemID = counter++; + type = buffer[32]; + cost = + HexFormatter.intValue (buffer[44], buffer[45]) + + HexFormatter.intValue (buffer[46], buffer[47]) * 10000 + + HexFormatter.intValue (buffer[48], buffer[49]) * 100000000L; + genericName = HexFormatter.getPascalString (buffer, 16); + damage = new Dice (buffer, 66); + armourClass = buffer[62]; + speed = buffer[72]; + } + + @Override + public String getText () + { + StringBuilder text = new StringBuilder (); + + text.append ("Name ......... : " + name); + // int length = HexFormatter.intValue (buffer[16]); + text.append ("\nGeneric name . : " + genericName); + text.append ("\nType ......... : " + type); + text.append ("\nCost ......... : " + cost); + text.append ("\nArmour class . : " + armourClass); + text.append ("\nDamage ....... : " + damage); + text.append ("\nSpeed ........ : " + speed); + text.append ("\nCursed? ...... : " + isCursed ()); + int stock = getStockOnHand (); + text.append ("\nStock on hand : " + stock); + if (stock < 0) + text.append (" (always in stock)"); + + return text.toString (); + } + + public int getType () + { + return type; + } + + // public int getArmourClass () + // { + // return buffer[62]; + // } + + // public int getSpeed () + // { + // return HexFormatter.intValue (buffer[72]); + // } + + public long getCost () + { + return cost; + } + + public boolean isCursed () + { + return buffer[36] != 0; + } + + public int getStockOnHand () + { + if (buffer[50] == -1 && buffer[51] == -1) + return -1; + + return HexFormatter.intValue (buffer[50], buffer[51]); + } + + public boolean canUse (int type2) + { + int users = HexFormatter.intValue (buffer[54]); + return ((users >>> type2) & 1) == 1; + } + + @Override + public String toString () + { + StringBuilder line = new StringBuilder (); + line.append (String.format ("%-16s", name)); + if (buffer[36] == -1) + line.append ("(c) "); + else + line.append (" "); + line.append (String.format ("%02X ", buffer[62])); + line.append (String.format ("%02X ", buffer[34])); + line.append (String.format ("%02X %02X", buffer[50], buffer[51])); + + // if (buffer[50] == -1 && buffer[51] == -1) + // line.append ("* "); + // else + // line.append (HexFormatter.intValue (buffer[50], buffer[51]) + " "); + + for (int i = 38; i < 44; i++) + line.append (HexFormatter.format2 (buffer[i]) + " "); + for (int i = 48; i < 50; i++) + line.append (HexFormatter.format2 (buffer[i]) + " "); + for (int i = 52; i < 62; i++) + line.append (HexFormatter.format2 (buffer[i]) + " "); + // for (int i = 64; i < 78; i++) + // line.append (HexFormatter.format2 (buffer[i]) + " "); + + return line.toString (); + } + + public String getDump (int block) + { + StringBuilder line = new StringBuilder (String.format ("%3d %-16s", itemID, name)); + int lo = block == 0 ? 32 : block == 1 ? 46 : 70; + int hi = lo + 24; + if (hi > buffer.length) + hi = buffer.length; + for (int i = lo; i < hi; i++) + line.append (String.format ("%02X ", buffer[i])); + return line.toString (); + } + + public int compareTo (Item otherItem) + { + Item item = otherItem; + return this.type - item.type; + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/wizardry/MazeAddress.java b/src/com/bytezone/diskbrowser/wizardry/MazeAddress.java new file mode 100755 index 0000000..bde1650 --- /dev/null +++ b/src/com/bytezone/diskbrowser/wizardry/MazeAddress.java @@ -0,0 +1,21 @@ +package com.bytezone.diskbrowser.wizardry; + +class MazeAddress +{ + public final int level; + public final int row; + public final int column; + + public MazeAddress (int level, int row, int column) + { + this.level = level; + this.row = row; + this.column = column; + } + + @Override + public String toString () + { + return level + "/" + row + "/" + column; + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/wizardry/MazeCell.java b/src/com/bytezone/diskbrowser/wizardry/MazeCell.java new file mode 100755 index 0000000..55c081b --- /dev/null +++ b/src/com/bytezone/diskbrowser/wizardry/MazeCell.java @@ -0,0 +1,322 @@ +package com.bytezone.diskbrowser.wizardry; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.util.List; + +import com.bytezone.diskbrowser.HexFormatter; + +class MazeCell +{ + static Dimension cellSize = new Dimension (22, 22); // size in pixels + + boolean northWall; + boolean southWall; + boolean eastWall; + boolean westWall; + + boolean northDoor; + boolean southDoor; + boolean eastDoor; + boolean westDoor; + + boolean darkness; + + boolean stairs; + boolean pit; + boolean spinner; + boolean chute; + boolean elevator; + boolean monsterLair; + boolean rock; + boolean teleport; + boolean spellsBlocked; + + int elevatorFrom; + int elevatorTo; + + int messageType; + int monsterID = -1; + int itemID; + + int unknown; + + MazeAddress address; + MazeAddress addressTo; // if teleport/stairs/chute + public Message message; + public List monsters; + public Item itemRequired; + public Item itemObtained; + + public MazeCell (MazeAddress address) + { + this.address = address; + } + + public void draw (Graphics2D g, int x, int y) + { + g.setColor (Color.WHITE); + + if (westWall) + drawWest (g, x, y); + if (eastWall) + drawEast (g, x, y); + if (northWall) + drawNorth (g, x, y); + if (southWall) + drawSouth (g, x, y); + + g.setColor (Color.RED); + + if (westDoor) + drawWest (g, x, y); + if (eastDoor) + drawEast (g, x, y); + if (northDoor) + drawNorth (g, x, y); + if (southDoor) + drawSouth (g, x, y); + + g.setColor (Color.GREEN); + + if (westDoor && westWall) + drawWest (g, x, y); + if (eastDoor && eastWall) + drawEast (g, x, y); + if (northDoor && northWall) + drawNorth (g, x, y); + if (southDoor && southWall) + drawSouth (g, x, y); + + g.setColor (Color.WHITE); + + if (monsterLair) + drawMonsterLair (g, x, y); + + if (stairs) + if (address.level < addressTo.level) + drawStairsDown (g, x, y); + else + drawStairsUp (g, x, y); + else if (message != null) + drawChar (g, x, y, "M", Color.RED); + else if (pit) + drawPit (g, x, y); + else if (chute) + drawChute (g, x, y); + else if (spinner) + g.drawString ("S", x + 8, y + 16); + else if (teleport) + drawTeleport (g, x, y); + else if (darkness) + drawDarkness (g, x, y); + else if (rock) + drawRock (g, x, y); + else if (elevator) + drawElevator (g, x, y, (elevatorTo - elevatorFrom + 1) / 2); + else if (monsterID >= 0) + drawMonster (g, x, y); + else if (spellsBlocked) + drawSpellsBlocked (g, x, y); + else if (unknown != 0) + drawChar (g, x, y, HexFormatter.format1 (unknown), Color.GRAY); + } + + public void drawWest (Graphics2D g, int x, int y) + { + g.drawLine (x + 1, y + 1, x + 1, y + cellSize.height - 1); + } + + private void drawEast (Graphics2D g, int x, int y) + { + g.drawLine (x + cellSize.width - 1, y + 1, x + cellSize.width - 1, y + cellSize.height - 1); + } + + private void drawNorth (Graphics2D g, int x, int y) + { + g.drawLine (x + 1, y + 1, x + cellSize.width - 1, y + 1); + } + + private void drawSouth (Graphics2D g, int x, int y) + { + g.drawLine (x + 1, y + cellSize.height - 1, x + cellSize.width - 1, y + cellSize.height - 1); + } + + public void drawStairsUp (Graphics2D g, int x, int y) + { + g.drawLine (x + 6, y + 18, x + 6, y + 14); + g.drawLine (x + 6, y + 14, x + 10, y + 14); + g.drawLine (x + 10, y + 14, x + 10, y + 10); + g.drawLine (x + 10, y + 10, x + 14, y + 10); + g.drawLine (x + 14, y + 10, x + 14, y + 6); + g.drawLine (x + 14, y + 6, x + 18, y + 6); + } + + public void drawStairsDown (Graphics2D g, int x, int y) + { + g.drawLine (x + 4, y + 7, x + 8, y + 7); + g.drawLine (x + 8, y + 7, x + 8, y + 11); + g.drawLine (x + 8, y + 11, x + 12, y + 11); + g.drawLine (x + 12, y + 11, x + 12, y + 15); + g.drawLine (x + 12, y + 15, x + 16, y + 15); + g.drawLine (x + 16, y + 15, x + 16, y + 19); + } + + public void drawPit (Graphics2D g, int x, int y) + { + g.drawLine (x + 5, y + 14, x + 5, y + 19); + g.drawLine (x + 5, y + 19, x + 17, y + 19); + g.drawLine (x + 17, y + 14, x + 17, y + 19); + } + + public void drawChute (Graphics2D g, int x, int y) + { + g.drawLine (x + 6, y + 6, x + 10, y + 6); + g.drawLine (x + 10, y + 6, x + 18, y + 18); + } + + public void drawElevator (Graphics2D g, int x, int y, int rows) + { + for (int i = 0; i < rows; i++) + { + g.drawOval (x + 7, y + i * 5 + 5, 2, 2); + g.drawOval (x + 14, y + i * 5 + 5, 2, 2); + } + } + + public void drawMonsterLair (Graphics2D g, int x, int y) + { + g.setColor (Color.YELLOW); + g.fillOval (x + 4, y + 4, 2, 2); + g.setColor (Color.WHITE); + } + + public void drawTeleport (Graphics2D g, int x, int y) + { + g.setColor (Color.GREEN); + g.fillOval (x + 8, y + 8, 8, 8); + g.setColor (Color.WHITE); + } + + public void drawSpellsBlocked (Graphics2D g, int x, int y) + { + g.setColor (Color.YELLOW); + g.fillOval (x + 8, y + 8, 8, 8); + g.setColor (Color.WHITE); + } + + public void drawMonster (Graphics2D g, int x, int y) + { + g.setColor (Color.RED); + g.fillOval (x + 8, y + 8, 8, 8); + g.setColor (Color.WHITE); + } + + public void drawDarkness (Graphics2D g, int x, int y) + { + g.setColor (Color.gray); + for (int h = 0; h < 15; h += 7) + for (int offset = 0; offset < 15; offset += 7) + g.drawOval (x + offset + 4, y + h + 4, 1, 1); + g.setColor (Color.white); + } + + public void drawRock (Graphics2D g, int x, int y) + { + for (int h = 0; h < 15; h += 7) + for (int offset = 0; offset < 15; offset += 7) + g.drawOval (x + offset + 4, y + h + 4, 1, 1); + } + + public void drawChar (Graphics2D g, int x, int y, String c, Color colour) + { + g.setColor (colour); + g.fillRect (x + 7, y + 6, 11, 11); + g.setColor (Color.WHITE); + g.drawString (c, x + 8, y + 16); + } + + public void drawHotDogStand (Graphics2D g, int x, int y) + { + g.drawRect (x + 5, y + 11, 12, 6); + g.drawOval (x + 6, y + 18, 3, 3); + g.drawOval (x + 13, y + 18, 3, 3); + g.drawLine (x + 8, y + 6, x + 8, y + 10); + g.drawLine (x + 14, y + 6, x + 14, y + 10); + g.drawLine (x + 5, y + 5, x + 17, y + 5); + } + + public String getTooltipText () + { + StringBuilder sign = new StringBuilder ("
");
+		sign.append (" ");
+		sign.append (address.row + "N ");
+		sign.append (address.column + "E 
"); + + if (message != null) + sign.append (message.toHTMLString ()); + + if (elevator) + sign.append (" Elevator: L" + elevatorFrom + "-L" + elevatorTo + " "); + if (stairs) + { + sign.append (" Stairs to "); + if (addressTo.level == 0) + sign.append ("castle "); + else + { + sign.append ("level " + addressTo.level + " "); + } + } + if (teleport) + { + sign.append (" Teleport to "); + if (addressTo.level == 0) + sign.append ("castle "); + else + { + sign.append ("L" + addressTo.level + " " + addressTo.row + "N " + addressTo.column + + "E "); + } + } + if (pit) + sign.append (" Pit"); + if (spinner) + sign.append (" Spinner "); + if (chute) + sign.append (" Chute"); + if (darkness) + sign.append (" Darkness "); + if (rock) + sign.append (" Rock "); + if (spellsBlocked) + sign.append (" Spells fizzle out "); + if (monsterID >= 0) + if (monsters == null || monsterID >= monsters.size ()) + sign.append (" Monster "); + else + { + Monster monster = monsters.get (monsterID); + sign.append (" " + monster.getRealName () + " "); + while (monster.partnerOdds == 100) + { + monster = monsters.get (monster.partnerID); + sign.append ("
 " + monster.getRealName () + " "); + } + } + if (itemRequired != null) + { + sign.append (" Requires: "); + sign.append (itemRequired.name + " "); + } + + if (itemObtained != null) + { + sign.append (" Obtain: "); + sign.append (itemObtained.name + " "); + } + sign.append ("
"); + return sign.toString (); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/wizardry/MazeLevel.java b/src/com/bytezone/diskbrowser/wizardry/MazeLevel.java new file mode 100755 index 0000000..b859b85 --- /dev/null +++ b/src/com/bytezone/diskbrowser/wizardry/MazeLevel.java @@ -0,0 +1,216 @@ +package com.bytezone.diskbrowser.wizardry; + +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.image.BufferedImage; +import java.util.List; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.applefile.AbstractFile; + +class MazeLevel extends AbstractFile +{ + public int level; + private List messages; + private List monsters; + private List items; + + public MazeLevel (byte[] buffer, int level) + { + super ("Level " + level, buffer); + this.level = level; + } + + @Override + public BufferedImage getImage () + { + Dimension cellSize = new Dimension (22, 22); + image = + new BufferedImage (20 * cellSize.width + 1, 20 * cellSize.height + 1, + BufferedImage.TYPE_USHORT_555_RGB); + Graphics2D g = image.createGraphics (); + g.setRenderingHint (RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + for (int row = 0; row < 20; row++) + for (int column = 0; column < 20; column++) + { + MazeCell cell = getLocation ((row) % 20, (column) % 20); + int x = column * cellSize.width; + int y = image.getHeight () - (row + 1) * cellSize.height - 1; + cell.draw (g, x, y); + } + return image; + } + + public void setMessages (List messages) + { + this.messages = messages; + } + + public void setMonsters (List monsters) + { + this.monsters = monsters; + } + + public void setItems (List items) + { + this.items = items; + } + + public MazeCell getLocation (int row, int column) + { + MazeAddress address = new MazeAddress (level, row, column); + MazeCell cell = new MazeCell (address); + + // doors and walls + + int offset = column * 6 + row / 4; // 6 bytes/column + + int value = HexFormatter.intValue (buffer[offset]); + value >>>= (row % 4) * 2; + cell.westWall = ((value & 1) == 1); + value >>>= 1; + cell.westDoor = ((value & 1) == 1); + + value = HexFormatter.intValue (buffer[offset + 120]); + value >>>= (row % 4) * 2; + cell.southWall = ((value & 1) == 1); + value >>>= 1; + cell.southDoor = ((value & 1) == 1); + + value = HexFormatter.intValue (buffer[offset + 240]); + value >>>= (row % 4) * 2; + cell.eastWall = ((value & 1) == 1); + value >>>= 1; + cell.eastDoor = ((value & 1) == 1); + + value = HexFormatter.intValue (buffer[offset + 360]); + value >>>= (row % 4) * 2; + cell.northWall = ((value & 1) == 1); + value >>>= 1; + cell.northDoor = ((value & 1) == 1); + + // monster table + + offset = column * 4 + row / 8; // 4 bytes/column, 1 bit/row + value = HexFormatter.intValue (buffer[offset + 480]); + value >>>= row % 8; + cell.monsterLair = ((value & 1) == 1); + + // stairs, pits, darkness etc. + + offset = column * 10 + row / 2; // 10 bytes/column, 4 bits/row + value = HexFormatter.intValue (buffer[offset + 560]); + int b = (row % 2 == 0) ? value % 16 : value / 16; + int c = HexFormatter.intValue (buffer[760 + b / 2]); + int d = (b % 2 == 0) ? c % 16 : c / 16; + + switch (d) + { + case 1: + cell.stairs = true; + cell.addressTo = getAddress (b); + break; + case 2: + cell.pit = true; + break; + case 3: + cell.chute = true; + cell.addressTo = getAddress (b); + break; + case 4: + cell.spinner = true; + break; + case 5: + cell.darkness = true; + break; + case 6: + cell.teleport = true; + cell.addressTo = getAddress (b); + break; + case 8: + cell.elevator = true; + cell.elevatorTo = HexFormatter.intValue (buffer[800 + b * 2], buffer[801 + b * 2]); + cell.elevatorFrom = HexFormatter.intValue (buffer[832 + b * 2], buffer[833 + b * 2]); + break; + case 9: + cell.rock = true; + break; + case 10: + cell.spellsBlocked = true; + break; + case 11: + int messageNum = HexFormatter.intValue (buffer[800 + b * 2], buffer[801 + b * 2]); + for (Message m : messages) + if (m.match (messageNum)) + { + cell.message = m; + break; + } + if (cell.message == null) + System.out.println ("message not found : " + messageNum); + cell.messageType = HexFormatter.intValue (buffer[832 + b * 2], buffer[833 + b * 2]); + + int itemID = -1; + + if (cell.messageType == 2) // obtain Item + { + itemID = HexFormatter.intValue (buffer[768 + b * 2], buffer[769 + b * 2]); + cell.itemObtained = items.get (itemID); + } + if (cell.messageType == 5) // requires Item + { + itemID = HexFormatter.intValue (buffer[768 + b * 2], buffer[769 + b * 2]); + cell.itemRequired = items.get (itemID); + } + if (cell.messageType == 4) + { + value = HexFormatter.intValue (buffer[768 + b * 2], buffer[769 + b * 2]); + if (value <= 100) + { + cell.monsterID = value; + cell.monsters = monsters; + } + else + { + int val = (value - 64536) * -1; + System.out.println ("Value : " + val); + // this gives Index error: 20410, Size 104 in Wizardry_III/legacy2.dsk + if (val < items.size ()) + cell.itemObtained = items.get (val); // check this + if (cell.itemObtained == null) + System.out.printf ("Item %d not found%n", val); + } + } + break; + case 12: + cell.monsterID = HexFormatter.intValue (buffer[832 + b * 2], buffer[833 + b * 2]); + cell.monsters = monsters; + break; + default: + cell.unknown = d; + break; + } + + return cell; + } + + private MazeAddress getAddress (int a) + { + int b = a * 2; + return new MazeAddress (HexFormatter.intValue (buffer[768 + b], buffer[769 + b]), HexFormatter + .intValue (buffer[800 + b], buffer[801 + b]), HexFormatter.intValue (buffer[832 + b], + buffer[833 + b])); + } + + public int getRows () + { + return 20; + } + + public int getColumns () + { + return 20; + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/wizardry/Message.java b/src/com/bytezone/diskbrowser/wizardry/Message.java new file mode 100755 index 0000000..b2908a6 --- /dev/null +++ b/src/com/bytezone/diskbrowser/wizardry/Message.java @@ -0,0 +1,71 @@ +package com.bytezone.diskbrowser.wizardry; + +import java.util.ArrayList; +import java.util.List; + +import com.bytezone.diskbrowser.applefile.AbstractFile; + +abstract class Message extends AbstractFile +{ + private static int nextId = 0; + protected String message; + private final int id; + private int totalLines; + List lines = new ArrayList (); + + public Message (byte[] buffer) + { + super ("Message " + nextId, buffer); + this.id = nextId; + + int recordLength = 42; + StringBuilder text = new StringBuilder (); + + for (int ptr = 0; ptr < buffer.length; ptr += recordLength) + { + nextId++; + totalLines++; + String line = getLine (ptr); + text.append (line + "\n"); + lines.add (line); + } + text.deleteCharAt (text.length () - 1); + message = text.toString (); + } + + protected abstract String getLine (int offset); + + public boolean match (int messageNum) + { + if (id == messageNum) + return true; + + // this code is to allow for a bug in scenario #1 + if (messageNum > id && messageNum < (id + totalLines)) + return true; + + return false; + } + + @Override + public String getText () + { + return message; + } + + public String toHTMLString () + { + StringBuilder message = new StringBuilder (); + for (String line : lines) + message.append (" " + line + " 
"); + if (message.length () > 0) + for (int i = 0; i < 4; i++) + message.deleteCharAt (message.length () - 1); // remove
tag + return message.toString (); + } + + public static void resetMessageId () + { + nextId = 0; + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/wizardry/Monster.java b/src/com/bytezone/diskbrowser/wizardry/Monster.java new file mode 100755 index 0000000..37035cd --- /dev/null +++ b/src/com/bytezone/diskbrowser/wizardry/Monster.java @@ -0,0 +1,268 @@ +package com.bytezone.diskbrowser.wizardry; + +import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.List; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.applefile.AbstractFile; + +class Monster extends AbstractFile implements Comparable +{ + public final String genericName; + public final String realName; + public final int monsterID; + List monsters; + Reward goldReward; + Reward chestReward; + + public final int type; + public final int imageID; + int rewardTable1; + int rewardTable2; + public final int partnerID; + public final int partnerOdds; + public final int armourClass; + public final int speed; + public final int mageSpellLevel; + public final int priestSpellLevel; + int levelDrain; + int bonus1; + int bonus2; + int bonus3; + int resistance; + int abilities; + public final Dice groupSize, hitPoints; + List damage = new ArrayList (); + + static int counter = 0; + static boolean debug = true; + static int[] pwr = { 0, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 0, 0, 0, 0, 0 }; + static int[] weight1 = { 0, 1, 2, 4, 8, 16, 32, 64, 253, 506, 0 }; + static int[] weight2 = { 0, 60, 120, 180, 300, 540, 1020, 0 }; + + public static String[] monsterClass = + { "Fighter", "Mage", "Priest", "Thief", "Midget", "Giant", "Mythical", "Dragon", "Animal", + "Were", "Undead", "Demon", "Insect", "Enchanted" }; + + private static int[] experience = + { 55, 235, 415, 230, 380, 620, 840, 520, 550, 350, // 00-09 + 475, 515, 920, 600, 735, 520, 795, 780, 990, 795, // 10-19 + 1360, 1320, 1275, 680, 960, 600, 755, 1120, 2075, 870, // 20-29 + 960, 1120, 1120, 2435, 1080, 2280, 975, 875, 1135, 1200, // 30-39 + 620, 740, 1460, 1245, 960, 1405, 1040, 1220, 1520, 1000, // 40-49 + 960, 2340, 2160, 2395, 790, 1140, 1235, 1790, 1720, 2240, // 50-59 + 1475, 1540, 1720, 1900, 1240, 1220, 1020, 20435, 5100, 3515, // 60-69 + 2115, 2920, 2060, 2140, 1400, 1640, 1280, 4450, 42840, 3300, // 70-79 + 40875, 5000, 3300, 2395, 1935, 1600, 3330, 44090, 40840, 5200, // 80-89 + 4155, 3000, 9200, 3160, 7460, 7320, 15880, 1600, 2200, 1000, 1900 // 90-100 + }; + + public Monster (String name, byte[] buffer, List rewards, List monsters) + { + super (name, buffer); + + realName = name; + genericName = HexFormatter.getPascalString (buffer, 0); + this.monsterID = counter++; + this.monsters = monsters; + goldReward = rewards.get (buffer[136]); + chestReward = rewards.get (buffer[138]); + goldReward.addMonster (this, 0); + chestReward.addMonster (this, 1); + + imageID = buffer[64]; + type = buffer[78]; + armourClass = buffer[80]; + speed = buffer[82]; + levelDrain = buffer[132]; + bonus1 = buffer[134]; + rewardTable1 = buffer[136]; + rewardTable2 = buffer[138]; + partnerID = buffer[140]; + partnerOdds = buffer[142]; + mageSpellLevel = buffer[144]; + priestSpellLevel = buffer[146]; + bonus2 = buffer[150]; + bonus3 = buffer[152]; + resistance = buffer[154]; + abilities = buffer[156]; + groupSize = new Dice (buffer, 66); + hitPoints = new Dice (buffer, 72); + + for (int i = 0, ptr = 84; i < 8; i++, ptr += 6) + { + if (buffer[ptr] == 0) + break; + damage.add (new Dice (buffer, ptr)); + } + } + + @Override + public String getText () + { + StringBuilder text = new StringBuilder (); + + // these values definitely affect the damage a monster does (when breathing?) + int exp2 = (HexFormatter.intValue (buffer[72]) * HexFormatter.intValue (buffer[74]) - 1) * 20; + int exp3 = weight2[speed]; // 1-6 + int exp4 = (10 - armourClass) * 40; + int exp5 = getBonus (35, mageSpellLevel); + int exp6 = getBonus (35, priestSpellLevel); + int exp10 = getBonus (200, levelDrain); + int exp8 = getBonus (90, bonus1); + int exp7 = weight1[bonus3 / 10] * 80; + int exp11 = bonus2 > 0 ? exp2 + 20 : 0; + int exp12 = getBonus (35, Integer.bitCount (resistance & 0x7E)); + int exp9 = getBonus (40, Integer.bitCount (abilities & 0x7F)); + + text.append ("ID .............. " + monsterID); + text.append ("\nMonster name .... " + realName); + text.append ("\nGeneric name .... " + genericName); + + text.append ("\n\nImage ID ........ " + imageID); + text.append ("\nGroup size ...... " + groupSize); + text.append ("\nHit points ...... " + hitPoints); + if (debug) + text.append (" " + exp2); + + text.append ("\n\nMonster class ... " + type + " " + monsterClass[type]); + text.append ("\nArmour class .... " + armourClass); + if (debug) + text.append (" " + exp4); + text.append ("\nSpeed ........... " + speed); + if (debug) + text.append (" " + exp3); + + text.append ("\n\nDamage .......... " + getDamage ()); + + text.append ("\n\nLevel drain ..... " + levelDrain); + if (debug) + text.append (" " + exp10); + text.append ("\nExtra hit pts? .. " + bonus1); + if (debug) + text.append (" " + exp8); + + text.append ("\n\nPartner ID ...... " + partnerID); + if (partnerOdds > 0) + text.append (" " + monsters.get (partnerID).name); + text.append ("\nPartner odds .... " + partnerOdds + "%"); + + text.append ("\n\nMage level ...... " + mageSpellLevel); + if (debug) + text.append (" " + exp5); + text.append ("\nPriest level .... " + priestSpellLevel); + if (debug) + text.append (" " + exp6); + + text.append ("\n\nExperience bonus " + bonus2); + if (debug) + text.append (" " + exp11); + text.append ("\nExperience bonus " + bonus3); + if (debug) + text.append (" " + exp7); + + text.append ("\n\nResistance ...... " + String.format ("%02X", resistance)); + if (debug) + text.append (" " + exp12); + text.append ("\nAbilities ....... " + String.format ("%02X", abilities)); + if (debug) + text.append (" " + exp9); + + text.append ("\n\nExperience ...... " + + (exp2 + exp3 + exp4 + exp5 + exp6 + exp7 + exp8 + exp9 + exp10 + exp11 + exp12)); + + text.append ("\n\n===== Gold reward ======"); + // text.append ("\nTable ........... " + rewardTable1); + text.append ("\n" + goldReward.getText (false)); + text.append ("===== Chest reward ====="); + // text.append ("\nTable ........... " + rewardTable2); + text.append ("\n" + chestReward.getText (false)); + + while (text.charAt (text.length () - 1) == 10) + text.deleteCharAt (text.length () - 1); + + return text.toString (); + } + + public int getExperience () + { + // these values definitely affect the damage a monster does (when breathing?) + int exp2 = (HexFormatter.intValue (buffer[72]) * HexFormatter.intValue (buffer[74]) - 1) * 20; + int exp3 = weight2[speed]; + int exp4 = (10 - armourClass) * 40; + int exp5 = getBonus (35, mageSpellLevel); + int exp6 = getBonus (35, priestSpellLevel); + int exp10 = getBonus (200, levelDrain); + int exp8 = getBonus (90, bonus1); + int exp7 = weight1[bonus3 / 10] * 80; + int exp11 = bonus2 > 0 ? exp2 + 20 : 0; + int exp12 = getBonus (35, Integer.bitCount (resistance & 0x7E)); + int exp9 = getBonus (40, Integer.bitCount (abilities & 0x7F)); + return exp2 + exp3 + exp4 + exp5 + exp6 + exp7 + exp8 + exp9 + exp10 + exp11 + exp12; + } + + private int getBonus (int base, int value) + { + return base * pwr[value]; + } + + public void setImage (BufferedImage image) + { + this.image = image; + } + + public String getName () + { + return realName; + } + + public String getRealName () + { + return realName; + } + + public String getDamage () + { + StringBuilder text = new StringBuilder (); + for (Dice d : damage) + text.append (d + ", "); + text.deleteCharAt (text.length () - 1); + text.deleteCharAt (text.length () - 1); + return text.toString (); + } + + public String getDump (int block) + { + StringBuilder line = new StringBuilder (String.format ("%3d %-16s", monsterID, realName)); + int lo = block == 0 ? 64 : block == 1 ? 88 : block == 2 ? 112 : 136; + int hi = lo + 24; + if (hi > buffer.length) + hi = buffer.length; + for (int i = lo; i < hi; i++) + line.append (String.format ("%02X ", buffer[i])); + if (block == 3) + { + int exp = getExperience (); + line.append (String.format (" %,6d", exp)); + if (exp != experience[monsterID]) + line.append (String.format (" %,6d", experience[monsterID])); + } + return line.toString (); + } + + public int compareTo (Monster other) // where is this used? + { + if (this.type == other.type) + return 0; + if (this.type < other.type) + return -1; + return 1; + } + + @Override + public String toString () + { + return realName; + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/wizardry/PlainMessage.java b/src/com/bytezone/diskbrowser/wizardry/PlainMessage.java new file mode 100755 index 0000000..ac82a54 --- /dev/null +++ b/src/com/bytezone/diskbrowser/wizardry/PlainMessage.java @@ -0,0 +1,18 @@ +package com.bytezone.diskbrowser.wizardry; + +import com.bytezone.diskbrowser.HexFormatter; + +class PlainMessage extends Message +{ + public PlainMessage (byte[] buffer) + { + super (buffer); + } + + @Override + protected String getLine (int offset) + { + int length = HexFormatter.intValue (buffer[offset]); + return HexFormatter.getString (buffer, offset + 1, length); + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/wizardry/Reward.java b/src/com/bytezone/diskbrowser/wizardry/Reward.java new file mode 100755 index 0000000..d0bd72d --- /dev/null +++ b/src/com/bytezone/diskbrowser/wizardry/Reward.java @@ -0,0 +1,141 @@ +package com.bytezone.diskbrowser.wizardry; + +import java.util.ArrayList; +import java.util.List; + +import com.bytezone.diskbrowser.applefile.AbstractFile; + +class Reward extends AbstractFile +{ + static String[] types = { "gold", "item" }; + static final int SEGMENT_LENGTH = 18; + int id; + int totalElements; + List elements; + List items; + List goldMonsters = new ArrayList (); + List chestMonsters = new ArrayList (); + + public Reward (String name, byte[] buffer, int id, List items) + { + super (name, buffer); + this.id = id; + this.items = items; + totalElements = buffer[4]; + elements = new ArrayList (totalElements); + + for (int i = 0; i < totalElements; i++) + { + byte[] buffer2 = new byte[SEGMENT_LENGTH]; + System.arraycopy (buffer, i * SEGMENT_LENGTH, buffer2, 0, SEGMENT_LENGTH); + elements.add (new RewardElement (buffer2)); + } + } + + public void addMonster (Monster monster, int location) + { + if (location == 0) + goldMonsters.add (monster); + else + chestMonsters.add (monster); + } + + @Override + public String getText () + { + return getText (true); + } + + public String getText (boolean showLinks) + { + StringBuilder text = new StringBuilder (); + for (RewardElement re : elements) + text.append (re.getDetail () + "\n"); + + if (showLinks) + { + if (goldMonsters.size () > 0) + { + text.append ("Without chest:\n\n"); + for (Monster m : goldMonsters) + text.append (" " + m + "\n"); + text.append ("\n"); + } + if (chestMonsters.size () > 0) + { + text.append ("With chest:\n\n"); + for (Monster m : chestMonsters) + text.append (" " + m + "\n"); + } + } + return text.toString (); + } + + public String getDump () + { + StringBuilder text = new StringBuilder (); + int seq = 0; + for (RewardElement re : elements) + { + text.append (seq++ == 0 ? String.format ("%02X : ", id) : " "); + text.append (re + "\n"); + } + + return text.toString (); + } + + private class RewardElement + { + int type; + int odds; + byte[] buffer; + + public RewardElement (byte[] buffer) + { + this.buffer = buffer; + type = buffer[8]; + odds = buffer[6]; + } + + @Override + public String toString () + { + StringBuilder text = new StringBuilder (); + for (int i = 0; i < SEGMENT_LENGTH; i += 2) + text.append (String.format ("%3d ", buffer[i] & 0xFF)); + return text.toString (); + } + + public String getDetail () + { + StringBuilder text = new StringBuilder (); + text.append ("Odds ............ " + odds + "%\n"); + + switch (type) + { + case 0: + text.append ("Gold ............ " + buffer[10] + "d" + buffer[12] + "\n"); + break; + case 1: + int lo = buffer[10] & 0xFF; + int qty = buffer[16] & 0xFF; + boolean title = true; + String[] lineItem = new String[4]; + for (int i = lo, max = lo + qty; i <= max; i += lineItem.length) + { + String lineTitle = title ? "Items ..........." : ""; + title = false; + for (int j = 0; j < lineItem.length; j++) + lineItem[j] = i + j <= max ? items.get (i + j).name : ""; + text.append (String.format ("%-17s %-16s %-16s %-16s %-16s%n", lineTitle, lineItem[0], + lineItem[1], lineItem[2], lineItem[3])); + } + break; + default: + System.out.println ("Unknown reward type " + type); + } + + return text.toString (); + } + } +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/wizardry/Spell.java b/src/com/bytezone/diskbrowser/wizardry/Spell.java new file mode 100755 index 0000000..bd3b10c --- /dev/null +++ b/src/com/bytezone/diskbrowser/wizardry/Spell.java @@ -0,0 +1,369 @@ +package com.bytezone.diskbrowser.wizardry; + +import com.bytezone.diskbrowser.applefile.AbstractFile; + +class Spell extends AbstractFile +{ + private final SpellType spellType; + private SpellThrown whenCast; + private final int level; + private String translation; + private SpellTarget target; + private String description; + + public enum SpellType { + MAGE, PRIEST + }; + + public enum SpellTarget { + PERSON, PARTY, MONSTER, MONSTER_GROUP, ALL_MONSTERS, VARIABLE, NONE, CASTER + }; + + public enum SpellThrown { + COMBAT, ANY_TIME, LOOTING, CAMP, COMBAT_OR_CAMP + }; + + private static int lastSpellFound = -1; + + private Spell (String spellName, SpellType type, int level, byte[] buffer) + { + super (spellName, buffer); + this.spellType = type; + this.level = level; + + if (lastSpellFound + 1 < spellNames.length && spellName.equals (spellNames[lastSpellFound + 1])) + setSpell (++lastSpellFound); + else + { + for (int i = 0; i < spellNames.length; i++) + if (spellName.equals (spellNames[i])) + { + setSpell (i); + lastSpellFound = i; + break; + } + } + } + + private void setSpell (int spellNo) + { + this.translation = translations[spellNo]; + this.description = descriptions[spellNo]; + this.whenCast = when[spellNo]; + this.target = affects[spellNo]; + } + + public static Spell getSpell (String spellName, SpellType type, int level, byte[] buffer) + { + return new Spell (spellName, type, level, buffer); + } + + public String getName () + { + return name; + } + + public SpellType getType () + { + return spellType; + } + + public int getLevel () + { + return level; + } + + public String getTranslation () + { + return translation; + } + + @Override + public String getText () + { + return description; + } + + public String getWhenCast () + { + switch (whenCast) + { + case COMBAT: + return "Combat"; + case LOOTING: + return "Looting"; + case ANY_TIME: + return "Any time"; + case CAMP: + return "Camp"; + case COMBAT_OR_CAMP: + return "Combat or camp"; + default: + return "?"; + } + + } + + public String getArea () + { + switch (target) + { + case PERSON: + return "1 Person"; + case PARTY: + return "Entire party"; + case MONSTER: + return "1 Monster"; + case MONSTER_GROUP: + return "1 Monster group"; + case ALL_MONSTERS: + return "All monsters"; + case VARIABLE: + return "Variable"; + case NONE: + return "None"; + case CASTER: + return "Caster"; + default: + return "?"; + } + + } + + public String toHTMLTable () + { + StringBuilder text = new StringBuilder ("
\n"); + + text.append (" \n \n"); + text.append (" \n \n"); + + text.append (" \n \n"); + text.append (" \n \n"); + + text.append (" \n \n"); + text.append (" \n \n"); + + text.append (" \n \n"); + text.append (" \n \n"); + + text.append (" \n \n"); + text.append (" \n \n"); + + text.append (" \n \n"); + text.append (" \n \n"); + + text.append ("
Spell name" + name + "
Translation" + translation + "
Spell level" + level + "
Spell type" + getWhenCast () + "
Area of effect" + getArea () + "
Description" + getText () + "
"); + return text.toString (); + } + + @Override + public String toString () + { + StringBuilder text = new StringBuilder (name); + while (text.length () < 14) + text.append (" "); + if (spellType == SpellType.PRIEST) + text.append ("P"); + else + text.append ("M"); + text.append (level); + while (text.length () < 20) + text.append (" "); + text.append (translation); + while (text.length () < 40) + text.append (" "); + text.append (getArea ()); + while (text.length () < 60) + text.append (" "); + text.append (getWhenCast ()); + return text.toString (); + } + + private static String[] spellNames = + { "KALKI", "DIOS", "BADIOS", "MILWA", "PORFIC", "MATU", "CALFO", "MANIFO", "MONTINO", + "LOMILWA", "DIALKO", "LATUMAPIC", "BAMATU", "DIAL", "BADIAL", "LATUMOFIS", "MAPORFIC", + "DIALMA", "BADIALMA", "LITOKAN", "KANDI", "DI", "BADI", "LORTO", "MADI", "MABADI", + "LOKTOFEIT", "MALIKTO", "KADORTO", + + "HALITO", "MOGREF", "KATINO", "DUMAPIC", "DILTO", "SOPIC", "MAHALITO", "MOLITO", + "MORLIS", "DALTO", "LAHALITO", "MAMORLIS", "MAKANITO", "MADALTO", "LAKANITO", "ZILWAN", + "MASOPIC", "HAMAN", "MALOR", "MAHAMAN", "TILTOWAIT" }; + + private static String[] translations = + { "Blessings", "Heal", "Harm", "Light", "Shield", "Blessing & zeal", "X-ray vision", + "Statue", "Still air", "More light", "Softness/supple", "Identification", "Prayer", + "Heal (more)", "Hurt (more)", "Cure poison", "Shield (big)", "Heal (greatly)", + "Hurt (greatly)", "Flame tower", "Location", "Life", "Death", "Blades", "Healing", + "Harm (incredibly)", "Recall", "The Word of Death", "Resurrection", + + "Little Fire", "Body Iron", "Bad Air", "Clarity", "Darkness", "Glass", "Big fire", + "Spark storm", "Fear", "Blizzard blast", "Flame storm", "Terror", "Deadly air", "Frost", + "Suffocation", "Dispell", "Big glass", "Change", "Apport", "Great change", + "(untranslatable)" }; + + private static SpellThrown[] when = + { SpellThrown.COMBAT, SpellThrown.ANY_TIME, SpellThrown.COMBAT, SpellThrown.ANY_TIME, + SpellThrown.COMBAT, SpellThrown.COMBAT, SpellThrown.LOOTING, SpellThrown.COMBAT, + SpellThrown.COMBAT, SpellThrown.ANY_TIME, SpellThrown.ANY_TIME, SpellThrown.COMBAT, + SpellThrown.COMBAT, SpellThrown.ANY_TIME, SpellThrown.COMBAT, SpellThrown.ANY_TIME, + SpellThrown.ANY_TIME, SpellThrown.ANY_TIME, SpellThrown.COMBAT, SpellThrown.COMBAT, + SpellThrown.CAMP, SpellThrown.CAMP, SpellThrown.COMBAT, SpellThrown.COMBAT, + SpellThrown.ANY_TIME, SpellThrown.COMBAT, SpellThrown.COMBAT, SpellThrown.COMBAT, + SpellThrown.ANY_TIME, + + SpellThrown.COMBAT, SpellThrown.COMBAT, SpellThrown.COMBAT, SpellThrown.CAMP, + SpellThrown.COMBAT, SpellThrown.COMBAT, SpellThrown.COMBAT, SpellThrown.COMBAT, + SpellThrown.COMBAT, SpellThrown.COMBAT, SpellThrown.COMBAT, SpellThrown.COMBAT, + SpellThrown.COMBAT, SpellThrown.COMBAT, SpellThrown.COMBAT, SpellThrown.COMBAT, + SpellThrown.COMBAT, SpellThrown.COMBAT, SpellThrown.COMBAT_OR_CAMP, SpellThrown.COMBAT, + SpellThrown.COMBAT, }; + + private static SpellTarget[] affects = + { SpellTarget.PARTY, SpellTarget.PERSON, SpellTarget.MONSTER, SpellTarget.PARTY, + SpellTarget.CASTER, SpellTarget.PARTY, SpellTarget.CASTER, SpellTarget.MONSTER_GROUP, + SpellTarget.MONSTER_GROUP, SpellTarget.PARTY, SpellTarget.PERSON, SpellTarget.PARTY, + SpellTarget.PARTY, SpellTarget.PERSON, SpellTarget.MONSTER, SpellTarget.PERSON, + SpellTarget.PARTY, SpellTarget.PERSON, SpellTarget.MONSTER, SpellTarget.PARTY, + SpellTarget.PERSON, SpellTarget.PERSON, SpellTarget.MONSTER, SpellTarget.MONSTER_GROUP, + SpellTarget.PERSON, SpellTarget.MONSTER, SpellTarget.PARTY, SpellTarget.MONSTER_GROUP, + SpellTarget.PERSON, + + SpellTarget.MONSTER, SpellTarget.CASTER, SpellTarget.MONSTER_GROUP, SpellTarget.NONE, + SpellTarget.MONSTER_GROUP, SpellTarget.CASTER, SpellTarget.MONSTER_GROUP, + SpellTarget.MONSTER_GROUP, SpellTarget.MONSTER_GROUP, SpellTarget.MONSTER_GROUP, + SpellTarget.MONSTER_GROUP, SpellTarget.ALL_MONSTERS, SpellTarget.ALL_MONSTERS, + SpellTarget.MONSTER_GROUP, SpellTarget.MONSTER_GROUP, SpellTarget.MONSTER, + SpellTarget.PARTY, SpellTarget.VARIABLE, SpellTarget.PARTY, SpellTarget.PARTY, + SpellTarget.ALL_MONSTERS }; + + private static String[] descriptions = + { + "KALKI reduces the AC of all party members by one, and thus makes" + + " them harder to hit.", + "DIOS restores from one to eight hit points of damage from a party" + + "member. It will not bring dead back to life.", + "BADIOS causes one to eight hit points of damage to a monster, and" + + " may kill it. It is the reverse of dios. Note the BA prefix which" + + " means 'not'.", + "MILWA causes a softly glowing light to follow the party, allowing" + + " them to see further into the maze, and also revealing all secret" + + " doors. See also LOMILWA. This spell lasts only a short while.", + "PORFIC lowers the AC of the caster considerably. The effects last" + + " for the duration of combat.", + "MATU has the same effects as KALKI, but at double the strength.", + "CALFO allows the caster to determine the exact nature of a trap" + + " on a chest 95% of the time.", + "MANIFO causes some of the monsters in a group to become stiff as" + + " statues for one or more melee rounds. The chance of success," + + " and the duration of the effects, depend on the power of the" + + " target monsters.", + "MONTINO causes the air around a group of monsters to stop" + + " transmitting sound. Like MANIFO, only some of the monsters will" + + " be affected, and for varying lengths of time. Monsters and" + + " Party members under the influence of this spell cannot cast" + + " spells, as they cannot utter the spell words!", + "LOMILWA is a MILWA spell with a much longer life span. Note that" + + " when this spell, or MILWA are active, the Q option while" + + " moving through the maze is active. If Q)UICK PLOTTING is on," + + " only the square you are in, and the next two squares, will" + + " plot. Normally you might see five or six squares ahead with" + + " LOMILWA on. Quick Plotting lets you move fast through known" + + " areas. Note that it will be turned off when you enter camp or" + + " combat mode.", + "DIALKO cures paralysis, and removes the effects of MANIFO and" + + " KATINO from one member of the party.", + "LATUMAPIC makes it readily apparent exactly what the opposing" + " monsters really are.", + "BAMATU has the effects of MATU at twice the effectiveness.", + "DIAL restores two to 16 hit points of damage, and is similar to" + " DIOS.", + "BADIAL causes two to 16 hit points of damage in the same way as" + " BADIOS.", + "LATUMOFIS makes a poisoned person whole and fit again. Note that" + + " poison causes a person to lose hit points steadily during" + + " movement and combat.", + "MAPORFIC is an improved PORFIC, with effects that last for the" + " entire expedition.", + "DIALMA restores three to 24 hit points.", + "BADIALMA causes three to 24 hit points of damage.", + "LITOKAN causes a pillar of flame to strike a group of monsters," + + " doing three to 24 hits of damage to each. However, as with" + + " many spells that affect entire groups, there is a chance that" + + " individual monsters will be able to avoid or minimise its" + + " effects. And some monsters will be resistant to it.", + "KANDI allows the user to locate characters in the maze. It tells on" + + " which level, and in which rough area the dead one can be found.", + "DI causes a dead person to be resurrected. However, the renewed" + + " character has but one hit point. Also, this spell is not as" + + " effective or as safe as using the Temple.", + "BADI gives the affected monster a coronary attack. It may or may" + + " not cause death to occur.", + "LORTO causes sharp blades to slice through a group, causing six to" + + " 36 points of damage.", + "MADI causes all hit points to be restored and cures any condition" + " but death.", + "MABADI causes all but one to eight hit points to be removed from" + " the target.", + "LOKTOFEIT causes all party members to be teleported back to the" + + " castle, minus all their equipment and most of their gold. There" + + " is also a good chance this spell will not function.", + "MALIKTO causes 12 to 72 hit points of damage to all monsters. None" + + " can escape or minimise its effects.", + "KADORTO restores the dead to life as does DI, but also restores all" + + " hit points. However, it has the same drawbacks as the DI spell." + + " KADORTO can be used to resurrect people even if they are ashes.", + + "HALITO causes a flame ball the size of a baseball to hit a monster," + + " doing from one to eight points of damage.", + "MOGREF reduces the caster's AC by two. The effect lasts the entire" + " encounter.", + "KATINO causes most of the monsters in a group to fall asleep." + + " Katino only effects normal, animal or humanoid monsters. The" + + " chance of the spell affecting an individual monster, and the" + + " duration of the effect, is inversely proportional to the power" + + " of the monster. While asleep, monsters are easier to hit and" + + " successful strikes do double damage.", + "DUMAPIC informs you of the party's exact displacement from the" + + " stairs to the castle, vertically, and North and East, and also" + + " tells you what direction you are facing.", + + "DILTO causes one group of monsters to be enveloped in darkness," + + " which reduces their ability to defend against your attacks.", + "SOPIC causes the caster to become transparent. This means that" + + " he is harder to see, and thus his AC is reduced by four.", + + "MAHALITO causes a fiery explosion in a monster group, doing four" + + " to 24 hit points of damage. As with other similar spells," + + " monsters may be able to minimise the damage done.", + "MOLITO causes sparks to fly out and damage about half of the" + + " monsters in a group. Three to 18 hit points of damage are done" + + " with no chance of avoiding the sparks.", + "MORLIS causes one group of monsters to fear the party greatly. The" + + " effects are the same as a double strength DILTO spell.", + "DALTO is similar to MAHALITO except that cold replaces flames." + + " Also, six to 36 hit points of damage are done.", + "LAHALITO is an improved MAHALITO, doing the same damage as DALTO.", + "MAMORLIS is similar to MORLIS, except that all monster groups are" + " affected.", + "Any monsters of less than eigth level (i.e. about 35-40 hit points)" + + " are killed by this spell outright.", + "An improved DALTO causing eight to 64 hit points of damage.", + "All monsters in the group affected by this spell die. Of course," + + " there is a chance that some of the monsters will not be affected.", + "This spell will destroy any one monster that is of the Undead" + " variety", + "This spell duplicates the effects of SOPIC for the entire party.", + "This spell is indeed terrible, and may backfire on the caster." + + " First, to even cast it, you must be of the thirteenth level or" + + " higher, and casting it will cost you one level of experience." + + " The effects of HAMAN are random, and usually help the party.", + "This spell's effects depend on the situation the party is in when it" + + " is cast.Basically, MALOR will teleport the entire party from one" + + " location to another. When used in melee, the teleport is random," + + " but when used in camp, where there is more chance for concentration" + + ", it can be used to move the party anywhere in the maze. Be warned," + + " however, that if you teleport outside of the maze, or into an" + + " area that is solid rock, you will be lost forever, so this spell" + + " is to be used with the greatest of care. Combat use of MALOR will" + + " never put you outside of the maze, but it may move you deeper in," + + " so it should be used only in panic situations.", + "The same restrictions and qualifications apply to this spell as do" + + " to HAMAN. However, the effects are even greater. Generally these" + + " spells are only used when there is no other hope for survival.", + "The effect of this spell can be described as similar to that of a" + + " nuclear fusion explosion. Luckily the party is shielded from its" + + " effects. Unluckily (for them) the monsters are not. This spell" + + " will do from 10-100 hit points of damage." }; +} \ No newline at end of file diff --git a/src/com/bytezone/diskbrowser/wizardry/WizardryScenarioDisk.java b/src/com/bytezone/diskbrowser/wizardry/WizardryScenarioDisk.java new file mode 100755 index 0000000..f3fee60 --- /dev/null +++ b/src/com/bytezone/diskbrowser/wizardry/WizardryScenarioDisk.java @@ -0,0 +1,599 @@ +package com.bytezone.diskbrowser.wizardry; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; + +import com.bytezone.diskbrowser.HexFormatter; +import com.bytezone.diskbrowser.applefile.AbstractFile; +import com.bytezone.diskbrowser.applefile.AppleFileSource; +import com.bytezone.diskbrowser.disk.*; +import com.bytezone.diskbrowser.gui.DataSource; +import com.bytezone.diskbrowser.pascal.PascalDisk; +import com.bytezone.diskbrowser.wizardry.Character.Attributes; +import com.bytezone.diskbrowser.wizardry.Character.Statistics; +import com.bytezone.diskbrowser.wizardry.Header.ScenarioData; +import com.bytezone.diskbrowser.wizardry.Spell.SpellType; + +public class WizardryScenarioDisk extends PascalDisk +{ + public Header scenarioHeader; + + public List images; + public List items; + public List characters; + public List spells; + public List messages; + public List monsters; + public List levels; + List experiences; + List rewards; + + // leave these here until I decide whether to use them or not + SectorType mazeSector = new SectorType ("Maze", Color.lightGray); + SectorType monsterSector = new SectorType ("Monsters", Color.black); + SectorType itemSector = new SectorType ("Items", Color.blue); + SectorType characterSector = new SectorType ("Characters", Color.magenta); + SectorType spellSector = new SectorType ("Spells", Color.orange); + SectorType messageSector = new SectorType ("Messages", Color.cyan); + SectorType imageSector = new SectorType ("Images", Color.red); + SectorType experienceSector = new SectorType ("Experience", Color.darkGray); + SectorType treasureSector = new SectorType ("Treasure", Color.pink); + + public WizardryScenarioDisk (Disk disk) + { + super (disk); + + if (false) + { + sectorTypesList.add (mazeSector); + sectorTypesList.add (monsterSector); + sectorTypesList.add (itemSector); + sectorTypesList.add (characterSector); + sectorTypesList.add (spellSector); + sectorTypesList.add (messageSector); + sectorTypesList.add (imageSector); + sectorTypesList.add (experienceSector); + sectorTypesList.add (treasureSector); + } + + CodedMessage.codeOffset = 185; + Monster.counter = 0; + Item.counter = 0; + + DefaultTreeModel model = (DefaultTreeModel) catalogTree.getModel (); + DefaultMutableTreeNode currentRoot = (DefaultMutableTreeNode) model.getRoot (); + DefaultMutableTreeNode dataNode = findNode (currentRoot, "SCENARIO.DATA"); + DefaultMutableTreeNode msgNode = findNode (currentRoot, "SCENARIO.MESGS"); + if (dataNode == null || msgNode == null) + { + System.out.println ("Wizardry data or msg node not found"); + return; + } + dataNode.setAllowsChildren (true); + msgNode.setAllowsChildren (true); + + scenarioHeader = new Header (dataNode, this); + + // Process SCENARIO.MESGS (requires scenario) + AppleFileSource afs = (AppleFileSource) msgNode.getUserObject (); + DefaultMutableTreeNode node = linkNode ("Messages", "Messages string", msgNode); + extractMessages (node, afs.getSectors ()); + // makeNodeVisible (node); + + // Process SCENARIO.DATA (requires scenario and messages) + afs = (AppleFileSource) dataNode.getUserObject (); + List sectors = afs.getSectors (); + + extractItems (linkNode ("Items", "Items string", dataNode), sectors); + extractRewards (linkNode ("Rewards", "Treasure string", dataNode), sectors); + extractMonsters (linkNode ("Monsters", "Monsters string", dataNode), sectors); + extractCharacters (linkNode ("Characters", "Characters string", dataNode), sectors); + extractImages (linkNode ("Images", "Images string", dataNode), sectors); + extractExperienceLevels (linkNode ("Experience", "Experience string", dataNode), sectors); + // node = linkNode ("Spells", "Spells string", dataNode); + node = null; + extractSpells (node, sectors); + extractLevels (linkNode ("Maze", "Levels string", dataNode), sectors); + // Make the Spells node (and its siblings) visible + // makeNodeVisible (node); + + // add information about each characters' baggage, spells known etc. + for (Character c : characters) + { + c.linkItems (items); + c.linkSpells (spells); + int type = c.getStatistics ().typeInt; + c.linkExperience (experiences.get (type)); + } + } + + private DefaultMutableTreeNode linkNode (String name, String text, + DefaultMutableTreeNode parent) + { + DefaultAppleFileSource afs = new DefaultAppleFileSource (name, text, this); + DefaultMutableTreeNode node = new DefaultMutableTreeNode (afs); + parent.add (node); + return node; + } + + public static boolean isWizardryFormat (Disk disk, boolean debug) + { + if (false) + return false; + byte[] buffer = disk.readSector (2); + int totalFiles = HexFormatter.intValue (buffer[16], buffer[17]); + if (totalFiles != 3) + return false; + + for (int i = 1, ptr = 32; i <= totalFiles; i++, ptr += 26) + { + String text = HexFormatter.getPascalString (buffer, ptr); + if (!text.equals ("SCENARIO.DATA") && !text.equals ("SCENARIO.MESGS") + && !text.equals ("WIZARDRY.CODE")) + return false; + } + return true; + } + + @Override + public AppleFileSource getFile (String fileName) + { + System.out.println ("Wizardry disk looking for : " + fileName); + return null; + } + + public String getCatalogText () + { + return null; + } + + @Override + public List getFileSectors (int fileNo) + { + return null; + } + + @Override + public DataSource getFile (int fileNo) + { + return null; + } + + private void extractRewards (DefaultMutableTreeNode node, List sectors) + { + List nodeSectors = new ArrayList (); + ScenarioData sd = scenarioHeader.data.get (Header.TREASURE_TABLE_AREA); + rewards = new ArrayList (sd.total); + int max = sd.totalBlocks / 2; + + int seq = 0; + for (int i = 0; i < max; i++) + { + List blocks = getTwoBlocks (sd, i, sectors); + nodeSectors.addAll (blocks); + byte[] buffer = disk.readSectors (blocks); + seq = addReward (buffer, blocks, node, seq); + } + + StringBuilder text = new StringBuilder (); + for (Reward t : rewards) + text.append (t.getDump () + "\n"); + + DefaultAppleFileSource afs = (DefaultAppleFileSource) node.getUserObject (); + afs.setSectors (nodeSectors); + DefaultDataSource dds = (DefaultDataSource) afs.getDataSource (); + dds.text = text.toString (); + } + + private int addReward (byte[] buffer, List blocks, DefaultMutableTreeNode node, + int seq) + { + int recLen = 168; + for (int ptr = 0; ptr < 1008; ptr += recLen) + { + byte[] data2 = new byte[recLen]; + System.arraycopy (buffer, ptr, data2, 0, recLen); + + Reward tt = new Reward ("Type " + seq, data2, seq++, items); + rewards.add (tt); + addToNode (tt, node, blocks, treasureSector); + } + return seq; + } + + private void extractCharacters (DefaultMutableTreeNode node, List sectors) + { + List nodeSectors = new ArrayList (); + ScenarioData sd = scenarioHeader.data.get (Header.CHARACTER_AREA); + characters = new ArrayList (sd.total); + int max = sd.totalBlocks / 2; + if (max < sd.total) + System.out.println ("Characters short in Wizardry disk"); + + for (int i = 0; i < max; i++) + { + List blocks = getTwoBlocks (sd, i, sectors); + nodeSectors.addAll (blocks); + byte[] buffer = disk.readSectors (blocks); + addCharacters (buffer, blocks, node); + } + + StringBuilder text = new StringBuilder (); + text.append ("Name Age Align Race Type " + + "HP St In Pi Vi Ag Lu Status\n"); + text.append ("------------- ---- -------- -------- ---------- " + + "-- -- -- -- -- -- -- ------\n"); + for (Character ch : characters) + { + Statistics stats = ch.getStatistics (); + Attributes att = ch.getAttributes (); + text.append (String.format ("%-15s %2d %-8s %-8s %-8s %3d", ch, + (stats.ageInWeeks / 52), stats.alignment, stats.race, + stats.type, stats.hitsMax)); + text.append (String.format (" %2d %2d %2d %2d %2d %2d", att.strength, + att.intelligence, att.piety, att.vitality, att.agility, + att.luck)); + text.append (String.format (" %5s %s%n", stats.status, ch.isOut () ? "* OUT *" : "")); + } + + DefaultAppleFileSource afs = (DefaultAppleFileSource) node.getUserObject (); + afs.setSectors (nodeSectors); + DefaultDataSource dds = (DefaultDataSource) afs.getDataSource (); + dds.text = text.toString (); + } + + private void addCharacters (byte[] buffer, List blocks, + DefaultMutableTreeNode node) + { + int recLen = 208; + for (int ptr = 0; ptr < 832; ptr += recLen) + { + int nameLength = HexFormatter.intValue (buffer[ptr]); + if (nameLength == 0xC3 || buffer[ptr + 40] == 0x07) + continue; + String name = HexFormatter.getString (buffer, ptr + 1, nameLength); + + byte[] data2 = new byte[recLen]; + System.arraycopy (buffer, ptr, data2, 0, recLen); + + Character c = new Character (name, data2, scenarioHeader.scenarioID); + characters.add (c); + addToNode (c, node, blocks, characterSector); + } + } + + private void extractMonsters (DefaultMutableTreeNode node, List sectors) + { + List nodeSectors = new ArrayList (); + ScenarioData sd = scenarioHeader.data.get (Header.MONSTER_AREA); + monsters = new ArrayList (sd.total); + int max = sd.totalBlocks / 2; + + for (int i = 0; i < max; i++) + { + List blocks = getTwoBlocks (sd, i, sectors); + nodeSectors.addAll (blocks); + byte[] buffer = disk.readSectors (blocks); + addMonsters (buffer, blocks, node); + } + + StringBuilder text = new StringBuilder (); + for (int block = 0; block < 4; block++) + { + text.append (" ID Name\n"); + text.append ("--- ---------------"); + for (int i = 0; i < 24; i++) + text.append (" --"); + text.append ("\n"); + for (Monster m : monsters) + text.append (m.getDump (block) + "\n"); + text.append ("\n"); + } + DefaultAppleFileSource afs = (DefaultAppleFileSource) node.getUserObject (); + afs.setSectors (nodeSectors); + DefaultDataSource dds = (DefaultDataSource) afs.getDataSource (); + dds.text = text.toString (); + } + + private void addMonsters (byte[] buffer, List blocks, + DefaultMutableTreeNode node) + { + int recLen = 158; + for (int ptr = 0; ptr < 948; ptr += recLen) + { + int nameLength = HexFormatter.intValue (buffer[ptr + 32]); + if (nameLength == 0 || nameLength == 255) + break; + String itemName = HexFormatter.getString (buffer, ptr + 33, nameLength); + + byte[] data2 = new byte[recLen]; + System.arraycopy (buffer, ptr, data2, 0, recLen); + + Monster m = new Monster (itemName, data2, rewards, monsters); + monsters.add (m); + addToNode (m, node, blocks, monsterSector); + } + } + + private void extractItems (DefaultMutableTreeNode node, List sectors) + { + List nodeSectors = new ArrayList (); + ScenarioData sd = scenarioHeader.data.get (Header.ITEM_AREA); + items = new ArrayList (sd.total); + int max = sd.totalBlocks / 2; + + for (int i = 0; i < max; i++) + { + List blocks = getTwoBlocks (sd, i, sectors); + nodeSectors.addAll (blocks); + byte[] buffer = disk.readSectors (blocks); + addItems (buffer, blocks, node); + } + + StringBuilder text = new StringBuilder (); + for (int block = 0; block < 3; block++) + { + text.append (" ID Name\n"); + text.append ("--- ---------------"); + for (int i = 0; i < 24; i++) + text.append (" --"); + text.append ("\n"); + for (Item item : items) + text.append (item.getDump (block) + "\n"); + text.append ("\n"); + } + DefaultAppleFileSource afs = (DefaultAppleFileSource) node.getUserObject (); + afs.setSectors (nodeSectors); + DefaultDataSource dds = (DefaultDataSource) afs.getDataSource (); + dds.text = text.toString (); + } + + private void addItems (byte[] buffer, List blocks, DefaultMutableTreeNode node) + { + int recLen = 78; + for (int ptr = 0; ptr < 1014; ptr += recLen) + { + if (buffer[ptr] == 0) + break; + String itemName = HexFormatter.getPascalString (buffer, ptr); + + byte[] data2 = new byte[recLen]; + System.arraycopy (buffer, ptr, data2, 0, recLen); + + Item i = new Item (itemName, data2); + items.add (i); + addToNode (i, node, blocks, itemSector); + } + } + + private void extractSpells (DefaultMutableTreeNode node, List sectors) + { + spells = new ArrayList (); + ArrayList blocks = new ArrayList (2); + int offset = scenarioHeader.scenarioID <= 2 ? 4 : 1; + blocks.add (sectors.get (offset)); + blocks.add (sectors.get (offset + 1)); + + SpellType spellType = SpellType.MAGE; + for (DiskAddress da : blocks) + { + byte[] buffer = disk.readSector (da); + int level = 1; + int ptr = -1; + while (ptr < 255) + { + ptr++; + int start = ptr; + while (ptr < 256 && buffer[ptr] != 0x0D) + ptr++; + if (ptr == start) + break; + String spell = HexFormatter.getString (buffer, start, ptr - start); + if (spell.startsWith ("*")) + { + spell = spell.substring (1); + ++level; + } + Spell s = Spell.getSpell (spell, spellType, level, buffer); + spells.add (s); + // addToNode (s, node, da, spellSector); + } + spellType = SpellType.PRIEST; + } + } + + private void extractMessages (DefaultMutableTreeNode node, List sectors) + { + Message.resetMessageId (); + messages = new ArrayList (); + + // Copy first 504 bytes from each sector to a single contiguous buffer + int recordLength = 42; + int max = recordLength * 12; + byte[] buffer = new byte[sectors.size () * max]; + int offset = 0; + + for (DiskAddress da : sectors) + { + byte[] tempBuffer = disk.readSector (da); + System.arraycopy (tempBuffer, 0, buffer, offset, max); + offset += max; + } + + int id = 0; + int totalLines = 0; + + for (int ptr = 0; ptr < buffer.length; ptr += recordLength) + { + int sequence = buffer[ptr + recordLength - 2]; + ++totalLines; + if (sequence == 1) // end of message + { + int totalBytes = totalLines * recordLength; + byte[] newBuffer = new byte[totalBytes]; + int messageEnd = ptr + recordLength; + int messageStart = messageEnd - totalBytes; + System.arraycopy (buffer, messageStart, newBuffer, 0, totalBytes); + + Message m; + if (scenarioHeader.scenarioID == 1) + m = new PlainMessage (newBuffer); + else + m = new CodedMessage (newBuffer); + messages.add (m); + + List messageBlocks = new ArrayList (); + int lastBlock = -1; + for (int p2 = messageStart; p2 < messageEnd; p2 += recordLength) + { + int blockNo = p2 / max; + offset = p2 % max; + if (blockNo != lastBlock) + { + messageBlocks.add (sectors.get (blockNo)); + lastBlock = blockNo; + } + } + addToNode (m, node, messageBlocks, messageSector); + id += totalLines; + totalLines = 0; + } + } + + DefaultAppleFileSource afs = (DefaultAppleFileSource) node.getUserObject (); + afs.setSectors (sectors); + } + + private void extractLevels (DefaultMutableTreeNode node, List sectors) + { + List nodeSectors = new ArrayList (); + ScenarioData sd = scenarioHeader.data.get (Header.MAZE_AREA); + levels = new ArrayList (sd.total); + int max = sd.totalBlocks / 2; + + for (int i = 0; i < max; i++) + { + List blocks = getTwoBlocks (sd, i, sectors); + nodeSectors.addAll (blocks); + byte[] buffer = disk.readSectors (blocks); + byte[] data2 = new byte[896]; + System.arraycopy (buffer, 0, data2, 0, 896); + + MazeLevel model = new MazeLevel (data2, i + 1); + model.setMessages (messages); + model.setMonsters (monsters); + model.setItems (items); + levels.add (model); + addToNode (model, node, blocks, mazeSector); + } + + StringBuilder text = new StringBuilder (); + for (MazeLevel level : levels) + text.append (level.name + "\n"); + + DefaultAppleFileSource afs = (DefaultAppleFileSource) node.getUserObject (); + afs.setSectors (nodeSectors); + DefaultDataSource dds = (DefaultDataSource) afs.getDataSource (); + dds.text = text.toString (); + } + + private void extractImages (DefaultMutableTreeNode node, List sectors) + { + List nodeSectors = new ArrayList (); + ScenarioData sd = scenarioHeader.data.get (Header.IMAGE_AREA); + int max = sd.totalBlocks; + images = new ArrayList (); + + for (int i = 0; i < max; i++) + { + DiskAddress da = sectors.get (sd.dataOffset + i); + nodeSectors.add (da); + byte[] buffer = disk.readSector (da); + byte[] exactBuffer = new byte[480]; + System.arraycopy (buffer, 0, exactBuffer, 0, exactBuffer.length); + + String name = "Unknown"; + for (Monster m : monsters) + if (m.imageID == i) + { + name = m.genericName; + break; + } + + AbstractImage mi = + scenarioHeader.scenarioID < 3 ? new Image (name, buffer) : new ImageV2 (name, + exactBuffer); + images.add (mi); + addToNode (mi, node, da, imageSector); + } + + StringBuilder text = new StringBuilder (); + for (AbstractImage image : images) + text.append (image.name + "\n"); + + DefaultAppleFileSource afs = (DefaultAppleFileSource) node.getUserObject (); + afs.setSectors (nodeSectors); + DefaultDataSource dds = (DefaultDataSource) afs.getDataSource (); + dds.text = text.toString (); + } + + private void + extractExperienceLevels (DefaultMutableTreeNode node, List sectors) + { + List nodeSectors = new ArrayList (); + ScenarioData sd = scenarioHeader.data.get (Header.EXPERIENCE_AREA); + experiences = new ArrayList (sd.total); + int max = sd.totalBlocks / 2; + + for (int i = 0; i < max; i++) + { + List blocks = getTwoBlocks (sd, i, sectors); + nodeSectors.addAll (blocks); + byte[] buffer = disk.readSectors (blocks); + + for (int ptr = 0; ptr <= buffer.length; ptr += 78) + { + if (buffer[ptr] == 0) + break; + + byte[] newBuffer = new byte[78]; + System.arraycopy (buffer, ptr, newBuffer, 0, newBuffer.length); + ExperienceLevel el = new ExperienceLevel ("exp", newBuffer); + experiences.add (el); + addToNode (el, node, blocks, experienceSector); + } + } + + DefaultAppleFileSource afs = (DefaultAppleFileSource) node.getUserObject (); + afs.setSectors (nodeSectors); + } + + private void addToNode (AbstractFile af, DefaultMutableTreeNode node, DiskAddress block, + SectorType type) + { + ArrayList blocks = new ArrayList (1); + blocks.add (block); + addToNode (af, node, blocks, type); + } + + private void addToNode (AbstractFile af, DefaultMutableTreeNode node, + List blocks, SectorType type) + { + DefaultAppleFileSource dafs = new DefaultAppleFileSource (af.name, af, this, blocks); + DefaultMutableTreeNode childNode = new DefaultMutableTreeNode (dafs); + node.add (childNode); + childNode.setAllowsChildren (false); + } + + private List getTwoBlocks (ScenarioData sd, int i, List sectors) + { + ArrayList blocks = new ArrayList (2); + blocks.add (sectors.get (sd.dataOffset + i * 2)); + blocks.add (sectors.get (sd.dataOffset + i * 2 + 1)); + return blocks; + } +} \ No newline at end of file