From 0d7d1b67e064c5665d7429c940981262cf944175 Mon Sep 17 00:00:00 2001 From: Andy McFadden Date: Tue, 27 Mar 2007 17:47:10 +0000 Subject: [PATCH] initial import --- CP.dsw | 107 + CP.sln | 67 + DIST/License.txt | 37 + DIST/NList.Data.TXT | 2516 +++++++++++++++ DIST/ReadMe.txt | 11 + DIST/with-mdc.deploy | 271 ++ LICENSE.txt | 25 + ReadMe.htm | 313 ++ app/ACUArchive.cpp | 906 ++++++ app/ACUArchive.h | 222 ++ app/AboutDialog.cpp | 208 ++ app/AboutDialog.h | 37 + app/ActionProgressDialog.cpp | 157 + app/ActionProgressDialog.h | 65 + app/Actions.cpp | 2561 +++++++++++++++ app/AddClashDialog.cpp | 60 + app/AddClashDialog.h | 39 + app/AddFilesDialog.cpp | 202 ++ app/AddFilesDialog.h | 80 + app/ArchiveInfoDialog.cpp | 433 +++ app/ArchiveInfoDialog.h | 118 + app/BNYArchive.cpp | 998 ++++++ app/BNYArchive.h | 218 ++ app/BasicImport.cpp | 714 +++++ app/BasicImport.h | 104 + app/CassImpTargetDialog.cpp | 184 ++ app/CassImpTargetDialog.h | 52 + app/CassetteDialog.cpp | 1192 +++++++ app/CassetteDialog.h | 160 + app/ChooseAddTargetDialog.cpp | 102 + app/ChooseAddTargetDialog.h | 47 + app/ChooseDirDialog.cpp | 199 ++ app/ChooseDirDialog.h | 53 + app/CiderPress.rc | 2428 +++++++++++++++ app/Clipboard.cpp | 1079 +++++++ app/ConfirmOverwriteDialog.cpp | 173 ++ app/ConfirmOverwriteDialog.h | 93 + app/ContentList.cpp | 1150 +++++++ app/ContentList.h | 136 + app/ConvDiskOptionsDialog.cpp | 344 ++ app/ConvDiskOptionsDialog.h | 53 + app/ConvFileOptionsDialog.cpp | 35 + app/ConvFileOptionsDialog.h | 38 + app/CreateImageDialog.cpp | 347 +++ app/CreateImageDialog.h | 75 + app/CreateSubdirDialog.cpp | 82 + app/CreateSubdirDialog.h | 45 + app/DEFileDialog.cpp | 69 + app/DEFileDialog.h | 60 + app/DiskArchive.cpp | 3155 +++++++++++++++++++ app/DiskArchive.h | 244 ++ app/DiskConvertDialog.cpp | 307 ++ app/DiskConvertDialog.h | 85 + app/DiskEditDialog.cpp | 1641 ++++++++++ app/DiskEditDialog.h | 315 ++ app/DiskEditOpenDialog.cpp | 53 + app/DiskEditOpenDialog.h | 49 + app/DiskFSTree.cpp | 252 ++ app/DiskFSTree.h | 81 + app/DoneOpenDialog.h | 16 + app/EOLScanDialog.cpp | 59 + app/EOLScanDialog.h | 37 + app/EditAssocDialog.cpp | 159 + app/EditAssocDialog.h | 45 + app/EditCommentDialog.cpp | 79 + app/EditCommentDialog.h | 47 + app/EditPropsDialog.cpp | 523 ++++ app/EditPropsDialog.h | 88 + app/EnterRegDialog.cpp | 211 ++ app/EnterRegDialog.h | 50 + app/ExtractOptionsDialog.cpp | 208 ++ app/ExtractOptionsDialog.h | 87 + app/FileNameConv.cpp | 1421 +++++++++ app/FileNameConv.h | 139 + app/GenericArchive.cpp | 1362 ++++++++ app/GenericArchive.h | 690 ++++ app/Graphics/ChooseFolder.bmp | Bin 0 -> 246 bytes app/Graphics/CiderPress.ico | Bin 0 -> 4846 bytes app/Graphics/FileViewer.ico | Bin 0 -> 1078 bytes app/Graphics/NewFolder.bmp | Bin 0 -> 246 bytes app/Graphics/binary2.ico | Bin 0 -> 4846 bytes app/Graphics/diskimage.ico | Bin 0 -> 4846 bytes app/Graphics/fslogo.bmp | Bin 0 -> 10294 bytes app/Graphics/hdrbar.bmp | Bin 0 -> 374 bytes app/Graphics/list-pics.bmp | Bin 0 -> 758 bytes app/Graphics/nufx.ico | Bin 0 -> 4846 bytes app/Graphics/toolbar1.bmp | Bin 0 -> 5086 bytes app/Graphics/tree_pics.bmp | Bin 0 -> 2102 bytes app/Graphics/vol_pics.bmp | Bin 0 -> 374 bytes app/Help/CIDERPRESS.HLP | Bin 0 -> 295476 bytes app/Help/CiderPress.cnt | 56 + app/Help/CiderPress.hmp | Bin 0 -> 329108 bytes app/HelpTopics.h | 43 + app/ImageFormatDialog.cpp | 359 +++ app/ImageFormatDialog.h | 81 + app/Main.cpp | 2710 ++++++++++++++++ app/Main.h | 435 +++ app/MyApp.cpp | 221 ++ app/MyApp.h | 52 + app/NewDiskSize.cpp | 166 + app/NewDiskSize.h | 38 + app/NewFolderDialog.cpp | 84 + app/NewFolderDialog.h | 50 + app/NufxArchive.cpp | 2697 ++++++++++++++++ app/NufxArchive.h | 181 ++ app/OpenVolumeDialog.cpp | 736 +++++ app/OpenVolumeDialog.h | 70 + app/PasteSpecialDialog.cpp | 54 + app/PasteSpecialDialog.h | 41 + app/Preferences.cpp | 618 ++++ app/Preferences.h | 298 ++ app/PrefsDialog.cpp | 713 +++++ app/PrefsDialog.h | 241 ++ app/Print.cpp | 821 +++++ app/Print.h | 143 + app/ProgressCounterDialog.h | 70 + app/RecompressOptionsDialog.cpp | 95 + app/RecompressOptionsDialog.h | 43 + app/Registry.cpp | 723 +++++ app/Registry.h | 85 + app/RenameEntryDialog.cpp | 135 + app/RenameEntryDialog.h | 70 + app/RenameVolumeDialog.cpp | 180 ++ app/RenameVolumeDialog.h | 50 + app/Squeeze.cpp | 411 +++ app/Squeeze.h | 15 + app/StdAfx.cpp | 13 + app/StdAfx.h | 45 + app/SubVolumeDialog.cpp | 64 + app/SubVolumeDialog.h | 47 + app/Tools.cpp | 2495 +++++++++++++++ app/TwoImgPropsDialog.cpp | 178 ++ app/TwoImgPropsDialog.h | 49 + app/UseSelectionDialog.cpp | 85 + app/UseSelectionDialog.h | 62 + app/ViewFilesDialog.cpp | 1413 +++++++++ app/ViewFilesDialog.h | 156 + app/VolumeCopyDialog.cpp | 880 ++++++ app/VolumeCopyDialog.h | 80 + app/app.dsp | 641 ++++ app/app.dsw | 29 + app/app.vcproj | 1211 ++++++++ app/resource.h | 574 ++++ app/resource.hm | 19 + diskimg/ASPI.cpp | 626 ++++ diskimg/ASPI.h | 199 ++ diskimg/CFFA.cpp | 597 ++++ diskimg/CPM.cpp | 772 +++++ diskimg/CP_WNASPI32.H | 325 ++ diskimg/CP_ntddscsi.h | 184 ++ diskimg/Container.cpp | 122 + diskimg/DDD.cpp | 638 ++++ diskimg/DIUtil.cpp | 379 +++ diskimg/DOS33.cpp | 3447 ++++++++++++++++++++ diskimg/DOSImage.cpp | 2905 +++++++++++++++++ diskimg/DiskFS.cpp | 558 ++++ diskimg/DiskImg.cpp | 3515 +++++++++++++++++++++ diskimg/DiskImg.h | 1659 ++++++++++ diskimg/DiskImgDetail.h | 2964 ++++++++++++++++++ diskimg/DiskImgPriv.h | 364 +++ diskimg/FAT.cpp | 523 ++++ diskimg/FDI.cpp | 1541 +++++++++ diskimg/FocusDrive.cpp | 362 +++ diskimg/GenericFD.cpp | 876 ++++++ diskimg/GenericFD.h | 317 ++ diskimg/Global.cpp | 189 ++ diskimg/HFS.cpp | 2348 ++++++++++++++ diskimg/ImageWrapper.cpp | 2531 +++++++++++++++ diskimg/MacPart.cpp | 479 +++ diskimg/Makefile | 49 + diskimg/MicroDrive.cpp | 404 +++ diskimg/Nibble.cpp | 1046 +++++++ diskimg/Nibble35.cpp | 557 ++++ diskimg/OuterWrapper.cpp | 1535 +++++++++ diskimg/OzDOS.cpp | 325 ++ diskimg/Pascal.cpp | 1907 ++++++++++++ diskimg/ProDOS.cpp | 5185 +++++++++++++++++++++++++++++++ diskimg/RDOS.cpp | 739 +++++ diskimg/SCSIDefs.h | 308 ++ diskimg/SPTI.cpp | 139 + diskimg/SPTI.h | 40 + diskimg/StdAfx.cpp | 13 + diskimg/StdAfx.h | 69 + diskimg/TwoImg.cpp | 576 ++++ diskimg/TwoImg.h | 136 + diskimg/UNIDOS.cpp | 365 +++ diskimg/VolumeUsage.cpp | 278 ++ diskimg/Win32BlockIO.cpp | 2164 +++++++++++++ diskimg/Win32BlockIO.h | 405 +++ diskimg/Win32Extra.h | 60 + diskimg/diskimg.dsp | 294 ++ diskimg/diskimg.vcproj | 560 ++++ diskimg/libhfs/COPYRIGHT | 21 + diskimg/libhfs/Makefile | 149 + diskimg/libhfs/README | 5 + diskimg/libhfs/apple.h | 272 ++ diskimg/libhfs/block.c | 807 +++++ diskimg/libhfs/block.h | 40 + diskimg/libhfs/btree.c | 700 +++++ diskimg/libhfs/btree.h | 33 + diskimg/libhfs/config.h | 60 + diskimg/libhfs/data.c | 485 +++ diskimg/libhfs/data.h | 58 + diskimg/libhfs/file.c | 520 ++++ diskimg/libhfs/file.h | 45 + diskimg/libhfs/hfs.c | 1991 ++++++++++++ diskimg/libhfs/hfs.h | 201 ++ diskimg/libhfs/libhfs.dsp | 200 ++ diskimg/libhfs/libhfs.h | 227 ++ diskimg/libhfs/libhfs.vcproj | 402 +++ diskimg/libhfs/low.c | 470 +++ diskimg/libhfs/low.h | 44 + diskimg/libhfs/medium.c | 318 ++ diskimg/libhfs/medium.h | 42 + diskimg/libhfs/memcmp.c | 50 + diskimg/libhfs/node.c | 473 +++ diskimg/libhfs/node.h | 34 + diskimg/libhfs/os.c | 182 ++ diskimg/libhfs/os.h | 34 + diskimg/libhfs/record.c | 557 ++++ diskimg/libhfs/record.h | 47 + diskimg/libhfs/version.c | 29 + diskimg/libhfs/version.h | 26 + diskimg/libhfs/volume.c | 1250 ++++++++ diskimg/libhfs/volume.h | 67 + linux/Convert.cpp | 491 +++ linux/GetFile.cpp | 220 ++ linux/MDC.cpp | 886 ++++++ linux/MakeDisk.cpp | 388 +++ linux/Makefile | 81 + linux/PackDDD.cpp | 766 +++++ linux/SSTAsm.cpp | 325 ++ linux/StringArray.h | 101 + mdc/AboutDlg.cpp | 56 + mdc/AboutDlg.h | 49 + mdc/ChooseFilesDlg.cpp | 29 + mdc/ChooseFilesDlg.h | 39 + mdc/Main.cpp | 1044 +++++++ mdc/Main.h | 72 + mdc/ProgressDlg.cpp | 78 + mdc/ProgressDlg.h | 59 + mdc/StdAfx.cpp | 13 + mdc/StdAfx.h | 49 + mdc/mdc.ICO | Bin 0 -> 4846 bytes mdc/mdc.clw | 82 + mdc/mdc.cpp | 77 + mdc/mdc.dsp | 174 ++ mdc/mdc.h | 43 + mdc/mdc.rc | 207 ++ mdc/mdc.vcproj | 315 ++ mdc/resource.h | 32 + prebuilt/NufxLib.h | 845 +++++ prebuilt/nufxlib2.dll | Bin 0 -> 110592 bytes prebuilt/nufxlib2.lib | Bin 0 -> 13282 bytes prebuilt/nufxlib2D.dll | Bin 0 -> 331829 bytes prebuilt/nufxlib2D.lib | Bin 0 -> 13344 bytes prebuilt/zconf.h | 332 ++ prebuilt/zdll.lib | Bin 0 -> 10590 bytes prebuilt/zlib.h | 1357 ++++++++ prebuilt/zlib1.dll | Bin 0 -> 59904 bytes reformat/AWGS.cpp | 546 ++++ reformat/AWGS.h | 92 + reformat/AppleWorks.cpp | 1107 +++++++ reformat/AppleWorks.h | 245 ++ reformat/Asm.cpp | 2383 ++++++++++++++ reformat/Asm.h | 292 ++ reformat/BASIC.cpp | 525 ++++ reformat/BASIC.h | 46 + reformat/CPMFiles.cpp | 145 + reformat/CPMFiles.h | 28 + reformat/Directory.cpp | 144 + reformat/Directory.h | 32 + reformat/Disasm.cpp | 1463 +++++++++ reformat/Disasm.h | 453 +++ reformat/DisasmTable.cpp | 784 +++++ reformat/DoubleHiRes.cpp | 538 ++++ reformat/DoubleHiRes.h | 54 + reformat/DreamGrafix.cpp | 370 +++ reformat/HiRes.cpp | 453 +++ reformat/HiRes.h | 44 + reformat/MacPaint.cpp | 183 ++ reformat/MacPaint.h | 41 + reformat/NiftyList.cpp | 378 +++ reformat/PascalFiles.cpp | 366 +++ reformat/PascalFiles.h | 103 + reformat/PrintShop.cpp | 215 ++ reformat/PrintShop.h | 35 + reformat/Reformat.cpp | 586 ++++ reformat/Reformat.h | 516 +++ reformat/ReformatBase.cpp | 1084 +++++++ reformat/ReformatBase.h | 387 +++ reformat/ResourceFork.cpp | 262 ++ reformat/ResourceFork.h | 39 + reformat/Simple.cpp | 133 + reformat/Simple.h | 56 + reformat/StdAfx.cpp | 11 + reformat/StdAfx.h | 32 + reformat/SuperHiRes.cpp | 1225 ++++++++ reformat/SuperHiRes.h | 310 ++ reformat/Teach.cpp | 420 +++ reformat/Teach.h | 211 ++ reformat/Text8.cpp | 174 ++ reformat/Text8.h | 33 + reformat/reformat.dsp | 266 ++ reformat/reformat.vcproj | 607 ++++ util/CancelDialog.h | 48 + util/FaddenStd.h | 20 + util/ImageDataObject.cpp | 138 + util/ImageDataObject.h | 137 + util/Modeless.h | 116 + util/MyBitmapButton.cpp | 100 + util/MyBitmapButton.h | 51 + util/MyDIBitmap.cpp | 1174 +++++++ util/MyDIBitmap.h | 176 ++ util/MyDebug.h | 77 + util/MyEdit.cpp | 76 + util/MyEdit.h | 55 + util/MySpinCtrl.cpp | 191 ++ util/MySpinCtrl.h | 50 + util/PathName.cpp | 668 ++++ util/PathName.h | 126 + util/Pidl.cpp | 350 +++ util/Pidl.h | 35 + util/ProgressCancelDialog.h | 83 + util/SelectFilesDialog.cpp | 589 ++++ util/SelectFilesDialog.h | 129 + util/ShellTree.cpp | 1565 ++++++++++ util/ShellTree.h | 84 + util/SoundFile.cpp | 228 ++ util/SoundFile.h | 98 + util/StdAfx.cpp | 11 + util/StdAfx.h | 37 + util/Util.cpp | 919 ++++++ util/Util.h | 133 + util/UtilLib.h | 31 + util/util.dsp | 213 ++ util/util.vcproj | 387 +++ 337 files changed, 140029 insertions(+) create mode 100644 CP.dsw create mode 100644 CP.sln create mode 100644 DIST/License.txt create mode 100644 DIST/NList.Data.TXT create mode 100644 DIST/ReadMe.txt create mode 100644 DIST/with-mdc.deploy create mode 100644 LICENSE.txt create mode 100644 ReadMe.htm create mode 100644 app/ACUArchive.cpp create mode 100644 app/ACUArchive.h create mode 100644 app/AboutDialog.cpp create mode 100644 app/AboutDialog.h create mode 100644 app/ActionProgressDialog.cpp create mode 100644 app/ActionProgressDialog.h create mode 100644 app/Actions.cpp create mode 100644 app/AddClashDialog.cpp create mode 100644 app/AddClashDialog.h create mode 100644 app/AddFilesDialog.cpp create mode 100644 app/AddFilesDialog.h create mode 100644 app/ArchiveInfoDialog.cpp create mode 100644 app/ArchiveInfoDialog.h create mode 100644 app/BNYArchive.cpp create mode 100644 app/BNYArchive.h create mode 100644 app/BasicImport.cpp create mode 100644 app/BasicImport.h create mode 100644 app/CassImpTargetDialog.cpp create mode 100644 app/CassImpTargetDialog.h create mode 100644 app/CassetteDialog.cpp create mode 100644 app/CassetteDialog.h create mode 100644 app/ChooseAddTargetDialog.cpp create mode 100644 app/ChooseAddTargetDialog.h create mode 100644 app/ChooseDirDialog.cpp create mode 100644 app/ChooseDirDialog.h create mode 100644 app/CiderPress.rc create mode 100644 app/Clipboard.cpp create mode 100644 app/ConfirmOverwriteDialog.cpp create mode 100644 app/ConfirmOverwriteDialog.h create mode 100644 app/ContentList.cpp create mode 100644 app/ContentList.h create mode 100644 app/ConvDiskOptionsDialog.cpp create mode 100644 app/ConvDiskOptionsDialog.h create mode 100644 app/ConvFileOptionsDialog.cpp create mode 100644 app/ConvFileOptionsDialog.h create mode 100644 app/CreateImageDialog.cpp create mode 100644 app/CreateImageDialog.h create mode 100644 app/CreateSubdirDialog.cpp create mode 100644 app/CreateSubdirDialog.h create mode 100644 app/DEFileDialog.cpp create mode 100644 app/DEFileDialog.h create mode 100644 app/DiskArchive.cpp create mode 100644 app/DiskArchive.h create mode 100644 app/DiskConvertDialog.cpp create mode 100644 app/DiskConvertDialog.h create mode 100644 app/DiskEditDialog.cpp create mode 100644 app/DiskEditDialog.h create mode 100644 app/DiskEditOpenDialog.cpp create mode 100644 app/DiskEditOpenDialog.h create mode 100644 app/DiskFSTree.cpp create mode 100644 app/DiskFSTree.h create mode 100644 app/DoneOpenDialog.h create mode 100644 app/EOLScanDialog.cpp create mode 100644 app/EOLScanDialog.h create mode 100644 app/EditAssocDialog.cpp create mode 100644 app/EditAssocDialog.h create mode 100644 app/EditCommentDialog.cpp create mode 100644 app/EditCommentDialog.h create mode 100644 app/EditPropsDialog.cpp create mode 100644 app/EditPropsDialog.h create mode 100644 app/EnterRegDialog.cpp create mode 100644 app/EnterRegDialog.h create mode 100644 app/ExtractOptionsDialog.cpp create mode 100644 app/ExtractOptionsDialog.h create mode 100644 app/FileNameConv.cpp create mode 100644 app/FileNameConv.h create mode 100644 app/GenericArchive.cpp create mode 100644 app/GenericArchive.h create mode 100644 app/Graphics/ChooseFolder.bmp create mode 100644 app/Graphics/CiderPress.ico create mode 100644 app/Graphics/FileViewer.ico create mode 100644 app/Graphics/NewFolder.bmp create mode 100644 app/Graphics/binary2.ico create mode 100644 app/Graphics/diskimage.ico create mode 100644 app/Graphics/fslogo.bmp create mode 100644 app/Graphics/hdrbar.bmp create mode 100644 app/Graphics/list-pics.bmp create mode 100644 app/Graphics/nufx.ico create mode 100644 app/Graphics/toolbar1.bmp create mode 100644 app/Graphics/tree_pics.bmp create mode 100644 app/Graphics/vol_pics.bmp create mode 100644 app/Help/CIDERPRESS.HLP create mode 100644 app/Help/CiderPress.cnt create mode 100644 app/Help/CiderPress.hmp create mode 100644 app/HelpTopics.h create mode 100644 app/ImageFormatDialog.cpp create mode 100644 app/ImageFormatDialog.h create mode 100644 app/Main.cpp create mode 100644 app/Main.h create mode 100644 app/MyApp.cpp create mode 100644 app/MyApp.h create mode 100644 app/NewDiskSize.cpp create mode 100644 app/NewDiskSize.h create mode 100644 app/NewFolderDialog.cpp create mode 100644 app/NewFolderDialog.h create mode 100644 app/NufxArchive.cpp create mode 100644 app/NufxArchive.h create mode 100644 app/OpenVolumeDialog.cpp create mode 100644 app/OpenVolumeDialog.h create mode 100644 app/PasteSpecialDialog.cpp create mode 100644 app/PasteSpecialDialog.h create mode 100644 app/Preferences.cpp create mode 100644 app/Preferences.h create mode 100644 app/PrefsDialog.cpp create mode 100644 app/PrefsDialog.h create mode 100644 app/Print.cpp create mode 100644 app/Print.h create mode 100644 app/ProgressCounterDialog.h create mode 100644 app/RecompressOptionsDialog.cpp create mode 100644 app/RecompressOptionsDialog.h create mode 100644 app/Registry.cpp create mode 100644 app/Registry.h create mode 100644 app/RenameEntryDialog.cpp create mode 100644 app/RenameEntryDialog.h create mode 100644 app/RenameVolumeDialog.cpp create mode 100644 app/RenameVolumeDialog.h create mode 100644 app/Squeeze.cpp create mode 100644 app/Squeeze.h create mode 100644 app/StdAfx.cpp create mode 100644 app/StdAfx.h create mode 100644 app/SubVolumeDialog.cpp create mode 100644 app/SubVolumeDialog.h create mode 100644 app/Tools.cpp create mode 100644 app/TwoImgPropsDialog.cpp create mode 100644 app/TwoImgPropsDialog.h create mode 100644 app/UseSelectionDialog.cpp create mode 100644 app/UseSelectionDialog.h create mode 100644 app/ViewFilesDialog.cpp create mode 100644 app/ViewFilesDialog.h create mode 100644 app/VolumeCopyDialog.cpp create mode 100644 app/VolumeCopyDialog.h create mode 100644 app/app.dsp create mode 100644 app/app.dsw create mode 100644 app/app.vcproj create mode 100644 app/resource.h create mode 100644 app/resource.hm create mode 100644 diskimg/ASPI.cpp create mode 100644 diskimg/ASPI.h create mode 100644 diskimg/CFFA.cpp create mode 100644 diskimg/CPM.cpp create mode 100644 diskimg/CP_WNASPI32.H create mode 100644 diskimg/CP_ntddscsi.h create mode 100644 diskimg/Container.cpp create mode 100644 diskimg/DDD.cpp create mode 100644 diskimg/DIUtil.cpp create mode 100644 diskimg/DOS33.cpp create mode 100644 diskimg/DOSImage.cpp create mode 100644 diskimg/DiskFS.cpp create mode 100644 diskimg/DiskImg.cpp create mode 100644 diskimg/DiskImg.h create mode 100644 diskimg/DiskImgDetail.h create mode 100644 diskimg/DiskImgPriv.h create mode 100644 diskimg/FAT.cpp create mode 100644 diskimg/FDI.cpp create mode 100644 diskimg/FocusDrive.cpp create mode 100644 diskimg/GenericFD.cpp create mode 100644 diskimg/GenericFD.h create mode 100644 diskimg/Global.cpp create mode 100644 diskimg/HFS.cpp create mode 100644 diskimg/ImageWrapper.cpp create mode 100644 diskimg/MacPart.cpp create mode 100644 diskimg/Makefile create mode 100644 diskimg/MicroDrive.cpp create mode 100644 diskimg/Nibble.cpp create mode 100644 diskimg/Nibble35.cpp create mode 100644 diskimg/OuterWrapper.cpp create mode 100644 diskimg/OzDOS.cpp create mode 100644 diskimg/Pascal.cpp create mode 100644 diskimg/ProDOS.cpp create mode 100644 diskimg/RDOS.cpp create mode 100644 diskimg/SCSIDefs.h create mode 100644 diskimg/SPTI.cpp create mode 100644 diskimg/SPTI.h create mode 100644 diskimg/StdAfx.cpp create mode 100644 diskimg/StdAfx.h create mode 100644 diskimg/TwoImg.cpp create mode 100644 diskimg/TwoImg.h create mode 100644 diskimg/UNIDOS.cpp create mode 100644 diskimg/VolumeUsage.cpp create mode 100644 diskimg/Win32BlockIO.cpp create mode 100644 diskimg/Win32BlockIO.h create mode 100644 diskimg/Win32Extra.h create mode 100644 diskimg/diskimg.dsp create mode 100644 diskimg/diskimg.vcproj create mode 100644 diskimg/libhfs/COPYRIGHT create mode 100644 diskimg/libhfs/Makefile create mode 100644 diskimg/libhfs/README create mode 100644 diskimg/libhfs/apple.h create mode 100644 diskimg/libhfs/block.c create mode 100644 diskimg/libhfs/block.h create mode 100644 diskimg/libhfs/btree.c create mode 100644 diskimg/libhfs/btree.h create mode 100644 diskimg/libhfs/config.h create mode 100644 diskimg/libhfs/data.c create mode 100644 diskimg/libhfs/data.h create mode 100644 diskimg/libhfs/file.c create mode 100644 diskimg/libhfs/file.h create mode 100644 diskimg/libhfs/hfs.c create mode 100644 diskimg/libhfs/hfs.h create mode 100644 diskimg/libhfs/libhfs.dsp create mode 100644 diskimg/libhfs/libhfs.h create mode 100644 diskimg/libhfs/libhfs.vcproj create mode 100644 diskimg/libhfs/low.c create mode 100644 diskimg/libhfs/low.h create mode 100644 diskimg/libhfs/medium.c create mode 100644 diskimg/libhfs/medium.h create mode 100644 diskimg/libhfs/memcmp.c create mode 100644 diskimg/libhfs/node.c create mode 100644 diskimg/libhfs/node.h create mode 100644 diskimg/libhfs/os.c create mode 100644 diskimg/libhfs/os.h create mode 100644 diskimg/libhfs/record.c create mode 100644 diskimg/libhfs/record.h create mode 100644 diskimg/libhfs/version.c create mode 100644 diskimg/libhfs/version.h create mode 100644 diskimg/libhfs/volume.c create mode 100644 diskimg/libhfs/volume.h create mode 100644 linux/Convert.cpp create mode 100644 linux/GetFile.cpp create mode 100644 linux/MDC.cpp create mode 100644 linux/MakeDisk.cpp create mode 100644 linux/Makefile create mode 100644 linux/PackDDD.cpp create mode 100644 linux/SSTAsm.cpp create mode 100644 linux/StringArray.h create mode 100644 mdc/AboutDlg.cpp create mode 100644 mdc/AboutDlg.h create mode 100644 mdc/ChooseFilesDlg.cpp create mode 100644 mdc/ChooseFilesDlg.h create mode 100644 mdc/Main.cpp create mode 100644 mdc/Main.h create mode 100644 mdc/ProgressDlg.cpp create mode 100644 mdc/ProgressDlg.h create mode 100644 mdc/StdAfx.cpp create mode 100644 mdc/StdAfx.h create mode 100644 mdc/mdc.ICO create mode 100644 mdc/mdc.clw create mode 100644 mdc/mdc.cpp create mode 100644 mdc/mdc.dsp create mode 100644 mdc/mdc.h create mode 100644 mdc/mdc.rc create mode 100644 mdc/mdc.vcproj create mode 100644 mdc/resource.h create mode 100644 prebuilt/NufxLib.h create mode 100644 prebuilt/nufxlib2.dll create mode 100644 prebuilt/nufxlib2.lib create mode 100644 prebuilt/nufxlib2D.dll create mode 100644 prebuilt/nufxlib2D.lib create mode 100644 prebuilt/zconf.h create mode 100644 prebuilt/zdll.lib create mode 100644 prebuilt/zlib.h create mode 100644 prebuilt/zlib1.dll create mode 100644 reformat/AWGS.cpp create mode 100644 reformat/AWGS.h create mode 100644 reformat/AppleWorks.cpp create mode 100644 reformat/AppleWorks.h create mode 100644 reformat/Asm.cpp create mode 100644 reformat/Asm.h create mode 100644 reformat/BASIC.cpp create mode 100644 reformat/BASIC.h create mode 100644 reformat/CPMFiles.cpp create mode 100644 reformat/CPMFiles.h create mode 100644 reformat/Directory.cpp create mode 100644 reformat/Directory.h create mode 100644 reformat/Disasm.cpp create mode 100644 reformat/Disasm.h create mode 100644 reformat/DisasmTable.cpp create mode 100644 reformat/DoubleHiRes.cpp create mode 100644 reformat/DoubleHiRes.h create mode 100644 reformat/DreamGrafix.cpp create mode 100644 reformat/HiRes.cpp create mode 100644 reformat/HiRes.h create mode 100644 reformat/MacPaint.cpp create mode 100644 reformat/MacPaint.h create mode 100644 reformat/NiftyList.cpp create mode 100644 reformat/PascalFiles.cpp create mode 100644 reformat/PascalFiles.h create mode 100644 reformat/PrintShop.cpp create mode 100644 reformat/PrintShop.h create mode 100644 reformat/Reformat.cpp create mode 100644 reformat/Reformat.h create mode 100644 reformat/ReformatBase.cpp create mode 100644 reformat/ReformatBase.h create mode 100644 reformat/ResourceFork.cpp create mode 100644 reformat/ResourceFork.h create mode 100644 reformat/Simple.cpp create mode 100644 reformat/Simple.h create mode 100644 reformat/StdAfx.cpp create mode 100644 reformat/StdAfx.h create mode 100644 reformat/SuperHiRes.cpp create mode 100644 reformat/SuperHiRes.h create mode 100644 reformat/Teach.cpp create mode 100644 reformat/Teach.h create mode 100644 reformat/Text8.cpp create mode 100644 reformat/Text8.h create mode 100644 reformat/reformat.dsp create mode 100644 reformat/reformat.vcproj create mode 100644 util/CancelDialog.h create mode 100644 util/FaddenStd.h create mode 100644 util/ImageDataObject.cpp create mode 100644 util/ImageDataObject.h create mode 100644 util/Modeless.h create mode 100644 util/MyBitmapButton.cpp create mode 100644 util/MyBitmapButton.h create mode 100644 util/MyDIBitmap.cpp create mode 100644 util/MyDIBitmap.h create mode 100644 util/MyDebug.h create mode 100644 util/MyEdit.cpp create mode 100644 util/MyEdit.h create mode 100644 util/MySpinCtrl.cpp create mode 100644 util/MySpinCtrl.h create mode 100644 util/PathName.cpp create mode 100644 util/PathName.h create mode 100644 util/Pidl.cpp create mode 100644 util/Pidl.h create mode 100644 util/ProgressCancelDialog.h create mode 100644 util/SelectFilesDialog.cpp create mode 100644 util/SelectFilesDialog.h create mode 100644 util/ShellTree.cpp create mode 100644 util/ShellTree.h create mode 100644 util/SoundFile.cpp create mode 100644 util/SoundFile.h create mode 100644 util/StdAfx.cpp create mode 100644 util/StdAfx.h create mode 100644 util/Util.cpp create mode 100644 util/Util.h create mode 100644 util/UtilLib.h create mode 100644 util/util.dsp create mode 100644 util/util.vcproj diff --git a/CP.dsw b/CP.dsw new file mode 100644 index 0000000..40a2beb --- /dev/null +++ b/CP.dsw @@ -0,0 +1,107 @@ +Microsoft Developer Studio Workspace File, Format Version 6.00 +# WARNING: DO NOT EDIT OR DELETE THIS WORKSPACE FILE! + +############################################################################### + +Project: "app"=.\app\app.dsp - Package Owner=<4> + +Package=<5> +{{{ +}}} + +Package=<4> +{{{ + Begin Project Dependency + Project_Dep_Name diskimg + End Project Dependency + Begin Project Dependency + Project_Dep_Name util + End Project Dependency + Begin Project Dependency + Project_Dep_Name reformat + End Project Dependency +}}} + +############################################################################### + +Project: "diskimg"=.\diskimg\diskimg.dsp - Package Owner=<4> + +Package=<5> +{{{ +}}} + +Package=<4> +{{{ + Begin Project Dependency + Project_Dep_Name libhfs + End Project Dependency +}}} + +############################################################################### + +Project: "libhfs"=.\diskimg\libhfs\libhfs.dsp - Package Owner=<4> + +Package=<5> +{{{ +}}} + +Package=<4> +{{{ +}}} + +############################################################################### + +Project: "mdc"=.\mdc\mdc.dsp - Package Owner=<4> + +Package=<5> +{{{ +}}} + +Package=<4> +{{{ + Begin Project Dependency + Project_Dep_Name diskimg + End Project Dependency + Begin Project Dependency + Project_Dep_Name util + End Project Dependency +}}} + +############################################################################### + +Project: "reformat"=.\reformat\reformat.dsp - Package Owner=<4> + +Package=<5> +{{{ +}}} + +Package=<4> +{{{ +}}} + +############################################################################### + +Project: "util"=.\util\util.dsp - Package Owner=<4> + +Package=<5> +{{{ +}}} + +Package=<4> +{{{ +}}} + +############################################################################### + +Global: + +Package=<5> +{{{ +}}} + +Package=<3> +{{{ +}}} + +############################################################################### + diff --git a/CP.sln b/CP.sln new file mode 100644 index 0000000..0107866 --- /dev/null +++ b/CP.sln @@ -0,0 +1,67 @@ +Microsoft Visual Studio Solution File, Format Version 8.00 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "app", "app\app.vcproj", "{B023611B-7086-46E1-847B-3B21C4732384}" + ProjectSection(ProjectDependencies) = postProject + {04BFAE2A-7AB3-4B63-B4AB-42FF1D6AD3C5} = {04BFAE2A-7AB3-4B63-B4AB-42FF1D6AD3C5} + {18BCF397-397E-460C-A1DC-3E26798966E4} = {18BCF397-397E-460C-A1DC-3E26798966E4} + {0CFE6FAD-0126-4E99-8625-C807D1D2AAF4} = {0CFE6FAD-0126-4E99-8625-C807D1D2AAF4} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "diskimg", "diskimg\diskimg.vcproj", "{0CFE6FAD-0126-4E99-8625-C807D1D2AAF4}" + ProjectSection(ProjectDependencies) = postProject + {0FA742E9-8C07-43DD-AFF8-CE31FAF70821} = {0FA742E9-8C07-43DD-AFF8-CE31FAF70821} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "mdc", "mdc\mdc.vcproj", "{7DF41D71-C8DC-48AA-B372-4613210310A4}" + ProjectSection(ProjectDependencies) = postProject + {04BFAE2A-7AB3-4B63-B4AB-42FF1D6AD3C5} = {04BFAE2A-7AB3-4B63-B4AB-42FF1D6AD3C5} + {0CFE6FAD-0126-4E99-8625-C807D1D2AAF4} = {0CFE6FAD-0126-4E99-8625-C807D1D2AAF4} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "util", "util\util.vcproj", "{04BFAE2A-7AB3-4B63-B4AB-42FF1D6AD3C5}" + ProjectSection(ProjectDependencies) = postProject + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libhfs", "diskimg\libhfs\libhfs.vcproj", "{0FA742E9-8C07-43DD-AFF8-CE31FAF70821}" + ProjectSection(ProjectDependencies) = postProject + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "reformat", "reformat\reformat.vcproj", "{18BCF397-397E-460C-A1DC-3E26798966E4}" + ProjectSection(ProjectDependencies) = postProject + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfiguration) = preSolution + Debug = Debug + Release = Release + EndGlobalSection + GlobalSection(ProjectConfiguration) = postSolution + {B023611B-7086-46E1-847B-3B21C4732384}.Debug.ActiveCfg = Debug|Win32 + {B023611B-7086-46E1-847B-3B21C4732384}.Debug.Build.0 = Debug|Win32 + {B023611B-7086-46E1-847B-3B21C4732384}.Release.ActiveCfg = Release|Win32 + {B023611B-7086-46E1-847B-3B21C4732384}.Release.Build.0 = Release|Win32 + {0CFE6FAD-0126-4E99-8625-C807D1D2AAF4}.Debug.ActiveCfg = Debug|Win32 + {0CFE6FAD-0126-4E99-8625-C807D1D2AAF4}.Debug.Build.0 = Debug|Win32 + {0CFE6FAD-0126-4E99-8625-C807D1D2AAF4}.Release.ActiveCfg = Release|Win32 + {0CFE6FAD-0126-4E99-8625-C807D1D2AAF4}.Release.Build.0 = Release|Win32 + {7DF41D71-C8DC-48AA-B372-4613210310A4}.Debug.ActiveCfg = Debug|Win32 + {7DF41D71-C8DC-48AA-B372-4613210310A4}.Debug.Build.0 = Debug|Win32 + {7DF41D71-C8DC-48AA-B372-4613210310A4}.Release.ActiveCfg = Release|Win32 + {7DF41D71-C8DC-48AA-B372-4613210310A4}.Release.Build.0 = Release|Win32 + {04BFAE2A-7AB3-4B63-B4AB-42FF1D6AD3C5}.Debug.ActiveCfg = Debug|Win32 + {04BFAE2A-7AB3-4B63-B4AB-42FF1D6AD3C5}.Debug.Build.0 = Debug|Win32 + {04BFAE2A-7AB3-4B63-B4AB-42FF1D6AD3C5}.Release.ActiveCfg = Release|Win32 + {04BFAE2A-7AB3-4B63-B4AB-42FF1D6AD3C5}.Release.Build.0 = Release|Win32 + {0FA742E9-8C07-43DD-AFF8-CE31FAF70821}.Debug.ActiveCfg = Debug|Win32 + {0FA742E9-8C07-43DD-AFF8-CE31FAF70821}.Debug.Build.0 = Debug|Win32 + {0FA742E9-8C07-43DD-AFF8-CE31FAF70821}.Release.ActiveCfg = Release|Win32 + {0FA742E9-8C07-43DD-AFF8-CE31FAF70821}.Release.Build.0 = Release|Win32 + {18BCF397-397E-460C-A1DC-3E26798966E4}.Debug.ActiveCfg = Debug|Win32 + {18BCF397-397E-460C-A1DC-3E26798966E4}.Debug.Build.0 = Debug|Win32 + {18BCF397-397E-460C-A1DC-3E26798966E4}.Release.ActiveCfg = Release|Win32 + {18BCF397-397E-460C-A1DC-3E26798966E4}.Release.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + EndGlobalSection + GlobalSection(ExtensibilityAddIns) = postSolution + EndGlobalSection +EndGlobal diff --git a/DIST/License.txt b/DIST/License.txt new file mode 100644 index 0000000..94c1b84 --- /dev/null +++ b/DIST/License.txt @@ -0,0 +1,37 @@ +End-User License Agreement for CiderPress +Copyright (C) 2007 FaddenSoft, LLC. All Rights Reserved. + +AGREEMENT. After reading this agreement carefully, if you ("Customer") do +not agree to all of the terms of this agreement, you may not use +CiderPress ("Software"). Your use of this Software indicates your +acceptance of this license agreement and warranty. All updates to the +Software shall be considered part of the Software and subject to the terms +of this Agreement. Changes to this Agreement may accompany updates to the +Software, in which case by installing such update Customer accepts the +terms of the Agreement as changed. The Agreement is not otherwise subject +to addition, amendment, modification, or exception unless in writing +signed by an officer of both Customer and FaddenSoft, LLC ("FaddenSoft"). + +1. LICENSE. This is free software, distributed under the terms of the +BSD License. See "LICENSE.txt" for details. + +2. LIMITED WARRANTY. THE SOFTWARE IS PROVIDED AS IS AND FADDENSOFT +DISCLAIMS ALL WARRANTIES RELATING TO THIS SOFTWARE, WHETHER EXPRESSED OR +IMPLIED, INCLUDING BUT NOT LIMITED TO ANY IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + +3. LIMITATION ON DAMAGES. NEITHER FADDENSOFT NOR ANYONE INVOLVED IN THE +CREATION, PRODUCTION, OR DELIVERY OF THIS SOFTWARE SHALL BE LIABLE FOR ANY +INDIRECT, CONSEQUENTIAL, OR INCIDENTAL DAMAGES ARISING OUT OF THE USE OR +INABILITY TO USE SUCH SOFTWARE EVEN IF FADDENSOFT HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES OR CLAIMS. IN NO EVENT SHALL FADDENSOFT'S +LIABILITY FOR ANY DAMAGES EXCEED THE PRICE PAID FOR THE LICENSE TO USE THE +SOFTWARE, REGARDLESS OF THE FORM OF CLAIM. THE PERSON USING THE SOFTWARE +BEARS ALL RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE. + +4. GOVERNING LAW AND GENERAL PROVISIONS. This Agreement will be governed +by the laws of the State of California, U.S.A. If any part of this +Agreement is found void and unenforceable, it will not affect the validity +of the balance of the Agreement, which shall remain valid and enforceable +according to its terms. This Agreement shall automatically terminate upon +failure by Customer to comply with its terms. diff --git a/DIST/NList.Data.TXT b/DIST/NList.Data.TXT new file mode 100644 index 0000000..b4efab5 --- /dev/null +++ b/DIST/NList.Data.TXT @@ -0,0 +1,2516 @@ +fff9 | NLIST Data File: Last mod 17-Oct-93 DAL (Loma Prieta + 4) +fffa | Based on Apple IIgs System Disk 6.0.1+UserTool#1,2 +fffb | Dave Lyons +fffc | dlyons@apple.com +0040 P8:ALLOC_INTERRUPT(2:IntNum/1,CodePtr) +0041 P8:DEALLOC_INTERRUPT(1:IntNum/1) +0042 P8:ATLK:AppleTalk(Async/1,Cmd/1,Result,...) +0043 P8:ATLK:SpecialOpenFork(4or84:pn,ioBuff,Ref/1,Mode/1) +0044 P8:ATLK:ByteRangeLock(4:Ref/1,Flag/1,Off/3,Len/3) +0065 P8:QUIT(4:Type/1,Path,zz/1,zz) +0080 P8:READ_BLOCK(3:Unit/1,Buff,BlkNum) +0081 P8:WRITE_BLOCK(3:Unit/1,Buff,BlkNum) +0082 P8:GET_TIME() +00C0 P8:CREATE(7:pn,acc/1,type/1,aux,stt/1,cD,cT) +00C1 P8:DESTROY(1:pn) +00C2 P8:RENAME(2:pn1,pn2) +00C3 P8:SetFileInfo(7:pn,a/1,t/1,aux,nul/3,mD,mT) +00C4 P8:GetFileInfo(10:pn,a/1,t/1,x,s/1,b,mDTcDT) +00C5 P8:ONLINE(2:UnitNum/1,Buff) +00C6 P8:SET_PREFIX(1:pn) +00C7 P8:GET_PREFIX(1:Buff) +00C8 P8:OPEN(3:pn,ioBuff,Ref/1) +00C9 P8:NEWLINE(3:Ref/1,Mask/1,Char/1) +00CA P8:READ(4:Ref/1,Where,reqCount,xfrCount) +00CB P8:WRITE(4:Ref/1,Where,reqCount,xfrCount) +00CC P8:CLOSE(1:Ref/1) +00CD P8:FLUSH(1:Ref/1) +00CE P8:SET_MARK(2:Ref/1,Position/3) +00CF P8:GET_MARK(2:Ref/1,Position/3) +00D0 P8:SET_EOF(2:Ref/1,Position/3) +00D1 P8:GET_EOF(2:Ref/1,Position/3) +00D2 P8:SET_BUF(2:Ref/1,ioBuff) +00D3 P8:GET_BUF(2:Ref/1,ioBuff) +* ProDOS 16 / GS/OS +0001 P16:CREATE(@Path,Acc,Typ,Aux/4,StT,CrD,CrT) +0002 P16:DESTROY(@Path) +0004 P16:CHANGE_PATH(@Path1,@Path2) +0005 P16:SET_FILE_INFO(@P,a,t,xt/4,z,cD,cT,mD,mT) +0006 P16:GET_FILE_INFO(@P,a,t,xt/4,s,cDT,mDT,b/4) +0008 P16:VOLUME(@DevN,@VolN,Blks/4,FreeBlks/4,fsID) +0009 P16:SET_PREFIX(Pfx#,@Prefix) +000A P16:GET_PREFIX(Pfx#,@Buff) +000B P16:CLEAR_BACKUP_BIT(@Path) +0010 P16:OPEN(Ref,@Path,xxx/4) +0011 P16:NEWLINE(Ref,Mask,Char) +0012 P16:READ(Ref,@Where,Count/4,xfCount/4) +0013 P16:WRITE(Ref,@Where,Count/4,xfCount/4) +0014 P16:CLOSE(Ref) +0015 P16:FLUSH(Ref) +0016 P16:SET_MARK(Ref,Pos/4) +0017 P16:GET_MARK(Ref,Pos/4) +0018 P16:SET_EOF(Ref,EOF/4) +0019 P16:GET_EOF(Ref,EOF/4) +001A P16:SET_LEVEL(Level) +001B P16:GET_LEVEL(Level) +001C P16:GET_DIR_ENTRY(Ref#,z,Bs,Dis,@Bf,dEnt/36) +0020 P16:GET_DEV_NUM(@DevName,Dev#) +0021 P16:GET_LAST_DEV(Dev#) +0022 P16:READ_BLOCK(Dev#,@Where,Blk#/4) +0023 P16:WRITE_BLOCK(Dev#,@Where,Blk#/4) +0024 P16:FORMAT(@DevName,@VolName,fsID) +0025 P16:ERASE_DISK(@DevName,@VolName,fsID) +0027 P16:GET_NAME(@Buff) +0028 P16:GET_BOOT_VOL(@Buff) +0029 P16:QUIT(@Path,Flags) +002A P16:GET_VERSION(Version) +002C P16:D_INFO(Dev#,@DevName) +0031 P16:ALLOC_INTERRUPT(Int#,@Code) +0032 P16:DEALLOCATE_INTERRUPT(Int#) +0101 Shell:Get_LInfo (...) +0102 Shell:Set_LInfo (...) +0103 Shell:Get_Lang(Lang) +0104 Shell:Set_Lang(Lang) +0105 Shell:Error(Error) +0106 Shell:Set_Variable(@VarName,Val/4) +0107 Shell:Version(Vers/4) +0108 Shell:Read_Indexed(@VarName,Val/4,Index) +0109 Shell:Init_Wildcard(@File,Flags) +010A Shell:Next_Wildcard(@NextFile) +010B Shell:Read_Variable(@VarName,Value/4) +010C Shell:ChangeVector(res,vec,@proc,@old) +010D Shell:Execute(Flag,@CmdStr) +010E Shell:FastFile(act,ind,flg,H,L/4,@n,...) +010F Shell:Direction(Dev,Direct) +0110 Shell:Redirect(Dev,ApndFlg,@File) +0113 Shell:Stop(StopFlag) +0114 Shell:ExpandDevices(@name) +0115 Shell:UnsetVariable(@var) +0116 Shell:Export(@var,flags) +0117 Shell:PopVariables() +0118 Shell:PushVariables() +0119 Shell:SetStopFlag(stopFlag) +011A Shell:ConsoleOut(Char) +011B Shell:SetIODevices(OutT,@out,ErrT,@err,InT,@in) +011C Shell:GetIODevices(OutT,@out,ErrT,@err,InT,@in) +011D Shell:GetCommand(idx,restart,rsv,cmd,name/16) +2001 GS/OS:Create(1-7:@P,Acc,Typ,Aux/4,Stg,EOF/4,rEOF/4) +2002 GS/OS:Destroy(1:@P) +2003 GS/OS:OSShutdown(1:Flags) +2004 GS/OS:ChangePath(2-3:@P1,@P2,TrustMeFlag) +2005 GS/OS:SetFileInfo(2-12:@P,A,T,X/4,,c/8,m/8,@Opt,,,,) +2006 GS/OS:GetFileInfo(2-12:@P,A,T,X/4,S,c/8,m/8,@Opt,EOF/4,B/4,rEOF/4,rB/4) +2007 GS/OS:JudgeName(3-6:fileSysID,Descr,@Rules,MaxLen,@Path,Result) +2008 GS/OS:Volume(2-8:@DevN,@vnOut,blks/4,free/4,fSys,BlkSz,char,devID) +2009 GS/OS:SetPrefix(1-2:pfxNum,@Pfx) +200A GS/OS:GetPrefix(2:pfxNum,@Pfx) +200B GS/OS:ClearBackup(1:@P) +200C GS/OS:SetSysPrefs(1:prefs) +200D GS/OS:Null(0:) +200E GS/OS:ExpandPath(2-3:@InPath,@OutPath,UpcaseFlg) +200F GS/OS:GetSysPrefs(1:prefs) +2010 GS/OS:Open(2-15:ref,@P,Acc,fork,gotAcc,+GET_FILE_INFO) +2011 GS/OS:NewLine(4:ref,ANDmask,NumChars,@NLtable) +2012 GS/OS:Read(4-5:ref,@buff,count/4,xfer/4,cacheFlg) +2013 GS/OS:Write(4-5:ref,@buff,count/4,xfer/4,cacheFlg) +2014 GS/OS:Close(1:ref) +2015 GS/OS:Flush(1-2:ref,flags) +2016 GS/OS:SetMark(3:ref,base,displ/4) +2017 GS/OS:GetMark(2:ref,pos/4) +2018 GS/OS:SetEOF(3:ref,base,displ/4) +2019 GS/OS:GetEOF(2:ref,eof/4) +201A GS/OS:SetLevel(1-2:level,levelMode) +201B GS/OS:GetLevel(1-2:level,levelMode) +201C GS/OS:GetDirEntry(5-17:rf,fl,bs,ds,@n,n,T,EOF/4,b/4,c/8,m/8,A,X/4,FS,@o,resEOF/4,resBk/4) +201D GS/OS:BeginSession(0:) +201E GS/OS:EndSession(0:) +201F GS/OS:SessionStatus(1:status) +2020 GS/OS:GetDevNumber(2:@DevN,devnum) +2024 GS/OS:Format(1-6:@DevN,@VolN,gotFS,wantFS,flags,realVolName) +2025 GS/OS:EraseDisk(1-6:@DevN,@VolN,gotFS,wantFS,flags,realVolName) +2026 GS/OS:ResetCache(0:) +2027 GS/OS:GetName(1:@n) +2028 GS/OS:GetBootvol(1:@n) +2029 GS/OS:Quit(0-2:@P,flags) +202A GS/OS:GetVersion(1:version) +202B GS/OS:GetFSTInfo(2-7:n,fs,@n,ver,attr,bSz,mxV/4,mxF/4) +202C GS/OS:DInfo(2-10:n,@n,chr,B/4,sl,unit,ver,dTyp,@hd,@nx) +202D GS/OS:DStatus(5:n,statusReq,@statList,count/4,xfer/4) +202E GS/OS:DControl(5:n,code,@ctlList,count/4,xfer/4) +202F GS/OS:DRead(6:n,@bf,count/4,blk/4,blkSz,xfer/4) +2030 GS/OS:DWrite(6:n,@bf,count/4,blk/4,blkSz,xfer/4) +2031 GS/OS:BindInt(3:IntNum,VecRefNum,@handler) +2032 GS/OS:UnbindInt(1:IntNum) +2033 GS/OS:FSTSpecific(2+...) +2034 GS/OS:AddNotifyProc(1:@proc) +2035 GS/OS:DelNotifyProc(1:@proc) +2036 GS/OS:DRename(2:n,@newName) +2037 GS/OS:GetStdRefNum(2:pfxNum,refNum) +2038 GS/OS:GetRefNum(2-6:@path,ref,acc,res,case,disp) +2039 GS/OS:GetRefInfo(2-5:ref,acc,@path,resNum,level) +203A GS/OS:SetStdRefNum(2:pfxNum,refNum) +* System tools +0000 === System Tools === +0001 === tool locator === +0101 TLBootInit() +0201 TLStartUp() +0301 TLShutDown() +0401 TLVersion():Vers +0501 TLReset() +0601 TLStatus():ActFlg +0901 GetTSPtr(SysFlg,TS#):@FPT +0A01 SetTSPtr(SysFlg,TS#,@FPT) +0B01 GetFuncPtr(SysFlg,Func):@Func +0C01 GetWAP(SysFlg,TS#):@WAP +0D01 SetWAP(SysFlg,TS#,@WAP) +0E01 LoadTools(@ToolTable) +0F01 LoadOneTool(TS#,MinVers) +1001 UnloadOneTool(TS#) +1101 TLMountVolume(X,Y,@L1,@L2,@B1,@B2):Btn# +1201 TLTextMountVolume(@L1,@L2,@B1,@B2):Btn# +1301 SaveTextState():StateH +1401 RestoreTextState(StateH) +1501 MessageCenter(Action,Type,MsgH) +1601 SetDefaultTPT() +1701 MessageByName(CreateF,@inpRec):Created,Type +1801 StartUpTools(MemID,ssDesc,ssRef/4):ssRef/4 +1901 ShutDownTools(ssDesc,ssRef/4) +1A01 GetMsgHandle(Flags,MsgRef/4):H +1B01 AcceptRequests(@NameStr,UserID,@ReqProc) +1C01 SendRequest(ReqCode,How,Target/4,@In,@Out) +0002 === memory manager === +0102 MMBootInit() +0202 MMStartUp():MemID +0302 MMShutDown(MemID) +0402 MMVersion():Vers +0502 MMReset() +0602 MMStatus():ActFlg +0902 NewHandle(Size/4,MemID,Attr,@loc):H +0A02 ReAllocHandle(Size/4,MemID,Attr,@loc,H) +0B02 RestoreHandle(H) +0C02 AddToOOMQueue(@header) +0D02 RemoveFromOOMQueue(@header) +1002 DisposeHandle(H) +1102 DisposeAll(MemID) +1202 PurgeHandle(H) +1302 PurgeAll(MemID) +1802 GetHandleSize(H):Size/4 +1902 SetHandleSize(Size/4,H) +1A02 FindHandle(@byte):H +1B02 FreeMem():FreeBytes/4 +1C02 MaxBlock():Size/4 +1D02 TotalMem():Size/4 +1E02 CheckHandle(H) +1F02 CompactMem() +2002 HLock(H) +2102 HLockAll(MemID) +2202 HUnlock(H) +2302 HUnlockAll(MemID) +2402 SetPurge(PrgLvl,H) +2502 SetPurgeAll(PrgLvl,MemID) +2802 PtrToHand(@Src,DestH,Count/4) +2902 HandToPtr(SrcH,@Dest,Count/4) +2A02 HandToHand(SrcH,DestH,Count/4) +2B02 BlockMove(@Source,@Dest,Count/4) +2F02 RealFreeMem():Size/4 +3002 SetHandleID(newMemID,theH):oldMemID +0003 === misc tools === +0103 MTBootInit() +0203 MTStartUp() +0303 MTShutDown() +0403 MTVersion():Vers +0503 MTReset() +0603 MTStatus():ActFlg +0903 WriteBRam(@Buff) +0A03 ReadBRam(@Buff) +0B03 WriteBParam(Data,Parm#) +0C03 ReadBParam(Parm#):Data +0D03 ReadTimeHex():WkDay,Mn&Dy,Yr&Hr,Mn&Sec +0E03 WriteTimeHex(Mn&Dy,Yr&Hr,Mn&Sec) +0F03 ReadAsciiTime(@Buff) +1003 SetVector(Vec#,@x) +1103 GetVector(Vec#):@x +1203 SetHeartBeat(@Task) +1303 DelHeartBeat(@Task) +1403 ClrHeartBeat() +1503 SysFailMgr(Code,@Msg) +1603 GetAddr(Ref#):@Parm +1703 ReadMouse():X,Y,Stat&Mode +1803 InitMouse(Slot) +1903 SetMouse(Mode) +1A03 HomeMouse() +1B03 ClearMouse() +1C03 ClampMouse(Xmn,Xmx,Ymn,Ymx) +1D03 GetMouseClamp():Xmn,Xmx,Ymn,Ymx +1E03 PosMouse(X,Y) +1F03 ServeMouse():IntStat +2003 GetNewID(Kind):MemID +2103 DeleteID(MemID) +2203 StatusID(MemID) +2303 IntSource(Ref#) +2403 FWEntry(A,X,Y,Address):P,A,X,Y +2503 GetTick():Ticks/4 +2603 PackBytes(@StartPtr,@Sz,@OutBf,OutSz):Size +2703 UnPackBytes(@Buff,BfSz,@StartPtr,@Sz):Size +2803 Munger(@Dst,@DstL,@t,tL,@Rpl,RplL,@Pad):N +2903 GetIRQEnable():IntStat +2A03 SetAbsClamp(Xmn,Xmx,Ymn,Ymx) +2B03 GetAbsClamp():Xmn,Xmx,Ymn,Ymx +2C03 SysBeep() +2E03 AddToQueue(@newTask,@queueHeader) +2F03 DeleteFromQueue(@task,@queueHeader) +3003 SetInterruptState(@stateRec,NumBytes) +3103 GetInterruptState(@stateRec,NumBytes) +3203 GetIntStateRecSize():Size +3303 ReadMouse2():xPos,yPos,StatMode +3403 GetCodeResConverter():@proc +3503 GetROMResource(???,???/4):???H +3603 ReleaseROMResource(???,???/4) +3703 ConvSeconds(convVerb,Secs/4,@Date):SecondsOut/4 +3803 SysBeep2(beepKind) +3903 VersionString(flags,Version/4,@Buffer) +3A03 WaitUntil(WaitFromTime,DelayTime):NewTime +3B03 StringToText(flags,@String,StrLen,@Buffer):ResFlags,PrntLen +3C03 ShowBootInfo(@String,@Icon) +3D03 ScanDevices():DevNum +3E03 AlertMessage(@Table,MsgNum,@Subs):Button +3F03 DoSysPrefs(bitsToClear,bitsToSet):SysPrefs +0004 === QuickDraw II === +0104 QDBootInit() +0204 QDStartUp(DirPg,MastSCB,MaxWid,MemID) +0304 QDShutDown() +0404 QDVersion():Vers +0504 QDReset() +0604 QDStatus():ActFlg +0904 GetAddress(what):@Table +0A04 GrafOn() +0B04 GrafOff() +0C04 GetStandardSCB():SCB +0D04 InitColorTable(@Table) +0E04 SetColorTable(Tab#,@SrcTab) +0F04 GetColorTable(Tab#,@DestTbl) +1004 SetColorEntry(Tab#,Ent#,NewCol) +1104 GetColorEntry(Tab#,Ent#):Color +1204 SetSCB(Line#,SCB) +1304 GetSCB(Line#):SCB +1404 SetAllSCBs(SCB) +1504 ClearScreen(Color) +1604 SetMasterSCB(SCB) +1704 GetMasterSCB():SCB +1804 OpenPort(@Port) +1904 InitPort(@Port) +1A04 ClosePort(@Port) +1B04 SetPort(@Port) +1C04 GetPort():@Port +1D04 SetPortLoc(@LocInfo) +1E04 GetPortLoc(@LocInfo) +1F04 SetPortRect(@Rect) +2004 GetPortRect(@Rect) +2104 SetPortSize(w,h) +2204 MovePortTo(h,v) +2304 SetOrigin(h,v) +2404 SetClip(RgnH) +2504 GetClip(RgnH) +2604 ClipRect(@Rect) +2704 HidePen() +2804 ShowPen() +2904 GetPen(@Pt) +2A04 SetPenState(@PenSt) +2B04 GetPenState(@PenSt) +2C04 SetPenSize(w,h) +2D04 GetPenSize(@Pt) +2E04 SetPenMode(Mode) +2F04 GetPenMode():Mode +3004 SetPenPat(@Patt) +3104 GetPenPat(@Patt) +3204 SetPenMask(@Mask) +3304 GetPenMask(@Mask) +3404 SetBackPat(@Patt) +3504 GetBackPat(@Patt) +3604 PenNormal() +3704 SetSolidPenPat(Color) +3804 SetSolidBackPat(Color) +3904 SolidPattern(Color,@Patt) +3A04 MoveTo(h,v) +3B04 Move(dh,dv) +3C04 LineTo(h,v) +3D04 Line(dh,dv) +3E04 SetPicSave(Val/4) +3F04 GetPicSave():Val/4 +4004 SetRgnSave(Val/4) +4104 GetRgnSave():Val/4 +4204 SetPolySave(Val/4) +4304 GetPolySave():Val/4 +4404 SetGrafProcs(@GrafProcs) +4504 GetGrafProcs():@GrafProcs +4604 SetUserField(Val/4) +4704 GetUserField():Val/4 +4804 SetSysField(Val/4) +4904 GetSysField():Val/4 +4A04 SetRect(@Rect,left,top,right,bot) +4B04 OffsetRect(@Rect,dh,dv) +4C04 InsetRect(@Rect,dh,dv) +4D04 SectRect(@R1,@R2,@DstR):nonEmptyF +4E04 UnionRect(@Rect1,@Rect2,@UnionRect) +4F04 PtInRect(@Pt,@Rect):Flag +5004 Pt2Rect(@Pt1,@Pt2,@Rect) +5104 EqualRect(@Rect1,@Rect2):Flag +5204 NotEmptyRect(@Rect):Flag +5304 FrameRect(@Rect) +5404 PaintRect(@Rect) +5504 EraseRect(@Rect) +5604 InvertRect(@Rect) +5704 FillRect(@Rect,@Patt) +5804 FrameOval(@Rect) +5904 PaintOval(@Rect) +5A04 EraseOval(@Rect) +5B04 InvertOval(@Rect) +5C04 FillOval(@Rect,@Patt) +5D04 FrameRRect(@Rect,OvalW,OvalHt) +5E04 PaintRRect(@Rect,OvalW,OvalHt) +5F04 EraseRRect(@Rect,OvalW,OvalHt) +6004 InvertRRect(@Rect,OvalW,OvalHt) +6104 FillRRect(@Rect,OvalW,OvalHt,@Patt) +6204 FrameArc(@Rect,Ang1,ArcAng) +6304 PaintArc(@Rect,Ang1,ArcAng) +6404 EraseArc(@Rect,Ang1,ArcAng) +6504 InvertArc(@Rect,Ang1,ArcAng) +6604 FillArc(@Rect,Ang1,ArcAng,@Patt) +6704 NewRgn():RgnH +6804 DisposeRgn(RgnH) +6904 CopyRgn(SrcRgnH,DestRgnH) +6A04 SetEmptyRgn(RgnH) +6B04 SetRectRgn(RgnH,left,top,right,bot) +6C04 RectRgn(RgnH,@Rect) +6D04 OpenRgn() +6E04 CloseRgn(RgnH) +6F04 OffsetRgn(RgnH,dh,dv) +7004 InsetRgn(RgnH,dh,dv) +7104 SectRgn(Rgn1H,Rgn2H,DstRgnH) +7204 UnionRgn(Rgn1H,Rgn2H,UnionRgnH) +7304 DiffRgn(Rgn1H,Rgn2H,DstRgnH) +7404 XorRgn(Rgn1H,Rgn2H,DstRgnH) +7504 PtInRgn(@Pt,RgnH):Flag +7604 RectInRgn(@Rect,RgnH):Flag +7704 EqualRgn(Rgn1H,Rgn2H):Flag +7804 EmptyRgn(RgnH):Flag +7904 FrameRgn(RgnH) +7A04 PaintRgn(RgnH) +7B04 EraseRgn(RgnH) +7C04 InvertRgn(RgnH) +7D04 FillRgn(RgnH,@Patt) +7E04 ScrollRect(@Rect,dh,dv,UpdtRgnH) +7F04 PaintPixels(@ppParms) +8004 AddPt(@SrcPt,@DestPt) +8104 SubPt(@SrcPt,@DstPt) +8204 SetPt(@Pt,h,v) +8304 EqualPt(@Pt1,@Pt2):Flag +8404 LocalToGlobal(@Pt) +8504 GlobalToLocal(@Pt) +8604 Random():N +8704 SetRandSeed(Seed/4) +8804 GetPixel(Hor,Vert):Pixel +8904 ScalePt(@Pt,@SrcRect,@DstRect) +8A04 MapPt(@Pt,@SrcRect,@DstRect) +8B04 MapRect(@Rect,@SrcRect,@DstRect) +8C04 MapRgn(MapRgnH,@SrcRect,@DstRect) +8D04 SetStdProcs(@StdProcRec) +8E04 SetCursor(@Curs) +8F04 GetCursorAdr():@Curs +9004 HideCursor() +9104 ShowCursor() +9204 ObscureCursor() +9304 SetMouseLoc ??? +9404 SetFont(FontH) +9504 GetFont():FontH +9604 GetFontInfo(@InfoRec) +9704 GetFontGlobals(@FGRec) +9804 SetFontFlags(Flags) +9904 GetFontFlags():Flags +9A04 SetTextFace(TextF) +9B04 GetTextFace():TextF +9C04 SetTextMode(TextM) +9D04 GetTextMode():TextM +9E04 SetSpaceExtra(SpEx/4f) +9F04 GetSpaceExtra():SpEx/4f +A004 SetForeColor(Color) +A104 GetForeColor():Color +A204 SetBackColor(BackCol) +A304 GetBackColor():BackCol +A404 DrawChar(Char) +A504 DrawString(@Str) +A604 DrawCString(@cStr) +A704 DrawText(@Text,Len) +A804 CharWidth(Char):Width +A904 StringWidth(@Str):Width +AA04 CStringWidth(@cStr):Width +AB04 TextWidth(@Text,Len):Width +AC04 CharBounds(Char,@Rect) +AD04 StringBounds(@Str,@Rect) +AE04 CStringBounds(@cStr,@Rect) +AF04 TextBounds(@Text,Len,@Rect) +B004 SetArcRot(ArcRot) +B104 GetArcRot():ArcRot +B204 SetSysFont(FontH) +B304 GetSysFont():FontH +B404 SetVisRgn(RgnH) +B504 GetVisRgn(RgnH) +B604 SetIntUse(Flag) +B704 OpenPicture(@FrameRect):PicH +B804 PicComment(Kind,DataSz,DataH) +B904 ClosePicture() +BA04 DrawPicture(PicH,@DstRect) +BB04 KillPicture(PicH) +BC04 FramePoly(PolyH) +BD04 PaintPoly(PolyH) +BE04 ErasePoly(PolyH) +BF04 InvertPoly(PolyH) +C004 FillPoly(PolyH,@Patt) +C104 OpenPoly():PolyH +C204 ClosePoly() +C304 KillPoly(PolyH) +C404 OffsetPoly(PolyH,dh,dv) +C504 MapPoly(PolyH,@SrcRect,@DstRect) +C604 SetClipHandle(RgnH) +C704 GetClipHandle():RgnH +C804 SetVisHandle(RgnH) +C904 GetVisHandle():RgnH +CA04 InitCursor() +CB04 SetBufDims(MaxW,MaxFontHt,MaxFBRext) +CC04 ForceBufDims(MaxW,MaxFontHt,MaxFBRext) +CD04 SaveBufDims(@SizeInfo) +CE04 RestoreBufDims(@SizeInfo) +CF04 GetFGSize():FGSize +D004 SetFontID(FontID/4) +D104 GetFontID():FontID/4 +D204 SetTextSize(TextSz) +D304 GetTextSize():TextSz +D404 SetCharExtra(ChEx/4f) +D504 GetCharExtra():ChEx/4f +D604 PPToPort(@SrcLoc,@SrcRect,X,Y,Mode) +D704 InflateTextBuffer(NewW,NewHt) +D804 GetRomFont(@Rec) +D904 GetFontLore(@Rec,RecSize):Size +DA04 Get640Colors():@PattTable +DB04 Set640Color(color) +0005 === desk manager === +0105 DeskBootInit() +0205 DeskStartUp() +0305 DeskShutDown() +0405 DeskVersion():Vers +0505 DeskReset() +0605 DeskStatus():ActFlg +0905 SaveScrn() +0A05 RestScrn() +0B05 SaveAll() +0C05 RestAll() +0E05 InstallNDA(ndaH) +0F05 InstallCDA(cdaH) +1105 ChooseCDA() +1305 SetDAStrPtr(AltDispH,@StrTbl) +1405 GetDAStrPtr():@StrTbl +1505 OpenNDA(ItemID):Ref# +1605 CloseNDA(Ref#) +1705 SystemClick(@EvRec,@Wind,fwRes) +1805 SystemEdit(eType):Flag +1905 SystemTask() +1A05 SystemEvent(Mods,Where/4,When/4,Msg/4,What):F +1B05 GetNumNDAs():N +1C05 CloseNDAbyWinPtr(@Wind) +1D05 CloseAllNDAs() +1E05 FixAppleMenu(MenuID) +1F05 AddToRunQ(@taskHeader) +2005 RemoveFromRunQ(@taskHeader) +2105 RemoveCDA(cdaH) +2205 RemoveNDA(ndaH) +2305 GetDeskAccInfo(flags,daRef/4,BufSize,@Buffer) +2405 CallDeskAcc(flags,daRef/4,Action,Data/4):Result +2505 GetDeskGlobal(selector):Value/4 +0006 === event manager === +0106 EMBootInit() +0206 EMStartUp(DirPg,qSz,Xmn,Xmx,Ymn,Ymx,MemID) +0306 EMShutDown() +0406 EMVersion():Vers +0506 EMReset() +0606 EMStatus():ActFlg +0906 DoWindows():DirPg +0A06 GetNextEvent(evMask,@EvRec):Flag +0B06 EventAvail(evMask,@EvRec):Flag +0C06 GetMouse(@Pt) +0D06 Button(Btn#):DownFlg +0E06 StillDown(Btn#):Flag +0F06 WaitMouseUp(Btn#):Flag +1006 TickCount():Ticks/4 +1106 GetDblTime():Ticks/4 +1206 GetCaretTime():Ticks/4 +1306 SetSwitch() +1406 PostEvent(code,Msg/4):Flag +1506 FlushEvents(evMask,StopMask):F +1606 GetOSEvent(evMask,@EvRec):Flag +1706 OSEventAvail(evMask,@EvRec):Flag +1806 SetEventMask(evMask) +1906 FakeMouse(ChFlg,Mods,X,Y,BtnStat) +1A06 SetAutoKeyLimit(NewLimit) +1B06 GetKeyTranslation():kTransID +1C06 SetKeyTranslation(kTransID) +0007 === scheduler === +0107 SchBootInit() +0207 SchStartUp() +0307 SchShutDown() +0407 SchVersion():Vers +0507 SchReset() +0607 SchStatus():ActFlg +0907 SchAddTask(@Task):Flag +0A07 SchFlush() +0008 === sound manager === +0108 SoundBootInit() +0208 SoundStartUp(DirPg) +0308 SoundShutDown() +0408 SoundVersion():Vers +0508 SoundReset() +0608 SoundToolStatus():ActFlg +0908 WriteRamBlock(@Src,DOCStart,Count) +0A08 ReadRamBlock(@Dest,DOCStart,Count) +0B08 GetTableAddress():@JumpTbl +0C08 GetSoundVolume(Gen#):Vol +0D08 SetSoundVolume(Vol,Gen#) +0E08 FFStartSound(GenN&mode,@Parms) +0F08 FFStopSound(GenMask) +1008 FFSoundStatus():ActFlg +1108 FFGeneratorStatus(Gen#):Stat +1208 SetSoundMIRQV(@IntHandler) +1308 SetUserSoundIRQV(@NewIRQ):@OldIRQ +1408 FFSoundDoneStatus(Gen#):Stat +1508 FFSetUpSound(ChannelGen,@Parms) +1608 FFStartPlaying(GenWord) +1708 SetDocReg(@DocRegParms) +1808 ReadDocReg(@DocRegParms) +0009 === desktop bus === +0109 ADBBootInit() +0209 ADBStartUp() +0309 ADBShutDown() +0409 ADBVersion():Vers +0509 ADBReset() +0609 ADBStatus():ActFlg +0909 SendInfo(NumB,@Data,Cmd) +0A09 ReadKeyMicroData(NumB,@Data,Cmd) +0B09 ReadKeyMicroMemory(@DataOut,@DataIn,Cmd) +0C09 [resynch--don't call] +0D09 AsyncADBReceive(@CompVec,Cmd) +0E09 SyncADBReceive(InputWrd,@CompVec,Cmd) +0F09 AbsOn() +1009 AbsOff() +1109 RdAbs():Flag +1209 SetAbsScale(@DataOut) +1309 GetAbsScale(@DataIn) +1409 SRQPoll(@CompVec,ADBreg) +1509 SRQRemove(ADBreg) +1609 ClearSRQTable() +FF09 [OBSOLETE: Use 09FF] +000A === SANE === +010A SANEBootInit() +020A SANEStartUp(DirPg) +030A SANEShutDown() +040A SANEVersion():Vers +050A SANEReset() +060A SANEStatus():ActFlg +090A FPNum (...) +0A0A DecStrNum (...) +0B0A ElemNum (...) +FF0A [OBSOLETE: USE $0AFF] +000B === integer math === +010B IMBootInit() +020B IMStartUp() +030B IMShutDown() +040B IMVersion():Vers +050B IMReset() +060B IMStatus():ActFlg +090B Multiply(A,B):Prod/4 +0A0B SDivide(Num,Den):Rem,Quot +0B0B UDivide(Num,Den):Rem,Quot +0C0B LongMul(A/4,B/4):Prod/8 +0D0B LongDivide(Num/4,Denom/4):Rem/4,Quot/4 +0E0B FixRatio(Numer,Denom):fxRatio/4 +0F0B FixMul(fx1/4,fx2/4):fxProd/4 +100B FracMul(fr1/4,fr2/4):frRes/4 +110B FixDiv(Quot/4,Divisor/4):fxRes/4 +120B FracDiv(Quot/4,Divisor/4):frRes/4 +130B FixRound(fxVal/4):Int +140B FracSqrt(frVal/4):frRes/4 +150B FracCos(fxAngle/4):frRes/4 +160B FracSin(fxAngle/4):frRes/4 +170B FixATan2(In1/4,In2/4):fxArcTan/4 +180B HiWord(Long/4):Int +190B LoWord(Long/4):Int +1A0B Long2Fix(Long/4):fxRes/4 +1B0B Fix2Long(Fix/4):Long/4 +1C0B Fix2Frac(fxVal/4):Frac/4 +1D0B Frac2Fix(frVal/4):fxRes/4 +1E0B Fix2X(Fix/4,@Extended) +1F0B Frac2X(frVal/4,@Extended) +200B X2Fix(@Extended):fxRes/4 +210B X2Frac(@Extended):frRes/4 +220B Int2Hex(Int,@Str,Len) +230B Long2Hex(Long/4,@Str,Len) +240B Hex2Int(@Str,Len):Int +250B Hex2Long(@Str,Len):Long/4 +260B Int2Dec(Int,@Str,Len,SgnFlg) +270B Long2Dec(Long/4,@Str,Len,SgnFlg) +280B Dec2Int(@Str,Len,SgnFlg):Int +290B Dec2Long(@Str,Len,SgnFlg):Long/4 +2A0B HexIt(Int):Hex/4 +000C === text tools === +010C TextBootInit() +020C TextStartUp() +030C TextShutDown() +040C TextVersion():Vers +050C TextReset() +060C TextStatus():ActFlg +090C SetInGlobals(ANDmsk,ORmsk) +0A0C SetOutGlobals(ANDmsk,ORmsk) +0B0C SetErrGlobals(ANDmsk,ORmsk) +0C0C GetInGlobals():ANDmsk,ORmsk +0D0C GetOutGlobals():ANDmsk,ORmsk +0E0C GetErrGlobals():ANDmsk,ORmsk +0F0C SetInputDevice(Type,@drvr|Slot/4) +100C SetOutputDevice(Type,@drvr|Slot/4) +110C SetErrorDevice(Type,@drvr|Slot/4) +120C GetInputDevice():Type,@drvr|Slot/4 +130C GetOutputDevice():Type,@drvr|Slot/4 +140C GetErrorDevice():Type,@drvr|Slot/4 +150C InitTextDev(dev) +160C CtlTextDev(dev,code) +170C StatusTextDev(dev,request) +180C WriteChar(Char) +190C ErrWriteChar(Char) +1A0C WriteLine(@Str) +1B0C ErrWriteLine(@Str) +1C0C WriteString(@Str) +1D0C ErrWriteString(@Str) +1E0C TextWriteBlock(@Text,Offset,Len) +1F0C ErrWriteBlock(@Text,Offset,Len) +200C WriteCString(@cStr) +210C ErrWriteCString(@cStr) +220C ReadChar(EchoFlg):Char +230C TextReadBlock(@Buff,Offset,Size,EchoFlg) +240C ReadLine(@Buff,Max,EOLch,EchoFlg):Count +000D === reserved === +000E === window manager === +010E WindBootInit() +020E WindStartUp(MemID) +030E WindShutDown() +040E WindVersion():Vers +050E WindReset() +060E WindStatus():ActFlg +090E NewWindow(@Parms):@Wind +0A0E CheckUpdate(@EvRec):Flag +0B0E CloseWindow(@Wind) +0C0E Desktop(Oper,param/4):result/4 +0D0E SetWTitle(@Title,@Wind) +0E0E GetWTitle(@Wind):@Title +0F0E SetFrameColor(@NewColTbl,@Wind) +100E GetFrameColor(@Table,@Wind) +110E SelectWindow(@Wind) +120E HideWindow(@Wind) +130E ShowWindow(@Wind) +140E SendBehind(@BehindWho,@Wind) +150E FrontWindow():@Wind +160E SetInfoDraw(@Proc,@Wind) +170E FindWindow(@WindVar,X,Y):Where +180E TrackGoAway(X,Y,@Wind):Flag +190E MoveWindow(X,Y,@Wind) +1A0E DragWindow(Grid,X,Y,Grace,@bRect,@Wind) +1B0E GrowWindow(mnW,mnH,X,Y,@Wind):nSize/4 +1C0E SizeWindow(w,h,@Wind) +1D0E TaskMaster(evMask,@TaskRec):Code +1E0E BeginUpdate(@Wind) +1F0E EndUpdate(@Wind) +200E GetWMgrPort():@Port +210E PinRect(X,Y,@Rect):Point/4 +220E HiliteWindow(Flag,@Wind) +230E ShowHide(Flag,@Wind) +240E BringToFront(@Wind) +250E WindNewRes() +260E TrackZoom(X,Y,@Wind):Flag +270E ZoomWindow(@Wind) +280E SetWRefCon(Refcon/4,@Wind) +290E GetWRefCon(@Wind):Refcon/4 +2A0E GetNextWindow(@Wind):@Wind +2B0E GetWKind(@Wind):Flag +2C0E GetWFrame(@Wind):Frame +2D0E SetWFrame(Frame,@Wind) +2E0E GetStructRgn(@Wind):StructRgnH +2F0E GetContentRgn(@Wind):ContRgnH +300E GetUpdateRgn(@Wind):UpdateRgnH +310E GetDefProc(@Wind):@Proc +320E SetDefProc(@Proc,@Wind) +330E GetWControls(@Wind):CtrlH +340E SetOriginMask(Mask,@Wind) +350E GetInfoRefCon(@Wind):Refcon/4 +360E SetInfoRefCon(Val/4,@Wind) +370E GetZoomRect(@Wind):@zRect +380E SetZoomRect(@zRect,@Wind) +390E RefreshDesktop(@Rect) +3A0E InvalRect(@Rect) +3B0E InvalRgn(RgnH) +3C0E ValidRect(@Rect) +3D0E ValidRgn(RgnH) +3E0E GetContentOrigin(@Wind):Origin/4 +3F0E SetContentOrigin(X,Y,@Wind) +400E GetDataSize(@Wind):DataSize/4 +410E SetDataSize(w,h,@Wind) +420E GetMaxGrow(@Wind):MaxGrow/4 +430E SetMaxGrow(maxWidth,maxHeight,@Wind) +440E GetScroll(@Wind):Scroll/4 +450E SetScroll(h,v,@Wind) +460E GetPage(@Wind):Page/4 +470E SetPage(h,v,@Wind) +480E GetContentDraw(@Wind):@Proc +490E SetContentDraw(@Proc,@Wind) +4A0E GetInfoDraw(@Wind):@Proc +4B0E SetSysWindow(@Wind) +4C0E GetSysWFlag(@Wind):Flag +4D0E StartDrawing(@Wind) +4E0E SetWindowIcons(NewFontH):OldFontH +4F0E GetRectInfo(@InfoRect,@Wind) +500E StartInfoDrawing(@iRect,@Wind) +510E EndInfoDrawing() +520E GetFirstWindow():@Wind +530E WindDragRect(@a,@P,X,Y,@R,@lR,@sR,F):M/4 +540E Private01():@func [GetDragRectPtr] +550E DrawInfoBar(@Wind) +560E WindowGlobal(Flags):Flags +570E SetContentOrigin2(ScrollFlag,X,Y,@Wind) +580E GetWindowMgrGlobals():@Globals +590E AlertWindow(AlertDesc,@SubArray,AlertRef/4):Btn +5A0E StartFrameDrawing(@Wind) +5B0E EndFrameDrawing() +5C0E ResizeWindow(hidden,@ContRect,@Wind) +5D0E TaskMasterContent +5E0E TaskMasterKey +5F0E TaskMasterDA(evMask,@bigTaskRec):taskCode +600E CompileText(subType,@subs,@text,size):H +610E NewWindow2(@T,RC/4,@draw,@def,pDesc,pRef/4,rType):@W +620E ErrorWindow(subType,@subs,ErrNum):Button +630E GetAuxWindInfo(@Wind):@Info +640E DoModalWindow(@Event,@Update,@EvHook,@Beep,Flags):Result/4 +650E MWGetCtlPart():Part +660E MWSetMenuProc(@NewMenuProc):@OldMenuProc +670E MWStdDrawProc() +680E MWSetUpEditMenu() +690E FindCursorCtl(@CtrlH,x,y,@Wind):PartCode +6A0E ResizeInfoBar(flags,newHeight,@Wind) +6B0E HandleDiskInsert(flags,devNum):resFlags,resDevNum +6C0E UpdateWindow(flags,@Wind) +000F === menu manager === +010F MenuBootInit() +020F MenuStartUp(MemID,DirPg) +030F MenuShutDown() +040F MenuVersion():Vers +050F MenuReset() +060F MenuStatus():ActFlg +090F MenuKey(@TaskRec,BarH) +0A0F GetMenuBar():BarH +0B0F MenuRefresh(@RedrawProc) +0C0F FlashMenuBar() +0D0F InsertMenu(MenuH,AfterWhat) +0E0F DeleteMenu(MenuID) +0F0F InsertMItem(@Item,AfterItem,MenuID) +100F DeleteMItem(ItemID) +110F GetSysBar():BarH +120F SetSysBar(BarH) +130F FixMenuBar():Height +140F CountMItems(MenuID):N +150F NewMenuBar(@Wind):BarH +160F GetMHandle(MenuID):MenuH +170F SetBarColors(BarCol,InvCol,OutCol) +180F GetBarColors():Colors/4 +190F SetMTitleStart(hStart) +1A0F GetMTitleStart():hStart +1B0F GetMenuMgrPort():@Port +1C0F CalcMenuSize(w,h,MenuID) +1D0F SetMTitleWidth(w,MenuID) +1E0F GetMTitleWidth(MenuID):TitleWidth +1F0F SetMenuFlag(Flags,MenuID) +200F GetMenuFlag(MenuID):Flags +210F SetMenuTitle(@Title,MenuID) +220F GetMenuTitle(MenuID):@Title +230F MenuGlobal(Flags):Flags +240F SetMItem(@Str,ItemID) +250F GetMItem(ItemID):@ItemName +260F SetMItemFlag(Flags,ItemID) +270F GetMItemFlag(ItemID):Flag +280F SetMItemBlink(Count) +290F MenuNewRes() +2A0F DrawMenuBar() +2B0F MenuSelect(@TaskRec,BarH) +2C0F HiliteMenu(Flag,MenuID) +2D0F NewMenu(@MenuStr):MenuH +2E0F DisposeMenu(MenuH) +2F0F InitPalette() +300F EnableMItem(ItemID) +310F DisableMItem(ItemID) +320F CheckMItem(Flag,ItemID) +330F SetMItemMark(MarkCh,ItemID) +340F GetMItemMark(ItemID):MarkChar +350F SetMItemStyle(TextStyle,ItemID) +360F GetMItemStyle(ItemID):TextStyle +370F SetMenuID(New,Old) +380F SetMItemID(New,Old) +390F SetMenuBar(BarH) +3A0F SetMItemName(@Str,ItemID) +3B0F GetPopUpDefProc():@proc +3C0F PopUpMenuSelect(SelID,left,top,flag,MenuH):id +3D0F [DrawPopUp(SelID,Flag,right,bottom,left,top,MenuH)] +3E0F NewMenu2(RefDesc,Ref/4):MenuH +3F0F InsertMItem2(RefDesc,Ref/4,After,MenuID) +400F SetMenuTitle2(RefDesc,TitleRef/4,MenuID) +410F SetMItem2(RefDesc,Ref/4,Item) +420F SetMItemName2(RefDesc,Ref/4,Item) +430F NewMenuBar2(RefDesc,Ref/4,@Wind):BarH +450F HideMenuBar() +460F ShowMenuBar() +470F SetMItemIcon(IconDesc,IconRef/4,ItemID) +480F GetMItemIcon(ItemID):IconRef/4 +490F SetMItemStruct(Desc,StructRef/4,ItemID) +4A0F GetMItemStruct(ItemID):ItemStruct/4 +4B0F RemoveMItemStruct(ItemID) +4C0F GetMItemFlag2(ItemID):ItemFlag2 +4D0F SetMItemFlag2(newValue,ItemID) +4F0F GetMItemBlink():Count +500F InsertPathMItems(flags,@Path,devnum,MenuID,AfterID,StartID,@Results) +0010 === control manager === +0110 CtlBootInit() +0210 CtlStartUp(MemID,DirPg) +0310 CtlShutDown() +0410 CtlVersion():Vers +0510 CtlReset() +0610 CtlStatus():ActFlg +0910 NewControl(@W,@R,@T,F,V,P1,P2,@p,r/4,@C):cH +0A10 DisposeControl(CtrlH) +0B10 KillControls(@Wind) +0C10 SetCtlTitle(@Title,CtrlH) +0D10 GetCtlTitle(CtrlH):@Title +0E10 HideControl(CtrlH) +0F10 ShowControl(CtrlH) +1010 DrawControls(@Wind) +1110 HiliteControl(Flag,CtrlH) +1210 CtlNewRes() +1310 FindControl(@CtrlHVar,X,Y,@Wind):Part +1410 TestControl(X,Y,CtrlH):Part +1510 TrackControl(X,Y,@ActProc,CtrlH):Part +1610 MoveControl(X,Y,CtrlH) +1710 DragControl(X,Y,@LimR,@slR,Axis,CtrlH) +1810 SetCtlIcons(FontH):OldFontH +1910 SetCtlValue(Val,CtrlH) +1A10 GetCtlValue(CtrlH):Val +1B10 SetCtlParams(P2,P1,CtrlH) +1C10 GetCtlParams(CtrlH):P1,P2 +1D10 DragRect(@acPr,@P,X,Y,@drR,@l,@slR,F):M/4 +1E10 GrowSize():Size/4 +1F10 GetCtlDpage():DirPg +2010 SetCtlAction(@ActProc,CtrlH) +2110 GetCtlAction(CtrlH):Action/4 +2210 SetCtlRefCon(Refcon/4,CtrlH) +2310 GetCtlRefCon(CtrlH):Refcon/4 +2410 EraseControl(CtrlH) +2510 DrawOneCtl(CtrlH) +2610 FindTargetCtl():CtrlH +2710 MakeNextCtlTarget():CtrlH +2810 MakeThisCtlTarget(CtrlH) +2910 SendEventToCtl(TgtOnly,@Wind,@eTask):Accepted +2A10 GetCtlID(CtrlH):CtlID/4 +2B10 SetCtlID(CtlID/4,CtrlH) +2C10 CallCtlDefProc(CtrlH,Msg,Param/4):Result/4 +2D10 NotifyCtls(Mask,Msg,Param/4,@Wind) +2E10 GetCtlMoreFlags(CtrlH):Flags +2F10 SetCtlMoreFlags(Flags,CtrlH) +3010 GetCtlHandleFromID(@Wind,CtlID/4):CtrlH +3110 NewControl2(@Wind,InKind,InRef/4):CtrlH +3210 CMLoadResource(rType,rID/4):resH +3310 CMReleaseResource(rType,rID/4) +3410 SetCtlParamPtr(@SubArray) +3510 GetCtlParamPtr():@SubArray +3710 InvalCtls(@Wind) +3810 [reserved] +3910 FindRadioButton(@Wind,FamilyNum):WhichRadio +3A10 SetLETextByID(@Wind,leID/4,@PString) +3B10 GetLETextByID(@Wind,leID/4,@PString) +3C10 SetCtlValueByID(Value,@Wind,CtlID/4) +3D10 GetCtlValueByID(@Wind,CtlID/4):Value +3E10 InvalOneCtlByID(@Wind,CtlID/4) +3F10 HiliteCtlByID(Hilite,@Wind,CtlID/4) +0011 === loader === +0111 LoaderBootInit() +0211 LoaderStartUp() +0311 LoaderShutDown() +0411 LoaderVersion():Vers +0511 LoaderReset() +0611 LoaderStatus():ActFlg +0911 InitialLoad(MemID,@path,F):dpsSz,dps,@l,MemID +0A11 Restart(MemID):dpsSz,dps,@loc,MemID +0B11 LoadSegNum(MemID,file#,seg#):@loc +0C11 UnloadSegNum(MemID,file#,seg#) +0D11 LoadSegName(MemID,@path,@segn):@loc,MemID,file#,sg# +0E11 UnloadSeg(@loc):seg#,file#,MemID +0F11 GetLoadSegInfo(MemID,file#,seg#,@buff) +1011 GetUserID(@Pathname):MemID +1111 LGetPathname(MemID,file#):@path +1211 UserShutDown(MemID,qFlag):MemID +1311 RenamePathname(@path1,@path2) +2011 InitialLoad2(MemID,@in,F,Type):dpsSz,dps,@l,MemID +2111 GetUserID2(@path):MemID +2211 LGetPathname2(MemID,file#):@path +0012 === QuickDraw Aux === +0112 QDAuxBootInit() +0212 QDAuxStartUp() +0312 QDAuxShutDown() +0412 QDAuxVersion():Vers +0512 QDAuxReset() +0612 QDAuxStatus():ActFlg +0912 CopyPixels(@sLoc,@dLoc,@sRect,@dRct,M,MskH) +0A12 WaitCursor() +0B12 DrawIcon(@Icon,Mode,X,Y) +0C12 SpecialRect(@Rect,FrameColor,FillColor) +0D12 SeedFill(@sLoc,@sR,@dLoc,@dR,X,Y,Mode,@Patt,@Leak) +0E12 CalcMask(@sLoc,@sR,@dLoc,@dR,Mode,@Patt,@Leak) +0F12 GetSysIcon(flags,value,aux/4):@Icon +1012 PixelMap2Rgn(@srcLoc,bitsPerPix,colorMask):RgnH +1312 IBeamCursor() +1412 WhooshRect(flags/4,@smallRect,@bigRect) +1512 DrawStringWidth(Flags,Ref/4,Width) +1612 UseColorTable(tableNum,@Table,Flags):ColorInfoH +1712 RestoreColorTable(ColorInfoH,Flags) +0013 === print manager === +0113 PMBootInit() +0213 PMStartUp(MemID,DirPg) +0313 PMShutDown() +0413 PMVersion():Vers +0513 PMReset() +0613 PMStatus():ActFlg +0913 PrDefault(PrRecH) +0A13 PrValidate(PrRecH):Flag +0B13 PrStlDialog(PrRecH):Flag +0C13 PrJobDialog(PrRecH):Flag +0D13 PrPixelMap(@LocInfo,@SrcRect,colorFlag) +0E13 PrOpenDoc(PrRecH,@Port):@Port +0F13 PrCloseDoc(@Port) +1013 PrOpenPage(@Port,@Frame) +1113 PrClosePage(@Port) +1213 PrPicFile(PrRecH,@Port,@StatRec) +1313 PrControl [obsolete] +1413 PrError():Error +1513 PrSetError(Error) +1613 PrChoosePrinter():DrvFlag +1813 PrGetPrinterSpecs():Type,Characteristics +1913 PrDevPrChanged(@PrinterName) +1A13 PrDevStartup(@PrinterName,@ZoneName) +1B13 PrDevShutDown() +1C13 PrDevOpen(@compProc,Reserved/4) +1D13 PrDevRead(@buffer,reqCount):xferCount +1E13 PrDevWrite(@compProc,@buff,bufLen) +1F13 PrDevClose() +2013 PrDevStatus(@statBuff) +2113 PrDevAsyncRead(@compPr,bufLen,@buff):xferCount +2213 PrDevWriteBackground(@compProc,bufLen,@buff) +2313 PrDriverVer():Vers +2413 PrPortVer():Vers +2513 PrGetZoneName():@ZoneName +2813 PrGetPrinterDvrName():@Name +2913 PrGetPortDvrName():@Name +2A13 PrGetUserName():@Name +2B13 PrGetNetworkName():@Name +3013 PrDevIsItSafe():safeFlag +3113 GetZoneList [obsolete?] +3213 GetMyZone [obsolete?] +3313 GetPrinterList [obsolete?] +3413 PMUnloadDriver(whichDriver) +3513 PMLoadDriver(whichDriver) +3613 PrGetDocName():@pStr +3713 PrSetDocName(@pStr) +3813 PrGetPgOrientation(PrRecH):Orientation +0014 === line edit === +0114 LEBootInit() +0214 LEStartUp(MemID,DirPg) +0314 LEShutDown() +0414 LEVersion():Vers +0514 LEReset() +0614 LEStatus():ActFlg +0914 LENew(@DstRect,@ViewRect,MaxL):leH +0A14 LEDispose(leH) +0B14 LESetText(@Text,Len,leH) +0C14 LEIdle(leH) +0D14 LEClick(@EvRec,leH) +0E14 LESetSelect(Start,End,leH) +0F14 LEActivate(leH) +1014 LEDeactivate(leH) +1114 LEKey(Key,Mods,leH) +1214 LECut(leH) +1314 LECopy(leH) +1414 LEPaste(leH) +1514 LEDelete(leH) +1614 LEInsert(@Text,Len,leH) +1714 LEUpdate(leH) +1814 LETextBox(@Text,Len,@Rect,Just) +1914 LEFromScrap() +1A14 LEToScrap() +1B14 LEScrapHandle():ScrapH +1C14 LEGetScrapLen():Len +1D14 LESetScrapLen(NewL) +1E14 LESetHilite(@HiliteProc,leH) +1F14 LESetCaret(@CaretProc,leH) +2014 LETextBox2(@Text,Len,@Rect,Just) +2114 LESetJust(Just,leH) +2214 LEGetTextHand(leH):TextH +2314 LEGetTextLen(leH):TxtLen +2414 GetLEDefProc():@proc +2514 LEClassifyKey(@Event):Flag +0015 === dialog manager === +0115 DialogBootInit() +0215 DialogStartUp(MemID) +0315 DialogShutDown() +0415 DialogVersion():Vers +0515 DialogReset() +0615 DialogStatus():ActFlg +0915 ErrorSound(@SoundProc) +0A15 NewModalDialog(@bR,vis,refcon/4):@Dlog +0B15 NewModelessDialog(@R,@T,@b,fr,rf/4,@zR):@D +0C15 CloseDialog(@Dlog) +0D15 NewDItem(@Dlog,dItem,@R,ty,Des/4,V,F,@Col) +0E15 RemoveDItem(@Dlog,dItem) +0F15 ModalDialog(@FilterProc):Hit +1015 IsDialogEvent(@EvRec):Flag +1115 DialogSelect(@EvRec,@Dlog,@Hit):Flag +1215 DlgCut(@Dlog) +1315 DlgCopy(@Dlog) +1415 DlgPaste(@Dlog) +1515 DlgDelete(@Dlog) +1615 DrawDialog(@Dlog) +1715 Alert(@AlertTmpl,@FiltProc):Hit +1815 StopAlert(@AlertTmpl,@FiltProc):Hit +1915 NoteAlert(@AlertTmpl,@FiltProc):Hit +1A15 CautionAlert(@AlertTmpl,@FiltProc):Hit +1B15 ParamText(@P0,@P1,@P2,@P3) +1C15 SetDAFont(FontH) +1E15 GetControlDItem(@Dlog,dItem):CtrlH +1F15 GetIText(@Dlog,dItem,@Str) +2015 SetIText(@Dlog,dItem,@Str) +2115 SelectIText(@Dlog,dItem,start,end) +2215 HideDItem(@Dlog,dItem) +2315 ShowDItem(@Dlog,dItem) +2415 FindDItem(@Dlog,Point/4):Hit +2515 UpdateDialog(@Dlog,UpdtRgnH) +2615 GetDItemType(@Dlog,dItem):type +2715 SetDItemType(type,@Dlog,dItem) +2815 GetDItemBox(@Dlog,dItem,@Rect) +2915 SetDItemBox(@Dlog,dItem,@Rect) +2A15 GetFirstDItem(@Dlog):dItem +2B15 GetNextDItem(@Dlog,dItem):dItem +2C15 ModalDialog2(@FilterProc):HitInfo/4 +2E15 GetDItemValue(@Dlog,dItem):Val +2F15 SetDItemValue(val,@Dlog,dItem) +3215 GetNewModalDialog(@DlogTmpl):@Dlog +3315 GetNewDItem(@Dlog,@ItemTmpl) +3415 GetAlertStage():Stage +3515 ResetAlertStage() +3615 DefaultFilter(@Dlog,@EvRec,@Hit):Flag +3715 GetDefButton(@Dlog):dItem +3815 SetDefButton(BtnID,@Dlog) +3915 DisableDItem(@Dlog,dItem) +3A15 EnableDItem(@Dlog,dItem) +0016 === scrap manager === +0116 ScrapBootInit() +0216 ScrapStartUp() +0316 ScrapShutDown() +0416 ScrapVersion():Vers +0516 ScrapReset() +0616 ScrapStatus():ActFlg +0916 UnloadScrap() +0A16 LoadScrap() +0B16 ZeroScrap() +0C16 PutScrap(Count/4,Type,@Src) +0D16 GetScrap(DestH,Type) +0E16 GetScrapHandle(Type):ScrapH +0F16 GetScrapSize(Type):Size/4 +1016 GetScrapPath():@Pathname +1116 SetScrapPath(@Pathname) +1216 GetScrapCount():Count +1316 GetScrapState():State +1416 GetIndScrap(Index,@buffer) +1516 ShowClipboard(flags,@rect):@Wind +0017 === standard file === +0117 SFBootInit() +0217 SFStartUp(MemID,DirPg) +0317 SFShutDown() +0417 SFVersion():Vers +0517 SFReset() +0617 SFStatus():ActFlg +0917 SFGetFile(X,Y,@Prmpt,@FPrc,@tL,@Reply) +0A17 SFPutFile(X,Y,@Prompt,@DfltName,mxL,@Reply) +0B17 SFPGetFile(X,Y,@P,@FPrc,@tL,@dTmp,@dHk,@Rp) +0C17 SFPPutFile(X,Y,@P,@Df,mxL,@dTmpl,@dHk,@Rply) +0D17 SFAllCaps(Flag) +0E17 SFGetFile2(X,Y,prDesc,prRef/4,@fProc,@tList,@rep) +0F17 SFPutFile2(X,Y,prDesc,prRef/4,nmDesc,nmRef/4,@rep) +1017 SFPGetFile2(X,Y,@draw,prD,prRf/4,@fP,@tL,@d,@hk,@rep) +1117 SFPPutFile2(X,Y,@draw,prD,prRf/4,nmD,nmRf/4,@d,@hk,@rep) +1217 SFShowInvisible(InvisState):OldState +1317 SFReScan(@filterProc,@typeList) +1417 SFMultiGet2(X,Y,prDesc,prRef/4,@fP,@tL,@rep) +1517 SFPMultiGet2(X,Y,@draw,prD,prRf/4,@fP,@tL,@d,@hk,@rep) +0019 === note synthesizer === +0119 NSBootInit() +0219 NSStartUp(Rate,@UpdProc) +0319 NSShutDown() +0419 NSVersion():Vers +0519 NSReset() +0619 NSStatus():ActFlg +0919 AllocGen(Priority):Gen# +0A19 DeallocGen(Gen#) +0B19 NoteOn(Gen#,Semitone,Vol,@Instr) +0C19 NoteOff(Gen#,Semitone) +0D19 AllNotesOff() +0E19 NSSetUpdateRate(NewRate):OldRate +0F19 NSSetUserUpdateRtn(@New):@Old +001A === note sequencer === +011A SeqBootInit() +021A SeqStartUp(DirPg,Mode,Rate,Incr) +031A SeqShutDown() +041A SeqVersion():Vers +051A SeqReset() +061A SeqStatus():ActFlg +091A SetIncr(Increment) +0A1A ClearIncr():OldIncr +0B1A GetTimer():Tick +0C1A GetLoc():Phrase,Patt,Level +0D1A SeqAllNotesOff() +0E1A SetTrkInfo(Priority,InstIndex,TrkNum) +0F1A StartSeq(@ErrRtn,@CompRtn,SeqH) +101A StepSeq() +111A StopSeq(NextFlag) +121A SetInstTable(TableH) +131A StartInts() +141A StopInts() +151A StartSeqRel(@errHndlr,@CompRtn,SeqH) +001B === font manager === +011B FMBootInit() +021B FMStartUp(MemID,DirPg) +031B FMShutDown() +041B FMVersion():Vers +051B FMReset() +061B FMStatus():ActFlg +091B CountFamilies(FamSpecs):Count +0A1B FindFamily(Specs,Pos,@Name):FamNum +0B1B GetFamInfo(FamNum,@Name):FamStats +0C1B GetFamNum(@Name):FamNum +0D1B AddFamily(FamNum,@Name) +0E1B InstallFont(ID/4,Scale) +0F1B SetPurgeStat(FontID/4,PrgStat) +101B CountFonts(ID/4,Specs):N +111B FindFontStats(ID/4,Specs,Pos,@FStatRec) +121B LoadFont(ID/4,Specs,Pos,@FStatRec) +131B LoadSysFont() +141B AddFontVar(FontH,NewSpecs) +151B FixFontMenu(MenuID,StartID,FamSpecs) +161B ChooseFont(CurrID/4,Famspecs):NewID/4 +171B ItemID2FamNum(ItemID):FamNum +181B FMSetSysFont(FontID/4) +191B FMGetSysFID():SysID/4 +1A1B FMGetCurFID():CurID/4 +1B1B FamNum2ItemID(FamNum):ItemID +1C1B InstallWithStats(ID/4,Scale,@ResultRec) +001C === List Manager === +011C ListBootInit() +021C ListStartUp() +031C ListShutDown() +041C ListVersion():Vers +051C ListReset() +061C ListStatus():ActFlg +091C CreateList(@Wind,@ListRec):CtrlH +0A1C SortList(@CompareProc,@ListRec) +0B1C NextMember(@Member,@ListRec):@NxtMemVal +0C1C DrawMember(@Member,@ListRec) +0D1C SelectMember(@Member,@ListRec) +0E1C GetListDefProc():@Proc +0F1C ResetMember(@ListRec):NxtMemVal/4 +101C NewList(@Member,@ListRec) +111C DrawMember2(itemNum,CtrlH) +121C NextMember2(itemNum,CtrlH):itemNum +131C ResetMember2(CtrlH):itemNum +141C SelectMember2(itemNum,CtrlH) +151C SortList2(@CompareProc,CtrlH) +161C NewList2(@draw,start,ref/4,refKind,size,CtrlH) +171C ListKey(flags,@EventRec,CtrlH) +181C CompareStrings(flags,@String1,@String2):Order +001D === Audio Compression/Expansion === +011D ACEBootInit() +021D ACEStartUp(DirPg) +031D ACEShutDown() +041D ACEVersion():Vers +051D ACEReset() +061D ACEStatus():ActFlg +071D ACEInfo(Code):Value/4 +091D ACECompress(SrcH,SrcOff/4,DestH,DestOff/4,Blks,Method) +0A1D ACEExpand(SrcH,SrcOff/4,DestH,DestOff/4,Blks,Method) +0B1D ACECompBegin() +0C1D ACEExpBegin() +0D1D GetACEExpState(@Buffer) +0E1D SetACEExpState(@Buffer) +001E === Resource Manager === +011E ResourceBootInit() +021E ResourceStartUp(MemID) +031E ResourceShutDown() +041E ResourceVersion():Vers +051E ResourceReset() +061E ResourceStatus():ActFlag +091E CreateResourceFile(aux/4,fType,Access,@n) +0A1E OpenResourceFile(reqAcc,@mapAddr,@n):fileID +0B1E CloseResourceFile(fileID) +0C1E AddResource(H,Attr,rType,rID/4) +0D1E UpdateResourcefile(fileID) +0E1E LoadResource(rType,rID/4):H +0F1E RemoveResource(rType,rID/4) +101E MarkResourceChange(changeFlag,rType,rID/4) +111E SetCurResourceFile(fileID) +121E GetCurResourceFile():fileID +131E SetCurResourceApp(MemID) +141E GetCurResourceApp():MemID +151E HomeResourceFile(rType,rID/4):fileID +161E WriteResource(rType,rID/4) +171E ReleaseResource(PurgeLevel,rType,rID/4) +181E DetachResource(rType,rID/4) +191E UniqueResourceID(IDrange,rType):rID/4 +1A1E SetResourceID(newID/4,rType,oldID/4) +1B1E GetResourceAttr(rType,rID/4):Attr +1C1E SetResourceAttr(rAttr,rType,rID/4) +1D1E GetResourceSize(rType,rID/4):Size/4 +1E1E MatchResourceHandle(@buffer,H) +1F1E GetOpenFileRefNum(fileID):RefNum +201E CountTypes():Num +211E GetIndType(tIndex):rType +221E CountResources(rType):Num/4 +231E GetIndResource(rType,rIndex/4):rID/4 +241E SetResourceLoad(Flag):oldFlag +251E SetResourceFileDepth(Depth):oldDepth +261E GetMapHandle(fileID):MapH +271E LoadAbsResource(@loc,MaxSize/4,rType,rID/4):Size/4 +281E ResourceConverter(@proc,rType,logFlags) +291E LoadResource2(flag,@AttrBuff,rType,rID/4):H +2A1E RMFindNamedResource(rType,@name,@fileID):rID/4 +2B1E RMGetResourceName(rType,rID/4,@nameBuffer) +2C1E RMLoadNamedResource(rType,@name):H +2D1E RMSetResourceName(rType,rID/4,@name) +2E1E OpenResourceFileByID(reqAcc,userID):oldResApp +2F1E CompactResourceFile(flags,fileID) +0020 === MIDI === +0120 MidiBootInit() +0220 MidiStartUp(MemID,DirPg) +0320 MidiShutDown() +0420 MidiVersion():Vers +0520 MidiReset() +0620 MidiStatus():ActFlg +0920 MidiControl(Function,Argument/4) +0A20 MidiDevice(Function,@DriverInfo) +0B20 MidiClock(Function,Argument/4) +0C20 MidiInfo(Function):Info/4 +0D20 MidiReadPacket(@buff,size):Count +0E20 MidiWritePacket(@buff):Count +0021 === Video Overlay === +0121 VDBootInit() +0221 VDStartUp() +0321 VDShutDown() +0421 VDVersion():Vers +0521 VDReset() +0621 VDStatus():ActFlg +0921 VDInStatus(Selector):Status +0A21 VDInSetStd(InStandard) +0B21 VDInGetStd():InStandard +0C21 VDInConvAdj(Selector,AdjFunction) +0D21 VDKeyControl(Selector,KeyerCtrlVal) +0E21 VDKeyStatus(Selector):KeyerStatus +0F21 VDKeySetKCol(Red,Green,Blue) +1021 VDKeyGetKRCol():RedValue +1121 VDKeyGetKGCol():GreenValue +1221 VDKeyGetKBCol():BlueValue +1321 VDKeySetKDiss(KDissolve) +1421 VDKeyGetKDiss():KDissolve +1521 VDKeySetNKDiss(NKDissolve) +1621 VDKeyGetNKDiss():NKDissolve +1721 VDOutSetStd(OutStandard) +1821 VDOutGetStd():OutStandard +1921 VDOutControl(Selector,Value) +1A21 VDOutStatus(Selector):OutStatus +1B21 VDGetFeatures(Feature):Info +1C21 VDInControl(Selector,Value) +1D21 VDGGControl(Selector,Value) +1E21 VDGGStatus(Selector):Value +0022 === Text Edit === +0122 TEBootInit() +0222 TEStartUp(MemID,DirPg) +0322 TEShutDown() +0422 TEVersion():Vers +0522 TEReset() +0622 TEStatus():ActFlg +0922 TENew(@parms):teH +0A22 TEKill(teH) +0B22 TESetText(tDesc,tRef/4,Len/4,stDesc,stRef/4,teH) +0C22 TEGetText(bDesc,bRef/4,bLen/4,stDesc,stRef/4,teH):L/4 +0D22 TEGetTextInfo(@infoRec,parmCount,teH) +0E22 TEIdle(teH) +0F22 TEActivate(teH) +1022 TEDeactivate(teH) +1122 TEClick(@eventRec,teH) +1222 TEUpdate(teH) +1322 TEPaintText(@Port,startLn/4,@R,Flags,teH):NextLn/4 +1422 TEKey(@eventRec,teH) +1522 [not supported] +1622 TECut(teH) +1722 TECopy(teH) +1822 TEPaste(teH) +1922 TEClear(teH) +1A22 TEInsert(tDesc,tRef/4,tLen/4,stDesc,stRef/4,teH) +1B22 TEReplace(tDesc,tRef/4,tLen/4,stDesc,stRef/4,teH) +1C22 TEGetSelection(@selStart,@selEnd,teH) +1D22 TESetSelection(selStart/4,selEnd/4,teH) +1E22 TEGetSelectionStyle(@stRec,stH,teH):comFlag +1F22 TEStyleChange(flags,@stRec,teH) +2022 TEOffsetToPoint(offset/4,@vertPos,@horPos,teH) +2122 TEPointToOffset(vertPos/4,horPos/4,teH):textOffset/4 +2222 TEGetDefProc():@defProc +2322 TEGetRuler(rulerDesc,rulerRef/4,teH) +2422 TESetRuler(rulerDesc,rulerRef/4,teH) +2522 TEScroll(scrDesc,vertAmt/4,horAmt/4,teH):Offset/4 +2622 TEGetInternalProc():@proc +2722 TEGetLastError(clearFlag,teH):lastError +2822 TECompactRecord(teH) +0023 === MIDI Synth === +0123 MSBootInit() +0223 MSStartUp() +0323 MSShutDown() +0423 MSVersion():Vers +0523 MSReset() +0623 MSStatus():ActFlg +0923 SetBasicChannel(Channel) +0A23 SetMIDIMode(Mode) +0B23 PlayNote(Channel,NoteNum,KeyVel) +0C23 StopNote(Channel,NoteNum) +0D23 KillAllNotes() +0E23 SetRecTrack(TrackNum) +0F23 SetPlayTrack(TrackNum,State) +1023 TrackToChannel(TrackNum,ChannelNum) +1123 Locate(TimeStamp/4,@SeqBuff):@SeqItem +1223 SetVelComp(VelocityOffset) +1323 SetMIDIPort(EnabInput,EnabOutput) +1423 SetInstrument(@InstRec,InstNum) +1523 SeqPlayer(@SeqPlayerRec) +1623 SetTempo(Tempo) +1723 SetCallBack(@CallBackRec) +1823 SysExOut(@Msg,Delay,@MonRoutine) +1923 SetBeat(BeatDuration) +1A23 MIDIMessage(Dest,nBytes,Message,Byte1,Byte2) +1B23 LocateEnd(@seqBuffer):@End +1C23 Merge(@Buffer1,@Buffer2) +1D23 DeleteTrack(TrackNum,@Seq) +1E23 SetMetro(Volume,Freq,@Wave) +1F23 GetMSData():Reserved/4,@DirPage +2023 ConvertToTime(TkPerBt,BtPerMsr,BeatNum,MsrNum):Ticks/4 +2123 ConvertToMeasure(TkPerBt,BtPerMsr,Ticks/4):Ticks,Beat,Msr +2223 MSSuspend() +2323 MSResume() +2423 SetTuningTable(@Table) +2523 GetTuningTable(@Buffer) +2623 SetTrackOut(TrackNum,PathVal) +2723 InitMIDIDriver(Slot,Internal,UserID,@Driver) +2823 RemoveMIDIDriver() +0026 === Media Controller === +0126 MCBootInit() +0226 MCStartUp(MemID) +0326 MCShutDown() +0426 MCVersion():Vers +0526 MCReset() +0626 MCStatus():ActFlg +0926 MCGetErrorMsg(mcErrorNo,@PStringBuff) +0A26 MCLoadDriver(mcChannelNo) +0B26 MCUnLoadDriver(mcChannelNo) +0C26 MCTimeToBin(mcTimeValue/4):result/4 +0D26 MCBinToTime(mcBinVal/4):result/4 +0E26 MCGetTrackTitle(mcDiskID/4,mcTrackNo,@PStringBuff) +0F26 MCSetTrackTitle(mcDiskID/4,TrackNum,@title) +1026 MCGetProgram(mcDiskID/4,@resultBuff) +1126 MCSetProgram(mcDiskID/4,@mcProg) +1226 MCGetDiscTitle(mcDiskID/4,@PStringBuff) +1326 MCSetDiscTitle(mcDiskID/4,@title) +1426 MCDStartUp(mcChannelNo,@portName,userID) +1526 MCDShutDown(mcChannelNo) +1626 MCGetFeatures(mcChannelNo,mcFeatSel):result/4 +1726 MCPlay(mcChannelNo) +1826 MCPause(mcChannelNo) +1926 MCSendRawData(mcChannelNo,@mcNative) +1A26 MCGetStatus(mcChannelNo,mcStatusSel):result +1B26 MCControl(mcChannelNo,ctlCommand) +1C26 MCScan(mcChannelNo,mcDirection) +1D26 MCGetSpeeds(mcChannelNo,@PStringBuff) +1E26 MCSpeed(mcChannelNo,mcFPS) +1F26 MCStopAt(mcChannelNo,mcUnitType,mcStopLoc/4) +2026 MCJog(mcChannelNo,mcUnitType,mcNJog/4,mcJogRepeat) +2126 MCSearchTo(mcChannelNo,mcUnitType,searchLoc/4) +2226 MCSearchDone(mcChannelNo):result +2326 MCSearchWait(mcChannelNo) +2426 MCGetPosition(mcChannelNo,mcUnitType):result/4 +2526 MCSetAudio(mcChannelNo,mcAudioCtl) +2626 MCGetTimes(mcChannelNo,mctimesSel):result/4 +2726 MCGetDiscTOC(mcChannelNo,mcTrackNo):result/4 +2826 MCGetDiscID(mcChannelNo):result/4 +2926 MCGetNoTracks(mcChannelNo):result +2A26 MCRecord(mcChannelNo) +2B26 MCStop(mcChannelNo) +2C26 MCWaitRawData(mcChannelNo,@result,tickWait,termMask) +2D26 MCGetName(mcChannelNo,@PStringBuff) +2E26 MCSetVolume(mcChannelNo,mcLeftVol,mcRightVol) +0032 === Male Voice === +0132 MaleBootInit() +0232 MaleStartUp() +0332 MaleShutDown() +0432 MaleVersion():Vers +0532 MaleReset() +0632 MaleStatus():ActFlg +0932 MaleSpeak(Vol,Speed,Pitch,@PhonStr) +0033 === Female Voice === +0133 FemaleBootInit() +0233 FemaleStartUp() +0333 FemaleShutDown() +0433 FemaleVersion():Vers +0533 FemaleReset() +0633 FemaleStatus():ActFlg +0933 FemaleSpeak(Vol,Speed,Pitch,@PhonStr) +0034 === TML Speech Toolkit parser === +0134 SpeechBootInit() +0234 SpeechStartUp(MemID) +0334 SpeechShutDown() +0434 SpeechVersion():Vers +0534 SpeechReset() +0634 SpeechStatus():ActFlg +0934 Parse(@EnglStr,@PhonStr,Start) +0A34 DictInsert(@EnglStr,@PhonStr) +0B34 DictDelete(@EnglStr) +0C34 DictDump(@EnglStr,@PhonStr):@Str; +0D34 SetSayGlobals(Gend,Tone,Pitch,Spd,Vol) +0E34 DictInit(Flag) +0F34 Say(@EnglishStr) +1034 Activate... +0042 === Finder (error codes only) === +00FF === GSBug === +04FF DebugVersion():Vers +06FF DebugStatus():ActFlg +09FF DebugStr(@PStr) +0AFF SetMileStone(@PStr) +0BFF DebugSetHook(@hook) +0CFF DebugGetInfo(selector):Info/4 +0DFF DebugControl(data/4,extraData/4,operation,type) +0EFF DebugQuery(data/4,operation,type):Info/4 +* User tools +0000 === User Tools === +0001 === fakeModalDialog (DTS) === +0101 fmdBootInit() +0201 fmdStartUp() +0301 fmdShutDown() +0401 fmdVersion():Vers +0501 fmdReset() +0601 fmdStatus():ActFlg +0901 fakeModalDialog(@Event,@Update,@EvHook,@Beep,Flags):Result/4 +0A01 fmdSetMenuProc(@MenuProc) +0B01 fmdGetMenuProc():@MenuProc +0C01 fmdStdDrawProc() +0D01 fmdEditMenu() +0E01 fmdFindCursorCtl(@CtrlH,x,y,@Wind):PartCode +0F01 fmdLESetText(@Wind,leID/4,@PString) +1001 fmdLEGetText(@Wind,leID/4,@PString) +1101 fmdWhichRadio(@Wind,RadioID/4):WhichRadio +1201 fmdIBeamCursor() +1301 fmdInitIBeam() +1401 fmdSetIBeam(@Cursor) +1501 fmdGetIBeamAdr():@Cursor +1601 fmdGetCtlPart():Part +1701 fmdGetError():Error +0002 === PixelMap Tools (DTS) === +0102 pmapBootInit() +0202 pmapStartUp() +0302 pmapShutDown() +0402 pmapVersion():Vers +0502 pmapReset() +0602 pmapStatus():ActFlg +0902 pixelMap2Rgn(@srcLoc,bitsPerPix,colorMask):RgnH +0A02 newPort(@pmapPortInfo):@port +0B02 killPort(@pmapPortInfo) +* E1xxxx vectors +0000 System Tool dispatcher +0004 System Tool dispatcher, glue entry +0008 User Tool dispatcher +000C User Tool dispatcher, glue entry +0010 Interrupt mgr +0014 COP mgr +0018 Abort mgr +001C System Death mgr +0020 AppleTalk interrupt +0024 Serial interrupt +0028 Scanline interrupt +002C Sound interrupt +0030 VertBlank interrupt +0034 Mouse interrupt +0038 1/4 sec interrupt +003C Keyboard interrupt +0040 ADB Response byte int +0044 ADB SRQ int +0048 Desk Acc mgr +004C FlushBuffer handler +0050 KbdMicro interrupt +0054 1 sec interrupt +0058 External VGC int +005C other interrupt +0060 Cursor update +0064 IncBusy +0068 DecBusy +006C Bell vector +0070 Break vector +0074 Trace vector +0078 Step vector +007C [install ROMdisk] +0080 ToWriteBram +0084 ToReadBram +0088 ToWriteTime +008C ToReadTime +0090 ToCtrlPanel +0094 ToBramSetup +0098 ToPrintMsg8 +009C ToPrintMsg16 +00A0 Native Ctrl-Y +00A4 ToAltDispCDA +00A8 ProDOS 16 [inline parms] +00AC OS vector +00B0 GS/OS(@parms,call) [stack parms] +00B4 OS_P8_Switch +00B8 OS_Public_Flags +00BC OS_KIND (byte: 0=P8,1=P16) +00BD OS_BOOT (byte) +00BE OS_BUSY (bit 15=busy) +00C0 MsgPtr +0180 ToBusyStrip +0184 ToStrip +01B2 MidiInputPoll +0200 Memory Mover +0204 Set System Speed +0208 Slot Arbiter +0220 HyperCard IIgs callback +0224 WordForRTL +1004 ATLK: BASIC +1008 ATLK: Pascal +100C ATLK: RamGoComp +1010 ATLK: SoftReset +1014 ATLK: RamDispatch +1018 ATLK: RamForbid +101C ATLK: RamPermit +1020 ATLK: ProEntry +1022 ATLK: ProDOS +1026 ATLK: SerStatus +102A ATLK: SerWrite +102E ATLK: SerRead +103A init file hook +103E ATLK: PFI Vector +D600 ATLK: CmdTable +DA00 ATLK: TickCount +* E0xxxx vectors +1E04 QD:StdText +1E08 QD:StdLine +1E0C QD:StdRect +1E10 QD:StdRRect +1E14 QD:StdOval +1E18 QD:StdArc +1E1C QD:StdPoly +1E20 QD:StdRgn +1E24 QD:StdPixels +1E28 QD:StdComment +1E2C QD:StdTxMeas +1E30 QD:StdTxBnds +1E34 QD:StdGetPic +1E38 QD:StdPutPic +1E98 QD:ShieldCursor +1E9C QD:UnShieldCursor +* softswitches and F8 ROM routines +B000 Dvx: xgetparm_ch +B003 Dvx: xgetparm_n +B006 Dvx: xmess +B009 Dvx: xprint_ftype +B00C Dvx: xprint_access +B00F Dvx: xprdec_2 +B012 Dvx: xprdec_3 +B015 Dvx: xprdec_pad +B018 Dvx: xprint_path +B01B Dvx: xbuild_local +B01E Dvx: xprint_sd +B021 Dvx: xprint_drvr +B024 Dvx: xredirect +B027 Dvx: xpercent +B02A Dvx: xyesno +B02D Dvx: xgetln +B030 Dvx: xbell +B033 Dvx: xdowncase +B036 Dvx: xplural +B039 Dvx: xcheck_wait +B03C Dvx: xpr_date_ay +B03F Dvx: xpr_time_ay +B042 Dvx: xProDOS_err +B045 Dvx: xProDOS_er +B048 Dvx: xerr +B04B Dvx: xprdec_pady +B04E Dvx: xdir_setup +B051 Dvx: xdir_finish +B054 Dvx: xread1dir +B057 Dvx: xpmgr +B05A Dvx: xmmgr +B05D Dvx: xpoll_io +B060 Dvx: xprint_ver +B063 Dvx: xpush_level +B066 Dvx: xfman_open +B069 Dvx: xfman_read +B06C Dvx: xrdkey (v1.1+) +B06F Dvx: xdirty (v1.1+) +B072 Dvx: xgetnump (v1.1+) +B075 Dvx: xyesno2 (v1.2+) +B078 Dvx: xdir_setup2 (v1.23+) +B07B Dvx: xshell_info (v1.25+) +C000 r:KBD w:CLR80COL +C001 w:SET80COL +C002 w:RDMAINRAM +C003 w:RDCARDRAM +C004 w:WRMAINRAM +C005 w:WRCARDRAM +C006 w:SETSLOTCXROM +C007 w:SETINTCXROM +C008 w:SETSTDZP +C009 w:SETALTZP +C00A w:SETINTC3ROM +C00B w:SETSLOTC3ROM +C00C w:CLR80VID +C00D w:SET80VID +C00E w:CLRALTCHAR +C00F w:SETALTCHAR +C010 r:KBDSTRB +C011 r:RDLCBNK2 +C012 r:RDLCRAM +C013 r:RDRAMRD +C014 r:RDRAMWRT +C015 r:RDCXROM +C016 r:RDALTZP +C017 r:RDC3ROM +C018 r:RD80COL +C019 r:RDVBLBAR +C01A r:RDTEXT +C01B r:RDMIX +C01C r:RDPAGE2 +C01D r:RDHIRES +C01E r:ALTCHARSET +C01F r:RD80VID +C020 reserved [cassette] +C021 rw:MONOCOLOR +C022 rw:TBCOLOR +C023 rw:VGCINT +C024 r:MOUSEDATA +C025 r:KEYMODREG +C026 rw:DATAREG [key micro] +C027 rw:KMSTATUS +C028 rw:ROMBANK [IIc Plus] +C029 rw:NEWVIDEO +C02B rw:LANGSEL +C02C r:CHARROM +C02D rw:SLTROMSEL +C02E r:VERTCNT +C02F r:HORIZCNT +C030 rw:SPKR +C031 rw:DISKREG +C032 w:SCANINT +C033 rw:CLOCKDATA +C034 rw:CLOCKCTL [+border color] +C035 rw:SHADOW +C036 rw:CYAREG +C037 rw:DMAREG +C038 rw:SCCBREG +C039 rw:SCCAREG +C03A rw:SCCBDATA +C03B rw:SCCADATA +C03C rw:SOUNDCTL +C03D rw:SOUNDDATA +C03E rw:SOUNDADRL +C03F rw:SOUNDADRH +C040 reserved [C040 Strobe] +C041 *rw:INTEN +C044 *r:MMDELTAX +C045 *r:MMDELTAY +C046 w:DIAGTYPE r:INTFLAG +C047 w:CLRVBLINT +C048 w:CLRXYINT +C050 rw:TXTCLR +C051 rw:TXTSET +C052 rw:MIXCLR +C053 rw:MIXSET +C054 rw:TXTPAGE1 +C055 rw:TXTPAGE2 +C056 rw:LORES +C057 rw:HIRES +C058 rw:SETAN0 +C059 rw:CLRAN0 +C05A rw:SETAN1 +C05B rw:CLRAN1 +C05C rw:SETAN2 +C05D rw:CLRAN2 +C05E rw:SETAN3 +C05F rw:CLRAN3 +C060 r:BUTN3 +C061 r:BUTN0 +C062 r:BUTN1 +C063 r:BUTN2 +C064 r:PADDL0 +C065 r:PADDL1 +C066 r:PADDL2 +C067 r:PADDL3 +C068 rw:STATEREG +C06D *TESTREG +C06E *CLTRM +C06F *ENTM +C070 rw:PTRIG +C081 rw:ROMIN +C083 rw:LCBANK2 +C08B rw:LCBANK1 +C0E0 IWM:PH0 off +C0E1 IWM:PH0 on +C0E2 IWM:PH1 off +C0E3 IWM:PH1 on +C0E4 IWM:PH2 off +C0E5 IWM:PH2 on +C0E6 IWM:PH3 off +C0E7 IWM:PH3 on +C0E8 IWM:motor off +C0E9 IWM:motor on +C0EA IWM:drive 1 +C0EB IWM:drive 2 +C0EC IWM:Q6 OFF (Read) +C0ED IWM:Q6 ON (WP-sense) +C0EE IWM:Q7 OFF (WP-sense/Read) +C0EF IWM:Q7 ON (Write) +C311 ROM:AUXMOVE +C314 ROM:XFER +CFFF rw:CLRROM +F800 F8ROM:PLOT +F80E F8ROM:PLOT1 +F819 F8ROM:HLINE +F828 F8ROM:VLINE +F832 F8ROM:CLRSCR +F836 F8ROM:CLRTOP +F847 F8ROM:GBASCALC +F85F F8ROM:NXTCOL +F864 F8ROM:SETCOL +F871 F8ROM:SCRN +F88C F8ROM:INSDS1.2 +F88E F8ROM:INSDS2 +F890 F8ROM:GET816LEN +F8D0 F8ROM:INSTDSP +F940 F8ROM:PRNTYX +F941 F8ROM:PRNTAX +F944 F8ROM:PRNTX +F948 F8ROM:PRBLNK +F94A F8ROM:PRBL2 +F953 F8ROM:PCADJ +F962 F8ROM:TEXT2COPY +FA40 F8ROM:OLDIRQ +FA4C F8ROM:BREAK +FA59 F8ROM:OLDBRK +FA62 F8ROM:RESET +FAA6 F8ROM:PWRUP +FABA F8ROM:SLOOP +FAD7 F8ROM:REGDSP +FB19 F8ROM:RTBL +FB1E F8ROM:PREAD +FB21 F8ROM:PREAD4 +FB2F F8ROM:INIT +FB39 F8ROM:SETTXT +FB40 F8ROM:SETGR +FB4B F8ROM:SETWND +FB51 F8ROM:SETWND2 +FB5B F8ROM:TABV +FB60 F8ROM:APPLEII +FB6F F8ROM:SETPWRC +FB78 F8ROM:VIDWAIT +FB88 F8ROM:KBDWAIT +FBB3 F8ROM:VERSION +FBBF F8ROM:ZIDBYTE2 +FBC0 F8ROM:ZIDBYTE +FBC1 F8ROM:BASCALC +FBDD F8ROM:BELL1 +FBE2 F8ROM:BELL1.2 +FBE4 F8ROM:BELL2 +FBF0 F8ROM:STORADV +FBF4 F8ROM:ADVANCE +FBFD F8ROM:VIDOUT +FC10 F8ROM:BS +FC1A F8ROM:UP +FC22 F8ROM:VTAB +FC24 F8ROM:VTABZ +FC42 F8ROM:CLREOP +FC58 F8ROM:HOME +FC62 F8ROM:CR +FC66 F8ROM:LF +FC70 F8ROM:SCROLL +FC9C F8ROM:CLREOL +FC9E F8ROM:CLREOLZ +FCA8 F8ROM:WAIT +FCB4 F8ROM:NXTA4 +FCBA F8ROM:NXTA1 +FCC9 F8ROM:HEADR +FD0C F8ROM:RDKEY +FD10 F8ROM:FD10 +FD18 F8ROM:RDKEY1 +FD1B F8ROM:KEYIN +FD35 F8ROM:RDCHAR +FD67 F8ROM:GETLNZ +FD6A F8ROM:GETLN +FD6C F8ROM:GETLN0 +FD6F F8ROM:GETLN1 +FD8B F8ROM:CROUT1 +FD8E F8ROM:CROUT +FD92 F8ROM:PRA1 +FDDA F8ROM:PRBYTE +FDE3 F8ROM:PRHEX +FDED F8ROM:COUT +FDF0 F8ROM:COUT1 +FDF6 F8ROM:COUTZ +FE1F F8ROM:IDROUTINE +FE2C F8ROM:MOVE +FE5E F8ROM:LIST (not on GS) +FE80 F8ROM:SETINV +FE84 F8ROM:SETNORM +FE89 F8ROM:SETKBD +FE8B F8ROM:INPORT +FE93 F8ROM:SETVID +FE95 F8ROM:OUTPORT +FEB6 F8ROM:GO +FECD F8ROM:WRITE +FEFD F8ROM:READ +FF2D F8ROM:PRERR +FF3A F8ROM:BELL +FF3F F8ROM:RESTORE +FF4A F8ROM:SAVE +FF58 F8ROM:IORTS +FF59 F8ROM:OLDRST +FF65 F8ROM:MON +FF69 F8ROM:MONZ +FF6C F8ROM:MONZ2 +FF70 F8ROM:MONZ4 +FF8A F8ROM:DIG +FFA7 F8ROM:GETNUM +FFAD F8ROM:NXTCHR +FFBE F8ROM:TOSUB +FFC7 F8ROM:ZMODE +* 01xxxx vectors +FC00 SysSrv: DEV_DISPATCHER +FC04 SysSrv: CACHE_FIND_BLK +FC08 SysSrv: CACHE_ADD_BLK +FC14 SysSrv: CACHE_DEL_BLK +FC1C SysSrv: ALLOC_SEG +FC20 SysSrv: RELEASE_SEG +FC34 SysSrv: SWAP_OUT +FC38 SysSrv: DEREF +FC50 SysSrv: SET_SYS_SPEED +FC68 SysSrv: LOCK_MEM +FC6C SysSrv: UNLOCK_MEM +FC70 SysSrv: MOVE_INFO +FC88 SysSrv: SIGNAL +FC90 SysSrv: SET_DISK_SW +FCA4 SysSrv: SUP_DRVR_DISP +FCA8 SysSrv: INSTALL_DRIVER +FCBC SysSrv: DYN_SLOT_ARBITER +FCD8 SysSrv: UNBIND_INT_VEC +* Nifty List service calls +0000 NLServ: nlRecover +0001 NLServ: nlEnter +0002 NLServ: nlRemoveNL +0003 NLServ: nlGetInfo +0004 NLServ: nlInstallHook +0005 NLServ: nlRemoveHook +0006 NLServ: nlGetDirectory():@dir +0007 NLServ: nlNewSession(@callback):sessRef +0008 NLServ: nlKillSession(sessRef) +0009 NLServ: nlSetSession(sessRef):oldRef +000A NLServ: nlWelcome +0010 NLServ: nlGetFirstHandle +0011 NLServ: nlGetHandleInfo +0012 NLServ: nlLookup +0013 NLServ: nlIndLookup +0014 NLServ: nlGetProcName(@proc):@pString +0020 NLServ: nlScanHandles +0021 NLServ: nlDisasm1 +0022 NLServ: nlExecCmdLine +0023 NLServ: nlGetRange +0024 NLServ: nlGetAGlobal(ref):value +0025 NLServ: nlSetAGlobal(@(ref,value)) +0026 NLServ: nlAbortToCmd +0030 NLServ: nlWriteChar +0031 NLServ: nlShowChar +0032 NLServ: nlWriteStr +0033 NLServ: nlShowStr +0034 NLServ: nlWriteCStr +0035 NLServ: nlShowCStr +0036 NLServ: nlWriteText +0037 NLServ: nlShowText +0038 NLServ: nlWriteByte +0039 NLServ: nlWriteWord +003A NLServ: nlWritePtr +003B NLServ: nlWriteLong +003C NLServ: nlGetLn +003D NLServ: nlGetChar +003E NLServ: nlCheckKey +003F NLServ: nlCrout +0040 NLServ: nlSpout +0041 NLServ: nlPause +0042 NLServ: nlHandleInfo +0043 NLServ: nlWriteNoVoice(@cStr) +0044 NLServ: nlShowWString(@wStr) +0050 NLServ: nlChrGet +0051 NLServ: nlChrGot +0052 NLServ: nlEatBlanks() +0054 NLServ: nlEvalExpr(@exprBuff):exprLen +0060 NLServ: nlGetByte(@addr):byte +0061 NLServ: nlGetWord(@addr):word +0062 NLServ: nlGetLong(@addr):long +* resource type names +8001 rIcon +8002 rPicture +8003 rControlList +8004 rControlTemplate +8005 rC1InputString +8006 rPString +8007 rStringList +8008 rMenuBar +8009 rMenu +800A rMenuItem +800B rTextForLETextBox2 +800C rCtlDefProc +800D rCtlColorTbl +800E rWindParam1 +800F rWindParam2 +8010 rWindColor +8011 rTextBlock +8012 rStyleBlock +8013 rToolStartup +8014 rResName +8015 rAlertString +8016 rText +8017 rCodeResource +8018 rCDEVCode +8019 rCDEVFlags +801A rTwoRects +801B rFileType +801C rListRef +801D rCString +801E rXCMD +801F rXFCN +8020 rErrorString +8021 rKTransTable +8022 rWString +8023 rC1OutputString +8024 rSoundSample +8025 rTERuler +8026 rFSequence +8027 rCursor +8028 rItemStruct +8029 rVersion +802A rComment +802B rBundle +802C rFinderPath +802D rPaletteWindow +802E rTaggedStrings +802F rPatternList +C001 rRectList +C002 rPrintRecord +C003 rFont +* Error codes +0001 OS:bad call number / dispatcher:toolset not found +0002 function not found +0004 OS:bad parameter count +0007 GS/OS is busy +0010 GS/OS:device not found +0011 GS/OS:bad device number +0020 GS/OS:invalid driver request +0021 GS/OS:invalid driver control or status code +0022 GS/OS:bad call parameter +0023 GS/OS:character device not open +0024 GS/OS:character device already open +0025 OS:interrupt table full +0026 GS/OS:resources not available +0027 OS:I/O error +0028 OS:no device connected +0029 GS/OS:driver is busy +002B OS:disk write protected +002C GS/OS:invalid byte count +002D GS/OS:invalid block address +002E OS:disk switched +002F OS:no disk +0040 OS:bad pathname +0042 OS:max number of files already open +0043 OS:bad file reference number +0044 OS:directory not found +0045 OS:volume not found +0046 OS:file not found +0047 OS:duplicate filename +0048 OS:volume full +0049 OS:volume directory full +004A OS:incompatible file format +004B OS:unsupported storage type +004C OS:end of file encountered +004D OS:position out of range +004E OS:access not allowed +004F GS/OS:buffer too small +0050 OS:file is open +0051 OS:directory damaged +0052 OS:unknown volume type +0053 OS:parameter out of range +0054 GS/OS:out of memory +0055 P8:volume control block table full +0056 P8:bad buffer address +0057 OS:duplicate volume name +0058 GS/OS:not a block device +0059 GS/OS:file level out of range +005A OS:bad bitmap address (block # too large) +005B GS/OS:invalid pathnames for ChangePath +005C GS/OS:not an executable file +005D GS/OS:Operating System not supported +005F GS/OS:too many applications on stack +0060 GS/OS:data unavailable +0061 GS/OS:end of directory +0062 GS/OS:invalid FST call class +0063 GS/OS:file doesn't have a resource fork +0064 GS/OS:invalidFSTID +0065 GS/OS:invalid FST operation +0066 GS/OS:fstCaution +0067 GS/OS:devNameErr +0068 GS/OS:devListFull +0069 GS/OS:supListFull +006A GS/OS:fstError (generic) +0070 GS/OS:resExistsErr +0071 GS/OS:resAddErr +0088 network error +0110 toolVersionErr +0111 messNotFoundErr +0112 messageOvfl +0113 srqNameTooLong +0120 reqNotAccepted +0121 duplicateName +0122 invalidSendRequest +0201 memErr (couldn't allocate memory) +0202 emptyErr +0203 notEmptyErr +0204 lockErr +0205 purgeErr +0206 handleErr +0207 idErr +0208 attrErr +0301 badInputErr +0302 noDevParamErr +0303 taskInstlErr +0304 noSigTaskErr +0305 queueDmgdErr +0306 taskNtFdErr +0307 firmTaskErr +0308 hbQueueBadErr +0309 unCnctdDevErr +030B idTagNtAvlErr +034F mtBuffTooSmall +0381 invalidTag +0382 alreadyInQueue +0390 badTimeVerb +0391 badTimeData +0401 alreadyInitialized +0402 cannotReset +0403 notInitialized +0410 screenReserved +0411 badRect +0420 notEqualChunkiness +0430 rgnAlreadyOpen +0431 rgnNotOpen +0432 rgnScanOverflow +0433 rgnFull +0440 polyAlreadyOpen +0441 polyNotOpen +0442 polyTooBig +0450 badTableNum +0451 badColorNum +0452 badScanLine +0510 daNotFound +0511 notSysWindow +0520 deskBadSelector +0601 emDupStrtUpErr +0602 emResetErr +0603 emNotActErr +0604 emBadEvtCodeErr +0605 emBadBttnNoErr +0606 emQSiz2LrgErr +0607 emNoMemQueueErr +0681 emBadEvtQErr +0682 emBadQHndlErr +0810 noDOCFndErr +0811 docAddrRngErr +0812 noSAppInitErr +0813 invalGenNumErr +0814 synthModeErr +0815 genBusyErr +0817 mstrIRQNotAssgnErr +0818 sndAlreadyStrtErr +08FF uncleamedSntIntErr +0910 cmndIncomplete +0911 cantSync +0982 adbBusy +0983 devNotAtAddr +0984 srqListFull +0B01 imBadInptParam +0B02 imIllegalChar +0B03 imOverflow +0B04 imStrOverflow +0C01 badDevType +0C02 badDevNum +0C03 badMode +0C04 unDefHW +0C05 lostDev +0C06 lostFile +0C07 badTitle +0C08 noRoom +0C09 noDevice +0C0B dupFile +0C0C notClosed +0C0D notOpen +0C0E badFormat +0C0F ringBuffOFlo +0C10 writeProtected +0C40 devErr +0E01 paramLenErr +0E02 allocateErr +0E03 taskMaskErr +0E04 compileTooLarge +0E05 cantUpdateErr +0F01 menuStarted +0F02 menuItemNotFound +0F03 menuNoStruct +0F04 dupMenuID +1001 wmNotStartedUp +1002 cmNotInitialized +1003 noCtlInList +1004 noCtlError +1005 notExtendedCtlError +1006 noCtlTargetError +1007 notExtendedCtlError +1008 canNotBeTargetError +1009 noSuchIDError +100A tooFewParmsError +100B noCtlToBeTargetError +100C noFrontWindowError +1101 idNotFound / segment not found? +1102 OMF version error +1103 idPathnameErr +1104 idNotLoadFile +1105 idBusyErr +1107 idFilVersErr +1108 idUserIDErr +1109 idSequenceErr +110A idBadRecordErr +110B idForeignSegErr +1210 picEmpty +1211 picAlreadyOpen / badRectSize? +1212 pictureError / destModeError? +121F bad picture opcode +1221 badRect +1222 badMode +1230 badGetSysIconInput +1301 missingDriver +1302 portNotOn +1303 noPrintRecord +1304 badLaserPrep +1305 badLPFile +1306 papConnNotOpen +1307 papReadWriteErr +1308 ptrConnFailed +1309 badLoadParam +130A callNotSupported +1321 startUpAlreadyMade +1401 leDupStartUpErr +1402 leResetErr +1403 leNotActiveErr +1404 leScrapErr +150A badItemType +150B newItemFailed +150C itemNotFound +150D notModalDialog +1610 badScrapType +1701 badPromptDesc +1702 badOrigNameDesc +1704 badReplyNameDesc +1705 badReplyPathDesc +1706 badCall +17FF sfNotStarted +1901 nsAlreadyInit +1902 nsSndNotInit +1921 nsNotAvail +1922 nsBadGenNum +1923 nsNotInit +1924 nsGenAlreadyOn +1925 soundWrongVer +1A00 noRoomMidiErr +1A01 noCommandErr +1A02 noRoomErr +1A03 startedErr +1A04 noNoteErr +1A05 noStartErr +1A06 instBndsErr +1A07 nsWrongVer +1B01 fmDupStartUpErr +1B02 fmResetErr +1B03 fmNotActiveErr +1B04 fmFamNotFndErr +1B05 fmFontNtFndErr +1B06 fmFontMemErr +1B07 fmSysFontErr +1B08 fmBadFamNumErr +1B09 fmBadSizeErr +1B0A fmBadNameErr +1B0B fmMenuErr +1B0C fmScaleSizeErr +1B0D fmBadParmErr +1C02 listRejectEvent +1D01 aceIsActive +1D02 aceBadDP +1D03 aceNotActive +1D04 aceNoSuchParam +1D05 aceBadMethod +1D06 aceBadSrc +1D07 aceBadDest +1D08 aceDataOverlap +1E01 resForkUsed +1E02 resBadFormat +1E03 resNoConverter +1E04 resNoCurFile +1E05 resDupID +1E06 resNotFound +1E07 resFileNotFound +1E08 resBadAppID +1E09 resNoUniqueID +1E0A resIndexRange +1E0B resSysIsOpen +1E0C resHasChanged +1E0D resDiffConverter +1E0E resDiskFull +1E0F resInvalidShutDown +1E10 resNameNotFound +1E11 resBadNameVers +1E12 resDupStartUp +1E13 resInvalidTypeOrID +2000 miStartUpErr +2001 miPacketErr +2002 miArrayErr +2003 miFullbufErr +2004 miToolsErr +2005 miOutOffErr +2007 miNoBufErr +2008 miDriverErr +2009 miBadFreqErr +200A miClockErr +200B miConflictErr +200C miNoDevErr +2080 miDevNotAvail +2081 miDevSlotBusy +2082 miDevBusy +2083 miDevOverrun +2084 miDevNoConnect +2085 miDevReadErr +2086 miDevVersion +2087 miDevIntHndlr +2110 vdNoVideoDevice +2111 vdAlreadyStarted +2112 vdInvalidSelector +2113 vdInvalidParam +21FF vdUnImplemented +2201 teAlreadyStarted +2202 teNotStarted +2203 teInvalidHandle +2204 teInvalidDescriptor +2205 teInvalidFlag +2206 teInvalidPCount +2208 teBufferOverflow +2209 teInvalidLine +220B teInvalidParameter +220C teInvalidTextBox2 +220D teNeedsTools +2301 msAlreadyStarted +2302 msNotStarted +2303 msNoDPMem +2304 msNoMemBlock +2305 msNoMiscTool +2306 msNoSoundTool +2307 msGenInUse +2308 msBadPortNum +2309 msPortBusy +230A msParamRangeErr +230B msMsgQueueFull +230C msRecBufFull +230D msOutputDisabled +230E msMessageError +230F msOutputBufFull +2310 msDriverNotStarted +2311 msDriverAlreadySet +2380 msDevNotAvail +2381 msDevSlotBusy +2382 msDevBusy +2383 msDevOverrun +2384 msDevNoConnect +2385 msDevReadErr +2386 msDevVersion +2387 msDevIntHndlr +2601 mcUnimp +2602 mcBadSpeed +2603 mcBadUnitType +2604 mcTimeOutErr +2605 mcNotLoaded +2606 mcBadAudio +2607 mcDevRtnError +2608 mcUnrecStatus +2609 mcBadSelector +260A mcFunnyData +260B mcInvalidPort +260C mcOnlyOnce +260D mcNoResMgr +260E mcItemNotThere +260F mcWasShutDown +2610 mcWasStarted +2611 mcBadChannel +2612 mcInvalidParam +2613 mcCallNotSupported +4201 fErrBadInput +4202 fErrFailed +4203 fErrCancel +4204 fErrDimmed +4205 fErrBusy +4206 fErrNotPrudent +4207 fErrBadBundle +42FF fErrNotImp +FF01 debugUnImpErr +FF02 debugBadSelErr +FF03 debugDupBreakErr +FF04 debugBreakNotSetErr +FF05 debugTableFullErr +FF06 debugTableEmptyErr +FF07 debugBreaksInErr +* HyperCardIIgs callbacks +0001 HC:SendCardMessage(@Str) +0002 HC:EvalExpr(@Str):H +0003 HC:StringLength(@Str):Length/4 +0004 HC:StringMatch(@Pattern,@Target):@Ptr +0005 HC:SendHCMessage(@Msg) +0006 HC:ZeroBytes(@Ptr,Count/4) +0007 HC:PasToZero(@Str):StringH +0008 HC:ZeroToPas(@ZeroStr,@Str) +0009 HC:StrToLong(@Str31):Long/4 +000A HC:StrToNum(@Str31):Long/4 +000B HC:StrToBool(@Str31):Boolean +000C HC:StrToExt(@Str31):@Extended +000D HC:LongToStr(posNum/4):@Str31 +000E HC:NumToStr(Num/4):@Str31 +000F HC:NumToHex(Num/4,nDigits):@Str31 +0010 HC:BoolToStr(Boolean):@Str31 +0011 HC:ExtToStr(@Extended):@Str31 +0012 HC:GetGlobal(@GlobalName):ValueH +0013 HC:SetGlobal(@GlobalName,GlobValueH) +0014 HC:GetFieldByName(cardFieldFlag,@FieldName):H +0015 HC:GetFieldByNum(cardFieldFlag,fieldNum):H +0016 HC:GetFieldByID(cardFieldFlag,fieldID):H +0017 HC:SetFieldByName(cardFieldFlag,@fieldName,ValueH) +0018 HC:SetFieldByNum(cardFieldFlag,fieldNum,ValueH) +0019 HC:SetFieldByID(cardFieldFlag,fieldID,ValueH) +001A HC:StringEqual(@Str1,@Str2):Boolean +001B HC:ReturnToPas(@ZeroStr,@Str) +001C HC:ScanToReturn(@PtrToPtr) +001D HC:ScanToZero(@PtrToPtr) +001E HC:GSToPString(GStringH):@Str +001F HC:PToGSString(@Str):GStringH +0020 HC:CopyGSString(GStringH):GString2H +0021 HC:GSConcat(GString1H,GString2H):NewGStringH +0022 HC:GSStringEqual(GString1H,GString2H):Boolean +0023 HC:GSToZero(GStringH):ZeroH +0024 HC:ZeroToGS(ZeroH):GStringH +0025 HC:LoadNamedResource(whichType,@Name):H +0026 HC:FindNamedResource(Type,@Name,@File,@ID/4):Bool +0027 HC:SetResourceName(Type,ID/4,@Name) +0028 HC:GetResourceName(Type,ID/4):@Str +0029 HC:BeginXSound() +002A HC:EndXSound() +002B HC:GetMaskAndData(@MaskLocInfo,@DataLocInfo) +002C HC:ChangedMaskAndData(whatChanged) +002D HC:PointToStr(Point/4,@String) +002E HC:RectToStr(@Rect,@String) +002F HC:StrToPoint(@String,@Point) +0030 HC:StrToRect(@String,@Rect) +0031 HC:NewXWindow(@BoundsR,@Title,visFlg,windStyle):WindowPtr +0032 HC:SetXWIdleTime(@Window,Interval/4) +0033 HC:CloseXWindow(@Window) +0034 HC:HideHCPalettes() +0035 HC:ShowHCPalettes() +0036 HC:SetXWindowValue(@Window,Value/4) +0037 HC:GetXWindowValue(@Window):Value/4 +0038 HC:XWAllowReEntrancy(@Window,SysEvts,HCEvts) +* Request Codes +0001 systemSaysBeep +0002 systemSaysUnknownDisk +0003 srqGoAway +0004 srqGetrSoundSample +0005 srqSynchronize +0006 srqPlayrSoundSample +0008 systemSaysNewDeskMsg +000C systemSaysDoClipboard +000D systemSaysForceUndim +000E systemSaysEjectingDev +0010 srqOpenOrPrint +0011 srqQuit +0100 finderSaysHello +0101 finderSaysGoodbye +0102 finderSaysSelectionChanged +0103 finderSaysMItemSelected +0104 finderSaysBeforeOpen +0105 finderSaysOpenFailed +0106 finderSaysBeforeCopy +0107 finderSaysIdle +0108 finderSaysExtrasChosen +0109 finderSaysBeforeRename +010A finderSaysKeyHit +0502 systemSaysDeskStartUp +0503 systemSaysDeskShutDown +051E systemSaysFixedAppleMenu +0F01 systemSaysMenuKey +1201 systemSaysGetSysIcon +8000 tellFinderGetDebugInfo (or srqMountServer to EasyMount) +8001 askFinderAreYouThere +8002 tellFinderOpenWindow +8003 tellFinderCloseWindow +8004 tellFinderGetSelectedIcons +8005 tellFinderSetSelectedIcons +8006 tellFinderLaunchThis +8007 tellFinderShutDown +8008 tellFinderMItemSelected +800A tellFinderMatchFileToIcon +800B tellFinderAddBundle +800C tellFinderAboutChange +800D tellFinderCheckDatabase +800E tellFinderColorSelection +800F tellFinderAddToExtras +8011 askFinderIdleHowLong +8012 tellFinderGetWindowIcons +8013 tellFinderGetWindowInfo +8014 tellFinderRemoveFromExtras +8015 tellFinderSpecialPreferences +8200 srqConvertRelPitch +9000 cpOpenCDev +9001 cpOpenControlPanels +* diff --git a/DIST/ReadMe.txt b/DIST/ReadMe.txt new file mode 100644 index 0000000..fe7d2c0 --- /dev/null +++ b/DIST/ReadMe.txt @@ -0,0 +1,11 @@ +faddenSoft CiderPress(tm) + +CiderPress is a Windows utility for accessing Apple II archives and +disk images. A wide range of formats are supported, including +ShrinkIt (NuFX) archives and disk images with DOS, ProDOS, Pascal, +CP/M, and RDOS filesystems. + +This program used to be shareware, but is now free. + +If you have any questions, please visit the faddenSoft CiderPress +web site at http://www.faddensoft.com/ciderpress/. diff --git a/DIST/with-mdc.deploy b/DIST/with-mdc.deploy new file mode 100644 index 0000000..6747712 --- /dev/null +++ b/DIST/with-mdc.deploy @@ -0,0 +1,271 @@ +DeployMaster Installation Script +9 +faddenSoft +http://www.faddensoft.com/ +CiderPress +http://www.faddensoft.com/ciderpress/ +3.0.0 +39166 +C:\DATA\faddenSoft\fs.ico +Copyright © 2007 faddenSoft, LLC. All rights reserved. +C:\Src\CiderPress\DIST\ReadMe.txt +C:\Src\CiderPress\DIST\License.txt + +%PROGRAMFILES%\faddenSoft\CiderPress +%COMMONFILES%\faddenSoft\ +%PROGRAMSMENU%\CiderPress +TRUE +TRUE +1 +-16777198 +MS Sans Serif +8 +FALSE +FALSE +FALSE +FALSE +FALSE +16711680 +0 + +TRUE +faddenSoft CiderPress 0.1 +1 +16777215 +Times New Roman +36 +TRUE +FALSE +FALSE +FALSE +TRUE +0 +Copyright © 2003 faddenSoft, LLC. All rights reserved. +1 +16777215 +Times New Roman +20 +TRUE +FALSE +FALSE +FALSE +TRUE +0 +English (Default) +FALSE +FALSE +FALSE +FALSE +TRUE +FALSE + +FALSE +0 +FALSE +0 ++ +CiderPress +0 +TRUE +FALSE +Main CiderPress application ++ +%APPFOLDER% +1 ++ +C:\Src\CiderPress\DIST\CiderPress.cnt +3 +0 +FALSE +FALSE +C:\Src\CiderPress\DIST\CiderPress.exe +3 +0 +FALSE +FALSE +C:\Src\CiderPress\DIST\CIDERPRESS.HLP +3 +0 +FALSE +FALSE +C:\Src\CiderPress\DIST\NList.Data.TXT +3 +1 +FALSE +FALSE +- +%APPCOMMONFOLDER% +1 +%APPMENU% +1 ++ +C:\Src\CiderPress\DIST\CiderPress.exe +4 +CiderPress +The ultimate Apple II archive utility + + +0 +C:\Src\CiderPress\DIST\CIDERPRESS.HLP +4 +CiderPress Help +Help file for CiderPress + + +0 +- +%DESKTOP% +1 +%SENDTO% +1 +%STARTUP% +1 +%WINDOWS% +1 +%SYSTEM% +1 +- +Common DLLs +0 +FALSE +FALSE +NuFX, zlib, and disk image access libraries. ++ +%APPFOLDER% +1 ++ +C:\Src\CiderPress\DIST\diskimg4.dll +3 +0 +FALSE +FALSE +C:\Src\CiderPress\DIST\nufxlib2.dll +3 +0 +FALSE +FALSE +C:\Src\CiderPress\DIST\zlib1.dll +3 +0 +FALSE +FALSE +- +%APPCOMMONFOLDER% +1 +%APPMENU% +1 +%DESKTOP% +1 +%SENDTO% +1 +%STARTUP% +1 +%WINDOWS% +1 +%SYSTEM% +1 +- +MDC +0 +TRUE +FALSE +Multi-Disk Catalog utility. ++ +%APPFOLDER% +1 ++ +C:\Src\CiderPress\DIST\mdc.exe +3 +0 +FALSE +FALSE +- +%APPCOMMONFOLDER% +1 +%APPMENU% +1 ++ +C:\Src\CiderPress\DIST\mdc.exe +4 +MDC +Multi-Disk Catalog for Apple II disk images + + +0 +- +%DESKTOP% +1 +%SENDTO% +1 +%STARTUP% +1 +%WINDOWS% +1 +%SYSTEM% +1 +- +- +1 +1 +0 +1 +1 ++ +HKEY_CURRENT_USER +0 +FALSE ++ +Software +0 +FALSE ++ +faddenSoft +0 +FALSE ++ +CiderPress +0 +TRUE +- +- +- +HKEY_LOCAL_MACHINE +0 +FALSE ++ +Software +0 +FALSE ++ +faddenSoft +0 +FALSE ++ +CiderPress +0 +TRUE +- +- +- +- +0 +TRUE +FALSE +TRUE +TRUE +C:\Src\CiderPress\DIST\CiderPress.exe +-install +C:\Src\CiderPress\DIST\CiderPress.exe +-uninstall +TRUE +TRUE +FALSE +36725 +0 +FALSE +36725 +0 +10 + +Setup300.exe +FALSE diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..e08f947 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,25 @@ +Copyright (c) 2007, FaddenSoft, LLC +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of FaddenSoft, LLC nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY FaddenSoft, LLC ``AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL FaddenSoft, LLC BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/ReadMe.htm b/ReadMe.htm new file mode 100644 index 0000000..5cbe863 --- /dev/null +++ b/ReadMe.htm @@ -0,0 +1,313 @@ + + + + + + +CiderPress Source README + + + +

CiderPress Source README

+Release notes for v3.0.0
+Last updated: 25-Mar-2007 + +

Contents:

+ + +
+

What's CiderPress?

+

CiderPress is a Windows utility for manipulating Apple II disk images +and file archives. It supports all major disk and archive formats used +on the Apple II and by Apple II emulators. +

CiderPress was sold by faddenSoft, LLC as a shareware product for about four +years, starting in March 2003. + + +

Why Bother?

+ +

Back in 2002 I decided it was time to learn how to write an application for +Microsoft Windows. I had been a professional software engineer for many +years -- including 2.5 years at Microsoft! -- but had never written a +Windows program more complex than "Hello, world!". + +

I decided to write a Windows version of GS/ShrinkIt. I had already written +NufxLib, which handled all of the ShrinkIt stuff, so I could focus +on writing the Windows user interface code. + +

Somewhere in the early stages of the project, it occurred to me that a +disk image isn't substantially different from a file archive. They're +both just collections of files laid out in a well-defined manner. The +decision to handle disk images as well as ShrinkIt archives seemed like +a simple improvement at the time. The rest is history. + +

CiderPress has allowed me to explore a variety of interesting technologies. +It has five different ways of reading a block from physical media, depending +on your operating system and what sort of device you're reading from. I +was able to take what I learned from a digital signal processing textbook +and apply it to a real-world problem (decoding Apple II cassette data). It +is also my first Shareware product, not to mention the initial product of +my first small business venture (faddenSoft, LLC). + +

I could have written other things. No doubt they would have made more money. +CiderPress is something that I find very useful, however, in the pursuit of +my Apple II hobby. + +

Above all, this has been a labor of love. I have tried to get the details +right, because in the end it's the little things that mean the difference +between "good" and merely "good enough".

+ +

License

+ +

The source code to CiderPress is being made available under the BSD license:

+ +
+Copyright (c) 2007, FaddenSoft, LLC
+All rights reserved. + +

Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: +

    +
  • Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +
  • Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +
  • Neither the name of FaddenSoft, LLC nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. +
+

THIS SOFTWARE IS PROVIDED BY FaddenSoft, LLC ``AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL FaddenSoft, LLC BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +

+CiderPress requires three other libraries, all of which are included as either +source or binaries: + +

The license allows you to do a great many things. For example, you could +take the source code to CiderPress, compile it, and sell it. I'm not sure why +anyone would buy it, but you're legally allowed to do so, as long as you retain +the appropriate copyright notice.

+

If you retain libhfs, any changes you make to any part of CiderPress must +be made available, due to the "viral" nature of the GPL license. If this is +not acceptable, you can remove HFS disk image support from CiderPress (look for +"EXCISE_GPL_CODE" in DiskImg.h).

+ + +
+

Building the Sources

+ +

How to build your own copy of CiderPress. + +

Windows

+

The sources are distributed with the necessary project/solution files for +Microsoft Visual Studio 6.0 and Microsoft Visual Studio 2003.  All you +really need is the C++ compiler.  The free or student editions of the +compilers will probably work. +

I usually work on CiderPress in Visual C++ 6.0, +and use that when building for distribution.  I chose to stick with 6.0 for +CiderPress because you need an additional MSVC DLL when you build with +2003.  Also, the newer tools cause the newer file selection dialogs to be +used, and the custom dialog code gets all wonky.

+ + +

In 6.0, you need to select "app" as the build target (Build->Set +active configuration), and hit F7.  This will build everything except MDC, +which you can then build by selecting "mdc" and hitting F7.  You +can also configure a batch build.  In 2003 just select "build +solution".  The necessary prebuilt libraries should be there for both +"debug" and "release" builds.  (You can tell how it has +been built by looking at the version string in the About box.)

+ + +

When the build completes, the necessary DLLs are copied around so you can +execute the binary by launching it within Visual Studio (hit F5).  You will +get a warning about not being able to find the NiftyList data file unless you +copy that from "DIST" into the app source directory.

+ + +

The distribution comes with prebuilt versions of NufxLib2 and zlib in DLL +form.  If you like, you can download and compile your own.

+ + +

There are two things you can't create with the standard tools:

+ + + +

Compiled help files are included, so you can still generate a new version of +CiderPress even if you can't update the help.  You don't strictly need the installer either, though it is +quite handy, and the uninstaller will clean up the registry entries CiderPress +creates.

+

Linux

+

The "linux" directory has a few command-line utilities and a simple +makefile.  It will build the diskimg and HFS libraries, then the sample utilities:

+ + + +

This are mostly intended for testing and illustration of the interfaces, but +they can be useful as well.  Some other code is also provided:

+ + + +

The "prebuilt" directory has a pre-built copy of NufxLib, so you +don't have to build your own.

+

If you're planning to hack on this stuff, use "make depend" before +"make" to fill in the dependencies.

+ + +
+

Source Notes

+ +

Some notes on what you'll find in the various directories. + +

Main Application

+ +

This is highly Windows-centric.  My goal was to learn how to write a +Windows application, so I made no pretense at portability.  For better or +worse, I avoided the Visual Studio "wizards" for the dialogs. + +

Much of the user interface text is in the resource file.  Much is not, +especially when it comes to error messages.  This will need to be addressed +if internationalization is attempted. + +

It may be possible to convert this for use +with wxWidgets, which uses an MFC-like structure, and runs on Mac and Linux as +well. The greatest barrier to +entry is probably the heavy reliance on the Rich Edit control. Despite +its bug-ridden history, the Rich Edit control allowed me to let Windows +deal with a lot of text formatting and image display stuff. + + +

MDC Application

+ +

MDC (Multi-Disk Catalog) was written as a simple demonstration of the value of having the DiskImg +code in a DLL instead of meshed with the main application.  There's not +much to it, and it hasn't changed substantially since it was first written. + + +

DiskImg Library

+ +

This library provides access to disk images.  It automatically handles a +wide variety of formats. + +

This library can be built under Linux or Windows. One of my key motivations +for making it work under Linux was the availability of "valgrind". Similar +tools for Windows usually very expensive or inferior (or both). + +

The basic classes, defined in DiskImg.h, are: +

+

Sub-classes are defined in DiskImgDetail.h.  Most applications won't +need to access this header file.  Each Apple II filesystem defines +sub-classes of DiskFS, A2File, and A2FileDescr.

+ +

In an ideal world, the code would mimic the GS/OS file operations.  In +practice, CiderPress didn't need the full range of capabilities, so the +functions have some basic limitations: +

+

Some things that can be improved: +

+ + +

The library depends on NufxLib and zlib for access to compressed images.

+ + +

Reformat Library

+

This is probably the most "fun" component of CiderPress. It converts Apple II files +to more easily accessible Windows equivalents. + +

Start in Reformat.h and ReformatBase.h. There are two basic kinds of +reformatter: text and graphics. Everything else is a sub-class of one of the +two basic types. + +

The general idea is to allow the reformatter to decide whether or not it is +capable of reformatting a file. To this end, the file type information and +file contents are presented to the "examine" function of each reformatter in +turn. The level of confidence is specified in a range. If it's better than +"no", it is presented to the user as an option, ordered by the strength of its +convictions. If chosen, the "process" function is called to convert the data. + +

Bear in mind that reformatters may be disabled from the preferences menu.  +Also, when extracting files for easy access in Windows, the "best" +reformatter is employed by the extraction code.

Most of the code should be portable, though some of it uses the MFC CString class.  +This could probably be altered to use STL strings or plain. + + + +

Util Library

+ +

Miscellaneous utility functions. + +

For a good time, look at SelectFilesDialog.cpp. + +

To enable debug logging for one of the applications, define _DEBUG_LOG in +MyDebug.h in this library. You will see "_DEBUG_LOG" in the version string +in the About box when this is defined. The log is written to C:\cplog.txt. +An existing log file will be appended to if the previous log was written to +less than 8 hours ago.

+ +
+

Enjoy!

+
Andy McFadden
+ + + diff --git a/app/ACUArchive.cpp b/app/ACUArchive.cpp new file mode 100644 index 0000000..a956266 --- /dev/null +++ b/app/ACUArchive.cpp @@ -0,0 +1,906 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * AppleLink Compression Utility file support. + * + * This was adapted from the Binary II support, which has a mixed history, + * so this is a little scrambled in spots. + */ +#include "stdafx.h" +#include "ACUArchive.h" +#include "NufxArchive.h" // uses NuError +#include "Preferences.h" +#include "Main.h" +#include "Squeeze.h" +#include + +/* ++00 2b Number of items in archive ++02 2b 0100 ++04 5b "fZink" ++09 11b 0136 0000 0000 0000 0000 dd + ++14 + ++00 1b ?? 00 ++01 1b Compression type, 00=none, 03=sq ++02 2b ?? 0000 0000 0000 0000 ++04 2b ?? 0a74 961f 7d85 af2c 2775 <- 0000 for dir (checksum?) ++06 2b ?? 0000 0000 0000 0000 ++08 2b ?? 0000 0000 0000 0000 ++0a 2b Storage size (in 512-byte blocks) ++0c 6b ?? 000000 000000 000000 000000 ++12 4b Length of file in this archive (compressed or uncompressed) ++16 2b ProDOS file permissions ++18 2b ProDOS file type ++1a 4b ProDOS aux type ++1e ?? 0000 ++20 1b ProDOS storage type (usually 02, 0d for dirs) ++21 ?? 00 ++22 ?? 0000 0000 ++26 4b Uncompressed file len ++2a 2b ProDOS date (create?) ++2c 2b ProDOS time ++2e 2b ProDOS date (mod?) ++30 2b ProDOS time ++32 2b Filename len ++34 2b ?? ac4a 2d02 for dir <- header checksum? ++36 FL Filename ++xx data start (dir has no data) +*/ + +/* + * =========================================================================== + * AcuEntry + * =========================================================================== + */ + +/* + * Extract data from an entry. + * + * If "*ppText" is non-nil, the data will be read into the pointed-to buffer + * so long as it's shorter than *pLength bytes. The value in "*pLength" + * will be set to the actual length used. + * + * If "*ppText" is nil, the uncompressed data will be placed into a buffer + * allocated with "new[]". + * + * Returns IDOK on success, IDCANCEL if the operation was cancelled by the + * user, and -1 value on failure. On failure, "*pErrMsg" holds an error + * message. + * + * "which" is an anonymous GenericArchive enum (e.g. "kDataThread"). + */ +int +AcuEntry::ExtractThreadToBuffer(int which, char** ppText, long* pLength, + CString* pErrMsg) const +{ + NuError nerr; + ExpandBuffer expBuf; + char* dataBuf = nil; + long len; + bool needAlloc = true; + int result = -1; + + ASSERT(fpArchive != nil); + ASSERT(fpArchive->fFp != nil); + + if (*ppText != nil) + needAlloc = false; + + if (which != kDataThread) { + *pErrMsg = "No such fork"; + goto bail; + } + + len = (long) GetUncompressedLen(); + if (len == 0) { + if (needAlloc) { + *ppText = new char[1]; + **ppText = '\0'; + } + *pLength = 0; + result = IDOK; + goto bail; + } + + SET_PROGRESS_BEGIN(); + + errno = 0; + if (fseek(fpArchive->fFp, fOffset, SEEK_SET) < 0) { + pErrMsg->Format("Unable to seek to offset %ld: %s", + fOffset, strerror(errno)); + goto bail; + } + + if (GetSqueezed()) { + nerr = UnSqueeze(fpArchive->fFp, (unsigned long) GetCompressedLen(), + &expBuf, false, 0); + if (nerr != kNuErrNone) { + pErrMsg->Format("File read failed: %s", NuStrError(nerr)); + goto bail; + } + + char* unsqBuf = nil; + long unsqLen = 0; + expBuf.SeizeBuffer(&unsqBuf, &unsqLen); + WMSG2("Unsqueezed %ld bytes to %d\n", + (unsigned long) GetCompressedLen(), unsqLen); + if (unsqLen == 0) { + // some bonehead squeezed a zero-length file + delete[] unsqBuf; + ASSERT(*ppText == nil); + WMSG0("Handling zero-length squeezed file!\n"); + if (needAlloc) { + *ppText = new char[1]; + **ppText = '\0'; + } + *pLength = 0; + } else { + if (needAlloc) { + /* just use the seized buffer */ + *ppText = unsqBuf; + *pLength = unsqLen; + } else { + if (*pLength < unsqLen) { + pErrMsg->Format("buf size %ld too short (%ld)", + *pLength, unsqLen); + delete[] unsqBuf; + goto bail; + } + + memcpy(*ppText, unsqBuf, unsqLen); + delete[] unsqBuf; + *pLength = unsqLen; + } + } + + } else { + if (needAlloc) { + dataBuf = new char[len]; + if (dataBuf == nil) { + pErrMsg->Format("allocation of %ld bytes failed", len); + goto bail; + } + } else { + if (*pLength < (long) len) { + pErrMsg->Format("buf size %ld too short (%ld)", + *pLength, len); + goto bail; + } + dataBuf = *ppText; + } + if (fread(dataBuf, len, 1, fpArchive->fFp) != 1) { + pErrMsg->Format("File read failed: %s", strerror(errno)); + goto bail; + } + + if (needAlloc) + *ppText = dataBuf; + *pLength = len; + } + + result = IDOK; + +bail: + if (result == IDOK) { + SET_PROGRESS_END(); + ASSERT(pErrMsg->IsEmpty()); + } else { + ASSERT(result == IDCANCEL || !pErrMsg->IsEmpty()); + if (needAlloc) { + delete[] dataBuf; + ASSERT(*ppText == nil); + } + } + return result; +} + +/* + * Extract data from a thread to a file. Since we're not copying to memory, + * we can't assume that we're able to hold the entire file all at once. + * + * Returns IDOK on success, IDCANCEL if the operation was cancelled by the + * user, and -1 value on failure. On failure, "*pMsg" holds an + * error message. + */ +int +AcuEntry::ExtractThreadToFile(int which, FILE* outfp, ConvertEOL conv, + ConvertHighASCII convHA, CString* pErrMsg) const +{ + NuError nerr; + long len; + int result = -1; + + ASSERT(IDOK != -1 && IDCANCEL != -1); + if (which != kDataThread) { + *pErrMsg = "No such fork"; + goto bail; + } + + len = (long) GetUncompressedLen(); + if (len == 0) { + WMSG0("Empty fork\n"); + result = IDOK; + goto bail; + } + + errno = 0; + if (fseek(fpArchive->fFp, fOffset, SEEK_SET) < 0) { + pErrMsg->Format("Unable to seek to offset %ld: %s", + fOffset, strerror(errno)); + goto bail; + } + + SET_PROGRESS_BEGIN(); + + /* + * Generally speaking, anything in a BNY file is going to be small. The + * major exception is a BXY file, which could be huge. However, the + * SHK embedded in a BXY is never squeezed. + * + * To make life easy, we either unsqueeze the entire thing into a buffer + * and then write that, or we do a file-to-file copy of the specified + * number of bytes. + */ + if (GetSqueezed()) { + ExpandBuffer expBuf; + bool lastCR = false; + char* buf; + long uncLen; + + nerr = UnSqueeze(fpArchive->fFp, (unsigned long) GetCompressedLen(), + &expBuf, false, 0); + if (nerr != kNuErrNone) { + pErrMsg->Format("File read failed: %s", NuStrError(nerr)); + goto bail; + } + + expBuf.SeizeBuffer(&buf, &uncLen); + WMSG2("Unsqueezed %ld bytes to %d\n", len, uncLen); + + // some bonehead squeezed a zero-length file + if (uncLen == 0) { + ASSERT(buf == nil); + WMSG0("Handling zero-length squeezed file!\n"); + result = IDOK; + goto bail; + } + + int err = GenericEntry::WriteConvert(outfp, buf, uncLen, &conv, + &convHA, &lastCR); + if (err != 0) { + pErrMsg->Format("File write failed: %s", strerror(err)); + delete[] buf; + goto bail; + } + + delete[] buf; + } else { + nerr = CopyData(outfp, conv, convHA, pErrMsg); + if (nerr != kNuErrNone) { + if (pErrMsg->IsEmpty()) { + pErrMsg->Format("Failed while copying data: %s\n", + NuStrError(nerr)); + } + goto bail; + } + } + + result = IDOK; + +bail: + SET_PROGRESS_END(); + return result; +} + +/* + * Copy data from the seeked archive to outfp, possibly converting EOL along + * the way. + */ +NuError +AcuEntry::CopyData(FILE* outfp, ConvertEOL conv, ConvertHighASCII convHA, + CString* pMsg) const +{ + NuError nerr = kNuErrNone; + const int kChunkSize = 8192; + char buf[kChunkSize]; + bool lastCR = false; + long srcLen, dataRem; + + srcLen = (long) GetUncompressedLen(); + ASSERT(srcLen > 0); // empty files should've been caught earlier + + /* + * Loop until all data copied. + */ + dataRem = srcLen; + while (dataRem) { + int chunkLen; + + if (dataRem > kChunkSize) + chunkLen = kChunkSize; + else + chunkLen = dataRem; + + /* read a chunk from the source file */ + nerr = fpArchive->AcuRead(buf, chunkLen); + if (nerr != kNuErrNone) { + pMsg->Format("File read failed: %s.", NuStrError(nerr)); + goto bail; + } + + /* write chunk to destination file */ + int err = GenericEntry::WriteConvert(outfp, buf, chunkLen, &conv, + &convHA, &lastCR); + if (err != 0) { + pMsg->Format("File write failed: %s.", strerror(err)); + nerr = kNuErrGeneric; + goto bail; + } + + dataRem -= chunkLen; + SET_PROGRESS_UPDATE(ComputePercent(srcLen - dataRem, srcLen)); + } + +bail: + return nerr; +} + + +/* + * Test this entry by extracting it. + * + * If the file isn't compressed, just make sure the file is big enough. If + * it's squeezed, invoke the un-squeeze function with a "nil" buffer pointer. + */ +NuError +AcuEntry::TestEntry(CWnd* pMsgWnd) +{ + NuError nerr = kNuErrNone; + CString errMsg; + long len; + int result = -1; + + len = (long) GetUncompressedLen(); + if (len == 0) + goto bail; + + errno = 0; + if (fseek(fpArchive->fFp, fOffset, SEEK_SET) < 0) { + nerr = kNuErrGeneric; + errMsg.Format("Unable to seek to offset %ld: %s\n", + fOffset, strerror(errno)); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + goto bail; + } + + if (GetSqueezed()) { + nerr = UnSqueeze(fpArchive->fFp, (unsigned long) GetCompressedLen(), + nil, false, 0); + if (nerr != kNuErrNone) { + errMsg.Format("Unsqueeze failed: %s.", NuStrError(nerr)); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + goto bail; + } + } else { + errno = 0; + if (fseek(fpArchive->fFp, fOffset + len, SEEK_SET) < 0) { + nerr = kNuErrGeneric; + errMsg.Format("Unable to seek to offset %ld (file truncated?): %s\n", + fOffset, strerror(errno)); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + goto bail; + } + } + + if (SET_PROGRESS_UPDATE(100) == IDCANCEL) + nerr = kNuErrAborted; + +bail: + return nerr; +} + + +/* + * =========================================================================== + * AcuArchive + * =========================================================================== + */ + +/* + * Perform one-time initialization. There really isn't any for us. + * + * Returns 0 on success, nonzero on error. + */ +/*static*/ CString +AcuArchive::AppInit(void) +{ + return ""; +} + +/* + * Open an ACU archive. + * + * Returns an error string on failure, or "" on success. + */ +GenericArchive::OpenResult +AcuArchive::Open(const char* filename, bool readOnly, CString* pErrMsg) +{ + CString errMsg; + + fIsReadOnly = true; // ignore "readOnly" + + errno = 0; + fFp = fopen(filename, "rb"); + if (fFp == nil) { + errMsg.Format("Unable to open %s: %s.", filename, strerror(errno)); + goto bail; + } + + { + CWaitCursor waitc; + int result; + + result = LoadContents(); + if (result < 0) { + errMsg.Format("The file is not an ACU archive."); + goto bail; + } else if (result > 0) { + errMsg.Format("Failed while reading data from ACU archive."); + goto bail; + } + } + + SetPathName(filename); + +bail: + *pErrMsg = errMsg; + if (!errMsg.IsEmpty()) + return kResultFailure; + else + return kResultSuccess; +} + +/* + * Finish instantiating an AcuArchive object by creating a new archive. + * + * Returns an error string on failure, or "" on success. + */ +CString +AcuArchive::New(const char* /*filename*/, const void* /*options*/) +{ + CString retmsg("Sorry, AppleLink Compression Utility files can't be created."); + return retmsg; +} + + +/* + * Our capabilities. + */ +long +AcuArchive::GetCapability(Capability cap) +{ + switch (cap) { + case kCapCanTest: + return true; + break; + case kCapCanRenameFullPath: + return true; + break; + case kCapCanRecompress: + return true; + break; + case kCapCanEditComment: + return false; + break; + case kCapCanAddDisk: + return false; + break; + case kCapCanConvEOLOnAdd: + return false; + break; + case kCapCanCreateSubdir: + return false; + break; + case kCapCanRenameVolume: + return false; + break; + default: + ASSERT(false); + return -1; + break; + } +} + + +/* + * Load the contents of the archive. + * + * Returns 0 on success, < 0 if this is not an ACU archive > 0 if this appears + * to be an ACU archive but it's damaged. + */ +int +AcuArchive::LoadContents(void) +{ + NuError nerr; + int numEntries; + + ASSERT(fFp != nil); + rewind(fFp); + + /* + * Read the master header. In an ACU file this holds the number of + * files and a bunch of stuff that doesn't seem to change. + */ + if (ReadMasterHeader(&numEntries) != 0) + return -1; + + while (numEntries) { + AcuFileEntry fileEntry; + + nerr = ReadFileHeader(&fileEntry); + if (nerr != kNuErrNone) + return 1; + + if (CreateEntry(&fileEntry) != 0) + return 1; + + /* if file isn't empty, seek past it */ + if (fileEntry.dataStorageLen) { + nerr = AcuSeek(fileEntry.dataStorageLen); + if (nerr != kNuErrNone) + return 1; + } + + numEntries--; + } + + return 0; +} + +/* + * Reload the contents of the archive. + */ +CString +AcuArchive::Reload(void) +{ + fReloadFlag = true; // tell everybody that cached data is invalid + + DeleteEntries(); + if (LoadContents() != 0) { + return "Reload failed."; + } + + return ""; +} + +/* + * Read the archive header. The archive file is left seeked to the point + * at the end of the header. + * + * Returns 0 on success, -1 on failure. Sets *pNumEntries to the number of + * entries in the archive. + */ +int +AcuArchive::ReadMasterHeader(int* pNumEntries) +{ + AcuMasterHeader header; + unsigned char buf[kAcuMasterHeaderLen]; + NuError nerr; + + nerr = AcuRead(buf, kAcuMasterHeaderLen); + if (nerr != kNuErrNone) + return -1; + + header.fileCount = buf[0x00] | buf[0x01] << 8; + header.unknown1 = buf[0x02] | buf[0x03] << 8; + memcpy(header.fZink, &buf[0x04], 5); + header.fZink[5] = '\0'; + memcpy(header.unknown2, &buf[0x09], 11); + + if (header.fileCount == 0 || + header.unknown1 != 1 || + strcmp((char*) header.fZink, "fZink") != 0) + { + WMSG0("Not an ACU archive\n"); + return -1; + } + + WMSG1("Looks like an ACU archive with %d entries\n", header.fileCount); + + *pNumEntries = header.fileCount; + return 0; +} + +/* + * Read and decode an AppleLink Compression Utility file entry header. + * This leaves the file seeked to the point immediately past the filename. + */ +NuError +AcuArchive::ReadFileHeader(AcuFileEntry* pEntry) +{ + NuError err = kNuErrNone; + unsigned char buf[kAcuEntryHeaderLen]; + + ASSERT(pEntry != nil); + + err = AcuRead(buf, kAcuEntryHeaderLen); + if (err != kNuErrNone) + goto bail; + + // unknown at 00 + pEntry->compressionType = buf[0x01]; + // unknown at 02-03 + pEntry->dataChecksum = buf[0x04] | buf[0x05] << 8; // not sure + // unknown at 06-09 + pEntry->blockCount = buf[0x0a] | buf[0x0b] << 8; + // unknown at 0c-11 + pEntry->dataStorageLen = buf[0x12] | buf [0x13] << 8 | buf[0x14] << 16 | + buf[0x15] << 24; + pEntry->access = buf[0x16] | buf[0x17] << 8; + pEntry->fileType = buf[0x18] | buf[0x19] << 8; + pEntry->auxType = buf[0x1a] | buf[0x1b] << 8; + // unknown at 1e-1f + pEntry->storageType = buf[0x20]; + // unknown at 21-25 + pEntry->dataEof = buf[0x26] | buf[0x27] << 8 | buf[0x28] << 16 | + buf[0x29] << 24; + pEntry->prodosModDate = buf[0x2a] | buf[0x2b] << 8; + pEntry->prodosModTime = buf[0x2c] | buf[0x2d] << 8; + AcuConvertDateTime(pEntry->prodosModDate, pEntry->prodosModTime, + &pEntry->modWhen); + pEntry->prodosCreateDate = buf[0x2e] | buf[0x2f] << 8; + pEntry->prodosCreateTime = buf[0x30] | buf[0x31] << 8; + AcuConvertDateTime(pEntry->prodosCreateDate, pEntry->prodosCreateTime, + &pEntry->createWhen); + pEntry->fileNameLen = buf[0x32] | buf[0x33] << 8; + pEntry->headerChecksum = buf[0x34] | buf[0x35] << 8; // not sure + + /* read the filename */ + if (pEntry->fileNameLen > kAcuMaxFileName) { + WMSG1("GLITCH: filename is too long (%d bytes)\n", + pEntry->fileNameLen); + err = kNuErrGeneric; + goto bail; + } + if (!pEntry->fileNameLen) { + WMSG0("GLITCH: filename missing\n"); + err = kNuErrGeneric; + goto bail; + } + + /* don't know if this is possible or not */ + if (pEntry->storageType == 5) { + WMSG0("HEY: EXTENDED FILE\n"); + } + + err = AcuRead(pEntry->fileName, pEntry->fileNameLen); + if (err != kNuErrNone) + goto bail; + pEntry->fileName[pEntry->fileNameLen] = '\0'; + + //DumpFileHeader(pEntry); + +bail: + return err; +} + +/* + * Dump the contents of an AcuFileEntry struct. + */ +void +AcuArchive::DumpFileHeader(const AcuFileEntry* pEntry) +{ + time_t createWhen, modWhen; + CString createStr, modStr; + + createWhen = NufxArchive::DateTimeToSeconds(&pEntry->createWhen); + modWhen = NufxArchive::DateTimeToSeconds(&pEntry->modWhen); + FormatDate(createWhen, &createStr); + FormatDate(modWhen, &modStr); + + WMSG1(" Header for file '%s':\n", pEntry->fileName); + WMSG4(" dataStorageLen=%d eof=%d blockCount=%d checksum=0x%04x\n", + pEntry->dataStorageLen, pEntry->dataEof, pEntry->blockCount, + pEntry->dataChecksum); + WMSG4(" fileType=0x%02x auxType=0x%04x storageType=0x%02x access=0x%04x\n", + pEntry->fileType, pEntry->auxType, pEntry->storageType, pEntry->access); + WMSG2(" created %s, modified %s\n", (const char*) createStr, + (const char*) modStr); + WMSG2(" fileNameLen=%d headerChecksum=0x%04x\n", + pEntry->fileNameLen, pEntry->headerChecksum); +} + + +/* + * Given an AcuFileEntry structure, add an appropriate entry to the list. + */ +int +AcuArchive::CreateEntry(const AcuFileEntry* pEntry) +{ + const int kAcuFssep = '/'; + NuError err = kNuErrNone; + AcuEntry* pNewEntry; + + /* + * Create the new entry. + */ + pNewEntry = new AcuEntry(this); + pNewEntry->SetPathName(pEntry->fileName); + pNewEntry->SetFssep(kAcuFssep); + pNewEntry->SetFileType(pEntry->fileType); + pNewEntry->SetAuxType(pEntry->auxType); + pNewEntry->SetAccess(pEntry->access); + pNewEntry->SetCreateWhen(NufxArchive::DateTimeToSeconds(&pEntry->createWhen)); + pNewEntry->SetModWhen(NufxArchive::DateTimeToSeconds(&pEntry->modWhen)); + + /* always ProDOS? */ + pNewEntry->SetSourceFS(DiskImg::kFormatProDOS); + + pNewEntry->SetHasDataFork(true); + pNewEntry->SetHasRsrcFork(false); // ? + if (IsDir(pEntry)) { + pNewEntry->SetRecordKind(GenericEntry::kRecordKindDirectory); + } else { + pNewEntry->SetRecordKind(GenericEntry::kRecordKindFile); + } + + pNewEntry->SetCompressedLen(pEntry->dataStorageLen); + pNewEntry->SetDataForkLen(pEntry->dataEof); + + if (pEntry->compressionType == kAcuCompNone) { + pNewEntry->SetFormatStr("Uncompr"); + } else if (pEntry->compressionType == kAcuCompSqueeze) { + pNewEntry->SetFormatStr("Squeeze"); + pNewEntry->SetSqueezed(true); + } else { + pNewEntry->SetFormatStr("(unknown)"); + pNewEntry->SetSqueezed(false); + } + + pNewEntry->SetOffset(ftell(fFp)); + + AddEntry(pNewEntry); + + return err; +} + + +/* + * =========================================================================== + * ACU functions + * =========================================================================== + */ + +/* + * Test if this entry is a directory. + */ +bool +AcuArchive::IsDir(const AcuFileEntry* pEntry) +{ + return (pEntry->storageType == 0x0d); +} + +/* + * Wrapper for fread(). Note the arguments resemble read(2) rather + * than fread(3S). + */ +NuError +AcuArchive::AcuRead(void* buf, size_t nbyte) +{ + size_t result; + + ASSERT(buf != nil); + ASSERT(nbyte > 0); + ASSERT(fFp != nil); + + errno = 0; + result = fread(buf, 1, nbyte, fFp); + if (result != nbyte) + return errno ? (NuError)errno : kNuErrFileRead; + return kNuErrNone; +} + +/* + * Seek within an archive. Because we need to handle streaming archives, + * and don't need to special-case anything, we only allow relative + * forward seeks. + */ +NuError +AcuArchive::AcuSeek(long offset) +{ + ASSERT(fFp != nil); + ASSERT(offset > 0); + + /*DBUG(("--- seeking forward %ld bytes\n", offset));*/ + + if (fseek(fFp, offset, SEEK_CUR) < 0) + return kNuErrFileSeek; + + return kNuErrNone; +} + + +/* + * Convert from ProDOS compact date format to the expanded DateTime format. + */ +void +AcuArchive::AcuConvertDateTime(unsigned short prodosDate, + unsigned short prodosTime, NuDateTime* pWhen) +{ + pWhen->second = 0; + pWhen->minute = prodosTime & 0x3f; + pWhen->hour = (prodosTime >> 8) & 0x1f; + pWhen->day = (prodosDate & 0x1f) -1; + pWhen->month = ((prodosDate >> 5) & 0x0f) -1; + pWhen->year = (prodosDate >> 9) & 0x7f; + if (pWhen->year < 40) + pWhen->year += 100; /* P8 uses 0-39 for 2000-2039 */ + pWhen->extra = 0; + pWhen->weekDay = 0; +} + + +/* + * =========================================================================== + * AcuArchive -- test files + * =========================================================================== + */ + +/* + * Test the records represented in the selection set. + */ +bool +AcuArchive::TestSelection(CWnd* pMsgWnd, SelectionSet* pSelSet) +{ + NuError nerr; + AcuEntry* pEntry; + CString errMsg; + bool retVal = false; + + ASSERT(fFp != nil); + + WMSG1("Testing %d entries\n", pSelSet->GetNumEntries()); + + SelectionEntry* pSelEntry = pSelSet->IterNext(); + while (pSelEntry != nil) { + pEntry = (AcuEntry*) pSelEntry->GetEntry(); + + WMSG2(" Testing '%s' (offset=%ld)\n", pEntry->GetDisplayName(), + pEntry->GetOffset()); + + SET_PROGRESS_UPDATE2(0, pEntry->GetDisplayName(), nil); + + nerr = pEntry->TestEntry(pMsgWnd); + if (nerr != kNuErrNone) { + if (nerr == kNuErrAborted) { + CString title; + title.LoadString(IDS_MB_APP_NAME); + errMsg = "Cancelled."; + pMsgWnd->MessageBox(errMsg, title, MB_OK); + } else { + errMsg.Format("Failed while testing '%s': %s.", + pEntry->GetPathName(), NuStrError(nerr)); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + } + goto bail; + } + + pSelEntry = pSelSet->IterNext(); + } + + /* show success message */ + errMsg.Format("Tested %d file%s, no errors found.", + pSelSet->GetNumEntries(), + pSelSet->GetNumEntries() == 1 ? "" : "s"); + pMsgWnd->MessageBox(errMsg); + retVal = true; + +bail: + SET_PROGRESS_END(); + return retVal; +} diff --git a/app/ACUArchive.h b/app/ACUArchive.h new file mode 100644 index 0000000..8724711 --- /dev/null +++ b/app/ACUArchive.h @@ -0,0 +1,222 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * AppleLink Compression Utility archive support. + */ +#ifndef __ACU_ARCHIVE__ +#define __ACU_ARCHIVE__ + +#include "GenericArchive.h" + + +class AcuArchive; + +/* + * One file in an ACU archive. + */ +class AcuEntry : public GenericEntry { +public: + AcuEntry(AcuArchive* pArchive) : + fpArchive(pArchive), fIsSqueezed(false), fOffset(-1) + {} + virtual ~AcuEntry(void) {} + + // retrieve thread data + virtual int ExtractThreadToBuffer(int which, char** ppText, long* pLength, + CString* pErrMsg) const; + virtual int ExtractThreadToFile(int which, FILE* outfp, ConvertEOL conv, + ConvertHighASCII convHA, CString* pErrMsg) const; + virtual long GetSelectionSerial(void) const { return -1; } // doesn't matter + + virtual bool GetFeatureFlag(Feature feature) const { + if (feature == kFeaturePascalTypes || feature == kFeatureDOSTypes || + feature == kFeatureHasSimpleAccess) + return false; + else + return true; + } + + NuError TestEntry(CWnd* pMsgWnd); + + bool GetSqueezed(void) const { return fIsSqueezed; } + void SetSqueezed(bool val) { fIsSqueezed = val; } + long GetOffset(void) const { return fOffset; } + void SetOffset(long offset) { fOffset = offset; } + +private: + NuError CopyData(FILE* outfp, ConvertEOL conv, ConvertHighASCII convHA, + CString* pMsg) const; + //NuError BNYUnSqueeze(ExpandBuffer* outExp) const; + + AcuArchive* fpArchive; // holds FILE* for archive + bool fIsSqueezed; + long fOffset; +}; + + +/* + * ACU archive definition. + */ +class AcuArchive : public GenericArchive { +public: + AcuArchive(void) : fIsReadOnly(false), fFp(nil) + {} + virtual ~AcuArchive(void) { (void) Close(); } + + // One-time initialization; returns an error string. + static CString AppInit(void); + + virtual OpenResult Open(const char* filename, bool readOnly, + CString* pErrMsg); + virtual CString New(const char* filename, const void* options); + virtual CString Flush(void) { return ""; } + virtual CString Reload(void); + virtual bool IsReadOnly(void) const { return fIsReadOnly; }; + virtual bool IsModified(void) const { return false; } + virtual void GetDescription(CString* pStr) const { *pStr = "AppleLink ACU"; } + virtual bool BulkAdd(ActionProgressDialog* pActionProgress, + const AddFilesDialog* pAddOpts) + { ASSERT(false); return false; } + virtual bool AddDisk(ActionProgressDialog* pActionProgress, + const AddFilesDialog* pAddOpts) + { ASSERT(false); return false; } + virtual bool CreateSubdir(CWnd* pMsgWnd, GenericEntry* pParentEntry, + const char* newName) + { ASSERT(false); return false; } + virtual bool TestSelection(CWnd* pMsgWnd, SelectionSet* pSelSet); + virtual bool DeleteSelection(CWnd* pMsgWnd, SelectionSet* pSelSet) + { ASSERT(false); return false; } + virtual bool RenameSelection(CWnd* pMsgWnd, SelectionSet* pSelSet) + { ASSERT(false); return false; } + virtual bool RenameVolume(CWnd* pMsgWnd, DiskFS* pDiskFS, + const char* newName) + { ASSERT(false); return false; } + virtual CString TestVolumeName(const DiskFS* pDiskFS, + const char* newName) const + { ASSERT(false); return "!"; } + virtual CString TestPathName(const GenericEntry* pGenericEntry, + const CString& basePath, const CString& newName, char newFssep) const + { ASSERT(false); return "!"; } + virtual bool RecompressSelection(CWnd* pMsgWnd, SelectionSet* pSelSet, + const RecompressOptionsDialog* pRecompOpts) + { ASSERT(false); return false; } + virtual XferStatus XferSelection(CWnd* pMsgWnd, SelectionSet* pSelSet, + ActionProgressDialog* pActionProgress, const XferFileOptions* pXferOpts) + { ASSERT(false); return kXferFailed; } + virtual bool GetComment(CWnd* pMsgWnd, const GenericEntry* pEntry, + CString* pStr) + { ASSERT(false); return false; } + virtual bool SetComment(CWnd* pMsgWnd, GenericEntry* pEntry, + const CString& str) + { ASSERT(false); return false; } + virtual bool DeleteComment(CWnd* pMsgWnd, GenericEntry* pEntry) + { ASSERT(false); return false; } + virtual bool SetProps(CWnd* pMsgWnd, GenericEntry* pEntry, + const FileProps* pProps) + { ASSERT(false); return false; } + virtual void PreferencesChanged(void) {} + virtual long GetCapability(Capability cap); + + friend class AcuEntry; + +private: + virtual CString Close(void) { + if (fFp != nil) { + fclose(fFp); + fFp = nil; + } + return ""; + } + virtual void XferPrepare(const XferFileOptions* pXferOpts) + { ASSERT(false); } + virtual CString XferFile(FileDetails* pDetails, unsigned char** pDataBuf, + long dataLen, unsigned char** pRsrcBuf, long rsrcLen) + { ASSERT(false); return "!"; } + virtual void XferAbort(CWnd* pMsgWnd) + { ASSERT(false); } + virtual void XferFinish(CWnd* pMsgWnd) + { ASSERT(false); } + + virtual ArchiveKind GetArchiveKind(void) { return kArchiveACU; } + virtual NuError DoAddFile(const AddFilesDialog* pAddOpts, + FileDetails* pDetails) + { ASSERT(false); return kNuErrGeneric; } + + enum { + kAcuMaxFileName = 256, // nice big number + + kAcuMasterHeaderLen = 20, + kAcuEntryHeaderLen = 54, + }; + + /* + * The header at the front of an ACU archive. + */ + typedef struct AcuMasterHeader { + unsigned short fileCount; + unsigned short unknown1; // 0x01 00 -- might be "version 1?" + unsigned char fZink[6]; // "fZink", low ASCII + unsigned char unknown2[11]; // 0x01 36 00 00 00 00 00 00 00 00 dd + } AcuMasterHeader; + + /* + * An entry in an ACU archive. Each archive is essentially a stream + * of files; only the "filesToFollow" value gives any indication that + * something else follows this entry. + * + * We read this from the archive and then unpack the interesting parts + * into GenericEntry fields in an AcuEntry. + */ + struct AcuFileEntry; + friend struct AcuFileEntry; + typedef struct AcuFileEntry { + unsigned char compressionType; + unsigned short dataChecksum; // ?? + unsigned short blockCount; // total blocks req'd to hold file + unsigned long dataStorageLen; // length of data within archive + unsigned short access; + unsigned short fileType; + unsigned long auxType; + unsigned char storageType; + unsigned long dataEof; + unsigned short prodosModDate; + unsigned short prodosModTime; + NuDateTime modWhen; // computed from previous two fields + unsigned short prodosCreateDate; + unsigned short prodosCreateTime; + NuDateTime createWhen; // computed from previous two fields + unsigned short fileNameLen; + unsigned short headerChecksum; // ?? + char fileName[kAcuMaxFileName+1]; + + // possibilities for mystery fields: + // - OS type (note ProDOS is $00) + // - forked file support + } AcuFileEntry; + + /* known compression types */ + enum CompressionType { + kAcuCompNone = 0, + kAcuCompSqueeze = 3, + }; + + int LoadContents(void); + int ReadMasterHeader(int* pNumEntries); + NuError ReadFileHeader(AcuFileEntry* pEntry); + void DumpFileHeader(const AcuFileEntry* pEntry); + int CreateEntry(const AcuFileEntry* pEntry); + + bool IsDir(const AcuFileEntry* pEntry); + NuError AcuRead(void* buf, size_t nbyte); + NuError AcuSeek(long offset); + void AcuConvertDateTime(unsigned short prodosDate, + unsigned short prodosTime, NuDateTime* pWhen); + + FILE* fFp; + bool fIsReadOnly; +}; + +#endif /*__ACU_ARCHIVE__*/ \ No newline at end of file diff --git a/app/AboutDialog.cpp b/app/AboutDialog.cpp new file mode 100644 index 0000000..ea3afa8 --- /dev/null +++ b/app/AboutDialog.cpp @@ -0,0 +1,208 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Implementation of our About box. + */ +#include "stdafx.h" +#include "AboutDialog.h" +#include "EnterRegDialog.h" +#include "HelpTopics.h" +#include "MyApp.h" +#include "resource.h" +#include "../prebuilt/NufxLib.h" +#include "../diskimg/DiskImg.h" +#define ZLIB_DLL +#include "../prebuilt/zlib.h" + + +BEGIN_MESSAGE_MAP(AboutDialog, CDialog) + ON_BN_CLICKED(IDC_ABOUT_CREDITS, OnAboutCredits) + //ON_BN_CLICKED(IDC_ABOUT_ENTER_REG, OnEnterReg) +END_MESSAGE_MAP() + +static const char* kVersionExtra = +#ifdef _DEBUG + " _DEBUG" +#else + "" +#endif +#ifdef _DEBUG_LOG + " _LOG" +#else + "" +#endif + ; + +/* + * Update the static strings with DLL version numbers. + */ +BOOL +AboutDialog::OnInitDialog(void) +{ + NuError nerr; + long major, minor, bug; + CString newVersion, tmpStr; + CStatic* pStatic; + //CString versionFmt; + + /* CiderPress version string */ + pStatic = (CStatic*) GetDlgItem(IDC_CIDERPRESS_VERS_TEXT); + ASSERT(pStatic != nil); + pStatic->GetWindowText(tmpStr); + newVersion.Format(tmpStr, + kAppMajorVersion, kAppMinorVersion, kAppBugVersion, + kAppDevString, kVersionExtra); + pStatic->SetWindowText(newVersion); + + /* grab the static text control with the NufxLib version info */ + pStatic = (CStatic*) GetDlgItem(IDC_NUFXLIB_VERS_TEXT); + ASSERT(pStatic != nil); + nerr = NuGetVersion(&major, &minor, &bug, NULL, NULL); + ASSERT(nerr == kNuErrNone); + + pStatic->GetWindowText(tmpStr); + newVersion.Format(tmpStr, major, minor, bug); + pStatic->SetWindowText(newVersion); + + /* grab the static text control with the DiskImg version info */ + pStatic = (CStatic*) GetDlgItem(IDC_DISKIMG_VERS_TEXT); + ASSERT(pStatic != nil); + DiskImgLib::Global::GetVersion(&major, &minor, &bug); + + pStatic->GetWindowText(tmpStr); + newVersion.Format(tmpStr, major, minor, bug); + pStatic->SetWindowText(newVersion); + + /* set the zlib version */ + pStatic = (CStatic*) GetDlgItem(IDC_ZLIB_VERS_TEXT); + ASSERT(pStatic != nil); + pStatic->GetWindowText(tmpStr); + newVersion.Format(tmpStr, zlibVersion()); + pStatic->SetWindowText(newVersion); + + /* and, finally, the ASPI version */ + pStatic = (CStatic*) GetDlgItem(IDC_ASPI_VERS_TEXT); + ASSERT(pStatic != nil); + if (DiskImgLib::Global::GetHasASPI()) { + CString versionStr; + DWORD version = DiskImgLib::Global::GetASPIVersion(); + versionStr.Format("%d.%d.%d.%d", + version & 0x0ff, + (version >> 8) & 0xff, + (version >> 16) & 0xff, + (version >> 24) & 0xff); + pStatic->GetWindowText(tmpStr); + newVersion.Format(tmpStr, versionStr); + } else { + newVersion.LoadString(IDS_ASPI_NOT_LOADED); + } + pStatic->SetWindowText(newVersion); + + //ShowRegistrationInfo(); + { + CWnd* pWnd = GetDlgItem(IDC_ABOUT_ENTER_REG); + if (pWnd != nil) { + pWnd->EnableWindow(FALSE); + pWnd->ShowWindow(FALSE); + } + } + + return CDialog::OnInitDialog(); +} + +#if 0 +/* + * Set the appropriate fields in the dialog box. + * + * This is called during initialization and after new registration data is + * entered successfully. + */ +void +AboutDialog::ShowRegistrationInfo(void) +{ + /* + * Pull out the registration info. We shouldn't need to do much in the + * way of validation, since it should have been validated either before + * the program finished initializing or before we wrote the values into + * the registry. It's always possible that somebody went and messed with + * the registry while we were running -- perhaps a different instance of + * CiderPress -- but that should be rare enough that we don't have to + * worry about the occasional ugliness. + */ + const int kDay = 24 * 60 * 60; + CString user, company, reg, versions, expire; + CWnd* pUserWnd; + CWnd* pCompanyWnd; + //CWnd* pExpireWnd; + + pUserWnd = GetDlgItem(IDC_REG_USER_NAME); + ASSERT(pUserWnd != nil); + pCompanyWnd = GetDlgItem(IDC_REG_COMPANY_NAME); + ASSERT(pCompanyWnd != nil); + //pExpireWnd = GetDlgItem(IDC_REG_EXPIRES); + //ASSERT(pExpireWnd != nil); + + if (gMyApp.fRegistry.GetRegistration(&user, &company, ®, &versions, + &expire) == 0) + { + if (reg.IsEmpty()) { + /* not registered, show blank stuff */ + CString unreg; + unreg.LoadString(IDS_ABOUT_UNREGISTERED); + pUserWnd->SetWindowText(unreg); + pCompanyWnd->SetWindowText(""); + + /* show expire date */ + time_t expireWhen; + expireWhen = atol(expire); + if (expireWhen > 0) { + CString expireStr; + time_t now = time(nil); + expireStr.Format(IDS_REG_EVAL_REM, + ((expireWhen - now) + kDay-1) / kDay); + /* leave pUserWnd and pCompanyWnd set to defaults */ + pCompanyWnd->SetWindowText(expireStr); + } else { + pCompanyWnd->SetWindowText(_T("Has already expired!")); + } + } else { + /* show registration info */ + pUserWnd->SetWindowText(user); + pCompanyWnd->SetWindowText(company); + //pExpireWnd->SetWindowText(""); + + /* remove "Enter Registration" button */ + CWnd* pWnd = GetDlgItem(IDC_ABOUT_ENTER_REG); + if (pWnd != nil) { + pWnd->EnableWindow(FALSE); + } + } + } +} +#endif + + +/* + * User hit the "Credits" button. + */ +void +AboutDialog::OnAboutCredits(void) +{ + WinHelp(HELP_TOPIC_CREDITS, HELP_CONTEXT /*HELP_CONTEXTPOPUP*/); +} + +#if 0 +/* + * User hit "enter registration" button. Bring up the appropriate dialog. + */ +void +AboutDialog::OnEnterReg(void) +{ + if (EnterRegDialog::GetRegInfo(this) == 0) { + ShowRegistrationInfo(); + } +} +#endif diff --git a/app/AboutDialog.h b/app/AboutDialog.h new file mode 100644 index 0000000..0785399 --- /dev/null +++ b/app/AboutDialog.h @@ -0,0 +1,37 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Class definition for About dialog. + */ +#ifndef __ABOUT_DIALOG__ +#define __ABOUT_DIALOG__ + +//#include +#include "resource.h" + +/* + * A simple dialog with an overridden initialization so we can tweak the + * controls slightly. + */ +class AboutDialog : public CDialog { +public: + AboutDialog(CWnd* pParentWnd = NULL) : + CDialog(IDD_ABOUTDLG, pParentWnd) + {} + +protected: + // overrides + virtual BOOL OnInitDialog(void); + + afx_msg void OnAboutCredits(void); + afx_msg void OnEnterReg(void); + + //void ShowRegistrationInfo(void); + + DECLARE_MESSAGE_MAP() +}; + +#endif /*__ABOUT_DIALOG__*/ \ No newline at end of file diff --git a/app/ActionProgressDialog.cpp b/app/ActionProgressDialog.cpp new file mode 100644 index 0000000..48e50d8 --- /dev/null +++ b/app/ActionProgressDialog.cpp @@ -0,0 +1,157 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ + /* + * Support for ActionProgressDialog class. + */ +#include "stdafx.h" +#include "ActionProgressDialog.h" +#include "AddFilesDialog.h" +#include "Main.h" + +BEGIN_MESSAGE_MAP(ActionProgressDialog, ProgressCancelDialog) + //ON_MESSAGE(WMU_START, OnStart) +END_MESSAGE_MAP() + +/* + * Initialize the static text controls to say something reasonable. + */ +BOOL +ActionProgressDialog::OnInitDialog(void) +{ + CDialog::OnInitDialog(); + + WMSG1("Action is %d\n", fAction); + + CenterWindow(AfxGetMainWnd()); + + CWnd* pWnd; + + // clear the filename fields + pWnd = GetDlgItem(IDC_PROG_ARC_NAME); + ASSERT(pWnd != nil); + pWnd->SetWindowText(_T("-")); + pWnd = GetDlgItem(IDC_PROG_FILE_NAME); + ASSERT(pWnd != nil); + pWnd->SetWindowText(_T("-")); + + pWnd->SetFocus(); // get the focus off the Cancel button + + if (fAction == kActionExtract) { + /* defaults are correct */ + } else if (fAction == kActionRecompress) { + CString tmpStr; + pWnd = GetDlgItem(IDC_PROG_VERB); + ASSERT(pWnd != nil); + tmpStr.LoadString(IDS_NOW_EXPANDING); + pWnd->SetWindowText(tmpStr); + + pWnd = GetDlgItem(IDC_PROG_TOFROM); + ASSERT(pWnd != nil); + tmpStr.LoadString(IDS_NOW_COMPRESSING); + pWnd->SetWindowText(tmpStr); + } else if (fAction == kActionAdd || fAction == kActionAddDisk || + fAction == kActionConvFile || fAction == kActionConvDisk) + { + CString tmpStr; + pWnd = GetDlgItem(IDC_PROG_VERB); + ASSERT(pWnd != nil); + tmpStr.LoadString(IDS_NOW_ADDING); + pWnd->SetWindowText(tmpStr); + + pWnd = GetDlgItem(IDC_PROG_TOFROM); + ASSERT(pWnd != nil); + tmpStr.LoadString(IDS_ADDING_AS); + pWnd->SetWindowText(tmpStr); + } else if (fAction == kActionDelete) { + CString tmpStr; + pWnd = GetDlgItem(IDC_PROG_VERB); + ASSERT(pWnd != nil); + tmpStr.LoadString(IDS_NOW_DELETING); + pWnd->SetWindowText(tmpStr); + + pWnd = GetDlgItem(IDC_PROG_TOFROM); + pWnd->DestroyWindow(); + pWnd = GetDlgItem(IDC_PROG_FILE_NAME); + ASSERT(pWnd != nil); + pWnd->SetWindowText(_T("")); + } else if (fAction == kActionTest) { + CString tmpStr; + pWnd = GetDlgItem(IDC_PROG_VERB); + ASSERT(pWnd != nil); + tmpStr.LoadString(IDS_NOW_TESTING); + pWnd->SetWindowText(tmpStr); + + pWnd = GetDlgItem(IDC_PROG_TOFROM); + pWnd->DestroyWindow(); + pWnd = GetDlgItem(IDC_PROG_FILE_NAME); + ASSERT(pWnd != nil); + pWnd->SetWindowText(_T("")); + } else { + ASSERT(false); + } + + return FALSE; +} + +/* + * Set the name of the file as it appears in the archive. + */ +void +ActionProgressDialog::SetArcName(const char* str) +{ + CString oldStr; + + CWnd* pWnd = GetDlgItem(IDC_PROG_ARC_NAME); + ASSERT(pWnd != nil); + pWnd->GetWindowText(oldStr); + if (oldStr != str) + pWnd->SetWindowText(str); +} + +const CString +ActionProgressDialog::GetFileName(void) +{ + CString str; + + CWnd* pWnd = GetDlgItem(IDC_PROG_FILE_NAME); + ASSERT(pWnd != nil); + pWnd->GetWindowText(str); + + return str; +} + +/* + * Set the name of the file as it appears under Windows. + */ +void +ActionProgressDialog::SetFileName(const char* str) +{ + CString oldStr; + + CWnd* pWnd = GetDlgItem(IDC_PROG_FILE_NAME); + ASSERT(pWnd != nil); + pWnd->GetWindowText(oldStr); + if (oldStr != str) + pWnd->SetWindowText(str); +} + +/* + * Update the progress meter. + * + * We take a percentage, but the underlying control uses 1000ths. + */ +int +ActionProgressDialog::SetProgress(int perc) +{ + ASSERT(perc >= 0 && perc <= 100); + MainWindow* pMainWin = (MainWindow*)::AfxGetMainWnd(); + + /* solicit input */ + pMainWin->PeekAndPump(); + + return ProgressCancelDialog::SetProgress(perc * + (kProgressResolution/100)); +} diff --git a/app/ActionProgressDialog.h b/app/ActionProgressDialog.h new file mode 100644 index 0000000..1fce87c --- /dev/null +++ b/app/ActionProgressDialog.h @@ -0,0 +1,65 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Show the progress of an action like "add" or "extract". + */ +#ifndef __ACTIONPROGRESSDIALOG__ +#define __ACTIONPROGRESSDIALOG__ + +#include "resource.h" + +/* + * Modeless dialog; must be allocated on the heap. + */ +class ActionProgressDialog : public ProgressCancelDialog { +public: + typedef enum { + kActionUnknown = 0, + kActionAdd, + kActionAddDisk, + kActionExtract, + kActionDelete, + kActionTest, + kActionRecompress, + kActionConvDisk, + kActionConvFile, + } Action; + + ActionProgressDialog(void) { + fAction = kActionUnknown; + //fpSelSet = nil; + //fpOptionsDlg = nil; + fCancel = false; + //fResult = 0; + } + virtual ~ActionProgressDialog(void) {} + + BOOL Create(Action action, CWnd* pParentWnd = NULL) { + fAction = action; + pParentWnd->EnableWindow(FALSE); + return ProgressCancelDialog::Create(&fCancel, IDD_ACTION_PROGRESS, + IDC_PROG_PROGRESS, pParentWnd); + } + void Cleanup(CWnd* pParentWnd) { + pParentWnd->EnableWindow(TRUE); + DestroyWindow(); + } + + void SetArcName(const char* str); + void SetFileName(const char* str); + const CString GetFileName(void); + int SetProgress(int perc); + +private: + virtual BOOL OnInitDialog(void); + + Action fAction; + bool fCancel; + + DECLARE_MESSAGE_MAP() +}; + +#endif /*__ACTIONPROGRESSDIALOG__*/ diff --git a/app/Actions.cpp b/app/Actions.cpp new file mode 100644 index 0000000..335a720 --- /dev/null +++ b/app/Actions.cpp @@ -0,0 +1,2561 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * File actions. These are actually part of MainWindow, but for readability + * these are split into their own file. + */ +#include "stdafx.h" +#include "Main.h" +#include "ViewFilesDialog.h" +//#include "ViewOptionsDialog.h" +#include "ChooseDirDialog.h" +#include "AddFilesDialog.h" +#include "CreateSubdirDialog.h" +#include "ExtractOptionsDialog.h" +#include "UseSelectionDialog.h" +#include "RecompressOptionsDialog.h" +#include "ConvDiskOptionsDialog.h" +#include "ConvFileOptionsDialog.h" +#include "EditCommentDialog.h" +#include "EditPropsDialog.h" +#include "RenameVolumeDialog.h" +#include "ConfirmOverwriteDialog.h" +#include "ImageFormatDialog.h" +#include "FileNameConv.h" +#include "GenericArchive.h" +#include "NufxArchive.h" +#include "DiskArchive.h" +#include "ChooseAddTargetDialog.h" +#include "CassetteDialog.h" +#include "BasicImport.h" +//#include "../util/UtilLib.h" +#include "../diskimg/TwoImg.h" +#include + + +/* + * ========================================================================== + * View + * ========================================================================== + */ + +/* + * View a file stored in the archive. + * + * Control bounces back through Get*FileText() to get the actual + * data to view. + */ +void +MainWindow::OnActionsView(void) +{ + HandleView(); +} +void +MainWindow::OnUpdateActionsView(CCmdUI* pCmdUI) +{ + pCmdUI->Enable(fpContentList != nil && + fpContentList->GetSelectedCount() > 0); +} + + +/* + * Handle a request to view stuff. + * + * If "query" is set, we ask the user to confirm some choices. If not, we + * just go with the defaults. + * + * We include "damaged" files so that we can show the user a nice message + * about how the file is damaged. + */ +void +MainWindow::HandleView(void) +{ + ASSERT(fpContentList != nil); + + SelectionSet selSet; + int threadMask = GenericEntry::kAnyThread | GenericEntry::kAllowDamaged | + GenericEntry::kAllowDirectory | GenericEntry::kAllowVolumeDir; + selSet.CreateFromSelection(fpContentList, threadMask); + selSet.Dump(); + + if (selSet.GetNumEntries() == 0) { + MessageBox("Nothing viewable found.", + "No match", MB_OK | MB_ICONEXCLAMATION); + return; + } + + //fpSelSet = &selSet; + + ViewFilesDialog vfd(this); + vfd.SetSelectionSet(&selSet); + vfd.SetTextTypeFace(fPreferences.GetPrefString(kPrViewTextTypeFace)); + vfd.SetTextPointSize(fPreferences.GetPrefLong(kPrViewTextPointSize)); + vfd.SetNoWrapText(fPreferences.GetPrefBool(kPrNoWrapText)); + vfd.DoModal(); + + //fpSelSet = nil; + + // remember which font they used (sticky pref, not in registry) + fPreferences.SetPrefString(kPrViewTextTypeFace, vfd.GetTextTypeFace()); + fPreferences.SetPrefLong(kPrViewTextPointSize, vfd.GetTextPointSize()); + WMSG2("Preferences: saving view font %d-point '%s'\n", + fPreferences.GetPrefLong(kPrViewTextPointSize), + fPreferences.GetPrefString(kPrViewTextTypeFace)); +} + + +/* + * ========================================================================== + * Open as disk image + * ========================================================================== + */ + +/* + * View a file stored in the archive. + * + * Control bounces back through Get*FileText() to get the actual + * data to view. + */ +void +MainWindow::OnActionsOpenAsDisk(void) +{ + ASSERT(fpContentList != nil); + ASSERT(fpContentList->GetSelectedCount() == 1); + + GenericEntry* pEntry = GetSelectedItem(fpContentList); + if (pEntry->GetHasDiskImage()) + TmpExtractAndOpen(pEntry, GenericEntry::kDiskImageThread, kModeDiskImage); + else + TmpExtractAndOpen(pEntry, GenericEntry::kDataThread, kModeDiskImage); +} +void +MainWindow::OnUpdateActionsOpenAsDisk(CCmdUI* pCmdUI) +{ + const int kMinLen = 512 * 7; + bool allow = false; + + if (fpContentList != nil && fpContentList->GetSelectedCount() == 1) { + GenericEntry* pEntry = GetSelectedItem(fpContentList); + if (pEntry != nil) { + if ((pEntry->GetHasDataFork() || pEntry->GetHasDiskImage()) && + pEntry->GetUncompressedLen() > kMinLen) + { + allow = true; + } + } + } + pCmdUI->Enable(allow); +} + + +/* + * ========================================================================== + * Add Files + * ========================================================================== + */ + +/* + * Add files to an archive. + */ +void +MainWindow::OnActionsAddFiles(void) +{ + WMSG0("Add files!\n"); + AddFilesDialog addFiles(this); + DiskImgLib::A2File* pTargetSubdir = nil; + + /* + * Special handling for adding files to disk images. + */ + if (fpOpenArchive->GetArchiveKind() == GenericArchive::kArchiveDiskImage) + { + if (!ChooseAddTarget(&pTargetSubdir, &addFiles.fpTargetDiskFS)) + return; + } + + addFiles.fStoragePrefix = ""; + addFiles.fIncludeSubfolders = + fPreferences.GetPrefBool(kPrAddIncludeSubFolders); + addFiles.fStripFolderNames = + fPreferences.GetPrefBool(kPrAddStripFolderNames); + addFiles.fOverwriteExisting = + fPreferences.GetPrefBool(kPrAddOverwriteExisting); + addFiles.fTypePreservation = + fPreferences.GetPrefLong(kPrAddTypePreservation); + addFiles.fConvEOL = + fPreferences.GetPrefLong(kPrAddConvEOL); + + /* if they can't convert EOL when adding files, disable the option */ + if (!fpOpenArchive->GetCapability(GenericArchive::kCapCanConvEOLOnAdd)) { + addFiles.fConvEOL = AddFilesDialog::kConvEOLNone; + addFiles.fConvEOLEnable = false; + } + + /* + * Disable editing of the storage prefix field. Force pathname + * stripping to be on for non-hierarchical filesystems (i.e. everything + * but ProDOS and HFS). + */ + if (addFiles.fpTargetDiskFS != nil) { + DiskImg::FSFormat format; + format = addFiles.fpTargetDiskFS->GetDiskImg()->GetFSFormat(); + + if (pTargetSubdir != nil) { + ASSERT(!pTargetSubdir->IsVolumeDirectory()); + addFiles.fStoragePrefix = pTargetSubdir->GetPathName(); + } + + addFiles.fStripFolderNamesEnable = false; + addFiles.fStoragePrefixEnable = false; + switch (format) { + case DiskImg::kFormatProDOS: + case DiskImg::kFormatMacHFS: + addFiles.fStripFolderNamesEnable = true; + break; + default: + break; + } + } + if (!addFiles.fStripFolderNamesEnable) { + /* if we disabled it, we did so because it's mandatory */ + addFiles.fStripFolderNames = true; + } + + addFiles.m_ofn.lpstrInitialDir = fPreferences.GetPrefString(kPrAddFileFolder); + + addFiles.DoModal(); + if (addFiles.GetExitStatus() == IDOK) { + fPreferences.SetPrefBool(kPrAddIncludeSubFolders, + addFiles.fIncludeSubfolders != 0); + if (addFiles.fStripFolderNamesEnable) { + // only update the pref if they had the ability to change it + fPreferences.SetPrefBool(kPrAddStripFolderNames, + addFiles.fStripFolderNames != 0); + } + fPreferences.SetPrefBool(kPrAddOverwriteExisting, + addFiles.fOverwriteExisting != 0); + fPreferences.SetPrefLong(kPrAddTypePreservation, + addFiles.fTypePreservation); + if (addFiles.fConvEOLEnable) + fPreferences.SetPrefLong(kPrAddConvEOL, + addFiles.fConvEOL); + + CString saveFolder = addFiles.GetFileNames(); + saveFolder = saveFolder.Left(addFiles.GetFileNameOffset()); + fPreferences.SetPrefString(kPrAddFileFolder, saveFolder); + + /* + * Set up a progress dialog and kick things off. + */ + bool result; + fpActionProgress = new ActionProgressDialog; + fpActionProgress->Create(ActionProgressDialog::kActionAdd, this); + + //fpContentList->Invalidate(); // don't allow redraws until done + result = fpOpenArchive->BulkAdd(fpActionProgress, &addFiles); + fpContentList->Reload(); + + fpActionProgress->Cleanup(this); + fpActionProgress = nil; + if (result) + SuccessBeep(); + } else { + WMSG0("SFD bailed with Cancel\n"); + } +} +void +MainWindow::OnUpdateActionsAddFiles(CCmdUI* pCmdUI) +{ + pCmdUI->Enable(fpContentList != nil && !fpOpenArchive->IsReadOnly()); +} + + +/* + * Figure out where they want to add files. + * + * If the volume directory of a disk is chosen, "*ppTargetSubdir" will + * be set to nil. + */ +bool +MainWindow::ChooseAddTarget(DiskImgLib::A2File** ppTargetSubdir, + DiskImgLib::DiskFS** ppTargetDiskFS) +{ + ASSERT(ppTargetSubdir != nil); + ASSERT(ppTargetDiskFS != nil); + + *ppTargetSubdir = nil; + *ppTargetDiskFS = nil; + + GenericEntry* pEntry = GetSelectedItem(fpContentList); + if (pEntry != nil && + (pEntry->GetRecordKind() == GenericEntry::kRecordKindDirectory || + pEntry->GetRecordKind() == GenericEntry::kRecordKindVolumeDir)) + { + /* + * They've selected a single subdirectory. Add the files there. + */ + DiskEntry* pDiskEntry = (DiskEntry*) pEntry; + *ppTargetSubdir = pDiskEntry->GetA2File(); + *ppTargetDiskFS = (*ppTargetSubdir)->GetDiskFS(); + } else { + /* + * Nothing selected, non-subdir selected, or multiple files + * selected. Whatever the case, pop up the choose target dialog. + * + * This works on DOS 3.3 and Pascal disks, because the absence + * of subdirectories means there's only one possible place to + * put the files. We could short-circuit this code for anything + * but ProDOS and HFS, but we have to be careful about embedded + * sub-volumes. + */ + DiskArchive* pDiskArchive = (DiskArchive*) fpOpenArchive; + + WMSG0("Trying ChooseAddTarget\n"); + + ChooseAddTargetDialog targetDialog(this); + targetDialog.fpDiskFS = pDiskArchive->GetDiskFS(); + if (targetDialog.DoModal() != IDOK) + return false; + + *ppTargetSubdir = targetDialog.fpChosenSubdir; + *ppTargetDiskFS = targetDialog.fpChosenDiskFS; + + /* make sure the subdir is part of the diskfs */ + ASSERT(*ppTargetSubdir == nil || + (*ppTargetSubdir)->GetDiskFS() == *ppTargetDiskFS); + } + + if (*ppTargetSubdir != nil && (*ppTargetSubdir)->IsVolumeDirectory()) + *ppTargetSubdir = nil; + + return true; +} + + +/* + * ========================================================================== + * Add Disks + * ========================================================================== + */ + +/* + * Add a disk to an archive. Not all archive formats support disk images. + * + * We open a single disk archive file as a DiskImg, get the format + * figured out, then write it block-by-block into a file chosen by the user. + * Standard open/save dialogs work fine here. + */ +void +MainWindow::OnActionsAddDisks(void) +{ + DIError dierr; + DiskImg img; + CString failed, errMsg; + CString openFilters, saveFolder; + AddFilesDialog addOpts; + + WMSG0("Add disks!\n"); + + failed.LoadString(IDS_FAILED); + + openFilters = kOpenDiskImage; + openFilters += kOpenAll; + openFilters += kOpenEnd; + CFileDialog dlg(TRUE, "dsk", NULL, OFN_FILEMUSTEXIST, openFilters, this); + dlg.m_ofn.lpstrTitle = "Add Disk Image"; + + /* file is always opened read-only */ + dlg.m_ofn.Flags |= OFN_HIDEREADONLY; + dlg.m_ofn.lpstrInitialDir = fPreferences.GetPrefString(kPrAddFileFolder); + + if (dlg.DoModal() != IDOK) + goto bail; + + saveFolder = dlg.m_ofn.lpstrFile; + saveFolder = saveFolder.Left(dlg.m_ofn.nFileOffset); + fPreferences.SetPrefString(kPrAddFileFolder, saveFolder); + + /* open the image file and analyze it */ + dierr = img.OpenImage(dlg.GetPathName(), PathProposal::kLocalFssep, true); + if (dierr != kDIErrNone) { + errMsg.Format("Unable to open disk image: %s.", + DiskImgLib::DIStrError(dierr)); + MessageBox(errMsg, failed, MB_OK|MB_ICONSTOP); + goto bail; + } + + if (img.AnalyzeImage() != kDIErrNone) { + errMsg.Format("The file '%s' doesn't seem to hold a valid disk image.", + dlg.GetPathName()); + MessageBox(errMsg, failed, MB_OK|MB_ICONSTOP); + goto bail; + } + + + /* if requested (or necessary), verify the format */ + if (/*img.GetFSFormat() == DiskImg::kFormatUnknown ||*/ + img.GetSectorOrder() == DiskImg::kSectorOrderUnknown || + fPreferences.GetPrefBool(kPrQueryImageFormat)) + { + ImageFormatDialog imf; + + imf.InitializeValues(&img); + imf.fFileSource = dlg.GetPathName(); + imf.SetQueryDisplayFormat(false); + + WMSG2(" On entry, sectord=%d format=%d\n", + imf.fSectorOrder, imf.fFSFormat); + if (imf.fFSFormat == DiskImg::kFormatUnknown) + imf.fFSFormat = DiskImg::kFormatGenericProDOSOrd; + + if (imf.DoModal() != IDOK) { + WMSG0("User bailed on IMF dialog\n"); + goto bail; + } + + WMSG2(" On exit, sectord=%d format=%d\n", + imf.fSectorOrder, imf.fFSFormat); + + if (imf.fSectorOrder != img.GetSectorOrder() || + imf.fFSFormat != img.GetFSFormat()) + { + WMSG0("Initial values overridden, forcing img format\n"); + dierr = img.OverrideFormat(img.GetPhysicalFormat(), imf.fFSFormat, + imf.fSectorOrder); + if (dierr != kDIErrNone) { + errMsg.Format("Unable to access disk image using selected" + " parameters. Error: %s.", + DiskImgLib::DIStrError(dierr)); + MessageBox(errMsg, failed, MB_OK | MB_ICONSTOP); + goto bail; + } + } + } + + /* + * We want to read from the image the way that a ProDOS application + * would, which means forcing the FSFormat to generic ProDOS ordering. + * This way, ProDOS disks load serially, and DOS 3.3 disks load with + * their sectors swapped around. + */ + dierr = img.OverrideFormat(img.GetPhysicalFormat(), + DiskImg::kFormatGenericProDOSOrd, img.GetSectorOrder()); + if (dierr != kDIErrNone) { + errMsg.Format("Internal error: couldn't switch to generic ProDOS: %s.", + DiskImgLib::DIStrError(dierr)); + MessageBox(errMsg, failed, MB_OK | MB_ICONSTOP); + goto bail; + } + + /* + * Set up an AddFilesDialog, but don't actually use it as a dialog. + * Instead, we just configure the various options appropriately. + * + * To conform to multi-file-selection semantics, we need to drop a + * null byte in right after the pathname. + */ + ASSERT(dlg.m_ofn.nFileOffset > 0); + int len; + len = strlen(dlg.m_ofn.lpstrFile) + 2; + dlg.m_ofn.lpstrFile[dlg.m_ofn.nFileOffset-1] = '\0'; + addOpts.SetFileNames(dlg.m_ofn.lpstrFile, len, dlg.m_ofn.nFileOffset); + addOpts.fStoragePrefix = ""; + addOpts.fIncludeSubfolders = false; + addOpts.fStripFolderNames = false; + addOpts.fOverwriteExisting = false; + addOpts.fTypePreservation = AddFilesDialog::kPreserveTypes; + addOpts.fConvEOL = AddFilesDialog::kConvEOLNone; + addOpts.fConvEOLEnable = false; // no EOL conversion on disk images! + addOpts.fpDiskImg = &img; + + bool result; + + fpActionProgress = new ActionProgressDialog; + fpActionProgress->Create(ActionProgressDialog::kActionAddDisk, this); + + //fpContentList->Invalidate(); // don't allow updates until done + result = fpOpenArchive->AddDisk(fpActionProgress, &addOpts); + fpContentList->Reload(); + + fpActionProgress->Cleanup(this); + fpActionProgress = nil; + + if (result) + SuccessBeep(); + +bail: + return; +} +void +MainWindow::OnUpdateActionsAddDisks(CCmdUI* pCmdUI) +{ + pCmdUI->Enable(fpContentList != nil && !fpOpenArchive->IsReadOnly() && + fpOpenArchive->GetCapability(GenericArchive::kCapCanAddDisk)); +} + + +/* + * ========================================================================== + * Create Subdirectory + * ========================================================================== + */ + +/* + * Create a subdirectory inside another subdirectory (or volume directory). + * + * Simply asserting that an existing subdir be selected in the list does + * away with all sorts of testing. Creating subdirs on DOS disks and NuFX + * archives is impossible because neither has subdirs. Nested volumes are + * selected for us by the user. + */ +void +MainWindow::OnActionsCreateSubdir(void) +{ + CreateSubdirDialog csDialog; + + ASSERT(fpContentList != nil); + ASSERT(fpOpenArchive != nil); + ASSERT(!fpOpenArchive->IsReadOnly()); + + GenericEntry* pEntry = GetSelectedItem(fpContentList); + if (pEntry == nil) { + // can happen for no selection or multi-selection; should not be here + ASSERT(false); + return; + } + if (pEntry->GetRecordKind() != GenericEntry::kRecordKindDirectory && + pEntry->GetRecordKind() != GenericEntry::kRecordKindVolumeDir) + { + CString errMsg; + errMsg = "Please select a subdirectory."; + ShowFailureMsg(this, errMsg, IDS_MB_APP_NAME); + return; + } + + WMSG1("Creating subdir in '%s'\n", pEntry->GetPathName()); + + csDialog.fBasePath = pEntry->GetPathName(); + csDialog.fpArchive = fpOpenArchive; + csDialog.fpParentEntry = pEntry; + csDialog.fNewName = "New.Subdir"; + if (csDialog.DoModal() != IDOK) + return; + + WMSG1("Creating '%s'\n", csDialog.fNewName); + + fpOpenArchive->CreateSubdir(this, pEntry, csDialog.fNewName); + fpContentList->Reload(); +} +void +MainWindow::OnUpdateActionsCreateSubdir(CCmdUI* pCmdUI) +{ + bool enable = fpContentList != nil && !fpOpenArchive->IsReadOnly() && + fpContentList->GetSelectedCount() == 1 && + fpOpenArchive->GetCapability(GenericArchive::kCapCanCreateSubdir); + + if (enable) { + /* second-level check: make sure it's a subdir */ + GenericEntry* pEntry = GetSelectedItem(fpContentList); + if (pEntry == nil) { + ASSERT(false); + return; + } + if (pEntry->GetRecordKind() != GenericEntry::kRecordKindDirectory && + pEntry->GetRecordKind() != GenericEntry::kRecordKindVolumeDir) + { + enable = false; + } + } + + pCmdUI->Enable(enable); +} + + +/* + * ========================================================================== + * Extract + * ========================================================================== + */ + +/* + * Extract files. + */ +void +MainWindow::OnActionsExtract(void) +{ + ASSERT(fpContentList != nil); + + /* + * Ask the user about various options. + */ + ExtractOptionsDialog extOpts(fpContentList->GetSelectedCount(), this); + extOpts.fExtractPath = fPreferences.GetPrefString(kPrExtractFileFolder); + extOpts.fConvEOL = fPreferences.GetPrefLong(kPrExtractConvEOL); + extOpts.fConvHighASCII = fPreferences.GetPrefBool(kPrExtractConvHighASCII); + extOpts.fIncludeDataForks = fPreferences.GetPrefBool(kPrExtractIncludeData); + extOpts.fIncludeRsrcForks = fPreferences.GetPrefBool(kPrExtractIncludeRsrc); + extOpts.fIncludeDiskImages = fPreferences.GetPrefBool(kPrExtractIncludeDisk); + extOpts.fEnableReformat = fPreferences.GetPrefBool(kPrExtractEnableReformat); + extOpts.fDiskTo2MG = fPreferences.GetPrefBool(kPrExtractDiskTo2MG); + extOpts.fAddTypePreservation = fPreferences.GetPrefBool(kPrExtractAddTypePreservation); + extOpts.fAddExtension = fPreferences.GetPrefBool(kPrExtractAddExtension); + extOpts.fStripFolderNames = fPreferences.GetPrefBool(kPrExtractStripFolderNames); + extOpts.fOverwriteExisting = fPreferences.GetPrefBool(kPrExtractOverwriteExisting); + + if (fpContentList->GetSelectedCount() > 0) + extOpts.fFilesToExtract = ExtractOptionsDialog::kExtractSelection; + else + extOpts.fFilesToExtract = ExtractOptionsDialog::kExtractAll; + + if (extOpts.DoModal() != IDOK) + return; + + if (extOpts.fExtractPath.Right(1) != "\\") + extOpts.fExtractPath += "\\"; + + /* push preferences back out */ + fPreferences.SetPrefString(kPrExtractFileFolder, extOpts.fExtractPath); + fPreferences.SetPrefLong(kPrExtractConvEOL, extOpts.fConvEOL); + fPreferences.SetPrefBool(kPrExtractConvHighASCII, extOpts.fConvHighASCII != 0); + fPreferences.SetPrefBool(kPrExtractIncludeData, extOpts.fIncludeDataForks != 0); + fPreferences.SetPrefBool(kPrExtractIncludeRsrc, extOpts.fIncludeRsrcForks != 0); + fPreferences.SetPrefBool(kPrExtractIncludeDisk, extOpts.fIncludeDiskImages != 0); + fPreferences.SetPrefBool(kPrExtractEnableReformat, extOpts.fEnableReformat != 0); + fPreferences.SetPrefBool(kPrExtractDiskTo2MG, extOpts.fDiskTo2MG != 0); + fPreferences.SetPrefBool(kPrExtractAddTypePreservation, extOpts.fAddTypePreservation != 0); + fPreferences.SetPrefBool(kPrExtractAddExtension, extOpts.fAddExtension != 0); + fPreferences.SetPrefBool(kPrExtractStripFolderNames, extOpts.fStripFolderNames != 0); + fPreferences.SetPrefBool(kPrExtractOverwriteExisting, extOpts.fOverwriteExisting != 0); + + WMSG1("Requested extract path is '%s'\n", extOpts.fExtractPath); + + /* + * Create a "selection set" of things to display. + */ + SelectionSet selSet; + int threadMask = 0; + if (extOpts.fIncludeDataForks) + threadMask |= GenericEntry::kDataThread; + if (extOpts.fIncludeRsrcForks) + threadMask |= GenericEntry::kRsrcThread; + if (extOpts.fIncludeDiskImages) + threadMask |= GenericEntry::kDiskImageThread; + + if (extOpts.fFilesToExtract == ExtractOptionsDialog::kExtractSelection) { + selSet.CreateFromSelection(fpContentList, threadMask); + } else { + selSet.CreateFromAll(fpContentList, threadMask); + } + //selSet.Dump(); + + if (selSet.GetNumEntries() == 0) { + MessageBox("No files matched the selection criteria.", + "No match", MB_OK | MB_ICONEXCLAMATION); + return; + } + + /* + * Set up the progress dialog then do the extraction. + */ + fpActionProgress = new ActionProgressDialog; + fpActionProgress->Create(ActionProgressDialog::kActionExtract, this); + DoBulkExtract(&selSet, &extOpts); + fpActionProgress->Cleanup(this); + fpActionProgress = nil; +} +void +MainWindow::OnUpdateActionsExtract(CCmdUI* pCmdUI) +{ + pCmdUI->Enable(fpContentList != nil); +} + +/* + * Handle a bulk extraction. + * + * IMPORTANT: since the pActionProgress dialog has the foreground, it's + * vital that any MessageBox calls go through that. Otherwise the + * progress dialog message handler won't get disabled by MessageBox and + * we can end up permanently hiding the dialog. (Could also use + * ::MessageBox or ::AfxMessageBox instead.) + */ +void +MainWindow::DoBulkExtract(SelectionSet* pSelSet, + const ExtractOptionsDialog* pExtOpts) +{ + ReformatHolder* pHolder = nil; + bool overwriteExisting, ovwrForAll; + + ASSERT(pSelSet != nil); + ASSERT(fpActionProgress != nil); + + pSelSet->IterReset(); + + /* set up our "overwrite existing files" logic */ + overwriteExisting = ovwrForAll = (pExtOpts->fOverwriteExisting != FALSE); + + while (true) { + SelectionEntry* pSelEntry; + bool result; + + /* make sure we service events */ + PeekAndPump(); + + pSelEntry = pSelSet->IterNext(); + if (pSelEntry == nil) { + SuccessBeep(); + break; // out of while (all done!) + } + + GenericEntry* pEntry = pSelEntry->GetEntry(); + if (pEntry->GetDamaged()) { + WMSG1("Skipping '%s' due to damage\n", pEntry->GetPathName()); + continue; + } + + /* + * Extract all parts of the file -- including those we don't actually + * intend to extract to disk -- and hold them in the ReformatHolder. + * Some formats, e.g. GWP, need the resource fork to do formatting, + * so it's important to have the resource fork available even if + * we're just going to throw it away. + * + * The selection set should have screened out anything totally + * inappropriate, e.g. files with nothing but a resource fork don't + * make it into the set if we're not extracting resource forks. + * + * We only want to reformat files, not disk images, directories, + * volume dirs, etc. We have a reformatter for ProDOS directories, + * but (a) we don't explicitly extract subdirs, and (b) we'd really + * like directories to be directories so we can extract files into + * them. + */ + if (pExtOpts->ShouldTryReformat() && + (pEntry->GetRecordKind() == GenericEntry::kRecordKindFile || + pEntry->GetRecordKind() == GenericEntry::kRecordKindForkedFile)) + { + fpActionProgress->SetArcName(pEntry->GetDisplayName()); + fpActionProgress->SetFileName(_T("-")); + SET_PROGRESS_BEGIN(); + + if (GetFileParts(pEntry, &pHolder) == 0) { + /* + * Use the prefs, but disable generic text conversion, so + * that we default to "raw". That way we will use the text + * conversion that the user has specified in the "extract" + * dialog. + * + * We might want to just disable any "always"-level + * reformatter, but that would require tweaking the reformat + * code to return "raw" when nothing applies. + */ + ConfigureReformatFromPreferences(pHolder); + pHolder->SetReformatAllowed(ReformatHolder::kReformatTextEOL_HA, + false); + + pHolder->SetSourceAttributes( + pEntry->GetFileType(), + pEntry->GetAuxType(), + ReformatterSourceFormat(pEntry->GetSourceFS()), + pEntry->GetFileNameExtension()); + pHolder->TestApplicability(); + } + } + + if (pExtOpts->fIncludeDataForks && pEntry->GetHasDataFork()) { + result = ExtractEntry(pEntry, GenericEntry::kDataThread, + pHolder, pExtOpts, &overwriteExisting, &ovwrForAll); + if (!result) + break; + } + if (pExtOpts->fIncludeRsrcForks && pEntry->GetHasRsrcFork()) { + result = ExtractEntry(pEntry, GenericEntry::kRsrcThread, + pHolder, pExtOpts, &overwriteExisting, &ovwrForAll); + if (!result) + break; + } + if (pExtOpts->fIncludeDiskImages && pEntry->GetHasDiskImage()) { + result = ExtractEntry(pEntry, GenericEntry::kDiskImageThread, + pHolder, pExtOpts, &overwriteExisting, &ovwrForAll); + if (!result) + break; + } + delete pHolder; + pHolder = nil; + } + + // if they cancelled, delete the "stray" + delete pHolder; +} + +/* + * Extract a single entry. + * + * If "pHolder" is non-nil, it holds the data from the file, and can be + * used for formatted or non-formatted output. If it's nil, we need to + * extract the data ourselves. + * + * Returns "true" on success, "false" on failure. + */ +bool +MainWindow::ExtractEntry(GenericEntry* pEntry, int thread, + ReformatHolder* pHolder, const ExtractOptionsDialog* pExtOpts, + bool* pOverwriteExisting, bool* pOvwrForAll) +{ + /* + * This first bit of setup is the same for every file. However, it's + * pretty quick, and it's easier to pass "pExtOpts" in than all of + * this stuff, so we just do it every time. + */ + GenericEntry::ConvertEOL convEOL; + GenericEntry::ConvertHighASCII convHA; + bool convTextByType = false; + + + /* translate the EOL conversion mode into GenericEntry terms */ + switch (pExtOpts->fConvEOL) { + case ExtractOptionsDialog::kConvEOLNone: + convEOL = GenericEntry::kConvertEOLOff; + break; + case ExtractOptionsDialog::kConvEOLType: + convEOL = GenericEntry::kConvertEOLOff; + convTextByType = true; + break; + case ExtractOptionsDialog::kConvEOLAuto: + convEOL = GenericEntry::kConvertEOLAuto; + break; + case ExtractOptionsDialog::kConvEOLAll: + convEOL = GenericEntry::kConvertEOLOn; + break; + default: + ASSERT(false); + convEOL = GenericEntry::kConvertEOLOff; + break; + } + if (pExtOpts->fConvHighASCII) + convHA = GenericEntry::kConvertHAAuto; + else + convHA = GenericEntry::kConvertHAOff; + + //WMSG2(" DBE initial text conversion: eol=%d ha=%d\n", + // convEOL, convHA); + + + ReformatHolder holder; + CString outputPath; + CString failed, errMsg; + failed.LoadString(IDS_FAILED); + bool writeFailed = false; + bool extractAs2MG = false; + char* reformatText = nil; + MyDIBitmap* reformatDib = nil; + + ASSERT(pEntry != nil); + + /* + * If we're interested in extracting disk images as 2MG files, + * see if we want to handle this one that way. + * + * If they said "don't extract disk images", the images should + * have been culled from the selection set earlier. + */ + if (pEntry->GetRecordKind() == GenericEntry::kRecordKindDisk) { + ASSERT(pExtOpts->fIncludeDiskImages); + if (pExtOpts->fDiskTo2MG) { + if (pEntry->GetUncompressedLen() > 0 && + (pEntry->GetUncompressedLen() % TwoImgHeader::kBlockSize) == 0) + { + extractAs2MG = true; + } else { + WMSG2("Not extracting funky image '%s' as 2MG (len=%ld)\n", + pEntry->GetPathName(), pEntry->GetUncompressedLen()); + } + } + } + + /* + * Convert the archived pathname to something suitable for the + * local machine (i.e. Win32). + */ + PathProposal pathProp; + CString convName, convNameExtd, convNameExtdPlus; + pathProp.Init(pEntry); + pathProp.fThreadKind = thread; + if (pExtOpts->fStripFolderNames) + pathProp.fJunkPaths = true; + + pathProp.ArchiveToLocal(); + convName = pathProp.fLocalPathName; + + /* run it through again, this time with optional type preservation */ + if (pExtOpts->fAddTypePreservation) + pathProp.fPreservation = true; + pathProp.ArchiveToLocal(); + convNameExtd = pathProp.fLocalPathName; + + /* and a 3rd time, also taking additional extensions into account */ + if (pExtOpts->fAddExtension) + pathProp.fAddExtension = true; + pathProp.ArchiveToLocal(); + convNameExtdPlus = pathProp.fLocalPathName; + + /* + * Prepend the extraction dir to the local pathname. We also add + * the sub-volume name (if any), which should already be a valid + * Win32 directory name. We can't add it earlier because the fssep + * char might be '\0'. + */ + ASSERT(pExtOpts->fExtractPath.Right(1) == "\\"); + CString adjustedExtractPath(pExtOpts->fExtractPath); + if (!pExtOpts->fStripFolderNames && pEntry->GetSubVolName() != nil) { + adjustedExtractPath += pEntry->GetSubVolName(); + adjustedExtractPath += "\\"; + } + outputPath = adjustedExtractPath; + outputPath += convNameExtdPlus; + + + ReformatOutput* pOutput = nil; + + /* + * If requested, try to reformat this file. + */ + if (pHolder != nil) { + ReformatHolder::ReformatPart part = ReformatHolder::kPartUnknown; + ReformatHolder::ReformatID id; + CString title; + //int result; + + switch (thread) { + case GenericEntry::kDataThread: + part = ReformatHolder::kPartData; + break; + case GenericEntry::kRsrcThread: + part = ReformatHolder::kPartRsrc; + break; + case GenericEntry::kDiskImageThread: + part = ReformatHolder::kPartData; + break; + case GenericEntry::kCommentThread: + default: + assert(false); + return false; + } + + fpActionProgress->SetFileName(_T("(reformatting)")); + id = pHolder->FindBest(part); + + { + CWaitCursor waitc; + pOutput = pHolder->Apply(part, id); + } + + if (pOutput != nil) { + /* use output pathname without preservation */ + CString tmpPath; + bool goodReformat = true; + bool noChangePath = false; + + tmpPath = adjustedExtractPath; + tmpPath += convName; + + CString lastFour = tmpPath.Right(4); + + /* + * Tack on a file extension identifying the reformatted + * contents. If the filename already has the correct + * extension, don't tack it on again. + */ + switch (pOutput->GetOutputKind()) { + case ReformatOutput::kOutputText: + if (lastFour.CompareNoCase(".txt") != 0) + tmpPath += ".txt"; + break; + case ReformatOutput::kOutputRTF: + if (lastFour.CompareNoCase(".rtf") != 0) + tmpPath += ".rtf"; + break; + case ReformatOutput::kOutputCSV: + if (lastFour.CompareNoCase(".csv") != 0) + tmpPath += ".csv"; + break; + case ReformatOutput::kOutputBitmap: + if (lastFour.CompareNoCase(".bmp") != 0) + tmpPath += ".bmp"; + break; + case ReformatOutput::kOutputRaw: + noChangePath = true; + break; + default: + // kOutputErrorMsg, kOutputUnknown + goodReformat = false; + break; + } + + if (goodReformat) { + if (!noChangePath) + outputPath = tmpPath; + } else { + delete pOutput; + pOutput = nil; + } + } + } + if (extractAs2MG) { + /* + * Reduce to base name and add 2IMG suffix. Would be nice to keep + * the non-extended file type preservation stuff, but right now we + * only expect unadorned sectors for that (and so does NuLib2). + */ + outputPath = adjustedExtractPath; + outputPath += convName; + outputPath += ".2mg"; + } + + /* update the display in case we renamed it */ + if (outputPath != fpActionProgress->GetFileName()) { + WMSG2(" Renamed our output, from '%s' to '%s'\n", + (LPCTSTR) fpActionProgress->GetFileName(), outputPath); + fpActionProgress->SetFileName(outputPath); + } + + /* + * Update the progress meter output filename, and reset the thermometer. + */ + fpActionProgress->SetArcName(pathProp.fStoredPathName); + fpActionProgress->SetFileName(outputPath); + WMSG2("Extracting from '%s' to '%s'\n", + pathProp.fStoredPathName, outputPath); + SET_PROGRESS_BEGIN(); + + /* + * Open the output file. + * + * Returns IDCANCEL on failures as well as user cancellation. + */ + FILE* fp = nil; + int result; + result = OpenOutputFile(&outputPath, pathProp, pEntry->GetModWhen(), + pOverwriteExisting, pOvwrForAll, &fp); + if (result == IDCANCEL) { + // no messagebox for this one + delete pOutput; + return false; + } + + + /* update the display in case they renamed the file */ + if (outputPath != fpActionProgress->GetFileName()) { + WMSG2(" Detected rename, from '%s' to '%s'\n", + (LPCTSTR) fpActionProgress->GetFileName(), outputPath); + fpActionProgress->SetFileName(outputPath); + } + + if (fp == nil) { + /* looks like they elected to skip extraction of this file */ + delete pOutput; + return true; + } + + //EventPause(500); // DEBUG DEBUG + + + /* + * Handle "extract as 2MG" by writing a 2MG header to the start + * of the file before we hand off to the extraction function. + * + * NOTE: we're currently assuming that we're extracting an image + * in ProDOS sector order. This is a valid assumption so long as + * we're only pulling disk images out of ShrinkIt archives. + * + * We don't currently use the WriteFooter call here, because we're + * not adding comments. + */ + if (extractAs2MG) { + TwoImgHeader header; + header.InitHeader(TwoImgHeader::kImageFormatProDOS, + (long) pEntry->GetUncompressedLen(), + (long) (pEntry->GetUncompressedLen() / TwoImgHeader::kBlockSize)); + int err; + ASSERT(ftell(fp) == 0); + err = header.WriteHeader(fp); + if (err != 0) { + errMsg.Format("Unable to save 2MG file '%s': %s\n", + outputPath, strerror(err)); + fpActionProgress->MessageBox(errMsg, failed, + MB_OK | MB_ICONERROR); + goto open_file_fail; + } + ASSERT(ftell(fp) == 64); // size of 2MG header + } + + + /* + * In some cases we want to override the automatic text detection. + * + * If we're in "auto" mode, force conversion on for DOS/RDOS text files. + * This is important when "convHA" is off, because in high-ASCII mode + * we might not recognize text files for what they are. We also + * consider 0x00 to be binary, which screws up random-access text. + * + * We don't want to do text conversion on disk images or resource + * forks, ever. Turn them off here. + */ + GenericEntry::ConvertEOL thisConv; + thisConv = convEOL; + if (thisConv == GenericEntry::kConvertEOLAuto) { + if (DiskImg::UsesDOSFileStructure(pEntry->GetSourceFS()) && + pEntry->GetFileType() == kFileTypeTXT) + { + WMSG0("Switching EOLAuto to EOLOn for DOS text file\n"); + thisConv = GenericEntry::kConvertEOLOn; + } + } else if (convTextByType) { + /* force it on or off when in conv-by-type mode */ + if (pEntry->GetFileType() == kFileTypeTXT || + pEntry->GetFileType() == kFileTypeSRC) + { + WMSG0("Enabling EOL conv for text file\n"); + thisConv = GenericEntry::kConvertEOLOn; + } else { + ASSERT(thisConv == GenericEntry::kConvertEOLOff); + } + } + if (thisConv != GenericEntry::kConvertEOLOff && + (thread == GenericEntry::kRsrcThread || + thread == GenericEntry::kDiskImageThread)) + { + WMSG0("Disabling EOL conv for resource fork or disk image\n"); + thisConv = GenericEntry::kConvertEOLOff; + } + + + /* + * Extract the contents to the file. + * + * In some cases, notably when the file size exceeds the limit of + * the reformatter, we will be trying to reformat but won't have + * loaded the original data. In such cases we fall through to the + * normal extraction mode, because we threw out pOutput above when + * the result was kOutputErrorMsg. + * + * (Could also be due to extraction failure, e.g. bad CRC.) + */ + if (pOutput != nil) { + /* + * We have the data in our buffer. Write it out. No need + * to tweak the progress updater, which already shows 100%. + * + * There are four possibilities: + * - Valid text/rtf/csv converted text. Write reformatted. + * - Valid bitmap converted. Write bitmap. + * - No reformatter found, type is "raw". Write raw. (Note + * this may be zero bytes long for an empty file.) + * - Error message encoded in result. Should not be here! + */ + if (pOutput->GetOutputKind() == ReformatOutput::kOutputText || + pOutput->GetOutputKind() == ReformatOutput::kOutputRTF || + pOutput->GetOutputKind() == ReformatOutput::kOutputCSV) + { + WMSG0(" Writing text, RTF, CSV, or raw\n"); + ASSERT(pOutput->GetTextBuf() != nil); + int err = 0; + if (fwrite(pOutput->GetTextBuf(), + pOutput->GetTextLen(), 1, fp) != 1) + err = errno; + if (err != 0) { + errMsg.Format("Unable to save reformatted file '%s': %s\n", + outputPath, strerror(err)); + fpActionProgress->MessageBox(errMsg, failed, + MB_OK | MB_ICONERROR); + writeFailed = true; + } else { + SET_PROGRESS_UPDATE(100); + } + } else if (pOutput->GetOutputKind() == ReformatOutput::kOutputBitmap) { + WMSG0(" Writing bitmap\n"); + ASSERT(pOutput->GetDIB() != nil); + int err = pOutput->GetDIB()->WriteToFile(fp); + if (err != 0) { + errMsg.Format("Unable to save bitmap '%s': %s\n", + outputPath, strerror(err)); + fpActionProgress->MessageBox(errMsg, failed, + MB_OK | MB_ICONERROR); + writeFailed = true; + } else { + SET_PROGRESS_UPDATE(100); + } + } else if (pOutput->GetOutputKind() == ReformatOutput::kOutputRaw) { + /* + * Send raw data through the text conversion configured in the + * extract files dialog. Any file for which no dedicated + * reformatter could be found ends up here. + * + * We could just send it through to the generic non-reformatter + * case, but that would require reading the file twice. + */ + WMSG1(" Writing un-reformatted data (%ld bytes)\n", + pOutput->GetTextLen()); + ASSERT(pOutput->GetTextBuf() != nil); + bool lastCR = false; + GenericEntry::ConvertHighASCII thisConvHA = convHA; + int err; + err = GenericEntry::WriteConvert(fp, pOutput->GetTextBuf(), + pOutput->GetTextLen(), &thisConv, &thisConvHA, &lastCR); + if (err != 0) { + errMsg.Format("Unable to write file '%s': %s\n", + outputPath, strerror(err)); + fpActionProgress->MessageBox(errMsg, failed, + MB_OK | MB_ICONERROR); + writeFailed = true; + } else { + SET_PROGRESS_UPDATE(100); + } + + } else { + /* something failed, and we don't have the file */ + WMSG0("How'd we get here?\n"); + ASSERT(false); + } + } else { + /* + * We don't have the data, probably because we aren't using file + * reformatters. Use the GenericEntry extraction routine to copy + * the data directly into the file. + * + * We also get here if the file has a length of zero. + */ + CString msg; + int result; + ASSERT(fpActionProgress != nil); + WMSG3("Extracting '%s', requesting thisConv=%d, convHA=%d\n", + outputPath, thisConv, convHA); + result = pEntry->ExtractThreadToFile(thread, fp, + thisConv, convHA, &msg); + if (result != IDOK) { + if (result == IDCANCEL) { + CString msg; + msg.LoadString(IDS_OPERATION_CANCELLED); + fpActionProgress->MessageBox(msg, + "CiderPress", MB_OK | MB_ICONEXCLAMATION); + } else { + WMSG2(" FAILED on '%s': %s\n", outputPath, msg); + errMsg.Format("Unable to extract file '%s': %s\n", + outputPath, (LPCTSTR) msg); + fpActionProgress->MessageBox(errMsg, failed, + MB_OK | MB_ICONERROR); + } + writeFailed = true; + } + } + +open_file_fail: + delete pOutput; + + fclose(fp); + if (writeFailed) { + // clean up + ::DeleteFile(outputPath); + return false; + } + + /* + * Fix the modification date. + */ + PathName datePath(outputPath); + datePath.SetModWhen(pEntry->GetModWhen()); +// datePath.SetAccess(pEntry->GetAccess()); + + return true; +} + + +/* + * Open an output file. + * + * "outputPath" holds the name of the file to create. "origPath" is the + * name as it was stored in the archive. "pOverwriteExisting" tells us + * if we should just go ahead and overwrite the existing file, while + * "pOvwrForAll" tells us if a "To All" button was hit previously. + * + * If the file exists, "*pOverwriteExisting" is false, and "*pOvwrForAll" + * is false, then we will put up the "do you want to overwrite?" dialog. + * One possible outcome of the dialog is renaming the output path. + * + * On success, "*pFp" will be non-nil, and IDOK will be returned. On + * failure, IDCANCEL will be returned. The values in "*pOverwriteExisting" + * and "*pOvwrForAll" may be updated, and "*pOutputPath" will change if + * the user chose to rename the file. + */ +int +MainWindow::OpenOutputFile(CString* pOutputPath, const PathProposal& pathProp, + time_t arcFileModWhen, bool* pOverwriteExisting, bool* pOvwrForAll, + FILE** pFp) +{ + const int kUserCancel = -2; // must not conflict with errno values + CString failed; + CString msg; + int err = 0; + + failed.LoadString(IDS_FAILED); + + *pFp = nil; + +did_rename: + PathName path(*pOutputPath); + if (path.Exists()) { + if (*pOverwriteExisting) { +do_overwrite: + /* delete existing */ + WMSG1(" Deleting existing '%s'\n", (LPCTSTR) *pOutputPath); + if (::unlink(*pOutputPath) != 0) { + err = errno; + WMSG2(" Failed deleting '%s', err=%d\n", + (LPCTSTR)*pOutputPath, err); + if (err == ENOENT) { + /* user might have removed it while dialog was up */ + err = 0; + } else { + /* unable to delete, we'd better bail out */ + goto bail; + } + } + } else if (*pOvwrForAll) { + /* never overwrite */ + WMSG1(" Skipping '%s'\n", (LPCTSTR) *pOutputPath); + goto bail; + } else { + /* no firm policy, ask the user */ + ConfirmOverwriteDialog confOvwr; + PathName path(*pOutputPath); + + confOvwr.fExistingFile = *pOutputPath; + confOvwr.fExistingFileModWhen = path.GetModWhen(); + confOvwr.fNewFileSource = pathProp.fStoredPathName; + confOvwr.fNewFileModWhen = arcFileModWhen; + if (confOvwr.DoModal() == IDCANCEL) { + err = kUserCancel; + goto bail; + } + if (confOvwr.fResultRename) { + *pOutputPath = confOvwr.fExistingFile; + goto did_rename; + } + if (confOvwr.fResultApplyToAll) { + *pOvwrForAll = confOvwr.fResultApplyToAll; + *pOverwriteExisting = confOvwr.fResultOverwrite; + } + if (confOvwr.fResultOverwrite) + goto do_overwrite; + else + goto bail; + } + + } + + /* create the subdirectories, if necessary */ + err = path.CreatePathIFN(); + if (err != 0) + goto bail; + + *pFp = fopen(*pOutputPath, "wb"); + if (*pFp == nil) + err = errno ? errno : -1; + /* fall through with error */ + +bail: + /* if we failed, tell the user why */ + if (err == ENOTDIR) { + /* part of the output path exists, but isn't a directory */ + msg.Format("Unable to create folders for '%s': part of the path " + "already exists but is not a folder.\n", + *pOutputPath); + fpActionProgress->MessageBox(msg, failed, MB_OK | MB_ICONERROR); + return IDCANCEL; + } else if (err == EINVAL) { + /* invalid argument; assume it's an invalid filename */ + msg.Format("Unable to create file '%s': invalid filename.\n", + *pOutputPath); + fpActionProgress->MessageBox(msg, failed, MB_OK | MB_ICONERROR); + return IDCANCEL; + } else if (err == kUserCancel) { + /* user elected to cancel */ + WMSG0("Cancelling due to user request\n"); + return IDCANCEL; + } else if (err != 0) { + msg.Format("Unable to create file '%s': %s\n", + *pOutputPath, strerror(err)); + fpActionProgress->MessageBox(msg, failed, MB_OK | MB_ICONERROR); + return IDCANCEL; + } + + return IDOK; +} + + +/* + * ========================================================================== + * Test + * ========================================================================== + */ + +/* + * Test files. + */ +void +MainWindow::OnActionsTest(void) +{ + ASSERT(fpContentList != nil); + ASSERT(fpOpenArchive != nil); + + /* + * Ask the user about various options. + */ + UseSelectionDialog selOpts(fpContentList->GetSelectedCount(), this); + selOpts.Setup(IDS_TEST_TITLE, IDS_TEST_OK, IDS_TEST_SELECTED_COUNT, + IDS_TEST_SELECTED_COUNTS_FMT, IDS_TEST_ALL_FILES); + if (fpContentList->GetSelectedCount() > 0) + selOpts.fFilesToAction = UseSelectionDialog::kActionSelection; + else + selOpts.fFilesToAction = UseSelectionDialog::kActionAll; + + if (selOpts.DoModal() != IDOK) { + WMSG0("Test cancelled\n"); + return; + } + + /* + * Create a "selection set" of things to test. + * + * We don't currently test directories, because we don't currently + * allow testing anything that has a directory (NuFX doesn't store + * them explicitly). We could probably add them to the threadMask. + */ + SelectionSet selSet; + int threadMask = GenericEntry::kAnyThread; + + if (selOpts.fFilesToAction == UseSelectionDialog::kActionSelection) { + selSet.CreateFromSelection(fpContentList, threadMask); + } else { + selSet.CreateFromAll(fpContentList, threadMask); + } + //selSet.Dump(); + + if (selSet.GetNumEntries() == 0) { + /* should be impossible */ + MessageBox("No files matched the selection criteria.", + "No match", MB_OK|MB_ICONEXCLAMATION); + return; + } + + /* + * Set up the progress window. + */ + bool result; + + fpActionProgress = new ActionProgressDialog; + fpActionProgress->Create(ActionProgressDialog::kActionTest, this); + + result = fpOpenArchive->TestSelection(fpActionProgress, &selSet); + + fpActionProgress->Cleanup(this); + fpActionProgress = nil; + //if (result) + // SuccessBeep(); +} +void +MainWindow::OnUpdateActionsTest(CCmdUI* pCmdUI) +{ + pCmdUI->Enable(fpContentList != nil && fpContentList->GetItemCount() > 0 + && fpOpenArchive->GetCapability(GenericArchive::kCapCanTest)); +} + + +/* + * ========================================================================== + * Delete + * ========================================================================== + */ + +/* + * Delete archive entries. + */ +void +MainWindow::OnActionsDelete(void) +{ + ASSERT(fpContentList != nil); + ASSERT(fpOpenArchive != nil); + ASSERT(!fpOpenArchive->IsReadOnly()); + + /* + * We handle deletions specially. If they have selected any + * subdirectories, we recursively select the files in those subdirs + * as well. We want to do it early so that the "#of files to delete" + * display accurately reflects what we're about to do. + */ + fpContentList->SelectSubdirContents(); + +#if 0 + /* + * Ask the user about various options. + */ + UseSelectionDialog delOpts(fpContentList->GetSelectedCount(), this); + delOpts.Setup(IDS_DEL_TITLE, IDS_DEL_OK, IDS_DEL_SELECTED_COUNT, + IDS_DEL_SELECTED_COUNTS_FMT, IDS_DEL_ALL_FILES); + if (fpContentList->GetSelectedCount() > 0) + delOpts.fFilesToAction = UseSelectionDialog::kActionSelection; + else + delOpts.fFilesToAction = UseSelectionDialog::kActionAll; + + if (delOpts.DoModal() != IDOK) { + WMSG0("Delete cancelled\n"); + return; + } +#endif + + /* + * Create a "selection set" of things to delete. + * + * We can't delete volume directories, so they're not included. + */ + SelectionSet selSet; + int threadMask = GenericEntry::kAnyThread | + GenericEntry::kAllowDirectory /*| GenericEntry::kAllowVolumeDir*/; + +#if 0 + if (delOpts.fFilesToAction == UseSelectionDialog::kActionSelection) { + selSet.CreateFromSelection(fpContentList, threadMask); + } else { + CString appName; + UINT response; + appName.LoadString(IDS_MB_APP_NAME); + response = MessageBox("Are you sure you want to delete everything?", + appName, MB_OKCANCEL | MB_ICONEXCLAMATION); + if (response == IDCANCEL) + return; + selSet.CreateFromAll(fpContentList, threadMask); + } + //selSet.Dump(); +#endif + + selSet.CreateFromSelection(fpContentList, threadMask); + if (selSet.GetNumEntries() == 0) { + /* can happen if they selected volume dir only */ + MessageBox("Nothing to delete.", + "No match", MB_OK | MB_ICONEXCLAMATION); + return; + } + + CString appName, msg; + + appName.LoadString(IDS_MB_APP_NAME); + msg.Format("Delete %d file%s?", selSet.GetNumEntries(), + selSet.GetNumEntries() == 1 ? "" : "s"); + if (MessageBox(msg, appName, MB_OKCANCEL | MB_ICONQUESTION) != IDOK) + return; + + bool result; + fpActionProgress = new ActionProgressDialog; + fpActionProgress->Create(ActionProgressDialog::kActionDelete, this); + + //fpContentList->Invalidate(); // don't allow updates until done + result = fpOpenArchive->DeleteSelection(fpActionProgress, &selSet); + fpContentList->Reload(); + + fpActionProgress->Cleanup(this); + fpActionProgress = nil; + + if (result) + SuccessBeep(); +} +void +MainWindow::OnUpdateActionsDelete(CCmdUI* pCmdUI) +{ + pCmdUI->Enable(fpContentList != nil && !fpOpenArchive->IsReadOnly() + && fpContentList->GetSelectedCount() > 0); +} + + +/* + * ========================================================================== + * Rename + * ========================================================================== + */ + +/* + * Rename archive entries. Depending on the structure of the underlying + * archive, we may only allow the user to alter the filename component. + * Anything else would constitute moving the file around in the filesystem. + */ +void +MainWindow::OnActionsRename(void) +{ + ASSERT(fpContentList != nil); + ASSERT(fpOpenArchive != nil); + ASSERT(!fpOpenArchive->IsReadOnly()); + + /* + * Create a "selection set" of entries to rename. We always go by + * the selection, so there's no need to present a "all or some?" dialog. + * + * Renaming the volume dir is not done from here, so we don't include + * it in the set. We could theoretically allow renaming of "damaged" + * files, since most of the time the damage is in the file structure + * not the directory, but the disk will be read-only anyway so there's + * no point. + */ + SelectionSet selSet; + int threadMask = GenericEntry::kAnyThread | GenericEntry::kAllowDirectory; + + selSet.CreateFromSelection(fpContentList, threadMask); + //selSet.Dump(); + + if (selSet.GetNumEntries() == 0) { + /* should be impossible */ + MessageBox("No files matched the selection criteria.", + "No match", MB_OK | MB_ICONEXCLAMATION); + return; + } + + //fpContentList->Invalidate(); // this might be unnecessary + fpOpenArchive->RenameSelection(this, &selSet); + fpContentList->Reload(); + + // user interaction on each step, so skip the SuccessBeep +} +void +MainWindow::OnUpdateActionsRename(CCmdUI* pCmdUI) +{ + pCmdUI->Enable(fpContentList != nil && !fpOpenArchive->IsReadOnly() + && fpContentList->GetSelectedCount() > 0); +} + + +/* + * ========================================================================== + * Edit Comment + * ========================================================================== + */ + +/* + * Edit a comment, creating it if necessary. + */ +void +MainWindow::OnActionsEditComment(void) +{ + ASSERT(fpContentList != nil); + ASSERT(fpOpenArchive != nil); + ASSERT(!fpOpenArchive->IsReadOnly()); + + EditCommentDialog editDlg(this); + CString oldComment; + + GenericEntry* pEntry = GetSelectedItem(fpContentList); + if (pEntry == nil) { + ASSERT(false); + return; + } + + if (!pEntry->GetHasComment()) { + CString question, title; + int result; + + question.LoadString(IDS_NO_COMMENT_ADD); + title.LoadString(IDS_EDIT_COMMENT); + result = MessageBox(question, title, MB_OKCANCEL | MB_ICONQUESTION); + if (result == IDCANCEL) + return; + + editDlg.fComment = ""; + editDlg.fNewComment = true; + } else { + fpOpenArchive->GetComment(this, pEntry, &editDlg.fComment); + } + + + int result; + result = editDlg.DoModal(); + if (result == IDOK) { + //fpContentList->Invalidate(); // probably unnecessary + fpOpenArchive->SetComment(this, pEntry, editDlg.fComment); + fpContentList->Reload(); + } else if (result == EditCommentDialog::kDeleteCommentID) { + //fpContentList->Invalidate(); // possibly unnecessary + fpOpenArchive->DeleteComment(this, pEntry); + fpContentList->Reload(); + } +} +void +MainWindow::OnUpdateActionsEditComment(CCmdUI* pCmdUI) +{ + pCmdUI->Enable(fpContentList != nil && !fpOpenArchive->IsReadOnly() && + fpContentList->GetSelectedCount() == 1 && + fpOpenArchive->GetCapability(GenericArchive::kCapCanEditComment)); +} + + +/* + * ========================================================================== + * Edit Properties + * ========================================================================== + */ + +/* + * Edit file properties. + * + * This causes a reload of the list, which isn't really necessary. We + * do need to re-evaluate the sort order if one of the fields they modified + * is the current sort key, but it would be nice if we could at least retain + * the selection. Since we're not reloading the GenericArchive, we *can* + * remember the selection. + */ +void +MainWindow::OnActionsEditProps(void) +{ + ASSERT(fpContentList != nil); + ASSERT(fpOpenArchive != nil); + + EditPropsDialog propsDlg(this); + CString oldComment; + + GenericEntry* pEntry = GetSelectedItem(fpContentList); + if (pEntry == nil) { + ASSERT(false); + return; + } + + propsDlg.InitProps(pEntry); + if (fpOpenArchive->IsReadOnly()) + propsDlg.fReadOnly = true; + else if (pEntry->GetRecordKind() == GenericEntry::kRecordKindVolumeDir) + propsDlg.fReadOnly = true; + + int result; + result = propsDlg.DoModal(); + if (result == IDOK && !propsDlg.fReadOnly) { + (void) fpOpenArchive->SetProps(this, pEntry, &propsDlg.fProps); + + // only needed if underlying archive reloads + fpContentList->Reload(true); + } +} +void +MainWindow::OnUpdateActionsEditProps(CCmdUI* pCmdUI) +{ + // allow it in read-only mode, so we can view the props + pCmdUI->Enable(fpContentList != nil && + fpContentList->GetSelectedCount() == 1); +} + + +/* + * ========================================================================== + * Rename Volume + * ========================================================================== + */ + +/* + * Change a volume name or volume number. + */ +void +MainWindow::OnActionsRenameVolume(void) +{ + RenameVolumeDialog rvDialog; + + ASSERT(fpContentList != nil); + ASSERT(fpOpenArchive != nil); + ASSERT(!fpOpenArchive->IsReadOnly()); + + /* only know how to deal with disk images */ + if (fpOpenArchive->GetArchiveKind() != GenericArchive::kArchiveDiskImage) { + ASSERT(false); + return; + } + + DiskImgLib::DiskFS* pDiskFS; + + pDiskFS = ((DiskArchive*) fpOpenArchive)->GetDiskFS(); + ASSERT(pDiskFS != nil); + + rvDialog.fpArchive = (DiskArchive*) fpOpenArchive; + if (rvDialog.DoModal() != IDOK) + return; + + //WMSG1("Creating '%s'\n", rvDialog.fNewName); + + /* rename the chosen disk to the specified name */ + bool result; + result = fpOpenArchive->RenameVolume(this, rvDialog.fpChosenDiskFS, + rvDialog.fNewName); + if (!result) { + WMSG0("RenameVolume FAILED\n"); + /* keep going -- reload just in case something partially happened */ + } + + /* + * We need to do two things: reload the content list, because the + * underlying DiskArchive got reloaded, and update the title bar. We + * put the "volume ID" in the title, and we most likely just changed it. + * + * SetCPTitle invokes fpOpenArchive->GetDescription(), which pulls the + * volume ID out of the primary DiskFS. + */ + fpContentList->Reload(); + SetCPTitle(fOpenArchivePathName, fpOpenArchive); +} +void +MainWindow::OnUpdateActionsRenameVolume(CCmdUI* pCmdUI) +{ + pCmdUI->Enable(fpContentList != nil && !fpOpenArchive->IsReadOnly() && + fpOpenArchive->GetCapability(GenericArchive::kCapCanRenameVolume)); +} + + +/* + * ========================================================================== + * Recompress + * ========================================================================== + */ + +/* + * Recompress files. + */ +void +MainWindow::OnActionsRecompress(void) +{ + ASSERT(fpContentList != nil); + ASSERT(fpOpenArchive != nil); + + /* + * Ask the user about various options. + */ + RecompressOptionsDialog selOpts(fpContentList->GetSelectedCount(), this); + selOpts.Setup(IDS_RECOMP_TITLE, IDS_RECOMP_OK, IDS_RECOMP_SELECTED_COUNT, + IDS_RECOMP_SELECTED_COUNTS_FMT, IDS_RECOMP_ALL_FILES); + if (fpContentList->GetSelectedCount() > 0) + selOpts.fFilesToAction = UseSelectionDialog::kActionSelection; + else + selOpts.fFilesToAction = UseSelectionDialog::kActionAll; + + selOpts.fCompressionType = fPreferences.GetPrefLong(kPrCompressionType); + + if (selOpts.DoModal() != IDOK) { + WMSG0("Recompress cancelled\n"); + return; + } + + /* + * Create a "selection set" of data forks, resource forks, and disk + * images. If an entry has nothing but a comment, ignore it. + */ + SelectionSet selSet; + int threadMask = GenericEntry::kDataThread | GenericEntry::kRsrcThread | + GenericEntry::kDiskImageThread; + + if (selOpts.fFilesToAction == UseSelectionDialog::kActionSelection) { + selSet.CreateFromSelection(fpContentList, threadMask); + } else { + selSet.CreateFromAll(fpContentList, threadMask); + } + //selSet.Dump(); + + if (selSet.GetNumEntries() == 0) { + /* should be impossible */ + MessageBox("No files matched the selection criteria.", + "No match", MB_OK|MB_ICONEXCLAMATION); + return; + } + + LONGLONG beforeUncomp, beforeComp; + LONGLONG afterUncomp, afterComp; + CalcTotalSize(&beforeUncomp, &beforeComp); + + /* + * Set up the progress window. + */ + int result; + + fpActionProgress = new ActionProgressDialog; + fpActionProgress->Create(ActionProgressDialog::kActionRecompress, this); + + //fpContentList->Invalidate(); // possibly unnecessary + result = fpOpenArchive->RecompressSelection(fpActionProgress, &selSet, + &selOpts); + fpContentList->Reload(); + + fpActionProgress->Cleanup(this); + fpActionProgress = nil; + + + if (result) { + CString msg, appName; + + CalcTotalSize(&afterUncomp, &afterComp); + ASSERT(beforeUncomp == afterUncomp); + + appName.LoadString(IDS_MB_APP_NAME); + msg.Format("Total uncompressed size of all files:\t%.1fK\r\n" + "Total size before recompress:\t\t%.1fK\r\n" + "Total size after recompress:\t\t%.1fK\r\n" + "Overall reduction:\t\t\t%.1fK", + beforeUncomp / 1024.0, beforeComp / 1024.0, afterComp / 1024.0, + (beforeComp - afterComp) / 1024.0); + MessageBox(msg, appName, MB_OK|MB_ICONINFORMATION); + } +} +void +MainWindow::OnUpdateActionsRecompress(CCmdUI* pCmdUI) +{ + pCmdUI->Enable(fpContentList != nil && !fpOpenArchive->IsReadOnly() && + fpContentList->GetItemCount() > 0 && + fpOpenArchive->GetCapability(GenericArchive::kCapCanRecompress)); +} + +/* + * Compute the total size of all files in the GenericArchive. + */ +void +MainWindow::CalcTotalSize(LONGLONG* pUncomp, LONGLONG* pComp) const +{ + GenericEntry* pEntry = fpOpenArchive->GetEntries(); + LONGLONG uncomp = 0, comp = 0; + + while (pEntry != nil) { + uncomp += pEntry->GetUncompressedLen(); + comp += pEntry->GetCompressedLen(); + pEntry = pEntry->GetNext(); + } + + *pUncomp = uncomp; + *pComp = comp; +} + + +/* + * ========================================================================== + * Convert to disk archive + * ========================================================================== + */ + +/* + * Select files to convert. + */ +void +MainWindow::OnActionsConvDisk(void) +{ + ASSERT(fpContentList != nil); + ASSERT(fpOpenArchive != nil); + + /* + * Ask the user about various options. + */ + ConvDiskOptionsDialog selOpts(fpContentList->GetSelectedCount(), this); + selOpts.Setup(IDS_CONVDISK_TITLE, IDS_CONVDISK_OK, IDS_CONVDISK_SELECTED_COUNT, + IDS_CONVDISK_SELECTED_COUNTS_FMT, IDS_CONVDISK_ALL_FILES); + if (fpContentList->GetSelectedCount() > 0) + selOpts.fFilesToAction = UseSelectionDialog::kActionSelection; + else + selOpts.fFilesToAction = UseSelectionDialog::kActionAll; + + //selOpts.fAllowLower = + // fPreferences.GetPrefBool(kPrConvDiskAllowLower); + //selOpts.fSparseAlloc = + // fPreferences.GetPrefBool(kPrConvDiskAllocSparse); + + if (selOpts.DoModal() != IDOK) { + WMSG0("ConvDisk cancelled\n"); + return; + } + + ASSERT(selOpts.fNumBlocks > 0); + + //fPreferences.SetPrefBool(kPrConvDiskAllowLower, + // selOpts.fAllowLower != 0); + //fPreferences.SetPrefBool(kPrConvDiskAllocSparse, + // selOpts.fSparseAlloc != 0); + + /* + * Create a "selection set" of data forks, resource forks, and + * disk images. We don't want comment threads, but we can ignore + * them later. + */ + SelectionSet selSet; + int threadMask = GenericEntry::kAnyThread; + + if (selOpts.fFilesToAction == UseSelectionDialog::kActionSelection) { + selSet.CreateFromSelection(fpContentList, threadMask); + } else { + selSet.CreateFromAll(fpContentList, threadMask); + } + //selSet.Dump(); + + if (selSet.GetNumEntries() == 0) { + /* should be impossible */ + MessageBox("No files matched the selection criteria.", + "No match", MB_OK|MB_ICONEXCLAMATION); + return; + } + + XferFileOptions xferOpts; + //xferOpts.fAllowLowerCase = + // fPreferences.GetPrefBool(kPrProDOSAllowLower) != 0; + //xferOpts.fUseSparseBlocks = + // fPreferences.GetPrefBool(kPrProDOSUseSparse) != 0; + + WMSG1("New volume name will be '%s'\n", selOpts.fVolName); + + /* + * Create a new disk image. + */ + CString filename, saveFolder, errStr; + + CFileDialog dlg(FALSE, _T("po"), NULL, + OFN_OVERWRITEPROMPT|OFN_NOREADONLYRETURN|OFN_HIDEREADONLY, + "Disk Images (*.po)|*.po||", this); + + dlg.m_ofn.lpstrTitle = "New Disk Image (.PO)"; + dlg.m_ofn.lpstrInitialDir = fPreferences.GetPrefString(kPrOpenArchiveFolder); + + if (dlg.DoModal() != IDOK) { + WMSG0(" User cancelled xfer from image create dialog\n"); + return; + } + + saveFolder = dlg.m_ofn.lpstrFile; + saveFolder = saveFolder.Left(dlg.m_ofn.nFileOffset); + fPreferences.SetPrefString(kPrOpenArchiveFolder, saveFolder); + + filename = dlg.GetPathName(); + WMSG1(" Will xfer to file '%s'\n", filename); + + /* remove file if it already exists */ + CString errMsg; + errMsg = RemoveFile(filename); + if (!errMsg.IsEmpty()) { + ShowFailureMsg(this, errMsg, IDS_FAILED); + return; + } + + DiskArchive::NewOptions options; + memset(&options, 0, sizeof(options)); + options.base.format = DiskImg::kFormatProDOS; + options.base.sectorOrder = DiskImg::kSectorOrderProDOS; + options.prodos.volName = selOpts.fVolName; + options.prodos.numBlocks = selOpts.fNumBlocks; + + xferOpts.fTarget = new DiskArchive; + errStr = xferOpts.fTarget->New(filename, &options); + if (!errStr.IsEmpty()) { + ShowFailureMsg(this, errStr, IDS_FAILED); + delete xferOpts.fTarget; + return; + } + + /* + * Set up the progress window. + */ + GenericArchive::XferStatus result; + + fpActionProgress = new ActionProgressDialog; + fpActionProgress->Create(ActionProgressDialog::kActionConvDisk, this); + + result = fpOpenArchive->XferSelection(fpActionProgress, &selSet, + fpActionProgress, &xferOpts); + + fpActionProgress->Cleanup(this); + fpActionProgress = nil; + + if (result == GenericArchive::kXferOK) + SuccessBeep(); + + /* clean up */ + delete xferOpts.fTarget; +} +void +MainWindow::OnUpdateActionsConvDisk(CCmdUI* pCmdUI) +{ + /* right now, only NufxArchive has the Xfer stuff implemented */ + pCmdUI->Enable(fpContentList != nil && + fpContentList->GetItemCount() > 0 && + fpOpenArchive->GetArchiveKind() == GenericArchive::kArchiveNuFX); +} + + +/* + * ========================================================================== + * Convert to file archive + * ========================================================================== + */ + +/* + * Select files to convert. + */ +void +MainWindow::OnActionsConvFile(void) +{ + ASSERT(fpContentList != nil); + ASSERT(fpOpenArchive != nil); + + /* + * Ask the user about various options. + */ + ConvFileOptionsDialog selOpts(fpContentList->GetSelectedCount(), this); + selOpts.Setup(IDS_CONVFILE_TITLE, IDS_CONVFILE_OK, IDS_CONVFILE_SELECTED_COUNT, + IDS_CONVFILE_SELECTED_COUNTS_FMT, IDS_CONVFILE_ALL_FILES); + if (fpContentList->GetSelectedCount() > 0) + selOpts.fFilesToAction = UseSelectionDialog::kActionSelection; + else + selOpts.fFilesToAction = UseSelectionDialog::kActionAll; + + //selOpts.fConvDOSText = + // fPreferences.GetPrefBool(kPrConvFileConvDOSText); + //selOpts.fConvPascalText = + // fPreferences.GetPrefBool(kPrConvFileConvPascalText); + selOpts.fPreserveEmptyFolders = + fPreferences.GetPrefBool(kPrConvFileEmptyFolders); + + if (selOpts.DoModal() != IDOK) { + WMSG0("ConvFile cancelled\n"); + return; + } + + //fPreferences.SetPrefBool(kPrConvFileConvDOSText, + // selOpts.fConvDOSText != 0); + //fPreferences.SetPrefBool(kPrConvFileConvPascalText, + // selOpts.fConvPascalText != 0); + fPreferences.SetPrefBool(kPrConvFileEmptyFolders, + selOpts.fPreserveEmptyFolders != 0); + + /* + * Create a "selection set" of data forks, resource forks, and + * directories. There are no comments or disk images on a disk image, + * so we just request "any" thread. + * + * We only need to explicitly include directories if "preserve + * empty folders" is set. + */ + SelectionSet selSet; + int threadMask = GenericEntry::kAnyThread; + if (selOpts.fPreserveEmptyFolders) + threadMask |= GenericEntry::kAllowDirectory; + + if (selOpts.fFilesToAction == UseSelectionDialog::kActionSelection) { + selSet.CreateFromSelection(fpContentList, threadMask); + } else { + selSet.CreateFromAll(fpContentList, threadMask); + } + //selSet.Dump(); + + if (selSet.GetNumEntries() == 0) { + MessageBox("No files matched the selection criteria.", + "No match", MB_OK|MB_ICONEXCLAMATION); + return; + } + + XferFileOptions xferOpts; + //xferOpts.fConvDOSText = (selOpts.fConvDOSText != 0); + //xferOpts.fConvPascalText = (selOpts.fConvPascalText != 0); + xferOpts.fPreserveEmptyFolders = (selOpts.fPreserveEmptyFolders != 0); + + /* + * Create a new NuFX archive. + */ + CString filename, saveFolder, errStr; + + CFileDialog dlg(FALSE, _T("shk"), NULL, + OFN_OVERWRITEPROMPT|OFN_NOREADONLYRETURN|OFN_HIDEREADONLY, + "ShrinkIt Archives (*.shk)|*.shk||", this); + + dlg.m_ofn.lpstrTitle = "New Archive"; + dlg.m_ofn.lpstrInitialDir = fPreferences.GetPrefString(kPrOpenArchiveFolder); + + if (dlg.DoModal() != IDOK) { + WMSG0(" User cancelled xfer from archive create dialog\n"); + return; + } + + saveFolder = dlg.m_ofn.lpstrFile; + saveFolder = saveFolder.Left(dlg.m_ofn.nFileOffset); + fPreferences.SetPrefString(kPrOpenArchiveFolder, saveFolder); + + filename = dlg.GetPathName(); + WMSG1(" Will xfer to file '%s'\n", filename); + + /* remove file if it already exists */ + CString errMsg; + errMsg = RemoveFile(filename); + if (!errMsg.IsEmpty()) { + ShowFailureMsg(this, errMsg, IDS_FAILED); + return; + } + + xferOpts.fTarget = new NufxArchive; + errStr = xferOpts.fTarget->New(filename, nil); + if (!errStr.IsEmpty()) { + ShowFailureMsg(this, errStr, IDS_FAILED); + delete xferOpts.fTarget; + return; + } + + /* + * Set up the progress window. + */ + GenericArchive::XferStatus result; + + fpActionProgress = new ActionProgressDialog; + fpActionProgress->Create(ActionProgressDialog::kActionConvFile, this); + + result = fpOpenArchive->XferSelection(fpActionProgress, &selSet, + fpActionProgress, &xferOpts); + + fpActionProgress->Cleanup(this); + fpActionProgress = nil; + if (result == GenericArchive::kXferOK) + SuccessBeep(); + + /* clean up */ + delete xferOpts.fTarget; +} +void +MainWindow::OnUpdateActionsConvFile(CCmdUI* pCmdUI) +{ + pCmdUI->Enable(fpContentList != nil && + fpContentList->GetItemCount() > 0 && + fpOpenArchive->GetArchiveKind() == GenericArchive::kArchiveDiskImage); +} + + +/* + * ========================================================================== + * Cassette WAV conversions + * ========================================================================== + */ + +/* + * Convert BAS, INT, or BIN to a cassette-audio WAV file. + */ +void +MainWindow::OnActionsConvToWav(void) +{ + // do this someday + WMSG0("Convert TO wav\n"); +} +void +MainWindow::OnUpdateActionsConvToWav(CCmdUI* pCmdUI) +{ + BOOL enable = false; + + if (fpContentList != nil && fpContentList->GetSelectedCount() == 1) { + /* only BAS, INT, and BIN shorter than 64K */ + GenericEntry* pEntry = GetSelectedItem(fpContentList); + + if ((pEntry->GetFileType() == kFileTypeBAS || + pEntry->GetFileType() == kFileTypeINT || + pEntry->GetFileType() == kFileTypeBIN) && + pEntry->GetDataForkLen() < 65536 && + pEntry->GetRecordKind() == GenericEntry::kRecordKindFile) + { + enable = true; + } + } + pCmdUI->Enable(enable); +} + +/* + * Convert a WAV file with a digitized Apple II cassette tape into an + * Apple II file, and add it to the current disk. + */ +void +MainWindow::OnActionsConvFromWav(void) +{ + CassetteDialog dlg; + CString fileName, saveFolder; + + CFileDialog fileDlg(TRUE, "wav", NULL, OFN_FILEMUSTEXIST|OFN_HIDEREADONLY, + "Sound Files (*.wav)|*.wav||", this); + fileDlg.m_ofn.lpstrTitle = "Open Sound File"; + fileDlg.m_ofn.lpstrInitialDir = fPreferences.GetPrefString(kPrOpenWAVFolder); + + if (fileDlg.DoModal() != IDOK) + goto bail; + + saveFolder = fileDlg.m_ofn.lpstrFile; + saveFolder = saveFolder.Left(fileDlg.m_ofn.nFileOffset); + fPreferences.SetPrefString(kPrOpenWAVFolder, saveFolder); + + fileName = fileDlg.GetPathName(); + WMSG1("Opening WAV file '%s'\n", fileName); + + dlg.fFileName = fileName; + // pass in fpOpenArchive? + + dlg.DoModal(); + if (dlg.IsDirty()) { + assert(fpContentList != nil); + fpContentList->Reload(); + } + +bail: + return; +} +void +MainWindow::OnUpdateActionsConvFromWav(CCmdUI* pCmdUI) +{ + pCmdUI->Enable(fpContentList != nil && !fpOpenArchive->IsReadOnly()); +} + + +/* + * Utility function used by cassette import. + * + * May modify contents of "pDetails". + * + * On failure, returns with an error message in "errMsg". + */ +/*static*/ bool +MainWindow::SaveToArchive(GenericArchive::FileDetails* pDetails, + const unsigned char* dataBufIn, long dataLen, + const unsigned char* rsrcBufIn, long rsrcLen, + CString& errMsg, CWnd* pDialog) +{ + MainWindow* pMain = GET_MAIN_WINDOW(); + GenericArchive* pArchive = pMain->GetOpenArchive(); + DiskImgLib::A2File* pTargetSubdir = nil; + XferFileOptions xferOpts; + CString storagePrefix; + unsigned char* dataBuf = nil; + unsigned char* rsrcBuf = nil; + + ASSERT(pArchive != nil); + ASSERT(errMsg.IsEmpty()); + + /* + * Make a copy of the data for XferFile. + */ + if (dataLen >= 0) { + if (dataLen == 0) + dataBuf = new unsigned char[1]; + else + dataBuf = new unsigned char[dataLen]; + if (dataBuf == nil) { + errMsg.Format("Unable to allocate %ld bytes", dataLen); + goto bail; + } + memcpy(dataBuf, dataBufIn, dataLen); + } + if (rsrcLen >= 0) { + assert(false); + } + + + /* + * Figure out where we want to put the files. For a disk archive + * this can be complicated. + * + * The target DiskFS (which could be a sub-volume) gets tucked into + * the xferOpts. + */ + if (pArchive->GetArchiveKind() == GenericArchive::kArchiveDiskImage) { + if (!pMain->ChooseAddTarget(&pTargetSubdir, &xferOpts.fpTargetFS)) + goto bail; + } else if (pArchive->GetArchiveKind() == GenericArchive::kArchiveNuFX) { + // Always use ':' separator for SHK; this is a matter of + // convenience, so they can specify a full path. + //details.storageName.Replace(':', '_'); + pDetails->fileSysInfo = ':'; + } + if (pTargetSubdir != nil) { + storagePrefix = pTargetSubdir->GetPathName(); + WMSG1("--- using storagePrefix '%s'\n", (const char*) storagePrefix); + } + if (!storagePrefix.IsEmpty()) { + CString tmpStr, tmpFileName; + tmpFileName = pDetails->storageName; + tmpFileName.Replace(':', '_'); // strip any ':'s in the name + pDetails->fileSysInfo = ':'; + tmpStr = storagePrefix; + tmpStr += ':'; + tmpStr += tmpFileName; + pDetails->storageName = tmpStr; + } + + /* + * Handle the transfer. + * + * On success, XferFile will null out our dataBuf and rsrcBuf pointers. + */ + pArchive->XferPrepare(&xferOpts); + + errMsg = pArchive->XferFile(pDetails, &dataBuf, dataLen, + &rsrcBuf, rsrcLen); + delete[] dataBuf; + delete[] rsrcBuf; + + if (errMsg.IsEmpty()) + pArchive->XferFinish(pDialog); + else + pArchive->XferAbort(pDialog); + +bail: + return (errMsg.IsEmpty() != 0); +} + + +/* + * ========================================================================== + * Import BASIC programs from a text file + * ========================================================================== + */ + +/* + * Import an Applesoft BASIC program from a text file. + * + * We currently allow the user to select a single file for import. Someday + * we may want to allow multi-file import. + */ +void +MainWindow::OnActionsImportBAS(void) +{ + ImportBASDialog dlg; + CString fileName, saveFolder; + + CFileDialog fileDlg(TRUE, "txt", NULL, OFN_FILEMUSTEXIST | OFN_HIDEREADONLY, + "Text files (*.txt)|*.txt||", this); + fileDlg.m_ofn.lpstrTitle = "Open Text File"; + fileDlg.m_ofn.lpstrInitialDir = fPreferences.GetPrefString(kPrAddFileFolder); + + if (fileDlg.DoModal() != IDOK) + goto bail; + + saveFolder = fileDlg.m_ofn.lpstrFile; + saveFolder = saveFolder.Left(fileDlg.m_ofn.nFileOffset); + fPreferences.SetPrefString(kPrAddFileFolder, saveFolder); + + fileName = fileDlg.GetPathName(); + WMSG1("Opening TXT file '%s'\n", fileName); + + dlg.fFileName = fileName; + // pass in fpOpenArchive? + + dlg.DoModal(); + if (dlg.IsDirty()) { + assert(fpContentList != nil); + fpContentList->Reload(); + } + +bail: + return; +} +void +MainWindow::OnUpdateActionsImportBAS(CCmdUI* pCmdUI) +{ + pCmdUI->Enable(fpContentList != nil && !fpOpenArchive->IsReadOnly()); +} + + +/* + * ========================================================================== + * Multiple file handling + * ========================================================================== + */ + +/* + * Extract every part of the file into "ReformatHolder". Does not try to + * reformat anything, just extracts the parts. + * + * Returns IDOK on success, IDCANCEL if the user cancelled, or -1 on error. + * On error, the reformatted text buffer gets the error message. + */ +int +MainWindow::GetFileParts(const GenericEntry* pEntry, + ReformatHolder** ppHolder) const +{ + ReformatHolder* pHolder = new ReformatHolder; + CString errMsg; + + if (pHolder == nil) + return -1; + + if (pEntry->GetHasDataFork()) + GetFilePart(pEntry, GenericEntry::kDataThread, pHolder); + if (pEntry->GetHasRsrcFork()) + GetFilePart(pEntry, GenericEntry::kRsrcThread, pHolder); + if (pEntry->GetHasComment()) + GetFilePart(pEntry, GenericEntry::kCommentThread, pHolder); + if (pEntry->GetHasDiskImage()) + GetFilePart(pEntry, GenericEntry::kDiskImageThread, pHolder); + + *ppHolder = pHolder; + + return 0; +} + +/* + * Load the requested part. + */ +void +MainWindow::GetFilePart(const GenericEntry* pEntry, int whichThread, + ReformatHolder* pHolder) const +{ + CString errMsg; + ReformatHolder::ReformatPart part; + char* buf = nil; + long len = 0; + di_off_t threadLen; + int result; + + switch (whichThread) { + case GenericEntry::kDataThread: + part = ReformatHolder::kPartData; + threadLen = pEntry->GetDataForkLen(); + break; + case GenericEntry::kRsrcThread: + part = ReformatHolder::kPartRsrc; + threadLen = pEntry->GetRsrcForkLen(); + break; + case GenericEntry::kCommentThread: + part = ReformatHolder::kPartCmmt; + threadLen = -1; // no comment len getter; assume it's small + break; + case GenericEntry::kDiskImageThread: + part = ReformatHolder::kPartData; // put disks into data thread + threadLen = pEntry->GetDataForkLen(); + break; + default: + ASSERT(false); + return; + } + + if (threadLen > fPreferences.GetPrefLong(kPrMaxViewFileSize)) { + errMsg.Format( + "[File size (%I64d KBytes) exceeds file viewer maximum (%ld KBytes).]\n", + ((LONGLONG) threadLen + 1023) / 1024, + (fPreferences.GetPrefLong(kPrMaxViewFileSize) + 1023) / 1024); + pHolder->SetErrorMsg(part, errMsg); + goto bail; + } + + + result = pEntry->ExtractThreadToBuffer(whichThread, &buf, &len, &errMsg); + + if (result == IDOK) { + /* on success, ETTB guarantees a buffer, even for zero-len file */ + ASSERT(buf != nil); + pHolder->SetSourceBuf(part, (unsigned char*) buf, len); + } else if (result == IDCANCEL) { + /* not expected */ + errMsg = "Cancelled!"; + pHolder->SetErrorMsg(part, errMsg); + ASSERT(buf == nil); + } else { + /* transfer error message to ReformatHolder buffer */ + WMSG1("Got error message from ExtractThread: '%s'\n", errMsg); + pHolder->SetErrorMsg(part, errMsg); + ASSERT(buf == nil); + } + +bail: + return; +} diff --git a/app/AddClashDialog.cpp b/app/AddClashDialog.cpp new file mode 100644 index 0000000..db2adc4 --- /dev/null +++ b/app/AddClashDialog.cpp @@ -0,0 +1,60 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Support for AddClashDialog class. + */ +#include "stdafx.h" +#include "ConfirmOverwriteDialog.h" +#include "AddClashDialog.h" + +BEGIN_MESSAGE_MAP(AddClashDialog, CDialog) + ON_BN_CLICKED(IDC_CLASH_RENAME, OnRename) + ON_BN_CLICKED(IDC_CLASH_SKIP, OnSkip) + //ON_WM_HELPINFO() +END_MESSAGE_MAP() + +/* + * Replace some static text fields. + */ +BOOL +AddClashDialog::OnInitDialog(void) +{ + CWnd* pWnd; + + pWnd = GetDlgItem(IDC_CLASH_WINNAME); + ASSERT(pWnd != nil); + pWnd->SetWindowText(fWindowsName); + + pWnd = GetDlgItem(IDC_CLASH_STORAGENAME); + ASSERT(pWnd != nil); + pWnd->SetWindowText(fStorageName); + + return CDialog::OnInitDialog(); +} + +/* + * One of the buttons was hit. + */ +void +AddClashDialog::OnSkip(void) +{ + fDoRename = false; + CDialog::OnOK(); +} +void +AddClashDialog::OnRename(void) +{ + RenameOverwriteDialog dlg; + + dlg.fNewFileSource = fWindowsName; + dlg.fExistingFile = fStorageName; + dlg.fNewName = fStorageName; + if (dlg.DoModal() == IDOK) { + fNewName = dlg.fNewName; + fDoRename = true; + CDialog::OnOK(); + } +} diff --git a/app/AddClashDialog.h b/app/AddClashDialog.h new file mode 100644 index 0000000..fdbe97f --- /dev/null +++ b/app/AddClashDialog.h @@ -0,0 +1,39 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Resolve a filename clash when adding files. + */ +#ifndef __ADDCLASHDIALOG__ +#define __ADDCLASHDIALOG__ + +/* + * + */ +class AddClashDialog : public CDialog { +public: + AddClashDialog(CWnd* pParentWnd = nil) : + CDialog(IDD_ADD_CLASH, pParentWnd) + { + fDoRename = false; + } + ~AddClashDialog(void) {} + + CString fWindowsName; + CString fStorageName; + + bool fDoRename; // if "false", skip this file + CString fNewName; + +private: + afx_msg void OnRename(void); + afx_msg void OnSkip(void); + + virtual BOOL OnInitDialog(void); + + DECLARE_MESSAGE_MAP() +}; + +#endif /*__ADDCLASHDIALOG__*/ diff --git a/app/AddFilesDialog.cpp b/app/AddFilesDialog.cpp new file mode 100644 index 0000000..ec27a96 --- /dev/null +++ b/app/AddFilesDialog.cpp @@ -0,0 +1,202 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Support for the "add files" dialog. + */ +#include "stdafx.h" +#include "AddFilesDialog.h" +#include "FileNameConv.h" +#include "HelpTopics.h" +#include "resource.h" + + +/* + * A lot like DoDataExchange, only different. + * + * We do some OnInitDialog-type stuff in here, because we're a subclass of + * SelectFilesDialog and don't really get to have one of those. + * + * Returns "true" if all is well, "false" if something failed. Usually a + * "false" indication occurs during saveAndValidate==true, and means that we + * shouldn't allow the dialog to close yet. + */ +bool +AddFilesDialog::MyDataExchange(bool saveAndValidate) +{ + CWnd* pWnd; + + if (saveAndValidate) { + if (GetDlgButtonCheck(this, IDC_ADDFILES_NOPRESERVE) == BST_CHECKED) + fTypePreservation = kPreserveNone; + else if (GetDlgButtonCheck(this, IDC_ADDFILES_PRESERVE) == BST_CHECKED) + fTypePreservation = kPreserveTypes; + else if (GetDlgButtonCheck(this, IDC_ADDFILES_PRESERVEPLUS) == BST_CHECKED) + fTypePreservation = kPreserveAndExtend; + else { + ASSERT(false); + fTypePreservation = kPreserveNone; + } + + if (GetDlgButtonCheck(this, IDC_ADDFILES_CONVEOLNONE) == BST_CHECKED) + fConvEOL = kConvEOLNone; + else if (GetDlgButtonCheck(this, IDC_ADDFILES_CONVEOLTYPE) == BST_CHECKED) + fConvEOL = kConvEOLType; + else if (GetDlgButtonCheck(this, IDC_ADDFILES_CONVEOLTEXT) == BST_CHECKED) + fConvEOL = kConvEOLAuto; + else if (GetDlgButtonCheck(this, IDC_ADDFILES_CONVEOLALL) == BST_CHECKED) + fConvEOL = kConvEOLAll; + else { + ASSERT(false); + fConvEOL = kConvEOLNone; + } + + fIncludeSubfolders = + (GetDlgButtonCheck(this, IDC_ADDFILES_INCLUDE_SUBFOLDERS) == BST_CHECKED); + fStripFolderNames = + (GetDlgButtonCheck(this, IDC_ADDFILES_STRIP_FOLDER) == BST_CHECKED); + fOverwriteExisting = + (GetDlgButtonCheck(this, IDC_ADDFILES_OVERWRITE) == BST_CHECKED); + + pWnd = GetDlgItem(IDC_ADDFILES_PREFIX); + ASSERT(pWnd != nil); + pWnd->GetWindowText(fStoragePrefix); + + if (!ValidateStoragePrefix()) + return false; + + return true; + } else { + SetDlgButtonCheck(this, IDC_ADDFILES_NOPRESERVE, + fTypePreservation == kPreserveNone); + SetDlgButtonCheck(this, IDC_ADDFILES_PRESERVE, + fTypePreservation == kPreserveTypes); + SetDlgButtonCheck(this, IDC_ADDFILES_PRESERVEPLUS, + fTypePreservation == kPreserveAndExtend); + + SetDlgButtonCheck(this, IDC_ADDFILES_CONVEOLNONE, + fConvEOL == kConvEOLNone); + SetDlgButtonCheck(this, IDC_ADDFILES_CONVEOLTYPE, + fConvEOL == kConvEOLType); + SetDlgButtonCheck(this, IDC_ADDFILES_CONVEOLTEXT, + fConvEOL == kConvEOLAuto); + SetDlgButtonCheck(this, IDC_ADDFILES_CONVEOLALL, + fConvEOL == kConvEOLAll); + + SetDlgButtonCheck(this, IDC_ADDFILES_INCLUDE_SUBFOLDERS, + fIncludeSubfolders != FALSE); + SetDlgButtonCheck(this, IDC_ADDFILES_STRIP_FOLDER, + fStripFolderNames != FALSE); + SetDlgButtonCheck(this, IDC_ADDFILES_OVERWRITE, + fOverwriteExisting != FALSE); + + pWnd = GetDlgItem(IDC_ADDFILES_PREFIX); + ASSERT(pWnd != nil); + pWnd->SetWindowText(fStoragePrefix); + if (!fStoragePrefixEnable) + pWnd->EnableWindow(FALSE); + + if (!fStripFolderNamesEnable) { + ::EnableControl(this, IDC_ADDFILES_STRIP_FOLDER, false); + } + + if (!fConvEOLEnable) { + ::EnableControl(this, IDC_ADDFILES_CONVEOLNONE, false); + ::EnableControl(this, IDC_ADDFILES_CONVEOLTYPE, false); + ::EnableControl(this, IDC_ADDFILES_CONVEOLTEXT, false); + ::EnableControl(this, IDC_ADDFILES_CONVEOLALL, false); + } + + return true; + } +} + +/* + * Make sure the storage prefix they entered is valid. + */ +bool +AddFilesDialog::ValidateStoragePrefix(void) +{ + if (fStoragePrefix.IsEmpty()) + return true; + + const char kFssep = PathProposal::kDefaultStoredFssep; + if (fStoragePrefix[0] == kFssep || fStoragePrefix.Right(1) == kFssep) { + CString errMsg; + errMsg.Format("The storage prefix may not start or end with '%c'.", + kFssep); + MessageBox(errMsg, m_ofn.lpstrTitle, MB_OK | MB_ICONWARNING); + return false; + } + + return true; +} + + +/* + * Override base class version. + */ +UINT +AddFilesDialog::MyOnCommand(WPARAM wParam, LPARAM lParam) +{ + switch (wParam) { + case IDHELP: + OnIDHelp(); + return 1; + default: + return SelectFilesDialog::MyOnCommand(wParam, lParam); + } +} + +/* + * Override base class version so we can move our stuff around. + * + * It's important that the base class be called last, because it calls + * Invalidate to redraw the dialog. + */ +void +AddFilesDialog::ShiftControls(int deltaX, int deltaY) +{ + /* + * These only need to be here so that the initial move puts them + * where they belong. Once the dialog has been created, the + * CFileDialog will move things where they need to go. + */ + MoveControl(this, IDC_ADDFILES_STATIC1, 0, deltaY, false); + MoveControl(this, IDC_ADDFILES_NOPRESERVE, 0, deltaY, false); + MoveControl(this, IDC_ADDFILES_PRESERVE, 0, deltaY, false); + MoveControl(this, IDC_ADDFILES_PRESERVEPLUS, 0, deltaY, false); + MoveControl(this, IDC_ADDFILES_STATIC2, 0, deltaY, false); + MoveControl(this, IDC_ADDFILES_STRIP_FOLDER, 0, deltaY, false); + MoveControl(this, IDC_ADDFILES_INCLUDE_SUBFOLDERS, 0, deltaY, false); + MoveControl(this, IDC_ADDFILES_OVERWRITE, 0, deltaY, false); + MoveControl(this, IDC_ADDFILES_STATIC3, 0, deltaY, false); + MoveControl(this, IDC_ADDFILES_PREFIX, 0, deltaY, false); + MoveControl(this, IDC_ADDFILES_STATIC4, 0, deltaY, false); + MoveControl(this, IDC_ADDFILES_CONVEOLNONE, 0, deltaY, false); + MoveControl(this, IDC_ADDFILES_CONVEOLTYPE, 0, deltaY, false); + MoveControl(this, IDC_ADDFILES_CONVEOLTEXT, 0, deltaY, false); + MoveControl(this, IDC_ADDFILES_CONVEOLALL, 0, deltaY, false); + + /* + * These actively move. + */ + MoveControl(this, IDHELP, deltaX, deltaY, false); + StretchControl(this, IDC_ADDFILES_PREFIX, deltaX, 0, false); + SelectFilesDialog::ShiftControls(deltaX, deltaY); +} + +/* + * User pressed the "Help" button. + */ +void +AddFilesDialog::OnIDHelp(void) +{ + CWnd* pWndMain = ::AfxGetMainWnd(); + CWinApp* pAppMain = ::AfxGetApp(); + + ::WinHelp(pWndMain->m_hWnd, pAppMain->m_pszHelpFilePath, + HELP_CONTEXT, HELP_TOPIC_ADD_FILES_DLG); +} diff --git a/app/AddFilesDialog.h b/app/AddFilesDialog.h new file mode 100644 index 0000000..e5a39db --- /dev/null +++ b/app/AddFilesDialog.h @@ -0,0 +1,80 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * File selection dialog, a sub-class of "Open" that allows multiple selection + * of both files and directories. + */ +#ifndef __ADDFILESDIALOG__ +#define __ADDFILESDIALOG__ + +#include "../diskimg/DiskImg.h" +#include "../util/UtilLib.h" +#include "resource.h" + +/* + * Choose files and folders to add. + * + * This gets passed down through the file add stuff, so it needs to carry some + * extra data along as well. + */ +class AddFilesDialog : public SelectFilesDialog { +public: + AddFilesDialog(CWnd* pParentWnd = NULL) : + SelectFilesDialog("IDD_ADD_FILES", pParentWnd) + { + SetWindowTitle(_T("Add Files...")); + fStoragePrefix = ""; + fStoragePrefixEnable = true; + fIncludeSubfolders = FALSE; + fStripFolderNames = FALSE; + fStripFolderNamesEnable = true; + fOverwriteExisting = FALSE; + fTypePreservation = 0; + fConvEOL = 0; + fConvEOLEnable = true; + + fAcceptButtonID = IDC_SELECT_ACCEPT; + + fpTargetDiskFS = nil; + //fpTargetSubdir = nil; + fpDiskImg = nil; + } + virtual ~AddFilesDialog(void) {} + + /* values from dialog */ + CString fStoragePrefix; + bool fStoragePrefixEnable; + BOOL fIncludeSubfolders; + BOOL fStripFolderNames; + bool fStripFolderNamesEnable; + BOOL fOverwriteExisting; + + enum { kPreserveNone = 0, kPreserveTypes, kPreserveAndExtend }; + int fTypePreservation; + + enum { kConvEOLNone = 0, kConvEOLType, kConvEOLAuto, kConvEOLAll }; + int fConvEOL; + bool fConvEOLEnable; + + /* carryover from ChooseAddTargetDialog */ + DiskImgLib::DiskFS* fpTargetDiskFS; + //DiskImgLib::A2File* fpTargetSubdir; + + /* kluge; we carry this around for the benefit of AddDisk */ + DiskImgLib::DiskImg* fpDiskImg; + +private: + virtual bool MyDataExchange(bool saveAndValidate); + virtual void ShiftControls(int deltaX, int deltaY); + virtual UINT MyOnCommand(WPARAM wParam, LPARAM lParam); + + void OnIDHelp(void); + bool ValidateStoragePrefix(void); + + //DECLARE_MESSAGE_MAP() +}; + +#endif /*__ADDFILESDIALOG__*/ \ No newline at end of file diff --git a/app/ArchiveInfoDialog.cpp b/app/ArchiveInfoDialog.cpp new file mode 100644 index 0000000..3edc3f5 --- /dev/null +++ b/app/ArchiveInfoDialog.cpp @@ -0,0 +1,433 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Implementation of the various ArchiveInfo dialog classes. + */ +#include "StdAfx.h" +#include "HelpTopics.h" +#include "ArchiveInfoDialog.h" +#include "../prebuilt/NufxLib.h" + +/* + * =========================================================================== + * ArchiveInfoDialog + * =========================================================================== + */ + +BEGIN_MESSAGE_MAP(ArchiveInfoDialog, CDialog) + ON_COMMAND(IDHELP, OnHelp) +END_MESSAGE_MAP() + +/* + * Show general help for the archive info dialogs. + */ +void +ArchiveInfoDialog::OnHelp(void) +{ + WinHelp(HELP_TOPIC_ARCHIVE_INFO, HELP_CONTEXT); +} + + +/* + * =========================================================================== + * NufxArchiveInfoDialog + * =========================================================================== + */ + + /* + * Set up fields with NuFX archive info. + */ +BOOL +NufxArchiveInfoDialog::OnInitDialog(void) +{ + CString notAvailable = "(not available)"; + NuArchive* pNuArchive; + const NuMasterHeader* pMasterHeader; + CWnd* pWnd; + CString tmpStr; + NuAttr attr; + NuError nerr; + time_t when; + + ASSERT(fpArchive != nil); + + pNuArchive = fpArchive->GetNuArchivePointer(); + ASSERT(pNuArchive != nil); + (void) NuGetMasterHeader(pNuArchive, &pMasterHeader); + ASSERT(pMasterHeader != nil); + + pWnd = GetDlgItem(IDC_AI_FILENAME); + pWnd->SetWindowText(fpArchive->GetPathName()); + + pWnd = GetDlgItem(IDC_AINUFX_RECORDS); + nerr = NuGetAttr(pNuArchive, kNuAttrNumRecords, &attr); + if (nerr == kNuErrNone) + tmpStr.Format("%ld", attr); + else + tmpStr = notAvailable; + pWnd->SetWindowText(tmpStr); + + pWnd = GetDlgItem(IDC_AINUFX_FORMAT); + nerr = NuGetAttr(pNuArchive, kNuAttrArchiveType, &attr); + switch (attr) { + case kNuArchiveNuFX: tmpStr = "NuFX"; break; + case kNuArchiveNuFXInBNY: tmpStr = "NuFX in Binary II"; break; + case kNuArchiveNuFXSelfEx: tmpStr = "Self-extracting NuFX"; break; + case kNuArchiveNuFXSelfExInBNY: tmpStr = "Self-extracting NuFX in Binary II"; + break; + case kNuArchiveBNY: tmpStr = "Binary II"; break; + default: + tmpStr = "(unknown)"; + break; + }; + pWnd->SetWindowText(tmpStr); + + pWnd = GetDlgItem(IDC_AINUFX_MASTERVERSION); + tmpStr.Format("%ld", pMasterHeader->mhMasterVersion); + pWnd->SetWindowText(tmpStr); + + pWnd = GetDlgItem(IDC_AINUFX_CREATEWHEN); + when = NufxArchive::DateTimeToSeconds(&pMasterHeader->mhArchiveCreateWhen); + tmpStr.Format("%.24s", ctime(&when)); + pWnd->SetWindowText(tmpStr); + + pWnd = GetDlgItem(IDC_AINUFX_MODIFYWHEN); + when = NufxArchive::DateTimeToSeconds(&pMasterHeader->mhArchiveModWhen); + tmpStr.Format("%.24s", ctime(&when)); + pWnd->SetWindowText(tmpStr); + + pWnd = GetDlgItem(IDC_AINUFX_JUNKSKIPPED); + nerr = NuGetAttr(pNuArchive, kNuAttrJunkOffset, &attr); + if (nerr == kNuErrNone) + tmpStr.Format("%ld bytes", attr); + else + tmpStr = notAvailable; + pWnd->SetWindowText(tmpStr); + + return ArchiveInfoDialog::OnInitDialog(); +} + + +/* + * =========================================================================== + * DiskArchiveInfoDialog + * =========================================================================== + */ + +BEGIN_MESSAGE_MAP(DiskArchiveInfoDialog, ArchiveInfoDialog) + ON_CBN_SELCHANGE(IDC_AIDISK_SUBVOLSEL, OnSubVolSelChange) +END_MESSAGE_MAP() + +/* + * Set up fields with disk archive info. + */ +BOOL +DiskArchiveInfoDialog::OnInitDialog(void) +{ + CWnd* pWnd; + CString tmpStr; + const DiskImg* pDiskImg; + const DiskFS* pDiskFS; + + ASSERT(fpArchive != nil); + + pDiskImg = fpArchive->GetDiskImg(); + ASSERT(pDiskImg != nil); + pDiskFS = fpArchive->GetDiskFS(); + ASSERT(pDiskFS != nil); + + /* + * Volume characteristics. + */ + pWnd = GetDlgItem(IDC_AI_FILENAME); + pWnd->SetWindowText(fpArchive->GetPathName()); + + pWnd = GetDlgItem(IDC_AIDISK_OUTERFORMAT); + pWnd->SetWindowText(DiskImg::ToString(pDiskImg->GetOuterFormat())); + + pWnd = GetDlgItem(IDC_AIDISK_FILEFORMAT); + pWnd->SetWindowText(DiskImg::ToString(pDiskImg->GetFileFormat())); + + pWnd = GetDlgItem(IDC_AIDISK_PHYSICALFORMAT); + DiskImg::PhysicalFormat physicalFormat = pDiskImg->GetPhysicalFormat(); + if (physicalFormat == DiskImg::kPhysicalFormatNib525_6656 || + physicalFormat == DiskImg::kPhysicalFormatNib525_6384 || + physicalFormat == DiskImg::kPhysicalFormatNib525_Var) + { + CString tmpStr; + const DiskImg::NibbleDescr* pNibbleDescr = pDiskImg->GetNibbleDescr(); + if (pNibbleDescr != nil) + tmpStr.Format("%s, layout is \"%s\"", + DiskImg::ToString(physicalFormat), pNibbleDescr->description); + else + tmpStr = DiskImg::ToString(physicalFormat); // unexpected + pWnd->SetWindowText(tmpStr); + } else { + pWnd->SetWindowText(DiskImg::ToString(physicalFormat)); + } + + FillInVolumeInfo(pDiskFS); + + /* + * Configure the sub-volume drop down menu. If there's only one item, + * we disable it. + */ + CComboBox* pCombo = (CComboBox*) GetDlgItem(IDC_AIDISK_SUBVOLSEL); + int idx = 0; + + AddSubVolumes(pDiskFS, "", &idx); + ASSERT(idx > 0); // must have at least the top-level DiskFS + + pCombo->SetCurSel(0); + if (idx == 1) + pCombo->EnableWindow(FALSE); + + return ArchiveInfoDialog::OnInitDialog(); +} + +/* + * Recursively add sub-volumes to the list. + */ +void +DiskArchiveInfoDialog::AddSubVolumes(const DiskFS* pDiskFS, const char* prefix, + int* pIdx) +{ + CComboBox* pCombo = (CComboBox*) GetDlgItem(IDC_AIDISK_SUBVOLSEL); + CString tmpStr; + + /* + * Add the current DiskFS. + */ + tmpStr = prefix; + tmpStr += pDiskFS->GetVolumeID(); + pCombo->AddString(tmpStr); + pCombo->SetItemData(*pIdx, (unsigned long) pDiskFS); + (*pIdx)++; + + /* + * Add everything beneath the current level. + */ + DiskFS::SubVolume* pSubVol; + pSubVol = pDiskFS->GetNextSubVolume(nil); + tmpStr = prefix; + tmpStr += " "; + while (pSubVol != nil) { + AddSubVolumes(pSubVol->GetDiskFS(), tmpStr, pIdx); + + pSubVol = pDiskFS->GetNextSubVolume(pSubVol); + } +} + +/* + * The user has changed their selection in the sub-volume pulldown menu. + */ +void +DiskArchiveInfoDialog::OnSubVolSelChange(void) +{ + CComboBox* pCombo = (CComboBox*) GetDlgItem(IDC_AIDISK_SUBVOLSEL); + ASSERT(pCombo != nil); + //WMSG1("+++ SELECTION IS NOW %d\n", pCombo->GetCurSel()); + + const DiskFS* pDiskFS; + pDiskFS = (DiskFS*) pCombo->GetItemData(pCombo->GetCurSel()); + ASSERT(pDiskFS != nil); + FillInVolumeInfo(pDiskFS); +} + +/* + * Fill in the volume-specific info fields. + */ +void +DiskArchiveInfoDialog::FillInVolumeInfo(const DiskFS* pDiskFS) +{ + const DiskImg* pDiskImg = pDiskFS->GetDiskImg(); + CString unknown = "(unknown)"; + CString tmpStr; + DIError dierr; + CWnd* pWnd; + + pWnd = GetDlgItem(IDC_AIDISK_SECTORORDER); + pWnd->SetWindowText(DiskImg::ToString(pDiskImg->GetSectorOrder())); + + pWnd = GetDlgItem(IDC_AIDISK_FSFORMAT); + pWnd->SetWindowText(DiskImg::ToString(pDiskImg->GetFSFormat())); + + pWnd = GetDlgItem(IDC_AIDISK_FILECOUNT); + tmpStr.Format("%ld", pDiskFS->GetFileCount()); + pWnd->SetWindowText(tmpStr); + + long totalUnits, freeUnits; + int unitSize; + CString reducedSize; + + dierr = pDiskFS->GetFreeSpaceCount(&totalUnits, &freeUnits, &unitSize); + if (dierr == kDIErrNone) { + + /* got the space; break it down by disk type */ + if (unitSize == DiskImgLib::kBlockSize) { + pWnd = GetDlgItem(IDC_AIDISK_CAPACITY); + GetReducedSize(totalUnits, unitSize, &reducedSize); + tmpStr.Format("%ld blocks (%s)", + totalUnits, reducedSize); + if (totalUnits != pDiskImg->GetNumBlocks()) { + CString tmpStr2; + tmpStr2.Format(", image has room for %ld blocks", + pDiskImg->GetNumBlocks()); + tmpStr += tmpStr2; + } + pWnd->SetWindowText(tmpStr); + + pWnd = GetDlgItem(IDC_AIDISK_FREESPACE); + GetReducedSize(freeUnits, unitSize, &reducedSize); + tmpStr.Format("%ld blocks (%s)", + freeUnits, reducedSize); + pWnd->SetWindowText(tmpStr); + } else { + ASSERT(unitSize == DiskImgLib::kSectorSize); + + pWnd = GetDlgItem(IDC_AIDISK_CAPACITY); + GetReducedSize(totalUnits, unitSize, &reducedSize); + tmpStr.Format("%ld sectors (%s)", + totalUnits, reducedSize); + pWnd->SetWindowText(tmpStr); + + pWnd = GetDlgItem(IDC_AIDISK_FREESPACE); + GetReducedSize(freeUnits, unitSize, &reducedSize); + tmpStr.Format("%ld sectors (%s)", + freeUnits, reducedSize); + pWnd->SetWindowText(tmpStr); + } + } else { + /* "free space" not supported; fill in what we do know */ + pWnd = GetDlgItem(IDC_AIDISK_CAPACITY); + if (pDiskImg->GetHasBlocks()) { + totalUnits = pDiskImg->GetNumBlocks(); + GetReducedSize(totalUnits, DiskImgLib::kBlockSize, &reducedSize); + tmpStr.Format("%ld blocks (%s)", + totalUnits, reducedSize); + } else if (pDiskImg->GetHasSectors()) { + tmpStr.Format("%ld tracks, %d sectors per track", + pDiskImg->GetNumTracks(), pDiskImg->GetNumSectPerTrack()); + } else { + tmpStr = unknown; + } + pWnd->SetWindowText(tmpStr); + + pWnd = GetDlgItem(IDC_AIDISK_FREESPACE); + pWnd->SetWindowText(unknown); + } + + pWnd = GetDlgItem(IDC_AIDISK_WRITEABLE); + tmpStr = pDiskFS->GetReadWriteSupported() ? "Yes" : "No"; + pWnd->SetWindowText(tmpStr); + + pWnd = GetDlgItem(IDC_AIDISK_DAMAGED); + tmpStr = pDiskFS->GetFSDamaged() ? "Yes" : "No"; + pWnd->SetWindowText(tmpStr); + + const char* cp; + char* outp; + + pWnd = GetDlgItem(IDC_AIDISK_NOTES); + cp = pDiskImg->GetNotes(); + outp = tmpStr.GetBuffer(strlen(cp) * 2 +1); + /* convert '\n' to '\r\n' */ + while (*cp != '\0') { + if (*cp == '\n') + *outp++ = '\r'; + *outp++ = *cp++; + } + *outp = '\0'; + tmpStr.ReleaseBuffer(); + /* drop the trailing linefeed */ + if (!tmpStr.IsEmpty() && tmpStr.GetAt(tmpStr.GetLength()-1) == '\n') + tmpStr.TrimRight(); // trim the whitespace chars off + pWnd->SetWindowText(tmpStr); +} + +/* + * Reduce a size to something meaningful (KB, MB, GB). + */ +void +DiskArchiveInfoDialog::GetReducedSize(long numUnits, int unitSize, + CString* pOut) const +{ + LONGLONG sizeInBytes = numUnits; + sizeInBytes *= unitSize; + long reducedSize; + + if (sizeInBytes < 0) { + ASSERT(false); + pOut->Format(""); + return; + } + + if (sizeInBytes >= 1024*1024*1024) { + reducedSize = (long) (sizeInBytes / (1024*1024)); + pOut->Format("%.2fGB", reducedSize / 1024.0); + } else if (sizeInBytes >= 1024*1024) { + reducedSize = (long) (sizeInBytes / 1024); + pOut->Format("%.2fMB", reducedSize / 1024.0); + } else { + pOut->Format("%.2fKB", ((long) sizeInBytes) / 1024.0); + } +} + + +/* + * =========================================================================== + * BnyArchiveInfoDialog + * =========================================================================== + */ + +/* + * Set up fields with Binary II info. + * + * Binary II files are pretty dull. + */ +BOOL +BnyArchiveInfoDialog::OnInitDialog(void) +{ + CWnd* pWnd; + CString tmpStr; + + ASSERT(fpArchive != nil); + + pWnd = GetDlgItem(IDC_AI_FILENAME); + pWnd->SetWindowText(fpArchive->GetPathName()); + tmpStr.Format("%ld", fpArchive->GetNumEntries()); + pWnd = GetDlgItem(IDC_AIBNY_RECORDS); + pWnd->SetWindowText(tmpStr); + + return ArchiveInfoDialog::OnInitDialog(); +} + + +/* + * =========================================================================== + * AcuArchiveInfoDialog + * =========================================================================== + */ + +/* + * Set up fields with ACU info. + */ +BOOL +AcuArchiveInfoDialog::OnInitDialog(void) +{ + CWnd* pWnd; + CString tmpStr; + + ASSERT(fpArchive != nil); + + pWnd = GetDlgItem(IDC_AI_FILENAME); + pWnd->SetWindowText(fpArchive->GetPathName()); + tmpStr.Format("%ld", fpArchive->GetNumEntries()); + pWnd = GetDlgItem(IDC_AIBNY_RECORDS); + pWnd->SetWindowText(tmpStr); + + return ArchiveInfoDialog::OnInitDialog(); +} diff --git a/app/ArchiveInfoDialog.h b/app/ArchiveInfoDialog.h new file mode 100644 index 0000000..53ced39 --- /dev/null +++ b/app/ArchiveInfoDialog.h @@ -0,0 +1,118 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Definitions for the ArchiveInfo set of dialog classes. + */ +#ifndef __ARCHIVEINFODIALOG__ +#define __ARCHIVEINFODIALOG__ + +#include "resource.h" +#include "GenericArchive.h" +#include "NufxArchive.h" +#include "DiskArchive.h" +#include "BnyArchive.h" +#include "AcuArchive.h" + +/* + * This is an abstract base class for the archive info dialogs. There is + * one dialog for each kind of archive (i.e. each GenericArchive sub-class). + */ +class ArchiveInfoDialog : public CDialog { +public: + ArchiveInfoDialog(UINT dialogID, CWnd* pParentWnd = NULL) : + CDialog(dialogID, pParentWnd) + {} + virtual ~ArchiveInfoDialog(void) {} + +private: + afx_msg void OnHelp(void); + + DECLARE_MESSAGE_MAP() +}; + +/* + * NuFX archive info. + */ +class NufxArchiveInfoDialog : public ArchiveInfoDialog { +public: + NufxArchiveInfoDialog(NufxArchive* pArchive, CWnd* pParentWnd = NULL) : + fpArchive(pArchive), + ArchiveInfoDialog(IDD_ARCHIVEINFO_NUFX, pParentWnd) + {} + virtual ~NufxArchiveInfoDialog(void) {} + +private: + // overrides + virtual BOOL OnInitDialog(void); + + NufxArchive* fpArchive; +}; + +/* + * Disk image info. + */ +class DiskArchiveInfoDialog : public ArchiveInfoDialog { +public: + DiskArchiveInfoDialog(DiskArchive* pArchive, CWnd* pParentWnd = NULL) : + fpArchive(pArchive), + ArchiveInfoDialog(IDD_ARCHIVEINFO_DISK, pParentWnd) + {} + virtual ~DiskArchiveInfoDialog(void) {} + +private: + // overrides + virtual BOOL OnInitDialog(void); + + afx_msg void OnSubVolSelChange(void); + + void FillInVolumeInfo(const DiskFS* pDiskFS); + void AddSubVolumes(const DiskFS* pDiskFS, const char* prefix, + int* pIdx); + void GetReducedSize(long numUnits, int unitSize, + CString* pOut) const; + + DiskArchive* fpArchive; + + DECLARE_MESSAGE_MAP() +}; + +/* + * Binary II archive info. + */ +class BnyArchiveInfoDialog : public ArchiveInfoDialog { +public: + BnyArchiveInfoDialog(BnyArchive* pArchive, CWnd* pParentWnd = NULL) : + fpArchive(pArchive), + ArchiveInfoDialog(IDD_ARCHIVEINFO_BNY, pParentWnd) + {} + virtual ~BnyArchiveInfoDialog(void) {} + +private: + // overrides + virtual BOOL OnInitDialog(void); + + BnyArchive* fpArchive; +}; + +/* + * ACU archive info. + */ +class AcuArchiveInfoDialog : public ArchiveInfoDialog { +public: + AcuArchiveInfoDialog(AcuArchive* pArchive, CWnd* pParentWnd = NULL) : + fpArchive(pArchive), + ArchiveInfoDialog(IDD_ARCHIVEINFO_ACU, pParentWnd) + {} + virtual ~AcuArchiveInfoDialog(void) {} + +private: + // overrides + virtual BOOL OnInitDialog(void); + + AcuArchive* fpArchive; +}; + +#endif /*__ARCHIVEINFODIALOG__*/ diff --git a/app/BNYArchive.cpp b/app/BNYArchive.cpp new file mode 100644 index 0000000..57f80c1 --- /dev/null +++ b/app/BNYArchive.cpp @@ -0,0 +1,998 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Binary II file support. + */ +#include "stdafx.h" +#include "BNYArchive.h" +#include "NufxArchive.h" +#include "Preferences.h" +#include "Main.h" +#include "Squeeze.h" +#include + + +/* + * =========================================================================== + * BnyEntry + * =========================================================================== + */ + +/* + * Extract data from an entry. + * + * If "*ppText" is non-nil, the data will be read into the pointed-to buffer + * so long as it's shorter than *pLength bytes. The value in "*pLength" + * will be set to the actual length used. + * + * If "*ppText" is nil, the uncompressed data will be placed into a buffer + * allocated with "new[]". + * + * Returns IDOK on success, IDCANCEL if the operation was cancelled by the + * user, and -1 value on failure. On failure, "*pErrMsg" holds an error + * message. + * + * "which" is an anonymous GenericArchive enum (e.g. "kDataThread"). + */ +int +BnyEntry::ExtractThreadToBuffer(int which, char** ppText, long* pLength, + CString* pErrMsg) const +{ + NuError nerr; + ExpandBuffer expBuf; + char* dataBuf = nil; + long len; + bool needAlloc = true; + int result = -1; + + ASSERT(fpArchive != nil); + ASSERT(fpArchive->fFp != nil); + + if (*ppText != nil) + needAlloc = false; + + if (which != kDataThread) { + *pErrMsg = "No such fork"; + goto bail; + } + + len = (long) GetUncompressedLen(); + if (len == 0) { + if (needAlloc) { + *ppText = new char[1]; + **ppText = '\0'; + } + *pLength = 0; + result = IDOK; + goto bail; + } + + SET_PROGRESS_BEGIN(); + + errno = 0; + if (fseek(fpArchive->fFp, fOffset, SEEK_SET) < 0) { + pErrMsg->Format("Unable to seek to offset %ld: %s", + fOffset, strerror(errno)); + goto bail; + } + + if (GetSqueezed()) { + nerr = UnSqueeze(fpArchive->fFp, (unsigned long) GetUncompressedLen(), + &expBuf, true, kBNYBlockSize); + if (nerr != kNuErrNone) { + pErrMsg->Format("File read failed: %s", NuStrError(nerr)); + goto bail; + } + + char* unsqBuf = nil; + long unsqLen = 0; + expBuf.SeizeBuffer(&unsqBuf, &unsqLen); + WMSG2("Unsqueezed %ld bytes to %d\n", len, unsqLen); + if (unsqLen == 0) { + // some bonehead squeezed a zero-length file + delete[] unsqBuf; + ASSERT(*ppText == nil); + WMSG0("Handling zero-length squeezed file!\n"); + if (needAlloc) { + *ppText = new char[1]; + **ppText = '\0'; + } + *pLength = 0; + } else { + if (needAlloc) { + /* just use the seized buffer */ + *ppText = unsqBuf; + *pLength = unsqLen; + } else { + if (*pLength < unsqLen) { + pErrMsg->Format("buf size %ld too short (%ld)", + *pLength, unsqLen); + delete[] unsqBuf; + goto bail; + } + + memcpy(*ppText, unsqBuf, unsqLen); + delete[] unsqBuf; + *pLength = unsqLen; + } + } + + } else { + if (needAlloc) { + dataBuf = new char[len]; + if (dataBuf == nil) { + pErrMsg->Format("allocation of %ld bytes failed", len); + goto bail; + } + } else { + if (*pLength < (long) len) { + pErrMsg->Format("buf size %ld too short (%ld)", + *pLength, len); + goto bail; + } + dataBuf = *ppText; + } + if (fread(dataBuf, len, 1, fpArchive->fFp) != 1) { + pErrMsg->Format("File read failed: %s", strerror(errno)); + goto bail; + } + + if (needAlloc) + *ppText = dataBuf; + *pLength = len; + } + + result = IDOK; + +bail: + if (result == IDOK) { + SET_PROGRESS_END(); + ASSERT(pErrMsg->IsEmpty()); + } else { + ASSERT(result == IDCANCEL || !pErrMsg->IsEmpty()); + if (needAlloc) { + delete[] dataBuf; + ASSERT(*ppText == nil); + } + } + return result; +} + +/* + * Extract data from a thread to a file. Since we're not copying to memory, + * we can't assume that we're able to hold the entire file all at once. + * + * Returns IDOK on success, IDCANCEL if the operation was cancelled by the + * user, and -1 value on failure. On failure, "*pMsg" holds an + * error message. + */ +int +BnyEntry::ExtractThreadToFile(int which, FILE* outfp, ConvertEOL conv, + ConvertHighASCII convHA, CString* pErrMsg) const +{ + NuError nerr; + long len; + int result = -1; + + ASSERT(IDOK != -1 && IDCANCEL != -1); + if (which != kDataThread) { + *pErrMsg = "No such fork"; + goto bail; + } + + len = (long) GetUncompressedLen(); + if (len == 0) { + WMSG0("Empty fork\n"); + result = IDOK; + goto bail; + } + + errno = 0; + if (fseek(fpArchive->fFp, fOffset, SEEK_SET) < 0) { + pErrMsg->Format("Unable to seek to offset %ld: %s", + fOffset, strerror(errno)); + goto bail; + } + + SET_PROGRESS_BEGIN(); + + /* + * Generally speaking, anything in a BNY file is going to be small. The + * major exception is a BXY file, which could be huge. However, the + * SHK embedded in a BXY is never squeezed. + * + * To make life easy, we either unsqueeze the entire thing into a buffer + * and then write that, or we do a file-to-file copy of the specified + * number of bytes. + */ + if (GetSqueezed()) { + ExpandBuffer expBuf; + bool lastCR = false; + char* buf; + long uncLen; + + nerr = UnSqueeze(fpArchive->fFp, (unsigned long) GetUncompressedLen(), + &expBuf, true, kBNYBlockSize); + if (nerr != kNuErrNone) { + pErrMsg->Format("File read failed: %s", NuStrError(nerr)); + goto bail; + } + + expBuf.SeizeBuffer(&buf, &uncLen); + WMSG2("Unsqueezed %ld bytes to %d\n", len, uncLen); + + // some bonehead squeezed a zero-length file + if (uncLen == 0) { + ASSERT(buf == nil); + WMSG0("Handling zero-length squeezed file!\n"); + result = IDOK; + goto bail; + } + + int err = GenericEntry::WriteConvert(outfp, buf, uncLen, &conv, + &convHA, &lastCR); + if (err != 0) { + pErrMsg->Format("File write failed: %s", strerror(err)); + delete[] buf; + goto bail; + } + + delete[] buf; + } else { + nerr = CopyData(outfp, conv, convHA, pErrMsg); + if (nerr != kNuErrNone) { + if (pErrMsg->IsEmpty()) { + pErrMsg->Format("Failed while copying data: %s\n", + NuStrError(nerr)); + } + goto bail; + } + } + + result = IDOK; + +bail: + SET_PROGRESS_END(); + return result; +} + +/* + * Copy data from the seeked archive to outfp, possibly converting EOL along + * the way. + */ +NuError +BnyEntry::CopyData(FILE* outfp, ConvertEOL conv, ConvertHighASCII convHA, + CString* pMsg) const +{ + NuError nerr = kNuErrNone; + const int kChunkSize = 8192; + char buf[kChunkSize]; + bool lastCR = false; + long srcLen, dataRem; + + srcLen = (long) GetUncompressedLen(); + ASSERT(srcLen > 0); // empty files should've been caught earlier + + /* + * Loop until all data copied. + */ + dataRem = srcLen; + while (dataRem) { + int chunkLen; + + if (dataRem > kChunkSize) + chunkLen = kChunkSize; + else + chunkLen = dataRem; + + /* read a chunk from the source file */ + nerr = fpArchive->BNYRead(buf, chunkLen); + if (nerr != kNuErrNone) { + pMsg->Format("File read failed: %s.", NuStrError(nerr)); + goto bail; + } + + /* write chunk to destination file */ + int err = GenericEntry::WriteConvert(outfp, buf, chunkLen, &conv, + &convHA, &lastCR); + if (err != 0) { + pMsg->Format("File write failed: %s.", strerror(err)); + nerr = kNuErrGeneric; + goto bail; + } + + dataRem -= chunkLen; + SET_PROGRESS_UPDATE(ComputePercent(srcLen - dataRem, srcLen)); + } + +bail: + return nerr; +} + + +/* + * Test this entry by extracting it. + * + * If the file isn't compressed, just make sure the file is big enough. If + * it's squeezed, invoke the un-squeeze function with a "nil" buffer pointer. + */ +NuError +BnyEntry::TestEntry(CWnd* pMsgWnd) +{ + NuError nerr = kNuErrNone; + CString errMsg; + long len; + int result = -1; + + len = (long) GetUncompressedLen(); + if (len == 0) + goto bail; + + errno = 0; + if (fseek(fpArchive->fFp, fOffset, SEEK_SET) < 0) { + nerr = kNuErrGeneric; + errMsg.Format("Unable to seek to offset %ld: %s\n", + fOffset, strerror(errno)); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + goto bail; + } + + if (GetSqueezed()) { + nerr = UnSqueeze(fpArchive->fFp, (unsigned long) GetUncompressedLen(), + nil, true, kBNYBlockSize); + if (nerr != kNuErrNone) { + errMsg.Format("Unsqueeze failed: %s.", NuStrError(nerr)); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + goto bail; + } + } else { + errno = 0; + if (fseek(fpArchive->fFp, fOffset + len, SEEK_SET) < 0) { + nerr = kNuErrGeneric; + errMsg.Format("Unable to seek to offset %ld (file truncated?): %s\n", + fOffset, strerror(errno)); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + goto bail; + } + } + + if (SET_PROGRESS_UPDATE(100) == IDCANCEL) + nerr = kNuErrAborted; + +bail: + return nerr; +} + + +/* + * =========================================================================== + * BnyArchive + * =========================================================================== + */ + +/* + * Perform one-time initialization. There really isn't any for us. Having + * this is kind of silly, but we include it for consistency. + * + * Returns 0 on success, nonzero on error. + */ +/*static*/ CString +BnyArchive::AppInit(void) +{ + return ""; +} + +/* + * Open a BNY archive. + * + * Returns an error string on failure, or "" on success. + */ +GenericArchive::OpenResult +BnyArchive::Open(const char* filename, bool readOnly, CString* pErrMsg) +{ + CString errMsg; + + fIsReadOnly = true; // ignore "readOnly" + + errno = 0; + fFp = fopen(filename, "rb"); + if (fFp == nil) { + errMsg.Format("Unable to open %s: %s.", filename, strerror(errno)); + goto bail; + } + + { + CWaitCursor waitc; + + if (LoadContents() != 0) { + errMsg.Format("Failed while loading contents of Binary II file."); + goto bail; + } + } + + SetPathName(filename); + +bail: + *pErrMsg = errMsg; + if (!errMsg.IsEmpty()) + return kResultFailure; + else + return kResultSuccess; +} + +/* + * Finish instantiating a BnyArchive object by creating a new archive. + * + * Returns an error string on failure, or "" on success. + */ +CString +BnyArchive::New(const char* /*filename*/, const void* /*options*/) +{ + CString retmsg("Sorry, Binary II files can't be created."); + return retmsg; +} + + +/* + * Our capabilities. + */ +long +BnyArchive::GetCapability(Capability cap) +{ + switch (cap) { + case kCapCanTest: + return true; + break; + case kCapCanRenameFullPath: + return true; + break; + case kCapCanRecompress: + return true; + break; + case kCapCanEditComment: + return false; + break; + case kCapCanAddDisk: + return false; + break; + case kCapCanConvEOLOnAdd: + return false; + break; + case kCapCanCreateSubdir: + return false; + break; + case kCapCanRenameVolume: + return false; + break; + default: + ASSERT(false); + return -1; + break; + } +} + + +/* + * Load the contents of the archive. + * + * Returns 0 on success, nonzero on failure. + */ +int +BnyArchive::LoadContents(void) +{ + NuError nerr; + + ASSERT(fFp != nil); + rewind(fFp); + + nerr = BNYIterate(); + WMSG1("BNYIterate returned %d\n", nerr); + return (nerr != kNuErrNone); +} + +/* + * Reload the contents of the archive. + */ +CString +BnyArchive::Reload(void) +{ + fReloadFlag = true; // tell everybody that cached data is invalid + + DeleteEntries(); + if (LoadContents() != 0) { + return "Reload failed."; + } + + return ""; +} + +/* + * Given a BnyFileEntry structure, add an appropriate entry to the list. + * + * Note this can mangle pEntry (notably the filename). + */ +NuError +BnyArchive::LoadContentsCallback(BnyFileEntry* pEntry) +{ + const int kBNYFssep = '/'; + NuError err = kNuErrNone; + BnyEntry* pNewEntry; + char* fileName; + + + /* make sure filename doesn't start with '/' (not allowed by BNY spec) */ + fileName = pEntry->fileName; + while (*fileName == kBNYFssep) + fileName++; + if (*fileName == '\0') + return kNuErrBadData; + + /* remove '.QQ' from end of squeezed files */ + bool isSqueezed = false; + if (pEntry->realEOF && IsSqueezed(pEntry->blockBuf[0], pEntry->blockBuf[1])) + isSqueezed = true; + + if (isSqueezed && strlen(fileName) > 3) { + char* ext; + ext = fileName + strlen(fileName) -3; + if (strcasecmp(ext, ".qq") == 0) + *ext = '\0'; + } + + /* + * Create the new entry. + */ + pNewEntry = new BnyEntry(this); + pNewEntry->SetPathName(fileName); + pNewEntry->SetFssep(kBNYFssep); + pNewEntry->SetFileType(pEntry->fileType); + pNewEntry->SetAuxType(pEntry->auxType); + pNewEntry->SetAccess(pEntry->access); + pNewEntry->SetCreateWhen(NufxArchive::DateTimeToSeconds(&pEntry->createWhen)); + pNewEntry->SetModWhen(NufxArchive::DateTimeToSeconds(&pEntry->modWhen)); + + /* always ProDOS */ + pNewEntry->SetSourceFS(DiskImg::kFormatProDOS); + + pNewEntry->SetHasDataFork(true); + pNewEntry->SetHasRsrcFork(false); + if (IsDir(pEntry)) { + pNewEntry->SetRecordKind(GenericEntry::kRecordKindDirectory); + } else { + pNewEntry->SetRecordKind(GenericEntry::kRecordKindFile); + } + + /* there's no way to get the uncompressed EOF from a squeezed file */ + pNewEntry->SetCompressedLen(pEntry->realEOF); + pNewEntry->SetDataForkLen(pEntry->realEOF); + + if (isSqueezed) + pNewEntry->SetFormatStr("Squeeze"); + else + pNewEntry->SetFormatStr("Uncompr"); + + pNewEntry->SetSqueezed(isSqueezed); + if (pEntry->realEOF != 0) + pNewEntry->SetOffset(ftell(fFp) - kBNYBlockSize); + else + pNewEntry->SetOffset(ftell(fFp)); + + AddEntry(pNewEntry); + + return err; +} + + +/* + * =========================================================================== + * Binary II functions + * =========================================================================== + */ + +/* + * Most of what follows was adapted directly from NuLib2 v2.0. There's no + * such thing as BnyLib, so all of the code for manipulating the file is + * included here. + */ + +/* + * Test for the magic number on a file in SQueezed format. + */ +bool +BnyArchive::IsSqueezed(uchar one, uchar two) +{ + return (one == 0x76 && two == 0xff); +} + +/* + * Test if this entry is a directory. + */ +bool +BnyArchive::IsDir(BnyFileEntry* pEntry) +{ + /* + * NuLib and "unblu.c" compared against file type 15 (DIR), so I'm + * going to do that too, but it would probably be better to compare + * against storageType 0x0d. + */ + return (pEntry->fileType == 15); +} + +/* + * Wrapper for fread(). Note the arguments resemble read(2) rather + * than fread(3S). + */ +NuError +BnyArchive::BNYRead(void* buf, size_t nbyte) +{ + size_t result; + + ASSERT(buf != nil); + ASSERT(nbyte > 0); + ASSERT(fFp != nil); + + errno = 0; + result = fread(buf, 1, nbyte, fFp); + if (result != nbyte) + return errno ? (NuError)errno : kNuErrFileRead; + return kNuErrNone; +} + +/* + * Seek within an archive. Because we need to handle streaming archives, + * and don't need to special-case anything, we only allow relative + * forward seeks. + */ +NuError +BnyArchive::BNYSeek(long offset) +{ + ASSERT(fFp != nil); + ASSERT(offset > 0); + + /*DBUG(("--- seeking forward %ld bytes\n", offset));*/ + + if (fseek(fFp, offset, SEEK_CUR) < 0) + return kNuErrFileSeek; + + return kNuErrNone; +} + + +/* + * Convert from ProDOS compact date format to the expanded DateTime format. + */ +void +BnyArchive::BNYConvertDateTime(unsigned short prodosDate, + unsigned short prodosTime, NuDateTime* pWhen) +{ + pWhen->second = 0; + pWhen->minute = prodosTime & 0x3f; + pWhen->hour = (prodosTime >> 8) & 0x1f; + pWhen->day = (prodosDate & 0x1f) -1; + pWhen->month = ((prodosDate >> 5) & 0x0f) -1; + pWhen->year = (prodosDate >> 9) & 0x7f; + if (pWhen->year < 40) + pWhen->year += 100; /* P8 uses 0-39 for 2000-2039 */ + pWhen->extra = 0; + pWhen->weekDay = 0; +} + +/* + * Decode a Binary II header. + * + * See the File Type Note for $e0/8000 to decipher the buffer offsets + * and meanings. + */ +NuError +BnyArchive::BNYDecodeHeader(BnyFileEntry* pEntry) +{ + NuError err = kNuErrNone; + uchar* raw; + int len; + + ASSERT(pEntry != nil); + + raw = pEntry->blockBuf; + + if (raw[0] != 0x0a || raw[1] != 0x47 || raw[2] != 0x4c || raw[18] != 0x02) { + err = kNuErrBadData; + WMSG0("this doesn't look like a Binary II header\n"); + goto bail; + } + + pEntry->access = raw[3] | raw[111] << 8; + pEntry->fileType = raw[4] | raw[112] << 8; + pEntry->auxType = raw[5] | raw[6] << 8 | raw[109] << 16 | raw[110] << 24; + pEntry->storageType = raw[7]; + pEntry->fileSize = raw[8] | raw[9] << 8; + pEntry->prodosModDate = raw[10] | raw[11] << 8; + pEntry->prodosModTime = raw[12] | raw[13] << 8; + BNYConvertDateTime(pEntry->prodosModDate, pEntry->prodosModTime, + &pEntry->modWhen); + pEntry->prodosCreateDate = raw[14] | raw[15] << 8; + pEntry->prodosCreateTime = raw[16] | raw[17] << 8; + BNYConvertDateTime(pEntry->prodosCreateDate, pEntry->prodosCreateTime, + &pEntry->createWhen); + pEntry->eof = raw[20] | raw[21] << 8 | raw[22] << 16 | raw[116] << 24; + len = raw[23]; + if (len > kBNYMaxFileName) { + err = kNuErrBadData; + WMSG1("invalid filename length %d\n", len); + goto bail; + } + memcpy(pEntry->fileName, &raw[24], len); + pEntry->fileName[len] = '\0'; + + pEntry->nativeName[0] = '\0'; + if (len <= 15 && raw[39] != 0) { + len = raw[39]; + if (len > kBNYMaxNativeName) { + err = kNuErrBadData; + WMSG1("invalid filename length %d\n", len); + goto bail; + } + memcpy(pEntry->nativeName, &raw[40], len); + pEntry->nativeName[len] = '\0'; + } + + pEntry->diskSpace = raw[117] | raw[118] << 8 | raw[119] << 16 | + raw[120] << 24; + + pEntry->osType = raw[121]; + pEntry->nativeFileType = raw[122] | raw[123] << 8; + pEntry->phantomFlag = raw[124]; + pEntry->dataFlags = raw[125]; + pEntry->version = raw[126]; + pEntry->filesToFollow = raw[127]; + + /* directories are given an EOF but don't actually have any content */ + if (IsDir(pEntry)) + pEntry->realEOF = 0; + else + pEntry->realEOF = pEntry->eof; + +bail: + return err; +} + +#if 0 +/* + * Normalize the pathname by running it through the usual NuLib2 + * function. The trick here is that the function usually takes a + * NuPathnameProposal, which we don't happen to have handy. Rather + * than generalize the NuLib2 code, we just create a fake proposal, + * which is a bit dicey but shouldn't break too easily. + * + * This takes care of -e, -ee, and -j. + * + * We return the new path, which is stored in NulibState's temporary + * filename buffer. + */ +const char* +BNYNormalizePath(BnyFileEntry* pEntry) +{ + NuPathnameProposal pathProposal; + NuRecord fakeRecord; + NuThread fakeThread; + + /* make uninitialized data obvious */ + memset(&fakeRecord, 0xa1, sizeof(fakeRecord)); + memset(&fakeThread, 0xa5, sizeof(fakeThread)); + + pathProposal.pathname = pEntry->fileName; + pathProposal.filenameSeparator = '/'; /* BNY always uses ProDOS conv */ + pathProposal.pRecord = &fakeRecord; + pathProposal.pThread = &fakeThread; + + pathProposal.newPathname = nil; + pathProposal.newFilenameSeparator = '\0'; + pathProposal.newDataSink = nil; + + /* need the filetype and auxtype for -e/-ee */ + fakeRecord.recFileType = pEntry->fileType; + fakeRecord.recExtraType = pEntry->auxType; + + /* need the components of a ThreadID */ + fakeThread.thThreadClass = kNuThreadClassData; + fakeThread.thThreadKind = 0x0000; /* data fork */ + + return NormalizePath(pBny->pState, &pathProposal); +} +#endif + +#if 0 +/* + * Copy all data from the Binary II file to "outfp", reading in 128-byte + * blocks. + * + * Uses pEntry->blockBuf, which already has the first 128 bytes in it. + */ +NuError +BnyArchive::BNYCopyBlocks(BnyFileEntry* pEntry, FILE* outfp) +{ + NuError err = kNuErrNone; + long bytesLeft; + + ASSERT(pEntry->realEOF > 0); + + bytesLeft = pEntry->realEOF; + while (bytesLeft > 0) { + long toWrite; + + toWrite = bytesLeft; + if (toWrite > kBNYBlockSize) + toWrite = kBNYBlockSize; + + if (outfp != nil) { + if (fwrite(pEntry->blockBuf, toWrite, 1, outfp) != 1) { + err = errno ? (NuError) errno : kNuErrFileWrite; + WMSG0("BNY write failed\n"); + goto bail; + } + } + + bytesLeft -= toWrite; + + if (bytesLeft) { + err = BNYRead(pEntry->blockBuf, kBNYBlockSize); + if (err != kNuErrNone) { + WMSG0("BNY read failed\n"); + goto bail; + } + } + } + +bail: + return err; +} +#endif + + +/* + * Iterate through a Binary II archive, loading the data. + */ +NuError +BnyArchive::BNYIterate(void) +{ + NuError err = kNuErrNone; + BnyFileEntry entry; + //bool consumed; + int first = true; + int toFollow; + + toFollow = 1; /* assume 1 file in archive */ + while (toFollow) { + err = BNYRead(entry.blockBuf, sizeof(entry.blockBuf)); + if (err != kNuErrNone) { + WMSG0("failed while reading header\n"); + goto bail; + } + + err = BNYDecodeHeader(&entry); + if (err != kNuErrNone) { + if (first) { + WMSG0("not a Binary II archive?\n"); + } + goto bail; + } + + /* + * If the file has one or more blocks, read the first block now. + * This will allow the various functions to evaluate the file + * contents for SQueeze compression. + */ + if (entry.realEOF != 0) { + err = BNYRead(entry.blockBuf, sizeof(entry.blockBuf)); + if (err != kNuErrNone) { + WMSG0("failed while reading\n"); + goto bail; + } + } + + /* + * Invoke the load function. + */ + //consumed = false; + + err = LoadContentsCallback(&entry); + if (err != kNuErrNone) + goto bail; + + /* + * If they didn't "consume" the entire BNY entry, we need to + * do it for them. We've already read the first block (if it + * existed), so we don't need to eat that one again. + */ + if (true /*!consumed*/) { + int nblocks = (entry.realEOF + kBNYBlockSize-1) / kBNYBlockSize; + + if (nblocks > 1) { + err = BNYSeek((nblocks-1) * kBNYBlockSize); + if (err != kNuErrNone) { + WMSG0("failed while seeking forward\n"); + goto bail; + } + } + } + + if (!first) { + if (entry.filesToFollow != toFollow -1) { + WMSG2("WARNING: filesToFollow %d, expected %d\n", + entry.filesToFollow, toFollow -1); + } + } + toFollow = entry.filesToFollow; + + first = false; + } + +bail: + if (err != kNuErrNone) { + WMSG1("--- Iterator returning failure %d\n", err); + } + return err; +} + + +/* + * =========================================================================== + * BnyArchive -- test files + * =========================================================================== + */ + +/* + * Test the records represented in the selection set. + */ +bool +BnyArchive::TestSelection(CWnd* pMsgWnd, SelectionSet* pSelSet) +{ + NuError nerr; + BnyEntry* pEntry; + CString errMsg; + bool retVal = false; + + ASSERT(fFp != nil); + + WMSG1("Testing %d entries\n", pSelSet->GetNumEntries()); + + SelectionEntry* pSelEntry = pSelSet->IterNext(); + while (pSelEntry != nil) { + pEntry = (BnyEntry*) pSelEntry->GetEntry(); + + WMSG2(" Testing '%s' (offset=%ld)\n", pEntry->GetDisplayName(), + pEntry->GetOffset()); + + SET_PROGRESS_UPDATE2(0, pEntry->GetDisplayName(), nil); + + nerr = pEntry->TestEntry(pMsgWnd); + if (nerr != kNuErrNone) { + if (nerr == kNuErrAborted) { + CString title; + title.LoadString(IDS_MB_APP_NAME); + errMsg = "Cancelled."; + pMsgWnd->MessageBox(errMsg, title, MB_OK); + } else { + errMsg.Format("Failed while testing '%s': %s.", + pEntry->GetPathName(), NuStrError(nerr)); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + } + goto bail; + } + + pSelEntry = pSelSet->IterNext(); + } + + /* show success message */ + errMsg.Format("Tested %d file%s, no errors found.", + pSelSet->GetNumEntries(), + pSelSet->GetNumEntries() == 1 ? "" : "s"); + pMsgWnd->MessageBox(errMsg); + retVal = true; + +bail: + SET_PROGRESS_END(); + return retVal; +} diff --git a/app/BNYArchive.h b/app/BNYArchive.h new file mode 100644 index 0000000..e4b07ac --- /dev/null +++ b/app/BNYArchive.h @@ -0,0 +1,218 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Binary II support. + */ +#ifndef __BNY_ARCHIVE__ +#define __BNY_ARCHIVE__ + +#include "GenericArchive.h" + + +class BnyArchive; + +/* + * One file in a BNY archive. + */ +class BnyEntry : public GenericEntry { +public: + BnyEntry(BnyArchive* pArchive) : + fpArchive(pArchive), fIsSqueezed(false), fOffset(-1) + {} + virtual ~BnyEntry(void) {} + + // retrieve thread data + virtual int ExtractThreadToBuffer(int which, char** ppText, long* pLength, + CString* pErrMsg) const; + virtual int ExtractThreadToFile(int which, FILE* outfp, ConvertEOL conv, + ConvertHighASCII convHA, CString* pErrMsg) const; + virtual long GetSelectionSerial(void) const { return -1; } // doesn't matter + + virtual bool GetFeatureFlag(Feature feature) const { + if (feature == kFeaturePascalTypes || feature == kFeatureDOSTypes || + feature == kFeatureHasSimpleAccess) + return false; + else + return true; + } + + NuError TestEntry(CWnd* pMsgWnd); + + bool GetSqueezed(void) const { return fIsSqueezed; } + void SetSqueezed(bool val) { fIsSqueezed = val; } + long GetOffset(void) const { return fOffset; } + void SetOffset(long offset) { fOffset = offset; } + + enum { + kBNYBlockSize = 128, + }; + +private: + NuError CopyData(FILE* outfp, ConvertEOL conv, ConvertHighASCII convHA, + CString* pMsg) const; + //NuError BNYUnSqueeze(ExpandBuffer* outExp) const; + + BnyArchive* fpArchive; // holds FILE* for archive + bool fIsSqueezed; + long fOffset; +}; + + +/* + * BNY archive definition. + */ +class BnyArchive : public GenericArchive { +public: + BnyArchive(void) : fIsReadOnly(false), fFp(nil) + {} + virtual ~BnyArchive(void) { (void) Close(); } + + // One-time initialization; returns an error string. + static CString AppInit(void); + + virtual OpenResult Open(const char* filename, bool readOnly, + CString* pErrMsg); + virtual CString New(const char* filename, const void* options); + virtual CString Flush(void) { return ""; } + virtual CString Reload(void); + virtual bool IsReadOnly(void) const { return fIsReadOnly; }; + virtual bool IsModified(void) const { return false; } + virtual void GetDescription(CString* pStr) const { *pStr = "Binary II"; } + virtual bool BulkAdd(ActionProgressDialog* pActionProgress, + const AddFilesDialog* pAddOpts) + { ASSERT(false); return false; } + virtual bool AddDisk(ActionProgressDialog* pActionProgress, + const AddFilesDialog* pAddOpts) + { ASSERT(false); return false; } + virtual bool CreateSubdir(CWnd* pMsgWnd, GenericEntry* pParentEntry, + const char* newName) + { ASSERT(false); return false; } + virtual bool TestSelection(CWnd* pMsgWnd, SelectionSet* pSelSet); + virtual bool DeleteSelection(CWnd* pMsgWnd, SelectionSet* pSelSet) + { ASSERT(false); return false; } + virtual bool RenameSelection(CWnd* pMsgWnd, SelectionSet* pSelSet) + { ASSERT(false); return false; } + virtual bool RenameVolume(CWnd* pMsgWnd, DiskFS* pDiskFS, + const char* newName) + { ASSERT(false); return false; } + virtual CString TestVolumeName(const DiskFS* pDiskFS, + const char* newName) const + { ASSERT(false); return "!"; } + virtual CString TestPathName(const GenericEntry* pGenericEntry, + const CString& basePath, const CString& newName, char newFssep) const + { ASSERT(false); return "!"; } + virtual bool RecompressSelection(CWnd* pMsgWnd, SelectionSet* pSelSet, + const RecompressOptionsDialog* pRecompOpts) + { ASSERT(false); return false; } + virtual XferStatus XferSelection(CWnd* pMsgWnd, SelectionSet* pSelSet, + ActionProgressDialog* pActionProgress, const XferFileOptions* pXferOpts) + { ASSERT(false); return kXferFailed; } + virtual bool GetComment(CWnd* pMsgWnd, const GenericEntry* pEntry, + CString* pStr) + { ASSERT(false); return false; } + virtual bool SetComment(CWnd* pMsgWnd, GenericEntry* pEntry, + const CString& str) + { ASSERT(false); return false; } + virtual bool DeleteComment(CWnd* pMsgWnd, GenericEntry* pEntry) + { ASSERT(false); return false; } + virtual bool SetProps(CWnd* pMsgWnd, GenericEntry* pEntry, + const FileProps* pProps) + { ASSERT(false); return false; } + virtual void PreferencesChanged(void) {} + virtual long GetCapability(Capability cap); + + friend class BnyEntry; + +private: + virtual CString Close(void) { + if (fFp != nil) { + fclose(fFp); + fFp = nil; + } + return ""; + } + virtual void XferPrepare(const XferFileOptions* pXferOpts) + { ASSERT(false); } + virtual CString XferFile(FileDetails* pDetails, unsigned char** pDataBuf, + long dataLen, unsigned char** pRsrcBuf, long rsrcLen) + { ASSERT(false); return "!"; } + virtual void XferAbort(CWnd* pMsgWnd) + { ASSERT(false); } + virtual void XferFinish(CWnd* pMsgWnd) + { ASSERT(false); } + + virtual ArchiveKind GetArchiveKind(void) { return kArchiveBNY; } + virtual NuError DoAddFile(const AddFilesDialog* pAddOpts, + FileDetails* pDetails) + { ASSERT(false); return kNuErrGeneric; } + + enum { + kBNYBlockSize = BnyEntry::kBNYBlockSize, + kBNYMaxFileName = 64, + kBNYMaxNativeName = 48, + kBNYFlagCompressed = (1<<7), + kBNYFlagEncrypted = (1<<6), + kBNYFlagSparse = (1), + }; + + typedef unsigned char uchar; + typedef unsigned short ushort; + typedef unsigned long ulong; + + /* + * An entry in a Binary II archive. Each archive is essentially a stream + * of files; only the "filesToFollow" value gives any indication that + * something else follows this entry. + * + * We read this from the archive and then unpack it into GenericEntry + * fields in a BnyEntry. + */ + struct BnyFileEntry; // VC++6 needs these to access private enums + friend struct BnyFileEntry; // in this class + typedef struct BnyFileEntry { + ushort access; + ushort fileType; + ulong auxType; + uchar storageType; + ulong fileSize; /* in 512-byte blocks */ + ushort prodosModDate; + ushort prodosModTime; + NuDateTime modWhen; /* computed from previous two fields */ + ushort prodosCreateDate; + ushort prodosCreateTime; + NuDateTime createWhen; /* computed from previous two fields */ + ulong eof; + ulong realEOF; /* eof is bogus for directories */ + char fileName[kBNYMaxFileName+1]; + char nativeName[kBNYMaxNativeName+1]; + ulong diskSpace; /* in 512-byte blocks */ + uchar osType; /* not exactly same as NuFileSysID */ + ushort nativeFileType; + uchar phantomFlag; + uchar dataFlags; /* advisory flags */ + uchar version; + uchar filesToFollow; /* #of files after this one */ + + uchar blockBuf[kBNYBlockSize]; + } BnyFileEntry; + + int LoadContents(void); + NuError LoadContentsCallback(BnyFileEntry* pEntry); + + bool IsSqueezed(uchar one, uchar two); + bool IsDir(BnyFileEntry* pEntry); + NuError BNYRead(void* buf, size_t nbyte); + NuError BNYSeek(long offset); + void BNYConvertDateTime(unsigned short prodosDate, + unsigned short prodosTime, NuDateTime* pWhen); + NuError BNYDecodeHeader(BnyFileEntry* pEntry); + NuError BNYIterate(void); + + FILE* fFp; + bool fIsReadOnly; +}; + +#endif /*__BNY_ARCHIVE__*/ \ No newline at end of file diff --git a/app/BasicImport.cpp b/app/BasicImport.cpp new file mode 100644 index 0000000..7751edc --- /dev/null +++ b/app/BasicImport.cpp @@ -0,0 +1,714 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Import BASIC programs stored in a text file. + * + * The current implementation is a bit lame. It just dumps text strings into + * a read-only edit buffer, instead of providing a nicer UI. The real trouble + * with this style of interface is that i18n is even more awkward. + */ +#include "StdAfx.h" +#include "../reformat/BASIC.h" +#include "BasicImport.h" +#include "HelpTopics.h" + +/* + * ========================================================================== + * BASTokenLookup + * ========================================================================== + */ + +/* + * Constructor. Pass in the info for the token blob. + */ +void +BASTokenLookup::Init(const char* tokenList, int numTokens, + int tokenLen) +{ + int i; + + ASSERT(tokenList != nil); + ASSERT(numTokens > 0); + ASSERT(tokenLen > 0); + + delete[] fTokenPtr; // in case we're being re-initialized + delete[] fTokenLen; + + fTokenPtr = new const char*[numTokens]; + fTokenLen = new int[numTokens]; + fNumTokens = numTokens; + + for (i = 0; i < numTokens; i++) { + fTokenPtr[i] = tokenList; + fTokenLen[i] = strlen(tokenList); + + tokenList += tokenLen; + } +} + +/* + * Return the index of the longest token that matches "str". + * + * Returns -1 if no match is found. + */ +int +BASTokenLookup::Lookup(const char* str, int len, int* pFoundLen) +{ + int longestIndex, longestLen; + int i; + + longestIndex = longestLen = -1; + for (i = 0; i < fNumTokens; i++) { + if (fTokenLen[i] <= len && fTokenLen[i] > longestLen && + strncasecmp(str, fTokenPtr[i], fTokenLen[i]) == 0) + { + longestIndex = i; + longestLen = fTokenLen[i]; + } + } + + *pFoundLen = longestLen; + return longestIndex; +} + + +/* + * ========================================================================== + * ImportBASDialog + * ========================================================================== + */ + +BEGIN_MESSAGE_MAP(ImportBASDialog, CDialog) + ON_COMMAND(IDHELP, OnHelp) +END_MESSAGE_MAP() + + +/* + * Set up the dialog. + */ +BOOL +ImportBASDialog::OnInitDialog(void) +{ + CDialog::OnInitDialog(); // base class init + + PathName path(fFileName); + CString fileNameOnly(path.GetFileName()); + CString ext(fileNameOnly.Right(4)); + if (ext.CompareNoCase(".txt") == 0) { + WMSG1("removing extension from '%s'\n", (const char*) fileNameOnly); + fileNameOnly = fileNameOnly.Left(fileNameOnly.GetLength() - 4); + } + + CEdit* pEdit = (CEdit*) GetDlgItem(IDC_IMPORT_BAS_SAVEAS); + pEdit->SetWindowText(fileNameOnly); + pEdit->SetSel(0, -1); + pEdit->SetFocus(); + + /* + * Do the actual import. If it fails, disable the "save" button. + */ + if (!ImportBAS(fFileName)) { + CButton* pButton = (CButton*) GetDlgItem(IDOK); + pButton->EnableWindow(FALSE); + pEdit->EnableWindow(FALSE); + } + + return FALSE; // keep our focus +} + +static const char* kFailed = "failed.\r\n\r\n"; +static const char* kSuccess = "success!\r\n\r\n"; + +/* + * Import an Applesoft BASIC program from the specified file. + */ +bool +ImportBASDialog::ImportBAS(const char* fileName) +{ + FILE* fp = NULL; + ExpandBuffer msgs(1024); + long fileLen, outLen, count; + char* buf = nil; + char* outBuf = nil; + bool result = false; + + msgs.Printf("Importing from '%s'...", fileName); + fp = fopen(fileName, "rb"); // EOL unknown, open as binary and deal + if (fp == NULL) { + msgs.Printf("%sUnable to open file.", kFailed); + goto bail; + } + + /* determine file length, and verify that it looks okay */ + fseek(fp, 0, SEEK_END); + fileLen = ftell(fp); + rewind(fp); + if (ferror(fp) || fileLen < 0) { + msgs.Printf("%sUnable to determine file length.", kFailed); + goto bail; + } + if (fileLen == 0) { + msgs.Printf("%sFile is empty.", kFailed); + goto bail; + } + if (fileLen >= 128*1024) { + msgs.Printf("%sFile is too large to be Applesoft.", kFailed); + goto bail; + } + + buf = new char[fileLen]; + if (buf == NULL) { + msgs.Printf("%sUnable to allocate memory.", kFailed); + goto bail; + } + + /* read the entire thing into memory */ + count = fread(buf, 1, fileLen, fp); + if (count != fileLen) { + msgs.Printf("%sCould only read %ld of %ld bytes.", kFailed, + count, fileLen); + goto bail; + } + + /* process it */ + if (!ConvertTextToBAS(buf, fileLen, &outBuf, &outLen, &msgs)) + goto bail; + + result = true; + SetOutput(outBuf, outLen); + +bail: + if (fp != NULL) + fclose(fp); + delete[] buf; + + /* copy our error messages out */ + CEdit* pEdit = (CEdit*) GetDlgItem(IDC_IMPORT_BAS_RESULTS); + char* msgBuf = nil; + long msgLen; + msgs.SeizeBuffer(&msgBuf, &msgLen); + pEdit->SetWindowText(msgBuf); + delete[] msgBuf; + + return result; +} + +/* + * Do the actual conversion. + */ +bool +ImportBASDialog::ConvertTextToBAS(const char* buf, long fileLen, + char** pOutBuf, long* pOutLen, ExpandBuffer* pMsgs) +{ + ExpandBuffer output(32768); + CString msg; + const char* lineStart; + const char* lineEnd; + long textRemaining; + int lineNum; + + fBASLookup.Init(ReformatApplesoft::GetApplesoftTokens(), + ReformatApplesoft::kTokenCount, ReformatApplesoft::kTokenLen); + + lineEnd = buf; + textRemaining = fileLen; + lineNum = 0; + + while (textRemaining > 0) { + lineNum++; + lineStart = lineEnd; + lineEnd = FindEOL(lineStart, textRemaining); + + if (!ProcessBASLine(lineStart, lineEnd - lineStart, &output, + /*ref*/ msg)) + { + pMsgs->Printf("%sLine %d: %s", kFailed, lineNum, (const char*) msg); + return false; + } + + textRemaining -= lineEnd - lineStart; + } + + /* output EOF marker */ + output.Putc(0x00); + output.Putc(0x00); + + /* grab the buffer */ + char* outBuf; + long outLen; + output.SeizeBuffer(&outBuf, &outLen); + + if (outLen >= 0xc000) { + pMsgs->Printf("%sOutput is too large to be valid", kFailed); + delete[] outBuf; + return false; + } + + /* go back and fix up the "next line" pointers, assuming a $0801 start */ + if (!FixBASLinePointers(outBuf, outLen, 0x0801)) { + pMsgs->Printf("%sFailed while fixing line pointers", kFailed); + delete[] outBuf; + return false; + } + + *pOutBuf = outBuf; + *pOutLen = outLen; + pMsgs->Printf("%sProcessed %d lines", kSuccess, lineNum); + pMsgs->Printf("\r\nTokenized file is %d bytes long", *pOutLen); + + return true; +} + +/* + * Process a line of Applesoft BASIC text. + * + * Writes output to "pOutput". + * + * On failure, writes an error message to "msg" and returns false. + */ +/* +From an Applesoft disassembly by Bob Sander-Cederlof: + +D56C- E8 1420 PARSE INX NEXT INPUT CHARACTER +D56D- BD 00 02 1430 .1 LDA INPUT.BUFFER,X +D570- 24 13 1440 BIT DATAFLG IN A "DATA" STATEMENT? +D572- 70 04 1450 BVS .2 YES (DATAFLG = $49) +D574- C9 20 1460 CMP #' ' IGNORE BLANKS +D576- F0 F4 1470 BEQ PARSE +D578- 85 0E 1480 .2 STA ENDCHR +D57A- C9 22 1490 CMP #'" START OF QUOTATION? +D57C- F0 74 1500 BEQ .13 +D57E- 70 4D 1510 BVS .9 BRANCH IF IN "DATA" STATEMENT +D580- C9 3F 1520 CMP #'? SHORTHAND FOR "PRINT"? +D582- D0 04 1530 BNE .3 NO +D584- A9 BA 1540 LDA #TOKEN.PRINT YES, REPLACE WITH "PRINT" TOKEN +D586- D0 45 1550 BNE .9 ...ALWAYS +D588- C9 30 1560 .3 CMP #'0 IS IT A DIGIT, COLON, OR SEMI-COLON? +D58A- 90 04 1570 BCC .4 NO, PUNCTUATION !"#$%&'()*+,-./ +D58C- C9 3C 1580 CMP #';'+1 +D58E- 90 3D 1590 BCC .9 YES, NOT A TOKEN + 1600 *-------------------------------- + 1610 * SEARCH TOKEN NAME TABLE FOR MATCH STARTING + 1620 * WITH CURRENT CHAR FROM INPUT LINE + 1630 *-------------------------------- +D590- 84 AD 1640 .4 STY STRNG2 SAVE INDEX TO OUTPUT LINE +D592- A9 D0 1650 LDA #TOKEN.NAME.TABLE-$100 +D594- 85 9D 1660 STA FAC MAKE PNTR FOR SEARCH +D596- A9 CF 1670 LDA /TOKEN.NAME.TABLE-$100 +D598- 85 9E 1680 STA FAC+1 +D59A- A0 00 1690 LDY #0 USE Y-REG WITH (FAC) TO ADDRESS TABLE +D59C- 84 0F 1700 STY TKN.CNTR HOLDS CURRENT TOKEN-$80 +D59E- 88 1710 DEY PREPARE FOR "INY" A FEW LINES DOWN +D59F- 86 B8 1720 STX TXTPTR SAVE POSITION IN INPUT LINE +D5A1- CA 1730 DEX PREPARE FOR "INX" A FEW LINES DOWN +D5A2- C8 1740 .5 INY ADVANCE POINTER TO TOKEN TABLE +D5A3- D0 02 1750 BNE .6 Y=Y+1 IS ENOUGH +D5A5- E6 9E 1760 INC FAC+1 ALSO NEED TO BUMP THE PAGE +D5A7- E8 1770 .6 INX ADVANCE POINTER TO INPUT LINE +D5A8- BD 00 02 1780 .7 LDA INPUT.BUFFER,X NEXT CHAR FROM INPUT LINE +D5AB- C9 20 1790 CMP #' ' THIS CHAR A BLANK? +D5AD- F0 F8 1800 BEQ .6 YES, IGNORE ALL BLANKS +D5AF- 38 1810 SEC NO, COMPARE TO CHAR IN TABLE +D5B0- F1 9D 1820 SBC (FAC),Y SAME AS NEXT CHAR OF TOKEN NAME? +D5B2- F0 EE 1830 BEQ .5 YES, CONTINUE MATCHING +D5B4- C9 80 1840 CMP #$80 MAYBE; WAS IT SAME EXCEPT FOR BIT 7? +D5B6- D0 41 1850 BNE .14 NO, SKIP TO NEXT TOKEN +D5B8- 05 0F 1860 ORA TKN.CNTR YES, END OF TOKEN; GET TOKEN # +D5BA- C9 C5 1870 CMP #TOKEN.AT DID WE MATCH "AT"? +D5BC- D0 0D 1880 BNE .8 NO, SO NO AMBIGUITY +D5BE- BD 01 02 1890 LDA INPUT.BUFFER+1,X "AT" COULD BE "ATN" OR "A TO" +D5C1- C9 4E 1900 CMP #'N "ATN" HAS PRECEDENCE OVER "AT" +D5C3- F0 34 1910 BEQ .14 IT IS "ATN", FIND IT THE HARD WAY +D5C5- C9 4F 1920 CMP #'O "TO" HAS PRECEDENCE OVER "AT" +D5C7- F0 30 1930 BEQ .14 IT IS "A TO", FIN IT THE HARD WAY +D5C9- A9 C5 1940 LDA #TOKEN.AT NOT "ATN" OR "A TO", SO USE "AT" + 1950 *-------------------------------- + 1960 * STORE CHARACTER OR TOKEN IN OUTPUT LINE + 1970 *-------------------------------- + +Note the special handling for "AT" and "TO". When it examines the next +character, it does NOT skip whitespace, making spaces significant when +differentiating between "at n"/"atn" and "at o"/"ato". +*/ +bool +ImportBASDialog::ProcessBASLine(const char* buf, int len, + ExpandBuffer* pOutput, CString& msg) +{ + const int kMaxTokenLen = 7; // longest token; must also hold linenum + const int kTokenAT = 0xc5 - 128; + const int kTokenATN = 0xe1 - 128; + char tokenBuf[kMaxTokenLen+1]; + bool gotOne = false; + bool haveLineNum = false; + char ch; + int tokenLen; + int lineNum; + int foundToken; + + if (!len) + return false; + + /* + * Remove the CR, LF, or CRLF from the end of the line. + */ + if (len > 1 && buf[len-2] == '\r' && buf[len-1] == '\n') { + //WMSG0("removed CRLF\n"); + len -= 2; + } else if (buf[len-1] == '\r') { + //WMSG0("removed CR\n"); + len--; + } else if (buf[len-1] == '\n') { + //WMSG0("removed LF\n"); + len--; + } else { + //WMSG0("no EOL marker found\n"); + } + + if (!len) + return true; // blank lines are okay + + /* + * Extract the line number. + */ + tokenLen = 0; + while (len > 0) { + if (!GetNextNWC(&buf, &len, &ch)) { + if (!gotOne) + return true; // blank lines with whitespace are okay + else { + // end of line reached while scanning line number is bad + msg = "found nothing except line number"; + return false; + } + } + gotOne = true; + + if (!isdigit(ch)) + break; + if (tokenLen == 5) { // theoretical max is "65535" + msg = "line number has too many digits"; + return false; + } + tokenBuf[tokenLen++] = ch; + } + + if (!tokenLen) { + msg = "line did not start with a line number"; + return false; + } + tokenBuf[tokenLen] = '\0'; + lineNum = atoi(tokenBuf); + WMSG1("FOUND line %d\n", lineNum); + + pOutput->Putc((char) 0xcc); // placeholder + pOutput->Putc((char) 0xcc); + pOutput->Putc(lineNum & 0xff); + pOutput->Putc((lineNum >> 8) & 0xff); + + /* + * Start scanning tokens. + * + * We need to find the longest matching token (i.e. prefer "ONERR" over + * "ON"). Grab a bunch of characters, ignoring whitespace, and scan + * for a match. + */ + buf--; // back up + len++; + foundToken = -1; + + while (len > 0) { + const char* dummy = buf; + int remaining = len; + + /* load up the buffer */ + for (tokenLen = 0; tokenLen < kMaxTokenLen; tokenLen++) { + if (!GetNextNWC(&dummy, &remaining, &ch)) + break; + if (ch == '"') + break; + tokenBuf[tokenLen] = ch; + } + + if (tokenLen == 0) { + if (ch == '"') { + /* + * Note it's possible for strings to be unterminated. This + * will go unnoticed by Applesoft if it's at the end of a + * line. + */ + GetNextNWC(&buf, &len, &ch); + pOutput->Putc(ch); + while (len--) { + ch = *buf++; + pOutput->Putc(ch); + if (ch == '"') + break; + } + } else { + /* end of line reached */ + break; + } + } else { + int token, foundLen; + + token = fBASLookup.Lookup(tokenBuf, tokenLen, &foundLen); + if (token >= 0) { + /* match! */ + if (token == kTokenAT || token == kTokenATN) { + /* have to go back and re-scan original */ + const char* tp = buf +1; + while (toupper(*tp++) != 'T') + ; + if (toupper(*tp) == 'N') { + /* keep this token */ + assert(token == kTokenATN); + } else if (toupper(*tp) == 'O') { + /* eat and emit the 'A' so we get the "TO" instead */ + goto output_single; + } else { + if (token == kTokenATN) { + /* reduce to "AT" */ + token = kTokenAT; + foundLen--; + } + } + } + pOutput->Putc(token + 128); + + /* consume token chars, including whitespace */ + for (int j = 0; j < foundLen; j++) + GetNextNWC(&buf, &len, &ch); + + //WMSG2("TOKEN '%s' (%d)\n", + // fBASLookup.GetToken(token), tokenLen); + + /* special handling for REM or DATA */ + if (token == 0xb2 - 128) { + /* for a REM statement, copy verbatim to end of line */ + if (*buf == ' ') { + /* eat one leading space, if present */ + buf++; + len--; + } + while (len--) { + ch = *buf++; + pOutput->Putc(ch); + } + } else if (token == 0x83 - 128) { + bool inQuote = false; + + /* for a DATA statement, copy until ':' */ + if (*buf == ' ') { + /* eat one leading space */ + buf++; + len--; + } + while (len--) { + ch = *buf++; + if (ch == '"') // ignore ':' in quoted strings + inQuote = !inQuote; + + if (!inQuote && ch == ':') { + len++; + buf--; + break; + } + pOutput->Putc(ch); + } + } + } else { + /* + * Not a quote, and no token begins with this character. + * Output it and advance. + */ +output_single: + GetNextNWC(&buf, &len, &ch); + pOutput->Putc(toupper(ch)); + } + } + } + + pOutput->Putc('\0'); + + return true; +} + +/* + * Fix up the line pointers. We left dummy nonzero values in them initially. + */ +bool +ImportBASDialog::FixBASLinePointers(char* buf, long len, unsigned short addr) +{ + unsigned short val; + char* start; + + while (len >= 4) { + start = buf; + val = (*buf) & 0xff | (*(buf+1)) << 8; + + if (val == 0) + break; + if (val != 0xcccc) { + WMSG1("unexpected value 0x%04x found\n", val); + return false; + } + + buf += 4; + len -= 4; + + /* + * Find the next end-of-line marker. + */ + while (*buf != '\0' && len > 0) { + buf++; + len--; + } + if (!len) { + WMSG0("ran off the end?\n"); + return false; + } + buf++; + len--; + + /* + * Set the value. + */ + val = (unsigned short) (buf - start); + ASSERT((int) val == buf - start); + addr += val; + + *start = addr & 0xff; + *(start+1) = (addr >> 8) & 0xff; + } + + return true; +} + +/* + * Look for the end of line. + * + * Returns a pointer to the first byte *past* the EOL marker, which will point + * at unallocated space for last line in the buffer. + */ +const char* +ImportBASDialog::FindEOL(const char* buf, long max) +{ + ASSERT(max >= 0); + if (max == 0) + return nil; + + while (max) { + if (*buf == '\r' || *buf == '\n') { + if (*buf == '\r' && max > 0 && *(buf+1) == '\n') + return buf+2; + return buf+1; + } + + buf++; + max--; + } + + /* + * Looks like the last line didn't have an EOL. That's okay. + */ + return buf; +} + +/* + * Find the next non-whitespace character. + * + * Updates the buffer pointer and length. + * + * Returns "false" if we run off the end without finding another non-ws char. + */ +bool +ImportBASDialog::GetNextNWC(const char** pBuf, int* pLen, char* pCh) +{ + static const char* kWhitespace = " \t\r\n"; + + while (*pLen > 0) { + const char* ptr; + char ch; + + ch = **pBuf; + ptr = strchr(kWhitespace, ch); + (*pBuf)++; + (*pLen)--; + + if (ptr == nil) { + *pCh = ch; + return true; + } + } + + return false; +} + + +/* + * Save the imported data. + */ +void ImportBASDialog::OnOK(void) +{ + CEdit* pEdit = (CEdit*) GetDlgItem(IDC_IMPORT_BAS_SAVEAS); + CString fileName; + + pEdit->GetWindowText(fileName); + if (fileName.IsEmpty()) { + CString appName; + appName.LoadString(IDS_MB_APP_NAME); + + MessageBox("You must specify a filename.", + appName, MB_OK); + } + + /* + * Write the file to the currently-open archive. + */ + GenericArchive::FileDetails details; + + details.entryKind = GenericArchive::FileDetails::kFileKindDataFork; + details.origName = "Imported BASIC"; + details.storageName = fileName; + details.access = 0xe3; // unlocked, backup bit set + details.fileType = kFileTypeBAS; + details.extraType = 0x0801; + details.storageType = DiskFS::kStorageSeedling; + time_t now = time(nil); + GenericArchive::UNIXTimeToDateTime(&now, &details.createWhen); + GenericArchive::UNIXTimeToDateTime(&now, &details.archiveWhen); + GenericArchive::UNIXTimeToDateTime(&now, &details.modWhen); + + CString errMsg; + + fDirty = true; + if (!MainWindow::SaveToArchive(&details, (const unsigned char*) fOutput, + fOutputLen, nil, -1, /*ref*/errMsg, this)) + { + goto bail; + } + + /* success! close the dialog */ + CDialog::OnOK(); + +bail: + if (!errMsg.IsEmpty()) { + CString msg; + msg.Format("Unable to import file: %s.", (const char *) errMsg); + ShowFailureMsg(this, msg, IDS_FAILED); + return; + } + return; +} + +/* + * User pressed the "Help" button. + */ +void +ImportBASDialog::OnHelp(void) +{ + WinHelp(HELP_TOPIC_IMPORT_BASIC, HELP_CONTEXT); +} diff --git a/app/BasicImport.h b/app/BasicImport.h new file mode 100644 index 0000000..5c5b639 --- /dev/null +++ b/app/BasicImport.h @@ -0,0 +1,104 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Import BASIC programs from text files. + * + * THOUGHT: change the way the dialog works so that it doesn't scan until + * you say "go". Have some options for selecting language (BAS vs. INT), + * and whether to try to identify listings with line breaks (i.e. they + * neglected to "poke 33,33"). Have an optional "check syntax" box if we + * want to get really fancy. + */ +#ifndef __BASICIMPORT__ +#define __BASICIMPORT__ + +/* + * This is a helper class to scan for a token in the list. + * + * Ideally we'd create a hash table to make it faster, but that's probably + * not necessary for the small data sets we're working with. + */ +class BASTokenLookup { +public: + BASTokenLookup(void) + : fTokenPtr(nil), fTokenLen(nil) + {} + ~BASTokenLookup(void) { + delete[] fTokenPtr; + delete[] fTokenLen; + } + + // Initialize the array. + void Init(const char* tokenList, int numTokens, int tokenLen); + + // Return the index of the matching token, or -1 if none found. + int Lookup(const char* str, int len, int* pFoundLen); + + // Return a printable string. + const char* GetToken(int idx) { + return fTokenPtr[idx]; + } + +private: + int fNumTokens; + const char** fTokenPtr; + int* fTokenLen; +}; + + +/* + * Import a BASIC program. + * + * Currently works for Applesoft. Might work for Integer someday. + */ +class ImportBASDialog : public CDialog { +public: + ImportBASDialog(CWnd* pParentWnd = NULL) : + CDialog(IDD_IMPORT_BAS, pParentWnd), fDirty(false), + fOutput(nil), fOutputLen(-1) + {} + virtual ~ImportBASDialog(void) { + delete[] fOutput; + } + + CString fFileName; // file to open + + // did we add something to the archive? + bool IsDirty(void) const { return fDirty; } + +private: + virtual BOOL OnInitDialog(void); + //virtual void DoDataExchange(CDataExchange* pDX); + virtual void OnOK(void); + + afx_msg void OnHelp(void); + + bool ImportBAS(const char* fileName); + bool ConvertTextToBAS(const char* buf, long fileLen, + char** pOutBuf, long* pOutLen, ExpandBuffer* pMsgs); + bool ProcessBASLine(const char* buf, int len, + ExpandBuffer* pOutput, CString& msg); + bool FixBASLinePointers(char* buf, long len, unsigned short addr); + + const char* FindEOL(const char* buf, long max); + bool GetNextNWC(const char** pBuf, int* pLen, char* pCh); + + void SetOutput(char* outBuf, long outLen) { + delete[] fOutput; + fOutput = outBuf; + fOutputLen = outLen; + } + + BASTokenLookup fBASLookup; + bool fDirty; + + char* fOutput; + long fOutputLen; + + DECLARE_MESSAGE_MAP() +}; + +#endif /*__BASICIMPORT__*/ \ No newline at end of file diff --git a/app/CassImpTargetDialog.cpp b/app/CassImpTargetDialog.cpp new file mode 100644 index 0000000..45e57d7 --- /dev/null +++ b/app/CassImpTargetDialog.cpp @@ -0,0 +1,184 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Choose file name and characteristics for a file imported from an audio + * cassette tape. + */ +#include "StdAfx.h" +#include "CassImpTargetDialog.h" +#include "GenericArchive.h" // just want kFileTypeXXX + +BEGIN_MESSAGE_MAP(CassImpTargetDialog, CDialog) + ON_BN_CLICKED(IDC_CASSIMPTARG_BAS, OnTypeChange) + ON_BN_CLICKED(IDC_CASSIMPTARG_INT, OnTypeChange) + ON_BN_CLICKED(IDC_CASSIMPTARG_BIN, OnTypeChange) + ON_EN_CHANGE(IDC_CASSIMPTARG_BINADDR, OnAddrChange) +END_MESSAGE_MAP() + +/* + * Set up the dialog. + */ +BOOL +CassImpTargetDialog::OnInitDialog(void) +{ + /* substitute our replacement edit control */ + fAddrEdit.ReplaceDlgCtrl(this, IDC_CASSIMPTARG_BINADDR); + fAddrEdit.SetProperties(MyEdit::kCapsOnly | MyEdit::kHexOnly); + + //CWnd* pWnd; + CEdit* pEdit; + + pEdit = (CEdit*) GetDlgItem(IDC_CASSIMPTARG_BINADDR); + pEdit->SetLimitText(4); // 4-digit hex value + + /* do the DDX thing, then update computed fields */ + CDialog::OnInitDialog(); + OnTypeChange(); + OnAddrChange(); + + pEdit = (CEdit*) GetDlgItem(IDC_CASSIMPTARG_FILENAME); + pEdit->SetSel(0, -1); + pEdit->SetFocus(); + return FALSE; // don't change the focus +} + +/* + * Copy values in and out of the dialog. + */ +void +CassImpTargetDialog::DoDataExchange(CDataExchange* pDX) +{ + DDX_Radio(pDX, IDC_CASSIMPTARG_BAS, fFileTypeIndex); + DDX_Text(pDX, IDC_CASSIMPTARG_FILENAME, fFileName); + + if (pDX->m_bSaveAndValidate) { + CString appName; + appName.LoadString(IDS_MB_APP_NAME); + + if (fFileTypeIndex == kTypeBIN) { + if (GetStartAddr() < 0) { + MessageBox("The address field must be a valid 4-digit " + " hexadecimal number.", + appName, MB_OK); + pDX->Fail(); + return; + } + fStartAddr = (unsigned short) GetStartAddr(); + } + if (fFileName.IsEmpty()) { + MessageBox("You must enter a filename.", appName, MB_OK); + pDX->Fail(); + return; + } + } else { + CWnd* pWnd; + CString tmpStr; + + pWnd = GetDlgItem(IDC_CASSIMPTARG_BINADDR); + tmpStr.Format("%04X", fStartAddr); + pWnd->SetWindowText(tmpStr); + } +} + +/* + * They selected a different file type. Enable or disable the address + * entry window. + */ +void +CassImpTargetDialog::OnTypeChange(void) +{ + CButton* pButton; + CWnd* pWnd; + + pButton = (CButton*) GetDlgItem(IDC_CASSIMPTARG_BIN); + pWnd = GetDlgItem(IDC_CASSIMPTARG_BINADDR); + + pWnd->EnableWindow(pButton->GetCheck() == BST_CHECKED); +} + +/* + * If the user changes the address, update the "end of range" field. + */ +void +CassImpTargetDialog::OnAddrChange(void) +{ + CWnd* pWnd; + CString tmpStr; + long val; + + val = GetStartAddr(); + if (val < 0) + val = 0; + + tmpStr.Format(".%04X", val + fFileLength-1); + + pWnd = GetDlgItem(IDC_CASSIMPTARG_RANGE); + pWnd->SetWindowText(tmpStr); +} + +/* + * Get the start address (entered as a 4-digit hex value). + * + * Returns -1 if something was wrong with the string (e.g. empty or has + * invalid chars). + */ +long +CassImpTargetDialog::GetStartAddr(void) const +{ + CWnd* pWnd = GetDlgItem(IDC_CASSIMPTARG_BINADDR); + ASSERT(pWnd != nil); + + CString aux; + pWnd->GetWindowText(aux); + + const char* str = aux; + char* end; + long val; + + if (str[0] == '\0') { + WMSG0(" HEY: blank addr, returning -1\n"); + return -1; + } + val = strtoul(aux, &end, 16); + if (end != str + strlen(str)) { + WMSG1(" HEY: found some garbage in addr '%s', returning -1\n", + (LPCTSTR) aux); + return -1; + } + return val; +} + +/* + * Get the selected file type. Call this after the modal dialog exits. + */ +long +CassImpTargetDialog::GetFileType(void) const +{ + switch (fFileTypeIndex) { + case kTypeBIN: return kFileTypeBIN; + case kTypeINT: return kFileTypeINT; + case kTypeBAS: return kFileTypeBAS; + default: + assert(false); + return -1; + } +} + +/* + * Convert a ProDOS file type into a radio button enum. + */ +void +CassImpTargetDialog::SetFileType(long type) +{ + switch (type) { + case kFileTypeBIN: fFileTypeIndex = kTypeBIN; break; + case kFileTypeINT: fFileTypeIndex = kTypeINT; break; + case kFileTypeBAS: fFileTypeIndex = kTypeBAS; break; + default: + assert(false); + break; + } +} diff --git a/app/CassImpTargetDialog.h b/app/CassImpTargetDialog.h new file mode 100644 index 0000000..262a873 --- /dev/null +++ b/app/CassImpTargetDialog.h @@ -0,0 +1,52 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Choose file name and characteristics for a file imported from an audio + * cassette tape. + */ +#ifndef __CASSIMPTARGETDIALOG__ +#define __CASSIMPTARGETDIALOG__ + +#include "resource.h" + +/* + * Get a filename, allow them to override the file type, and get a hexadecimal + * start address for binary files. + */ +class CassImpTargetDialog : public CDialog { +public: + CassImpTargetDialog(CWnd* pParentWnd = NULL) : + CDialog(IDD_CASSIMPTARGET, pParentWnd), + fStartAddr(0x0800), + fFileTypeIndex(0) + {} + virtual ~CassImpTargetDialog(void) {} + + long GetFileType(void) const; + void SetFileType(long type); + + CString fFileName; + unsigned short fStartAddr; // start addr for BIN files + long fFileLength; // used for BIN display + +private: + virtual BOOL OnInitDialog(void); + virtual void DoDataExchange(CDataExchange* pDX); + afx_msg void OnTypeChange(void); + afx_msg void OnAddrChange(void); + + MyEdit fAddrEdit; // replacement edit ctrl for addr field + + long GetStartAddr(void) const; + + /* for radio button; enum must match order of controls in dialog */ + enum { kTypeBAS = 0, kTypeINT, kTypeBIN }; + int fFileTypeIndex; + + DECLARE_MESSAGE_MAP() +}; + +#endif /*__CASSIMPTARGETDIALOG__*/ diff --git a/app/CassetteDialog.cpp b/app/CassetteDialog.cpp new file mode 100644 index 0000000..6ce5f3c --- /dev/null +++ b/app/CassetteDialog.cpp @@ -0,0 +1,1192 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Apple II cassette I/O functions. + */ +#include "StdAfx.h" +#include "CassetteDialog.h" +#include "CassImpTargetDialog.h" +#include "HelpTopics.h" +#include "GenericArchive.h" +#include "Main.h" +#include "../diskimg/DiskImg.h" // need kStorageSeedling +#include + +/* + * Tape layout: + * 10.6 seconds of 770Hz (8192 cycles * 1300 usec/cycle) + * 1/2 cycle at 400 usec/cycle, followed by 1/2 cycle at 500 usec/cycle + * Data, using 500 usec/cycle for '0' and 1000 usec/cycle for '1' + * There is no "end" marker, except perhaps for the absence of data + * + * The last byte of data is an XOR checksum (seeded with 0xff). + * + * BASIC uses two sections, each with the full 10-second lead-in and a + * checksum byte). Integer BASIC writes a two-byte section with the length + * of the program, while Applesoft BASIC writes a three-byte section with + * the length followed by a one-byte "run" flag (seen: 0x55 and 0xd5). + * + * Applesoft arrays, loaded with "RECALL", have a three-byte header, and + * may be confused with BASIC programs. Shape tables, loaded with "SHLOAD", + * have a two-byte header and may be confused with Integer programs. + * + * The monitor ROM routine uses a detection threshold of 700 usec to tell + * the difference between 0s and 1s. When reading, it *outputs* a tone for + * 3.5 seconds before listening. It doesn't try to detect the 770Hz tone, + * just waits for something under (40*12=)440 usec. + * + * The Apple II hardware changes the high bit read from $c060 every time it + * detects a zero-crossing on the cassette input. I assume the polarity + * of the input signal is reflected by the polarity of the high bit, but + * I'm not sure, and in the end it doesn't really matter. + * + * Typical instructions for loading data from tape look like this: + * - Type "LOAD" or "xxxx.xxxxR", but don't hit . + * - Play tape until you here the tone. + * - Immediately hit stop. + * - Plug the cable from the Apple II into the tape player. + * - Hit "play" on the recorder, then immediately hit . + * - When the Apple II beeps, it's done. Stop the tape. + * + * How quickly do we need to sample? The highest frequency we expect to + * find is 2KHz, so anything over 4KHz should be sufficient. However, we + * need to be able to resolve the time between zero transitions to some + * reasonable resolution. We need to tell the difference between a 650usec + * half-cycle and a 200usec half-cycle for the start, and 250/500usec for + * the data section. Our measurements can comfortably be off by 200 usec + * with no ill effects on the lead-in, assuming a perfect signal. (Sampling + * every 200 usec would be 5Hz.) The data itself needs to be +/- 125usec + * for half-cycles, though we can get a little sloppier if we average the + * error out by combining half-cycles. + * + * The signal is less than perfect, sometimes far less, so we need better + * sampling to avoid magnifying distortions in the signal. If we sample + * at 22.05KHz, we could see a 650usec gap as 590, 635, or 680, depending + * on when we sample and where we think the peaks lie. We're off by 15usec + * before we even start. We can reasonably expect to be off +/- twice the + * "usecPerSample" value. At 8KHz, that's +/- 250usec, which isn't + * acceptable. At 11KHz we're at +/- 191usec, which is scraping along. + * + * We can get mitigate some problems by doing an interpolation of the + * two points nearest the zero-crossing, which should give us a more + * accurate fix on the zero point than simply choosing the closest point. + * This does potentially increase our risk of errors due to noise spikes at + * points near the zero. Since we're reading from cassette, any noise spikes + * are likely to be pretty wide, so averaging the data or interpolating + * across multiple points isn't likely to help us. + * + * Some tapes seem to have a low-frequency distortion that amounts to a DC + * bias when examining a single sample. Timing the gaps between zero + * crossings is therefore not sufficient unless we also correct for the + * local DC bias. In some cases the recorder or media was unable to + * respond quickly enough, and as a result 0s have less amplitude + * than 1s. This throws off some simple correction schemes. + * + * The easiest approach is to figure out where one cycle starts and stops, and + * use the timing of the full cycle. This gets a little ugly because the + * original output was a square wave, so there's a bit of ringing in the + * peaks, especially the 1s. Of course, we have to look at half-cycles + * initially, because we need to identify the first "short 0" part. Once + * we have that, we can use full cycles, which distributes any error over + * a larger set of samples. + * + * In some cases the positive half-cycle is longer than the negative + * half-cycle (e.g. reliably 33 samples vs. 29 samples at 48KHz, when + * 31.2 is expected for 650us). Slight variations can lead to even + * greater distortion, even though the timing for the full signal is + * within tolerances. This means we need to accumulate the timing for + * a full cycle before making an evaluation, though we still need to + * examine the half-cycle timing during the lead-in to catch the "short 0". + * + * Because of these distortions, 8-bit 8KHz audio is probably not a good + * idea. 16-bit 22.05KHz sampling is a better choice for tapes that have + * been sitting around for 25-30 years. + */ +/* +; Monitor ROM dump, with memory locations rearranged for easier reading. + +; Increment 16-bit value at 0x3c (A1) and compare it to 16-bit value at +; 0x3e (A2). Returns with carry set if A1 >= A2. +; Requires 26 cycles in common case, 30 cycles in rare case. +FCBA: A5 3C 709 NXTA1 LDA A1L ;INCR 2-BYTE A1. +FCBC: C5 3E 710 CMP A2L +FCBE: A5 3D 711 LDA A1H ; AND COMPARE TO A2 +FCC0: E5 3F 712 SBC A2H +FCC2: E6 3C 713 INC A1L ; (CARRY SET IF >=) +FCC4: D0 02 714 BNE RTS4B +FCC6: E6 3D 715 INC A1H +FCC8: 60 716 RTS4B RTS + +; Write data from location in A1L up to location in A2L. +FECD: A9 40 975 WRITE LDA #$40 +FECF: 20 C9 FC 976 JSR HEADR ;WRITE 10-SEC HEADER +; Write loop. Continue until A1 reaches A2. +FED2: A0 27 977 LDY #$27 +FED4: A2 00 978 WR1 LDX #$00 +FED6: 41 3C 979 EOR (A1L,X) +FED8: 48 980 PHA +FED9: A1 3C 981 LDA (A1L,X) +FEDB: 20 ED FE 982 JSR WRBYTE +FEDE: 20 BA FC 983 JSR NXTA1 +FEE1: A0 1D 984 LDY #$1D +FEE3: 68 985 PLA +FEE4: 90 EE 986 BCC WR1 +; Write checksum byte, then beep the speaker. +FEE6: A0 22 987 LDY #$22 +FEE8: 20 ED FE 988 JSR WRBYTE +FEEB: F0 4D 989 BEQ BELL + +; Write one byte (8 bits, or 16 half-cycles). +; On exit, Z-flag is set. +FEED: A2 10 990 WRBYTE LDX #$10 +FEEF: 0A 991 WRBYT2 ASL +FEF0: 20 D6 FC 992 JSR WRBIT +FEF3: D0 FA 993 BNE WRBYT2 +FEF5: 60 994 RTS + +; Write tape header. Called by WRITE with A=$40, READ with A=$16. +; On exit, A holds $FF. +; First time through, X is undefined, so we may get slightly less than +; A*256 half-cycles (i.e. A*255 + X). If the carry is clear on entry, +; the first ADC will subtract two (yielding A*254+X), and the first X +; cycles will be "long 0s" instead of "long 1s". Doesn't really matter. +FCC9: A0 4B 717 HEADR LDY #$4B ;WRITE A*256 'LONG 1' +FCCB: 20 DB FC 718 JSR ZERDLY ; HALF CYCLES +FCCE: D0 F9 719 BNE HEADR ; (650 USEC EACH) +FCD0: 69 FE 720 ADC #$FE +FCD2: B0 F5 721 BCS HEADR ;THEN A 'SHORT 0' +; Fall through to write bit. Note carry is clear, so we'll use the zero +; delay. We've initialized Y to $21 instead of $32 to get a short '0' +; (165usec) for the first half and a normal '0' for the second half; +FCD4: A0 21 722 LDY #$21 ; (400 USEC) +; Write one bit. Called from WRITE with Y=$27. +FCD6: 20 DB FC 723 WRBIT JSR ZERDLY ;WRITE TWO HALF CYCLES +FCD9: C8 724 INY ; OF 250 USEC ('0') +FCDA: C8 725 INY ; OR 500 USEC ('0') +; Delay for '0'. X typically holds a bit count or half-cycle count. +; Y holds delay period in 5-usec increments: +; (carry clear) $21=165us $27=195us $2C=220 $4B=375us +; (carry set) $21=165+250=415us $27=195+250=445us $4B=375+250=625us +; Remember that TOTAL delay, with all other instructions, must equal target +; On exit, Y=$2C, Z-flag is set if X decremented to zero. The 2C in Y +; is for WRBYTE, which is in a tight loop and doesn't need much padding. +FCDB: 88 726 ZERDLY DEY +FCDC: D0 FD 727 BNE ZERDLY +FCDE: 90 05 728 BCC WRTAPE ;Y IS COUNT FOR +; Additional delay for '1' (always 250us). +FCE0: A0 32 729 LDY #$32 ; TIMING LOOP +FCE2: 88 730 ONEDLY DEY +FCE3: D0 FD 731 BNE ONEDLY +; Write a transition to the tape. +FCE5: AC 20 C0 732 WRTAPE LDY TAPEOUT +FCE8: A0 2C 733 LDY #$2C +FCEA: CA 734 DEX +FCEB: 60 735 RTS + +; Read data from location in A1L up to location in A2L. +FEFD: 20 FA FC 999 READ JSR RD2BIT ;FIND TAPEIN EDGE +FF00: A9 16 1000 LDA #$16 +FF02: 20 C9 FC 1001 JSR HEADR ;DELAY 3.5 SECONDS +FF05: 85 2E 1002 STA CHKSUM ;INIT CHKSUM=$FF +FF07: 20 FA FC 1003 JSR RD2BIT ;FIND TAPEIN EDGE +; Loop, waiting for edge. 11 cycles/iteration, plus 432+14 = 457usec. +FF0A: A0 24 1004 RD2 LDY #$24 ;LOOK FOR SYNC BIT +FF0C: 20 FD FC 1005 JSR RDBIT ; (SHORT 0) +FF0F: B0 F9 1006 BCS RD2 ; LOOP UNTIL FOUND +; Timing of next transition, a normal '0' half-cycle, doesn't matter. +FF11: 20 FD FC 1007 JSR RDBIT ;SKIP SECOND SYNC H-CYCLE +; Main byte read loop. Continue until A1 reaches A2. +FF14: A0 3B 1008 LDY #$3B ;INDEX FOR 0/1 TEST +FF16: 20 EC FC 1009 RD3 JSR RDBYTE ;READ A BYTE +FF19: 81 3C 1010 STA (A1L,X) ;STORE AT (A1) +FF1B: 45 2E 1011 EOR CHKSUM +FF1D: 85 2E 1012 STA CHKSUM ;UPDATE RUNNING CHKSUM +FF1F: 20 BA FC 1013 JSR NXTA1 ;INC A1, COMPARE TO A2 +FF22: A0 35 1014 LDY #$35 ;COMPENSATE 0/1 INDEX +FF24: 90 F0 1015 BCC RD3 ;LOOP UNTIL DONE +; Read checksum byte and check it. +FF26: 20 EC FC 1016 JSR RDBYTE ;READ CHKSUM BYTE +FF29: C5 2E 1017 CMP CHKSUM +FF2B: F0 0D 1018 BEQ BELL ;GOOD, SOUND BELL AND RETURN + +; Print "ERR", beep speaker. +FF2D: A9 C5 1019 PRERR LDA #$C5 +FF2F: 20 ED FD 1020 JSR COUT ;PRINT "ERR", THEN BELL +FF32: A9 D2 1021 LDA #$D2 +FF34: 20 ED FD 1022 JSR COUT +FF37: 20 ED FD 1023 JSR COUT +FF3A: A9 87 1024 BELL LDA #$87 ;OUTPUT BELL AND RETURN +FF3C: 4C ED FD 1025 JMP COUT + +; Read a byte from the tape. Y is $3B on first call, $35 on subsequent +; calls. The bits are shifted left, meaning that the high bit is read +; first. +FCEC: A2 08 736 RDBYTE LDX #$08 ;8 BITS TO READ +FCEE: 48 737 RDBYT2 PHA ;READ TWO TRANSITIONS +FCEF: 20 FA FC 738 JSR RD2BIT ; (FIND EDGE) +FCF2: 68 739 PLA +FCF3: 2A 740 ROL ;NEXT BIT +FCF4: A0 3A 741 LDY #$3A ;COUNT FOR SAMPLES +FCF6: CA 742 DEX +FCF7: D0 F5 743 BNE RDBYT2 +FCF9: 60 744 RTS + +; Read two bits from the tape. +FCFA: 20 FD FC 745 RD2BIT JSR RDBIT +; Read one bit from the tape. On entry, Y is the expected transition time: +; $3A=696usec $35=636usec $24=432usec +; Returns with the carry set if the transition time exceeds the Y value. +FCFD: 88 746 RDBIT DEY ;DECR Y UNTIL +FCFE: AD 60 C0 747 LDA TAPEIN ; TAPE TRANSITION +FD01: 45 2F 748 EOR LASTIN +FD03: 10 F8 749 BPL RDBIT +; the above loop takes 12 usec per iteration, what follows takes 14. +FD05: 45 2F 750 EOR LASTIN +FD07: 85 2F 751 STA LASTIN +FD09: C0 80 752 CPY #$80 ;SET CARRY ON Y +FD0B: 60 753 RTS + +*/ + + +/* + * ========================================================================== + * CassetteDialog + * ========================================================================== + */ + +BEGIN_MESSAGE_MAP(CassetteDialog, CDialog) + ON_NOTIFY(LVN_ITEMCHANGED, IDC_CASSETTE_LIST, OnListChange) + ON_NOTIFY(NM_DBLCLK, IDC_CASSETTE_LIST, OnListDblClick) + //ON_MESSAGE(WMU_DIALOG_READY, OnDialogReady) + ON_COMMAND(IDC_IMPORT_CHUNK, OnImport) + ON_COMMAND(IDHELP, OnHelp) + ON_CBN_SELCHANGE(IDC_CASSETTE_ALG, OnAlgorithmChange) +END_MESSAGE_MAP() + + +/* + * Set up the dialog. + */ +BOOL +CassetteDialog::OnInitDialog(void) +{ + CRect rect; + const Preferences* pPreferences = GET_PREFERENCES(); + + CDialog::OnInitDialog(); // does DDX init + + CWnd* pWnd; + pWnd = GetDlgItem(IDC_IMPORT_CHUNK); + pWnd->EnableWindow(FALSE); + + pWnd = GetDlgItem(IDC_CASSETTE_INPUT); + pWnd->SetWindowText(fFileName); + + /* prep the combo box */ + CComboBox* pCombo = (CComboBox*) GetDlgItem(IDC_CASSETTE_ALG); + ASSERT(pCombo != nil); + int defaultAlg = pPreferences->GetPrefLong(kPrCassetteAlgorithm); + if (defaultAlg > CassetteData::kAlgorithmMIN && + defaultAlg < CassetteData::kAlgorithmMAX) + { + pCombo->SetCurSel(defaultAlg); + } else { + WMSG1("GLITCH: invalid defaultAlg in prefs (%d)\n", defaultAlg); + pCombo->SetCurSel(CassetteData::kAlgorithmZero); + } + fAlgorithm = (CassetteData::Algorithm) defaultAlg; + + /* + * Prep the listview control. + * + * Columns: + * [icon] Index | Format | Length | Checksum OK + */ + CListCtrl* pListView = (CListCtrl*) GetDlgItem(IDC_CASSETTE_LIST); + ASSERT(pListView != nil); + ListView_SetExtendedListViewStyleEx(pListView->m_hWnd, + LVS_EX_FULLROWSELECT, LVS_EX_FULLROWSELECT); + + int width0, width1, width2, width3, width4; + + pListView->GetClientRect(&rect); + width0 = pListView->GetStringWidth("XXIndexX"); + width1 = pListView->GetStringWidth("XXFormatXmmmmmmmmmmmmmm"); + width2 = pListView->GetStringWidth("XXLengthXm"); + width3 = pListView->GetStringWidth("XXChecksumXm"); + width4 = pListView->GetStringWidth("XXStart sampleX"); + //width5 = pListView->GetStringWidth("XXEnd sampleX"); + + pListView->InsertColumn(0, "Index", LVCFMT_LEFT, width0); + pListView->InsertColumn(1, "Format", LVCFMT_LEFT, width1); + pListView->InsertColumn(2, "Length", LVCFMT_LEFT, width2); + pListView->InsertColumn(3, "Checksum", LVCFMT_LEFT, width3); + pListView->InsertColumn(4, "Start sample", LVCFMT_LEFT, width4); + pListView->InsertColumn(5, "End sample", LVCFMT_LEFT, + rect.Width() - (width0+width1+width2+width3+width4) + /*- ::GetSystemMetrics(SM_CXVSCROLL)*/ ); + + /* add images for list; this MUST be loaded before header images */ +// LoadListImages(); +// pListView->SetImageList(&fListImageList, LVSIL_SMALL); + +// LoadList(); + + CenterWindow(); + + //int cc = PostMessage(WMU_DIALOG_READY, 0, 0); + //ASSERT(cc != 0); + + if (!AnalyzeWAV()) + OnCancel(); + + return TRUE; +} + +#if 0 +/* + * Dialog construction has completed. Start the WAV analysis. + */ +LONG +CassetteDialog::OnDialogReady(UINT, LONG) +{ + //AnalyzeWAV(); + return 0; +} +#endif + + +/* + * Something changed in the list. Update the "OK" button. + */ +void +CassetteDialog::OnListChange(NMHDR*, LRESULT* pResult) +{ + WMSG0("List change\n"); + CListCtrl* pListView = (CListCtrl*) GetDlgItem(IDC_CASSETTE_LIST); + CButton* pButton = (CButton*) GetDlgItem(IDC_IMPORT_CHUNK); + pButton->EnableWindow(pListView->GetSelectedCount() != 0); + + *pResult = 0; +} + + +/* + * Double click. + */ +void +CassetteDialog::OnListDblClick(NMHDR* pNotifyStruct, LRESULT* pResult) +{ + WMSG0("Double click!\n"); + CListCtrl* pListView = (CListCtrl*) GetDlgItem(IDC_CASSETTE_LIST); + + if (pListView->GetSelectedCount() == 1) + OnImport(); + + *pResult = 0; +} + +/* + * The volume filter drop-down box has changed. + */ +void +CassetteDialog::OnAlgorithmChange(void) +{ + CComboBox* pCombo = (CComboBox*) GetDlgItem(IDC_CASSETTE_ALG); + ASSERT(pCombo != nil); + WMSG1("+++ SELECTION IS NOW %d\n", pCombo->GetCurSel()); + fAlgorithm = (CassetteData::Algorithm) pCombo->GetCurSel(); + AnalyzeWAV(); +} + +/* + * User pressed the "Help" button. + */ +void +CassetteDialog::OnHelp(void) +{ + WinHelp(HELP_TOPIC_IMPORT_CASSETTE, HELP_CONTEXT); +} + +/* + * User pressed "import" button. Add the selected item to the current + * archive or disk image. + */ +void +CassetteDialog::OnImport(void) +{ + /* + * Figure out which item they have selected. + */ + CListCtrl* pListView = (CListCtrl*) GetDlgItem(IDC_CASSETTE_LIST); + ASSERT(pListView != nil); + assert(pListView->GetSelectedCount() == 1); + + POSITION posn; + posn = pListView->GetFirstSelectedItemPosition(); + if (posn == nil) { + ASSERT(false); + return; + } + int idx = pListView->GetNextSelectedItem(posn); + + /* + * Set up the import dialog. + */ + CassImpTargetDialog impDialog(this); + + impDialog.fFileName = "From.Tape"; + impDialog.fFileLength = fDataArray[idx].GetDataLen(); + impDialog.SetFileType(fDataArray[idx].GetFileType()); + + if (impDialog.DoModal() != IDOK) + return; + + /* + * Write the file to the currently-open archive. + */ + GenericArchive::FileDetails details; + + details.entryKind = GenericArchive::FileDetails::kFileKindDataFork; + details.origName = "Cassette WAV"; + details.storageName = impDialog.fFileName; + details.access = 0xe3; // unlocked, backup bit set + details.fileType = impDialog.GetFileType(); + if (details.fileType == kFileTypeBIN) + details.extraType = impDialog.fStartAddr; + else if (details.fileType == kFileTypeBAS) + details.extraType = 0x0801; + else + details.extraType = 0x0000; + details.storageType = DiskFS::kStorageSeedling; + time_t now = time(nil); + GenericArchive::UNIXTimeToDateTime(&now, &details.createWhen); + GenericArchive::UNIXTimeToDateTime(&now, &details.archiveWhen); + + CString errMsg; + + fDirty = true; + if (!MainWindow::SaveToArchive(&details, fDataArray[idx].GetDataBuf(), + fDataArray[idx].GetDataLen(), nil, -1, /*ref*/errMsg, this)) + { + goto bail; + } + + +bail: + if (!errMsg.IsEmpty()) { + CString msg; + msg.Format("Unable to import file: %s.", (const char *) errMsg); + ShowFailureMsg(this, msg, IDS_FAILED); + return; + } +} + + +/* + * Analyze the contents of a WAV file. + * + * Returns "true" if it found anything at all, "false" if not. + */ +bool +CassetteDialog::AnalyzeWAV(void) +{ + SoundFile soundFile; + CWaitCursor waitc; + CListCtrl* pListCtrl = (CListCtrl*) GetDlgItem(IDC_CASSETTE_LIST); + CString errMsg; + long sampleOffset; + int idx; + + if (soundFile.Create(fFileName, &errMsg) != 0) { + ShowFailureMsg(this, errMsg, IDS_FAILED); + return false; + } + + const WAVEFORMATEX* pFormat = soundFile.GetWaveFormat(); + if (pFormat->nChannels < 1 || pFormat->nChannels > 2 || + (pFormat->wBitsPerSample != 8 && pFormat->wBitsPerSample != 16)) + { + errMsg.Format("Unexpected PCM format (%d channels, %d bits/sample)", + pFormat->nChannels, pFormat->wBitsPerSample); + ShowFailureMsg(this, errMsg, IDS_FAILED); + return false; + } + if (soundFile.GetDataLen() % soundFile.GetBPS() != 0) { + errMsg.Format("Unexpected sound data length (%ld, samples are %d bytes)", + soundFile.GetDataLen(), soundFile.GetBPS()); + ShowFailureMsg(this, errMsg, IDS_FAILED); + return false; + } + + pListCtrl->DeleteAllItems(); + + sampleOffset = 0; + for (idx = 0; idx < kMaxRecordings; idx++) { + long fileType; + bool result; + + result = fDataArray[idx].Scan(&soundFile, fAlgorithm, &sampleOffset); + if (!result) + break; + + AddEntry(idx, pListCtrl, &fileType); + fDataArray[idx].SetFileType(fileType); + } + + if (idx == 0) { + WMSG0("No Apple II files found\n"); + /* that's okay, just show the empty list */ + } + + return true; +} + +/* + * Add an entry to the list. + * + * Layout: index format length checksum start-offset + */ +void +CassetteDialog::AddEntry(int idx, CListCtrl* pListCtrl, long* pFileType) +{ + CString tmpStr; + const CassetteData* pData = &fDataArray[idx]; + const unsigned char* pDataBuf = pData->GetDataBuf(); + + ASSERT(pDataBuf != nil); + + tmpStr.Format("%d", idx); + pListCtrl->InsertItem(idx, tmpStr); + + *pFileType = kFileTypeBIN; + if (pData->GetDataLen() == 2) { + tmpStr.Format("Integer header ($%04X)", + pDataBuf[0] | pDataBuf[1] << 8); + } else if (pData->GetDataLen() == 3) { + tmpStr.Format("Applesoft header ($%04X $%02x)", + pDataBuf[0] | pDataBuf[1] << 8, pDataBuf[2]); + } else if (pData->GetDataLen() > 3 && idx > 0 && + fDataArray[idx-1].GetDataLen() == 2) + { + tmpStr = "Integer BASIC"; + *pFileType = kFileTypeINT; + } else if (pData->GetDataLen() > 3 && idx > 0 && + fDataArray[idx-1].GetDataLen() == 3) + { + tmpStr = "Applesoft BASIC"; + *pFileType = kFileTypeBAS; + } else { + tmpStr = "Binary"; + } + pListCtrl->SetItemText(idx, 1, tmpStr); + + tmpStr.Format("%d", pData->GetDataLen()); + pListCtrl->SetItemText(idx, 2, tmpStr); + if (pData->GetDataChkGood()) + tmpStr.Format("Good (0x%02x)", pData->GetDataChecksum()); + else + tmpStr.Format("BAD (0x%02x)", pData->GetDataChecksum()); + pListCtrl->SetItemText(idx, 3, tmpStr); + tmpStr.Format("%ld", pData->GetDataOffset()); + pListCtrl->SetItemText(idx, 4, tmpStr); + tmpStr.Format("%ld", pData->GetDataEndOffset()); + pListCtrl->SetItemText(idx, 5, tmpStr); +} + + +/* + * ========================================================================== + * CassetteData + * ========================================================================== + */ + +/* + * Scan the WAV file, starting from the specified byte offset. + * + * Returns "true" if we found a file, "false" if not (indicating that the + * end of the input has been reached). Updates "*pStartOffset" to point + * past the end of the data we've read. + */ +bool +CassetteDialog::CassetteData::Scan(SoundFile* pSoundFile, Algorithm alg, + long* pStartOffset) +{ + const int kSampleChunkSize = 65536; // should be multiple of 4 + const WAVEFORMATEX* pFormat; + ScanState scanState; + long initialLen, dataLen, chunkLen, byteOffset; + long sampleStartIndex; + unsigned char* buf = nil; + float* sampleBuf = nil; + int bytesPerSample; + bool result = false; + unsigned char checkSum; + int outByteIndex, bitAcc; + + bytesPerSample = pSoundFile->GetBPS(); + assert(bytesPerSample >= 1 && bytesPerSample <= 4); + assert(kSampleChunkSize % bytesPerSample == 0); + byteOffset = *pStartOffset; + initialLen = dataLen = pSoundFile->GetDataLen() - byteOffset; + sampleStartIndex = byteOffset/bytesPerSample; + WMSG4("CassetteData::Scan(off=%ld / %ld) len=%ld alg=%d\n", + byteOffset, sampleStartIndex, dataLen, alg); + + pFormat = pSoundFile->GetWaveFormat(); + + buf = new unsigned char[kSampleChunkSize]; + sampleBuf = new float[kSampleChunkSize/bytesPerSample]; + if (fOutputBuf == nil) // alloc on first use + fOutputBuf = new unsigned char[kMaxFileLen]; + if (buf == nil || sampleBuf == nil || fOutputBuf == nil) { + WMSG0("Buffer alloc failed\n"); + goto bail; + } + + memset(&scanState, 0, sizeof(scanState)); + scanState.algorithm = alg; + scanState.phase = kPhaseScanFor770Start; + scanState.mode = kModeInitial0; + scanState.positive = false; + scanState.usecPerSample = 1000000.0f / (float) pFormat->nSamplesPerSec; + + checkSum = 0xff; + outByteIndex = 0; + bitAcc = 1; + + /* + * Loop until done or out of data. + */ + while (dataLen > 0) { + int cc; + + chunkLen = dataLen; + if (chunkLen > kSampleChunkSize) + chunkLen = kSampleChunkSize; + + cc = pSoundFile->ReadData(buf, byteOffset, chunkLen); + if (cc < 0) { + WMSG1("ReadData(%d) failed\n", chunkLen); + goto bail; + } + + ConvertSamplesToReal(pFormat, buf, chunkLen, sampleBuf); + + for (int i = 0; i < chunkLen / bytesPerSample; i++) { + int bitVal; + if (ProcessSample(sampleBuf[i], sampleStartIndex + i, + &scanState, &bitVal)) + { + if (outByteIndex >= kMaxFileLen) { + WMSG0("Cassette data overflow\n"); + scanState.phase = kPhaseEndReached; + } else { + /* output a bit, shifting until bit 8 lights up */ + assert(bitVal == 0 || bitVal == 1); + bitAcc = (bitAcc << 1) | bitVal; + if (bitAcc > 0xff) { + fOutputBuf[outByteIndex++] = (unsigned char) bitAcc; + checkSum ^= (unsigned char) bitAcc; + bitAcc = 1; + } + } + } + if (scanState.phase == kPhaseEndReached) { + dataLen -= i * bytesPerSample; + break; + } + } + if (scanState.phase == kPhaseEndReached) + break; + + dataLen -= chunkLen; + byteOffset += chunkLen; + sampleStartIndex += chunkLen / bytesPerSample; + } + + switch (scanState.phase) { + case kPhaseScanFor770Start: + case kPhaseScanning770: + // expected case for trailing part of file + WMSG0("Scan ended while searching for 770\n"); + goto bail; + case kPhaseScanForShort0: + case kPhaseShort0B: + WMSG0("Scan ended while searching for short 0/0B\n"); + //DebugBreak(); // unusual + goto bail; + case kPhaseReadData: + WMSG0("Scan ended while reading data\n"); + //DebugBreak(); // truncated WAV file? + goto bail; + case kPhaseEndReached: + WMSG0("Scan found end\n"); + // winner! + break; + default: + WMSG1("Unknown phase %d\n", scanState.phase); + assert(false); + goto bail; + } + + WMSG3("*** Output %d bytes (bitAcc=0x%02x, checkSum=0x%02x)\n", + outByteIndex, bitAcc, checkSum); + + if (outByteIndex == 0) { + fOutputLen = 0; + fChecksum = 0x00; + fChecksumGood = false; + } else { + fOutputLen = outByteIndex-1; + fChecksum = fOutputBuf[outByteIndex-1]; + fChecksumGood = (checkSum == 0x00); + } + fStartSample = scanState.dataStart; + fEndSample = scanState.dataEnd; + + /* we're done with this file; advance the start offset */ + *pStartOffset = *pStartOffset + (initialLen - dataLen); + + result = true; + +bail: + delete[] buf; + delete[] sampleBuf; + return result; +} + +/* + * Convert a block of samples from PCM to float. + * + * Only the first (left) channel is converted in multi-channel formats. + */ +void +CassetteDialog::CassetteData::ConvertSamplesToReal(const WAVEFORMATEX* pFormat, + const unsigned char* buf, long chunkLen, float* sampleBuf) +{ + int bps = ((pFormat->wBitsPerSample+7)/8) * pFormat->nChannels; + int bitsPerSample = pFormat->wBitsPerSample; + int offset = 0; + + assert(chunkLen % bps == 0); + + if (bitsPerSample == 8) { + while (chunkLen > 0) { + *sampleBuf++ = (*buf - 128) / 128.0f; + //WMSG3("Sample8(%5d)=%d float=%.3f\n", offset, *buf, *(sampleBuf-1)); + //offset++; + buf += bps; + chunkLen -= bps; + } + } else if (bitsPerSample == 16) { + while (chunkLen > 0) { + short sample = *buf | *(buf+1) << 8; + *sampleBuf++ = sample / 32768.0f; + //WMSG3("Sample16(%5d)=%d float=%.3f\n", offset, sample, *(sampleBuf-1)); + //offset++; + buf += bps; + chunkLen -= bps; + } + } else { + assert(false); + } + + //WMSG1("Conv %d\n", bitsPerSample); +} + +/* width of 1/2 cycle in 770Hz lead-in */ +const float kLeadInHalfWidth = 650.0f; // usec +/* max error when detecting 770Hz lead-in, in usec */ +const float kLeadInMaxError = 108.0f; // usec (542 - 758) +/* width of 1/2 cycle of "short 0" */ +const float kShortZeroHalfWidth = 200.0f; // usec +/* max error when detection short 0 */ +const float kShortZeroMaxError = 150.0f; // usec (50 - 350) +/* width of 1/2 cycle of '0' */ +const float kZeroHalfWidth = 250.0f; // usec +/* max error when detecting '0' */ +const float kZeroMaxError = 94.0f; // usec +/* width of 1/2 cycle of '1' */ +const float kOneHalfWidth = 500.0f; // usec +/* max error when detecting '1' */ +const float kOneMaxError = 94.0f; // usec +/* after this many 770Hz half-cycles, start looking for short 0 */ +const long kLeadInHalfCycThreshold = 1540; // 1 full second + +/* amplitude must change by this much before we switch out of "peak" mode */ +const float kPeakThreshold = 0.2f; // 10% +/* amplitude must change by at least this much to stay in "transition" mode */ +const float kTransMinDelta = 0.02f; // 1% +/* kTransMinDelta happens over this range */ +const float kTransDeltaBase = 45.35f; // usec (1 sample at 22.05KHz) + + +/* + * Process one audio sample. Updates "pScanState" appropriately. + * + * If we think we found a bit, this returns "true" with 0 or 1 in "*pBitVal". + */ +bool +CassetteDialog::CassetteData::ProcessSample(float sample, long sampleIndex, + ScanState* pScanState, int* pBitVal) +{ + if (pScanState->algorithm == kAlgorithmZero) + return ProcessSampleZero(sample, sampleIndex, pScanState, pBitVal); + else if (pScanState->algorithm == kAlgorithmRoundPeak || + pScanState->algorithm == kAlgorithmSharpPeak || + pScanState->algorithm == kAlgorithmShallowPeak) + return ProcessSamplePeak(sample, sampleIndex, pScanState, pBitVal); + else { + assert(false); + return false; + } +} + +/* + * Process the data by measuring the distance between zero crossings. + * + * This is very similar to the way the Apple II does it, though + * we have to scan for the 770Hz lead-in instead of simply assuming the + * the user has queued up the tape. + * + * To offset the effects of DC bias, we examine full cycles instead of + * half cycles. + */ +bool +CassetteDialog::CassetteData::ProcessSampleZero(float sample, long sampleIndex, + ScanState* pScanState, int* pBitVal) +{ + long timeDelta; + bool crossedZero = false; + bool emitBit = false; + + /* + * Analyze the mode, changing to a new one when appropriate. + */ + switch (pScanState->mode) { + case kModeInitial0: + assert(pScanState->phase == kPhaseScanFor770Start); + pScanState->mode = kModeRunning; + break; + case kModeRunning: + if (pScanState->prevSample < 0.0f && sample >= 0.0f || + pScanState->prevSample >= 0.0f && sample < 0.0f) + { + crossedZero = true; + } + break; + default: + assert(false); + break; + } + + /* + * Deal with a zero crossing. + * + * We currently just grab the first point after we cross. We should + * be grabbing the closest point or interpolating across. + */ + if (crossedZero) { + float halfCycleUsec; + int bias; + + if (fabs(pScanState->prevSample) < fabs(sample)) + bias = -1; // previous sample was closer to zero point + else + bias = 0; // current sample is closer + + /* delta time for zero-to-zero (half cycle) */ + timeDelta = (sampleIndex+bias) - pScanState->lastZeroIndex; + + halfCycleUsec = timeDelta * pScanState->usecPerSample; + //WMSG3("Zero %6ld: half=%.1fusec full=%.1fusec\n", + // sampleIndex, halfCycleUsec, + // halfCycleUsec + pScanState->halfCycleWidth); + + emitBit = UpdatePhase(pScanState, sampleIndex+bias, halfCycleUsec, + pBitVal); + + pScanState->lastZeroIndex = sampleIndex + bias; + } + + /* record this sample for the next go-round */ + pScanState->prevSample = sample; + + return emitBit; +} + +/* + * Process the data by finding and measuring the distance between peaks. + */ +bool +CassetteDialog::CassetteData::ProcessSamplePeak(float sample, long sampleIndex, + ScanState* pScanState, int* pBitVal) +{ + /* values range from [-1.0,1.0), so range is 2.0 total */ + long timeDelta; + float ampDelta; + float transitionLimit; + bool hitPeak = false; + bool emitBit = false; + + /* + * Analyze the mode, changing to a new one when appropriate. + */ + switch (pScanState->mode) { + case kModeInitial0: + assert(pScanState->phase == kPhaseScanFor770Start); + pScanState->mode = kModeInitial1; + break; + case kModeInitial1: + assert(pScanState->phase == kPhaseScanFor770Start); + if (sample >= pScanState->prevSample) + pScanState->positive = true; + else + pScanState->positive = false; + pScanState->mode = kModeInTransition; + /* set these up with something reasonable */ + pScanState->lastPeakStartIndex = sampleIndex; + pScanState->lastPeakStartValue = sample; + break; + + case kModeInTransition: + /* + * Stay here until two adjacent samples are very close in amplitude + * (or we change direction). We need to adjust our amplitude + * threshold based on sampling frequency, or at higher sample + * rates we're going to think everything is a transition. + * + * The approach here is overly simplistic, and is prone to failure + * when the sampling rate is high, especially with 8-bit samples + * or sound cards that don't really have 16-bit resolution. The + * proper way to do this is to keep a short history, and evaluate + * the delta amplitude over longer periods. [At this point I'd + * rather just tell people to record at 22.05KHz.] + * + * Set the "hitPeak" flag and handle the consequences below. + */ + if (pScanState->algorithm == kAlgorithmRoundPeak) + transitionLimit = kTransMinDelta * + (pScanState->usecPerSample / kTransDeltaBase); + else + transitionLimit = 0.0f; + + if (pScanState->positive) { + if (sample < pScanState->prevSample + transitionLimit) { + pScanState->mode = kModeAtPeak; + hitPeak = true; + } + } else { + if (sample > pScanState->prevSample - transitionLimit) { + pScanState->mode = kModeAtPeak; + hitPeak = true; + } + } + break; + case kModeAtPeak: + /* + * Stay here until we're a certain distance above or below the + * previous peak. This also keeps us in a holding pattern for + * large flat areas. + */ + transitionLimit = kPeakThreshold; + if (pScanState->algorithm == kAlgorithmShallowPeak) + transitionLimit /= 4.0f; + + ampDelta = pScanState->lastPeakStartValue - sample; + if (ampDelta < 0) + ampDelta = -ampDelta; + if (ampDelta > transitionLimit) { + if (sample >= pScanState->lastPeakStartValue) + pScanState->positive = true; // going up + else + pScanState->positive = false; // going down + + /* mark the end of the peak; could be same as start of peak */ + pScanState->mode = kModeInTransition; + } + break; + default: + assert(false); + break; + } + + /* + * If we hit "peak" criteria, we regard the *previous* sample as the + * peak. This is very important for lower sampling rates (e.g. 8KHz). + */ + if (hitPeak) { + /* compute half-cycle amplitude and time */ + float halfCycleUsec; //, fullCycleUsec; + + /* delta time for peak-to-peak (half cycle) */ + timeDelta = (sampleIndex-1) - pScanState->lastPeakStartIndex; + /* amplitude peak-to-peak */ + ampDelta = pScanState->lastPeakStartValue - pScanState->prevSample; + if (ampDelta < 0) + ampDelta = -ampDelta; + + halfCycleUsec = timeDelta * pScanState->usecPerSample; + //if (sampleIndex > 584327 && sampleIndex < 590000) { + // WMSG4("Peak %6ld: amp=%.3f height=%.3f peakWidth=%.1fusec\n", + // sampleIndex-1, pScanState->prevSample, ampDelta, + // halfCycleUsec); + // ::Sleep(10); + //} + if (sampleIndex == 32739) + WMSG0("whee\n"); + + emitBit = UpdatePhase(pScanState, sampleIndex-1, halfCycleUsec, pBitVal); + + /* set the "peak start" values */ + pScanState->lastPeakStartIndex = sampleIndex-1; + pScanState->lastPeakStartValue = pScanState->prevSample; + } + + /* record this sample for the next go-round */ + pScanState->prevSample = sample; + + return emitBit; +} + + +/* + * Given the width of a half-cycle, update "phase" and decide whether or not + * it's time to emit a bit. + * + * Updates "halfCycleWidth" too, alternating between 0.0 and a value. + * + * The "sampleIndex" parameter is largely just for display. We use it to + * set the "start" and "end" pointers, but those are also ultimately just + * for display to the user. + */ +bool +CassetteDialog::CassetteData::UpdatePhase(ScanState* pScanState, long sampleIndex, + float halfCycleUsec, int* pBitVal) +{ + float fullCycleUsec; + bool emitBit = false; + + if (pScanState->halfCycleWidth != 0.0f) + fullCycleUsec = halfCycleUsec + pScanState->halfCycleWidth; + else + fullCycleUsec = 0.0f; // only have first half + + switch (pScanState->phase) { + case kPhaseScanFor770Start: + /* watch for a cycle of the appropriate length */ + if (fullCycleUsec != 0.0f && + fullCycleUsec > kLeadInHalfWidth*2.0f - kLeadInMaxError*2.0f && + fullCycleUsec < kLeadInHalfWidth*2.0f + kLeadInMaxError*2.0f) + { + //WMSG1(" scanning 770 at %ld\n", sampleIndex); + pScanState->phase = kPhaseScanning770; + pScanState->num770 = 1; + } + break; + case kPhaseScanning770: + /* count up the 770Hz cycles */ + if (fullCycleUsec != 0.0f && + fullCycleUsec > kLeadInHalfWidth*2.0f - kLeadInMaxError*2.0f && + fullCycleUsec < kLeadInHalfWidth*2.0f + kLeadInMaxError*2.0f) + { + pScanState->num770++; + if (pScanState->num770 > kLeadInHalfCycThreshold/2) { + /* looks like a solid tone, advance to next phase */ + pScanState->phase = kPhaseScanForShort0; + WMSG0(" looking for short 0\n"); + } + } else if (fullCycleUsec != 0.0f) { + /* pattern lost, reset */ + if (pScanState->num770 > 5) { + WMSG3(" lost 770 at %ld width=%.1f (count=%ld)\n", + sampleIndex, fullCycleUsec, pScanState->num770); + } + pScanState->phase = kPhaseScanFor770Start; + } + /* else we only have a half cycle, so do nothing */ + break; + case kPhaseScanForShort0: + /* found what looks like a 770Hz field, find the short 0 */ + if (halfCycleUsec > kShortZeroHalfWidth - kShortZeroMaxError && + halfCycleUsec < kShortZeroHalfWidth + kShortZeroMaxError) + { + WMSG3(" found short zero (half=%.1f) at %ld after %ld 770s\n", + halfCycleUsec, sampleIndex, pScanState->num770); + pScanState->phase = kPhaseShort0B; + /* make sure we treat current sample as first half */ + pScanState->halfCycleWidth = 0.0f; + } else + if (fullCycleUsec != 0.0f && + fullCycleUsec > kLeadInHalfWidth*2.0f - kLeadInMaxError*2.0f && + fullCycleUsec < kLeadInHalfWidth*2.0f + kLeadInMaxError*2.0f) + { + /* found another 770Hz cycle */ + pScanState->num770++; + } else if (fullCycleUsec != 0.0f) { + /* full cycle of the wrong size, we've lost it */ + WMSG3(" Lost 770 at %ld width=%.1f (count=%ld)\n", + sampleIndex, fullCycleUsec, pScanState->num770); + pScanState->phase = kPhaseScanFor770Start; + } + break; + case kPhaseShort0B: + /* pick up the second half of the start cycle */ + assert(fullCycleUsec != 0.0f); + if (fullCycleUsec > (kShortZeroHalfWidth + kZeroHalfWidth) - kZeroMaxError*2.0f && + fullCycleUsec < (kShortZeroHalfWidth + kZeroHalfWidth) + kZeroMaxError*2.0f) + { + /* as expected */ + WMSG2(" Found 0B %.1f (total %.1f), advancing to 'read data' phase\n", + halfCycleUsec, fullCycleUsec); + pScanState->dataStart = sampleIndex; + pScanState->phase = kPhaseReadData; + } else { + /* must be a false-positive at end of tone */ + WMSG2(" Didn't find post-short-0 value (half=%.1f + %.1f)\n", + pScanState->halfCycleWidth, halfCycleUsec); + pScanState->phase = kPhaseScanFor770Start; + } + break; + + case kPhaseReadData: + /* check width of full cycle; don't double error allowance */ + if (fullCycleUsec != 0.0f) { + if (fullCycleUsec > kZeroHalfWidth*2 - kZeroMaxError*2 && + fullCycleUsec < kZeroHalfWidth*2 + kZeroMaxError*2) + { + *pBitVal = 0; + emitBit = true; + } else + if (fullCycleUsec > kOneHalfWidth*2 - kOneMaxError*2 && + fullCycleUsec < kOneHalfWidth*2 + kOneMaxError*2) + { + *pBitVal = 1; + emitBit = true; + } else { + /* bad cycle, assume end reached */ + WMSG2(" Bad full cycle time %.1f in data at %ld, bailing\n", + fullCycleUsec, sampleIndex); + pScanState->dataEnd = sampleIndex; + pScanState->phase = kPhaseEndReached; + } + } + break; + default: + assert(false); + break; + } + + /* save the half-cycle stats */ + if (pScanState->halfCycleWidth == 0.0f) + pScanState->halfCycleWidth = halfCycleUsec; + else + pScanState->halfCycleWidth = 0.0f; + + return emitBit; +} diff --git a/app/CassetteDialog.h b/app/CassetteDialog.h new file mode 100644 index 0000000..bebb688 --- /dev/null +++ b/app/CassetteDialog.h @@ -0,0 +1,160 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Apple II cassette I/O functions. + */ +#ifndef __CASSETTEDIALOG__ +#define __CASSETTEDIALOG__ + +/* + * The dialog box is primarily concerned with extracting the original data + * from a WAV file recording of an Apple II cassette tape. + */ +class CassetteDialog : public CDialog { +public: + CassetteDialog(CWnd* pParentWnd = NULL) : + CDialog(IDD_IMPORTCASSETTE, pParentWnd), fDirty(false) + {} + virtual ~CassetteDialog(void) {} + + CString fFileName; // file to open + + bool IsDirty(void) const { return fDirty; } + +private: + virtual BOOL OnInitDialog(void); + //virtual void DoDataExchange(CDataExchange* pDX); + //virtual void OnOK(void); + + //enum { WMU_DIALOG_READY = WM_USER+2 }; + + afx_msg void OnListChange(NMHDR* pNotifyStruct, LRESULT* pResult); + afx_msg void OnListDblClick(NMHDR* pNotifyStruct, LRESULT* pResult); + afx_msg void OnAlgorithmChange(void); + afx_msg void OnHelp(void); + afx_msg void OnImport(void); + + /* + * This holds converted data from the WAV file, plus some meta-data + * like what type of file we think this is. + */ + class CassetteData { + public: + CassetteData(void) : fFileType(0x00), fOutputBuf(nil), fOutputLen(-1), + fStartSample(-1), fEndSample(-1), fChecksum(0x00), + fChecksumGood(false) + {} + virtual ~CassetteData(void) { delete[] fOutputBuf; } + + /* + * Algorithm to use. This must match up with the order of the items + * in the dialog IDC_CASSETTE_ALG combo box. + */ + typedef enum Algorithm { + kAlgorithmMIN = -1, + + kAlgorithmZero = 0, + kAlgorithmSharpPeak, + kAlgorithmRoundPeak, + kAlgorithmShallowPeak, + + kAlgorithmMAX + } Algorithm; + + bool Scan(SoundFile* pSoundFile, Algorithm alg, long* pSampleOffset); + unsigned char* GetDataBuf(void) const { return fOutputBuf; } + int GetDataLen(void) const { return fOutputLen; } + int GetDataOffset(void) const { return fStartSample; } + int GetDataEndOffset(void) const { return fEndSample; } + unsigned char GetDataChecksum(void) const { return fChecksum; } + bool GetDataChkGood(void) const { return fChecksumGood; } + + long GetFileType(void) const { return fFileType; } + void SetFileType(long fileType) { fFileType = fileType; } + + private: + typedef enum Phase { + kPhaseUnknown = 0, + kPhaseScanFor770Start, + kPhaseScanning770, + kPhaseScanForShort0, + kPhaseShort0B, + kPhaseReadData, + kPhaseEndReached, + // kPhaseError, + } Phase; + typedef enum Mode { + kModeUnknown = 0, + kModeInitial0, + kModeInitial1, + + kModeInTransition, + kModeAtPeak, + + kModeRunning, + } Mode; + + typedef struct ScanState { + Algorithm algorithm; + Phase phase; + Mode mode; + bool positive; // rising or at +peak if true + + long lastZeroIndex; // in samples + long lastPeakStartIndex; // in samples + float lastPeakStartValue; + + float prevSample; + + float halfCycleWidth; // in usec + long num770; // #of consecutive 770Hz cycles + long dataStart; + long dataEnd; + + /* constants */ + float usecPerSample; + } ScanState; + void ConvertSamplesToReal(const WAVEFORMATEX* pFormat, + const unsigned char* buf, long chunkLen, float* sampleBuf); + bool ProcessSample(float sample, long sampleIndex, + ScanState* pScanState, int* pBitVal); + bool ProcessSampleZero(float sample, long sampleIndex, + ScanState* pScanState, int* pBitVal); + bool ProcessSamplePeak(float sample, long sampleIndex, + ScanState* pScanState, int* pBitVal); + bool UpdatePhase(ScanState* pScanState, long sampleIndex, + float halfCycleUsec, int* pBitVal); + + enum { + kMaxFileLen = 65535+2+1+1, // 64K + length + checksum + 1 slop + }; + + long fFileType; // 0x06, 0xfa, or 0xfc + unsigned char* fOutputBuf; + int fOutputLen; + long fStartSample; + long fEndSample; + unsigned char fChecksum; + bool fChecksumGood; + }; + + bool AnalyzeWAV(void); + void AddEntry(int idx, CListCtrl* pListCtrl, long* pFileType); + + enum { + kMaxRecordings = 100, // max A2 files per WAV file + }; + + /* array with one entry per file */ + CassetteData fDataArray[kMaxRecordings]; + + CassetteData::Algorithm fAlgorithm; + bool fDirty; + + DECLARE_MESSAGE_MAP() +}; + +#endif /*__CASSETTEDIALOG__*/ diff --git a/app/ChooseAddTargetDialog.cpp b/app/ChooseAddTargetDialog.cpp new file mode 100644 index 0000000..81a7423 --- /dev/null +++ b/app/ChooseAddTargetDialog.cpp @@ -0,0 +1,102 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Functions for the ChooseAddTarget dialog box. + */ +#include "StdAfx.h" +#include "ChooseAddTargetDialog.h" +#include "HelpTopics.h" +#include "DiskFSTree.h" + +using namespace DiskImgLib; + +BEGIN_MESSAGE_MAP(ChooseAddTargetDialog, CDialog) + ON_COMMAND(IDHELP, OnHelp) +END_MESSAGE_MAP() + +/* + * Initialize the dialog box. This requires scanning the provided disk + * archive. + */ +BOOL +ChooseAddTargetDialog::OnInitDialog(void) +{ + CDialog::OnInitDialog(); + + CTreeCtrl* pTree = (CTreeCtrl*) GetDlgItem(IDC_ADD_TARGET_TREE); + + ASSERT(fpDiskFS != nil); + ASSERT(pTree != nil); + + fDiskFSTree.fIncludeSubdirs = true; + fDiskFSTree.fExpandDepth = -1; + if (!fDiskFSTree.BuildTree(fpDiskFS, pTree)) { + WMSG0("Tree load failed!\n"); + OnCancel(); + } + + int count = pTree->GetCount(); + WMSG1("ChooseAddTargetDialog tree has %d items\n", count); + if (count <= 1) { + WMSG0(" Skipping out of target selection\n"); + // adding to root volume of the sole DiskFS + fpChosenDiskFS = fpDiskFS; + ASSERT(fpChosenSubdir == nil); + OnOK(); + } + + return TRUE; +} + +/* + * Not much to do on the way in. On the way out, make sure that they've + * selected something acceptable, and copy the values to an easily + * accessible location. + */ +void +ChooseAddTargetDialog::DoDataExchange(CDataExchange* pDX) +{ + if (pDX->m_bSaveAndValidate) { + CTreeCtrl* pTree = (CTreeCtrl*) GetDlgItem(IDC_ADD_TARGET_TREE); + CString errMsg, appName; + appName.LoadString(IDS_MB_APP_NAME); + + /* shortcut for simple disk images */ + if (pTree->GetCount() == 1 && fpChosenDiskFS != nil) + return; + + HTREEITEM selected; + selected = pTree->GetSelectedItem(); + if (selected == nil) { + errMsg = "Please select a disk or subdirectory to add files to."; + MessageBox(errMsg, appName, MB_OK); + pDX->Fail(); + return; + } + + DiskFSTree::TargetData* pTargetData; + pTargetData = (DiskFSTree::TargetData*) pTree->GetItemData(selected); + if (!pTargetData->selectable) { + errMsg = "You can't add files there."; + MessageBox(errMsg, appName, MB_OK); + pDX->Fail(); + return; + } + + fpChosenDiskFS = pTargetData->pDiskFS; + fpChosenSubdir = pTargetData->pFile; + } +} + + +/* + * User pressed the "Help" button. + */ +void +ChooseAddTargetDialog::OnHelp(void) +{ + WinHelp(HELP_TOPIC_CHOOSE_TARGET, HELP_CONTEXT); +} diff --git a/app/ChooseAddTargetDialog.h b/app/ChooseAddTargetDialog.h new file mode 100644 index 0000000..eb87af7 --- /dev/null +++ b/app/ChooseAddTargetDialog.h @@ -0,0 +1,47 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Choose the sub-volume and directory where added files will be put. + */ +#ifndef __CHOOSE_ADD_TARGET_DIALOG__ +#define __CHOOSE_ADD_TARGET_DIALOG__ + +#include "resource.h" +#include "DiskFSTree.h" +#include "../diskimg/DiskImg.h" + +/* + * The dialog has a tree structure representing the sub-volumes and the + * directory structure within each sub-volume. + */ +class ChooseAddTargetDialog : public CDialog { +public: + ChooseAddTargetDialog(CWnd* pParentWnd = NULL) : + CDialog(IDD_CHOOSE_ADD_TARGET, pParentWnd) + { + fpDiskFS = fpChosenDiskFS = nil; + fpChosenSubdir = nil; + } + virtual ~ChooseAddTargetDialog(void) {} + + /* set this before calling DoModal */ + DiskImgLib::DiskFS* fpDiskFS; + + /* results; fpChosenSubdir will be nil if root vol selected */ + DiskImgLib::DiskFS* fpChosenDiskFS; + DiskImgLib::A2File* fpChosenSubdir; + +private: + virtual BOOL OnInitDialog(void); + virtual void DoDataExchange(CDataExchange* pDX); + afx_msg void OnHelp(void); + + DiskFSTree fDiskFSTree; + + DECLARE_MESSAGE_MAP() +}; + +#endif /*__CHOOSE_ADD_TARGET_DIALOG__*/ diff --git a/app/ChooseDirDialog.cpp b/app/ChooseDirDialog.cpp new file mode 100644 index 0000000..efa6a1e --- /dev/null +++ b/app/ChooseDirDialog.cpp @@ -0,0 +1,199 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Support for "choose a directory" dialog. + */ +#include "stdafx.h" +#include "ChooseDirDialog.h" +#include "NewFolderDialog.h" +#include "DiskFSTree.h" +#include "HelpTopics.h" + +BEGIN_MESSAGE_MAP(ChooseDirDialog, CDialog) + ON_NOTIFY(TVN_SELCHANGED, IDC_CHOOSEDIR_TREE, OnSelChanged) + ON_BN_CLICKED(IDC_CHOOSEDIR_EXPAND_TREE, OnExpandTree) + ON_BN_CLICKED(IDC_CHOOSEDIR_NEW_FOLDER, OnNewFolder) + ON_WM_HELPINFO() + //ON_COMMAND(ID_HELP, OnIDHelp) + ON_BN_CLICKED(IDHELP, OnHelp) +END_MESSAGE_MAP() + + +/* + * Initialize dialog components. + */ +BOOL +ChooseDirDialog::OnInitDialog(void) +{ + CDialog::OnInitDialog(); + + /* set up the "new folder" button */ + fNewFolderButton.ReplaceDlgCtrl(this, IDC_CHOOSEDIR_NEW_FOLDER); + fNewFolderButton.SetBitmapID(IDB_NEW_FOLDER); + + /* replace the tree control with a ShellTree */ + if (fShellTree.ReplaceDlgCtrl(this, IDC_CHOOSEDIR_TREE) != TRUE) { + WMSG0("WARNING: ShellTree replacement failed\n"); + ASSERT(false); + } + + //enable images + fShellTree.EnableImages(); + //populate for the with Shell Folders for the first time + fShellTree.PopulateTree(/*CSIDL_DRIVES*/); + + if (fPathName.IsEmpty()) { + // start somewhere reasonable + fShellTree.ExpandMyComputer(); + } else { + CString msg(""); + fShellTree.TunnelTree(fPathName, &msg); + if (!msg.IsEmpty()) { + /* failed */ + WMSG2("TunnelTree failed on '%s' (%s), using MyComputer instead\n", + fPathName, msg); + fShellTree.ExpandMyComputer(); + } + } + + fShellTree.SetFocus(); + return FALSE; // leave focus on shell tree +} + +/* + * Special keypress handling. + */ +BOOL +ChooseDirDialog::PreTranslateMessage(MSG* pMsg) +{ + if (pMsg->message == WM_KEYDOWN && + pMsg->wParam == VK_RETURN) + { + //WMSG0("RETURN!\n"); + if (GetFocus() == GetDlgItem(IDC_CHOOSEDIR_PATHEDIT)) { + OnExpandTree(); + return TRUE; + } + } + + return CDialog::PreTranslateMessage(pMsg); +} + +/* + * F1 key hit, or '?' button in title bar used to select help for an + * item in the dialog. + */ +BOOL +ChooseDirDialog::OnHelpInfo(HELPINFO* lpHelpInfo) +{ + DWORD context = lpHelpInfo->iCtrlId; + WinHelp(context, HELP_CONTEXTPOPUP); + return TRUE; // indicate success?? +} + +/* + * User pressed Ye Olde Helppe Button. + */ +void +ChooseDirDialog::OnHelp(void) +{ + WinHelp(HELP_TOPIC_CHOOSE_FOLDER, HELP_CONTEXT); +} + +/* + * Replace the ShellTree's default SELCHANGED handler with this so we can + * track changes to the edit control. + */ +void +ChooseDirDialog::OnSelChanged(NMHDR* pnmh, LRESULT* pResult) +{ + CString path; + CWnd* pWnd = GetDlgItem(IDC_CHOOSEDIR_PATH); + ASSERT(pWnd != nil); + + if (fShellTree.GetFolderPath(&path)) + fPathName = path; + else + fPathName = ""; + pWnd->SetWindowText(fPathName); + + // disable the "Select" button when there's no path ready + pWnd = GetDlgItem(IDOK); + ASSERT(pWnd != nil); + pWnd->EnableWindow(!fPathName.IsEmpty()); + + // It's confusing to have two different paths showing, so wipe out the + // free entry field when the selection changes. + pWnd = GetDlgItem(IDC_CHOOSEDIR_PATHEDIT); + pWnd->SetWindowText(""); + + *pResult = 0; +} + +/* + * User pressed "Expand Tree" button. + */ +void +ChooseDirDialog::OnExpandTree(void) +{ + CWnd* pWnd; + CString str; + CString msg; + + pWnd = GetDlgItem(IDC_CHOOSEDIR_PATHEDIT); + ASSERT(pWnd != nil); + pWnd->GetWindowText(str); + + if (!str.IsEmpty()) { + fShellTree.TunnelTree(str, &msg); + if (!msg.IsEmpty()) { + CString failed; + failed.LoadString(IDS_FAILED); + MessageBox(msg, failed, MB_OK | MB_ICONERROR); + } + } +} + +/* + * User pressed "New Folder" button. + */ +void +ChooseDirDialog::OnNewFolder(void) +{ + if (fPathName.IsEmpty()) { + MessageBox("You can't create a folder in this part of the tree.", + "Bad Location", MB_OK | MB_ICONERROR); + return; + } + + NewFolderDialog newFolderDlg; + + newFolderDlg.fCurrentFolder = fPathName; + if (newFolderDlg.DoModal() == IDOK) { + if (newFolderDlg.GetFolderCreated()) { + /* + * They created a new folder. We want to add it to the tree + * and then select it. This is not too hard because we know + * that the folder was created under the currently-selected + * tree node. + */ + if (fShellTree.AddFolderAtSelection(newFolderDlg.fNewFolder)) { + CString msg; + WMSG1("Success, tunneling to '%s'\n", + newFolderDlg.fNewFullPath); + fShellTree.TunnelTree(newFolderDlg.fNewFullPath, &msg); + if (!msg.IsEmpty()) { + WMSG1("TunnelTree failed: %s\n", (LPCTSTR) msg); + } + } else { + WMSG0("AddFolderAtSelection FAILED\n"); + ASSERT(false); + } + } else { + WMSG0("NewFolderDialog returned IDOK but no create\n"); + } + } +} diff --git a/app/ChooseDirDialog.h b/app/ChooseDirDialog.h new file mode 100644 index 0000000..014376e --- /dev/null +++ b/app/ChooseDirDialog.h @@ -0,0 +1,53 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Dialog for choosing a directory. + */ +#ifndef __CHOOSEDIRDIALOG__ +#define __CHOOSEDIRDIALOG__ + +#include "../util/UtilLib.h" +#include "resource.h" + +/* + * Choose a directory. This is distinctly different from what the standard + * "Open" and "Save As" dialogs do, because those want to choose normal files + * only, while this wants to select a folder. + */ +class ChooseDirDialog : public CDialog { +public: + ChooseDirDialog(CWnd* pParent = NULL, int dialogID = IDD_CHOOSEDIR) : + CDialog(dialogID, pParent) + { + fPathName = ""; + } + virtual ~ChooseDirDialog(void) {} + + const char* GetPathName(void) const { return fPathName; } + + // set the pathname; when DoModal is called this will tunnel in + void SetPathName(const char* str) { fPathName = str; } + +protected: + virtual BOOL OnInitDialog(void); + virtual BOOL PreTranslateMessage(MSG* pMsg); + + afx_msg void OnSelChanged(NMHDR* pnmh, LRESULT* pResult); + afx_msg BOOL OnHelpInfo(HELPINFO* lpHelpInfo); + afx_msg void OnExpandTree(void); + afx_msg void OnNewFolder(void); + afx_msg void OnHelp(void); + +private: + CString fPathName; + + ShellTree fShellTree; + MyBitmapButton fNewFolderButton; + + DECLARE_MESSAGE_MAP() +}; + +#endif /*__CHOOSEDIRDIALOG__*/ \ No newline at end of file diff --git a/app/CiderPress.rc b/app/CiderPress.rc new file mode 100644 index 0000000..bc6e6d6 --- /dev/null +++ b/app/CiderPress.rc @@ -0,0 +1,2428 @@ +//Microsoft Developer Studio generated resource script. +// +#include "resource.h" + +// Generated Help ID header file +#define APSTUDIO_HIDDEN_SYMBOLS +#include "resource.hm" +#undef APSTUDIO_HIDDEN_SYMBOLS + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "afxres.h" +#include "Dlgs.h" +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (U.S.) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +#ifdef _WIN32 +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) +#endif //_WIN32 + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE DISCARDABLE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE DISCARDABLE +BEGIN + "#include ""afxres.h""\r\n" + "#include ""Dlgs.h""\0" +END + +3 TEXTINCLUDE DISCARDABLE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDR_MAINFRAME ICON DISCARDABLE "Graphics\\CiderPress.ico" +IDI_FILE_BINARY2 ICON DISCARDABLE "Graphics\\binary2.ico" +IDI_FILE_NUFX ICON DISCARDABLE "Graphics\\nufx.ico" +IDI_FILE_DISKIMAGE ICON DISCARDABLE "Graphics\\diskimage.ico" +IDI_FILE_VIEWER ICON DISCARDABLE "Graphics\\FileViewer.ico" + +///////////////////////////////////////////////////////////////////////////// +// +// Menu +// + +IDR_MAINFRAME MENU DISCARDABLE +BEGIN + POPUP "&File" + BEGIN + POPUP "New" + BEGIN + MENUITEM "&Disk image...", IDM_TOOLS_IMAGECREATOR + MENUITEM "ShrinkIt Archive...\tCtrl+N", IDM_FILE_NEW_ARCHIVE + END + MENUITEM "&Open...\tCtrl-O", IDM_FILE_OPEN + MENUITEM "Open &volume...", IDM_FILE_OPEN_VOLUME + MENUITEM "&Reopen", IDM_FILE_REOPEN + MENUITEM "&Save changes\tCtrl-S", IDM_FILE_SAVE + MENUITEM "&Close\tCtrl-W", IDM_FILE_CLOSE + MENUITEM SEPARATOR + MENUITEM "Archive &Info\tCtrl-I", IDM_FILE_ARCHIVEINFO + MENUITEM "&Print...\tCtrl-P", IDM_FILE_PRINT + MENUITEM SEPARATOR + MENUITEM "&Exit", IDM_FILE_EXIT + END + POPUP "&Edit" + BEGIN + MENUITEM "&Copy\tCtrl-C", IDM_EDIT_COPY + MENUITEM "&Paste\tCtrl-V", IDM_EDIT_PASTE + MENUITEM "Paste Special", IDM_EDIT_PASTE_SPECIAL + MENUITEM SEPARATOR + MENUITEM "&Find...\tCtrl-F", IDM_EDIT_FIND + MENUITEM SEPARATOR + POPUP "&Sort" + BEGIN + MENUITEM "By original order", IDM_SORT_ORIGINAL + MENUITEM "By pathname", IDM_SORT_PATHNAME + MENUITEM "By file type", IDM_SORT_TYPE + MENUITEM "By auxtype", IDM_SORT_AUXTYPE + MENUITEM "By modification date", IDM_SORT_MODDATE + MENUITEM "By format", IDM_SORT_FORMAT + MENUITEM "By size", IDM_SORT_SIZE + MENUITEM "By ratio", IDM_SORT_RATIO + MENUITEM "By packed size", IDM_SORT_PACKED + MENUITEM "By access", IDM_SORT_ACCESS + END + MENUITEM "Select &all\tCtrl-A", IDM_EDIT_SELECT_ALL + MENUITEM "&Invert selection", IDM_EDIT_INVERT_SELECTION + MENUITEM "&Preferences...", IDM_EDIT_PREFERENCES + END + POPUP "&Actions" + BEGIN + MENUITEM "&View...\tTab", IDM_ACTIONS_VIEW + MENUITEM "&Extract...", IDM_ACTIONS_EXTRACT + MENUITEM "&Test...", IDM_ACTIONS_TEST + MENUITEM "&Rename...", IDM_ACTIONS_RENAME + MENUITEM "&Delete...\tDel", IDM_ACTIONS_DELETE + MENUITEM "&Re-compress...", IDM_ACTIONS_RECOMPRESS + MENUITEM SEPARATOR + MENUITEM "Add &files...", IDM_ACTIONS_ADD_FILES + MENUITEM "Add &disk image...", IDM_ACTIONS_ADD_DISKS + MENUITEM "Create &subdirectory...", IDM_ACTIONS_CREATE_SUBDIR + MENUITEM SEPARATOR + MENUITEM "&Open as disk image", IDM_ACTIONS_OPENASDISK + MENUITEM "Edit &comment...", IDM_ACTIONS_EDIT_COMMENT + MENUITEM "Edit &attributes...", IDM_ACTIONS_EDIT_PROPS + MENUITEM "Rename volume...", IDM_ACTIONS_RENAME_VOLUME + MENUITEM SEPARATOR + MENUITEM "&Convert to disk image...", IDM_ACTIONS_CONV_DISK + MENUITEM "&Convert to file archive...", IDM_ACTIONS_CONV_FILE + MENUITEM "Import file from WAV...", IDM_ACTIONS_CONV_FROMWAV + MENUITEM "Import &BAS from text...", IDM_ACTIONS_IMPORT_BAS + END + POPUP "Tools" + BEGIN + MENUITEM "&Disk sector viewer", IDM_TOOLS_DISKEDIT + MENUITEM "Disk &image converter", IDM_TOOLS_DISKCONV + MENUITEM "&Bulk disk image converter", IDM_TOOLS_BULKDISKCONV + MENUITEM SEPARATOR + MENUITEM "&Volume copier (open volume)", + IDM_TOOLS_VOLUMECOPIER_VOLUME + + MENUITEM "Volume copier (open file)", IDM_TOOLS_VOLUMECOPIER_FILE + MENUITEM "&Merge SST images", IDM_TOOLS_SST_MERGE + MENUITEM SEPARATOR + MENUITEM "2MG properties editor", IDM_TOOLS_TWOIMGPROPS + MENUITEM "EOL scanner", IDM_TOOLS_EOLSCANNER + END + POPUP "&Help" + BEGIN + MENUITEM "&Contents...\tF1", IDM_HELP_CONTENTS + MENUITEM "Visit faddenSoft &web site", IDM_HELP_WEBSITE + MENUITEM SEPARATOR + MENUITEM "&About CiderPress", IDM_HELP_ABOUT + END +END + +IDR_RIGHTCLICKMENU MENU DISCARDABLE +BEGIN + POPUP "RightClickMenu" + BEGIN + MENUITEM "View...", IDM_ACTIONS_VIEW + MENUITEM "Extract...", IDM_ACTIONS_EXTRACT + MENUITEM "Test...", IDM_ACTIONS_TEST + MENUITEM "Rename...", IDM_ACTIONS_RENAME + MENUITEM "Delete...", IDM_ACTIONS_DELETE + MENUITEM "Re-compress...", IDM_ACTIONS_RECOMPRESS + MENUITEM SEPARATOR + MENUITEM "&Copy\tCtrl-C", IDM_EDIT_COPY + MENUITEM "&Paste\tCtrl-V", IDM_EDIT_PASTE + MENUITEM SEPARATOR + MENUITEM "Add &files...", IDM_ACTIONS_ADD_FILES + MENUITEM "Create &subdirectory...", IDM_ACTIONS_CREATE_SUBDIR + MENUITEM SEPARATOR + MENUITEM "Open as disk image", IDM_ACTIONS_OPENASDISK + MENUITEM "Edit comment...", IDM_ACTIONS_EDIT_COMMENT + MENUITEM "Edit attributes...", IDM_ACTIONS_EDIT_PROPS + END +END + + +///////////////////////////////////////////////////////////////////////////// +// +// Accelerator +// + +IDR_MAINFRAME ACCELERATORS DISCARDABLE +BEGIN + "A", IDM_EDIT_SELECT_ALL, VIRTKEY, CONTROL, NOINVERT + "C", IDM_EDIT_COPY, VIRTKEY, CONTROL, NOINVERT + "F", IDM_EDIT_FIND, VIRTKEY, CONTROL, NOINVERT + "I", IDM_FILE_ARCHIVEINFO, VIRTKEY, CONTROL, NOINVERT + "N", IDM_FILE_NEW_ARCHIVE, VIRTKEY, CONTROL, NOINVERT + "O", IDM_FILE_OPEN, VIRTKEY, CONTROL, NOINVERT + "O", IDM_FILE_OPEN_VOLUME, VIRTKEY, SHIFT, CONTROL, + NOINVERT + "P", IDM_FILE_PRINT, VIRTKEY, CONTROL, NOINVERT + "S", IDM_FILE_SAVE, VIRTKEY, CONTROL, NOINVERT + "V", IDM_EDIT_PASTE, VIRTKEY, CONTROL, NOINVERT + VK_DELETE, IDM_ACTIONS_DELETE, VIRTKEY, NOINVERT + VK_F1, IDHELP, VIRTKEY, NOINVERT + VK_F4, IDM_FILE_CLOSE, VIRTKEY, CONTROL, NOINVERT + VK_TAB, IDM_ACTIONS_VIEW, VIRTKEY, NOINVERT + "W", IDM_FILE_CLOSE, VIRTKEY, CONTROL, NOINVERT +END + + +///////////////////////////////////////////////////////////////////////////// +// +// Dialog +// + +IDD_ABOUTDLG DIALOGEX 0, 0, 212, 154 +STYLE DS_MODALFRAME | DS_CENTER | WS_POPUP | WS_VISIBLE | WS_CAPTION | + WS_SYSMENU +CAPTION "About CiderPress" +FONT 8, "MS Sans Serif" +BEGIN + DEFPUSHBUTTON "OK",IDOK,7,132,50,14 + PUSHBUTTON "Credits",IDC_ABOUT_CREDITS,63,132,50,14,0,0, + HIDC_ABOUT_CREDITS + PUSHBUTTON "Enter registration code",IDC_ABOUT_ENTER_REG,119,132,86, + 14 + CONTROL 106,IDC_STATIC,"Static",SS_BITMAP,7,6,64,59 + LTEXT "CiderPress v%d.%d.%d%s%s",IDC_CIDERPRESS_VERS_TEXT,77,7, + 128,9 + LTEXT "Copyright © 2007 by FaddenSoft, LLC.\rAll Rights Reserved.", + IDC_STATIC,77,20,128,19 + ICON IDR_MAINFRAME,IDC_STATIC,77,45,21,20 + ICON IDI_FILE_NUFX,IDC_STATIC,106,45,21,20 + ICON IDI_FILE_BINARY2,IDC_STATIC,136,45,21,20 + ICON IDI_FILE_DISKIMAGE,IDC_STATIC,166,45,21,20 + GROUPBOX "Libraries",IDC_STATIC,7,71,198,56 + LTEXT "Using NufxLib DLL v%d.%d.%d",IDC_NUFXLIB_VERS_TEXT,16, + 82,170,10 + LTEXT "Using DiskImg DLL v%d.%d.%d",IDC_DISKIMG_VERS_TEXT,16, + 92,172,8 + LTEXT "Using zlib DLL v%s",IDC_ZLIB_VERS_TEXT,16,103,172,8 + LTEXT "Using ASPI DLL v%s",IDC_ASPI_VERS_TEXT,16,114,173,8 +END + +IDD_PREF_GENERAL DIALOG DISCARDABLE 0, 0, 263, 180 +STYLE DS_CONTEXTHELP | WS_CHILD | WS_DISABLED | WS_CAPTION +CAPTION "General" +FONT 8, "MS Sans Serif" +BEGIN + GROUPBOX "Columns",IDC_STATIC,4,7,80,144 + CONTROL "Pathname",IDC_COL_PATHNAME,"Button",BS_AUTOCHECKBOX | + WS_DISABLED | WS_TABSTOP,12,19,65,10 + CONTROL "Type",IDC_COL_TYPE,"Button",BS_AUTOCHECKBOX | + WS_TABSTOP,12,31,66,10 + CONTROL "Aux Type",IDC_COL_AUXTYPE,"Button",BS_AUTOCHECKBOX | + WS_TABSTOP,12,43,65,10 + CONTROL "Mod Date",IDC_COL_MODDATE,"Button",BS_AUTOCHECKBOX | + WS_TABSTOP,12,55,66,10 + CONTROL "Format",IDC_COL_FORMAT,"Button",BS_AUTOCHECKBOX | + WS_TABSTOP,12,67,68,10 + CONTROL "Size",IDC_COL_SIZE,"Button",BS_AUTOCHECKBOX | + WS_TABSTOP,12,79,66,10 + CONTROL "Ratio",IDC_COL_RATIO,"Button",BS_AUTOCHECKBOX | + WS_TABSTOP,12,91,67,10 + CONTROL "Packed",IDC_COL_PACKED,"Button",BS_AUTOCHECKBOX | + WS_TABSTOP,12,103,66,10 + CONTROL "Access",IDC_COL_ACCESS,"Button",BS_AUTOCHECKBOX | + WS_TABSTOP,12,115,67,10 + PUSHBUTTON "&Defaults",IDC_COL_DEFAULTS,12,131,63,13 + GROUPBOX "NuFX (ShrinkIt) archives",IDC_STATIC,96,7,162,51 + CONTROL "&Mimic ShrinkIt quirks",IDC_PREF_SHRINKIT_COMPAT,"Button", + BS_AUTOCHECKBOX | WS_TABSTOP,104,19,142,10 + CONTROL "Handle ""&bad Mac"" archives",IDC_PREF_SHK_BAD_MAC, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,104,31,149,10 + CONTROL "Reduce error checking (not recommended)", + IDC_PREF_REDUCE_SHK_ERROR_CHECKS,"Button", + BS_AUTOCHECKBOX | WS_TABSTOP,104,43,151,10 + GROUPBOX "Filename munging",IDC_STATIC,96,60,162,39 + CONTROL "Display &DOS 3.3 filenames in lower case", + IDC_PREF_COERCE_DOS,"Button",BS_AUTOCHECKBOX | + WS_TABSTOP,104,72,144,8 + CONTROL "Show spaces as &underscores",IDC_PREF_SPACES_TO_UNDER, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,104,84,145,10 + GROUPBOX "System",IDC_STATIC,96,101,162,33 + PUSHBUTTON "File type &associations...",IDC_PREF_ASSOCIATIONS,104, + 113,92,14 + GROUPBOX "Miscellaneous",IDC_STATIC,96,136,162,38 + CONTROL "Strip pathnames when pasting files", + IDC_PREF_PASTE_JUNKPATHS,"Button",BS_AUTOCHECKBOX | + WS_TABSTOP,104,147,144,10 + CONTROL "Beep when actions complete successfully", + IDC_PREF_SUCCESS_BEEP,"Button",BS_AUTOCHECKBOX | + WS_TABSTOP,104,159,145,10 +END + +IDD_PREF_COMPRESSION DIALOGEX 0, 0, 212, 212 +STYLE DS_CONTEXTHELP | WS_CHILD | WS_DISABLED | WS_CAPTION +CAPTION "Compression" +FONT 8, "MS Sans Serif", 0, 0, 0x1 +BEGIN + LTEXT "Default compression method:",IDC_STATIC,4,6,184,10 + CONTROL "No compression",IDC_DEFC_UNCOMPRESSED,"Button", + BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,4,18,92,8 + CONTROL "Squeeze",IDC_DEFC_SQUEEZE,"Button",BS_AUTORADIOBUTTON | + WS_TABSTOP,4,30,92,8 + CONTROL "Dynamic LZW/1",IDC_DEFC_LZW1,"Button", + BS_AUTORADIOBUTTON | WS_TABSTOP,4,57,96,8 + CONTROL "Dynamic LZW/2 (recommended)",IDC_DEFC_LZW2,"Button", + BS_AUTORADIOBUTTON | WS_TABSTOP,4,78,152,8 + CONTROL "12-bit LZC",IDC_DEFC_LZC12,"Button",BS_AUTORADIOBUTTON | + WS_TABSTOP,4,99,92,8 + CONTROL "16-bit LZC",IDC_DEFC_LZC16,"Button",BS_AUTORADIOBUTTON | + WS_TABSTOP,4,126,96,8 + CONTROL "Deflate",IDC_DEFC_DEFLATE,"Button",BS_AUTORADIOBUTTON | + WS_TABSTOP,4,153,100,8 + CONTROL "Bzip2",IDC_DEFC_BZIP2,"Button",BS_AUTORADIOBUTTON | + WS_TABSTOP,4,180,108,8 + LTEXT "Uses a combination of RLE and Huffman.\rNot compatible with ProDOS 8 ShrinkIt.", + IDC_STATIC,16,39,192,18 + LTEXT "The compression method used by ProDOS 8 ShrinkIt.", + IDC_STATIC,16,66,180,10,0,0,HIDC_STATIC + LTEXT "The compression method used by GS/ShrinkIt.",IDC_STATIC, + 16,87,185,8,0,0,HIDC_STATIC + LTEXT "Compression used by UNIX ""compress"" command.\rNot compatible with ProDOS 8 ShrinkIt.", + IDC_STATIC,16,108,190,18 + LTEXT "Compression used by UNIX ""compress"" command.\rNot compatible with ProDOS 8 ShrinkIt.", + IDC_STATIC,16,135,195,18 + LTEXT "Compression used by ZIP and gzip.\rNot compatible with any Apple II applications.", + IDC_STATIC,16,162,196,18 + LTEXT "Compression used by bzip2.\rNot compatible with any Apple II applications.", + IDC_STATIC,16,189,196,18 +END + +IDD_FILE_VIEWER DIALOGEX 0, 0, 486, 257 +STYLE DS_3DLOOK | WS_MAXIMIZEBOX | WS_POPUP | WS_VISIBLE | WS_CLIPCHILDREN | + WS_CAPTION | WS_SYSMENU | WS_THICKFRAME +CAPTION "File Viewer" +FONT 8, "MS Sans Serif", 0, 0, 0x1 +BEGIN + DEFPUSHBUTTON "&Done",IDOK,432,224,50,14,WS_GROUP + CONTROL "",IDC_FVIEW_EDITBOX,"RICHEDIT",ES_MULTILINE | + ES_AUTOVSCROLL | ES_AUTOHSCROLL | ES_READONLY | + WS_BORDER | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP,4,3,478, + 217,WS_EX_CLIENTEDGE + COMBOBOX IDC_FVIEW_FORMATSEL,152,225,148,54,CBS_DROPDOWNLIST | + WS_VSCROLL | WS_TABSTOP + CONTROL "Data fork",IDC_FVIEW_DATA,"Button",BS_AUTORADIOBUTTON | + WS_GROUP | WS_TABSTOP,5,224,68,10 + CONTROL "Resource fork",IDC_FVIEW_RSRC,"Button", + BS_AUTORADIOBUTTON | WS_TABSTOP,5,235,69,10 + CONTROL "Comment",IDC_FVIEW_CMMT,"Button",BS_AUTORADIOBUTTON | + WS_TABSTOP,5,246,69,10 + PUSHBUTTON "&Next",IDC_FVIEW_NEXT,82,224,50,14,WS_GROUP + PUSHBUTTON "&Prev",IDC_FVIEW_PREV,82,242,50,14 + PUSHBUTTON "Best",IDC_FVIEW_FMT_BEST,151,242,32,14,WS_GROUP + PUSHBUTTON "&Hex",IDC_FVIEW_FMT_HEX,187,242,32,14 + PUSHBUTTON "&Raw",IDC_FVIEW_FMT_RAW,223,242,32,14 + PUSHBUTTON "&Find...",IDC_FVIEW_FIND,320,224,50,14 + PUSHBUTTON "Prin&t",IDC_FVIEW_PRINT,320,242,50,14 + PUSHBUTTON "F&ont",IDC_FVIEW_FONT,376,224,50,14 + PUSHBUTTON "Help",IDHELP,376,242,50,14 +END + +IDD_PREF_FVIEW DIALOG DISCARDABLE 0, 0, 216, 263 +STYLE DS_CONTEXTHELP | WS_CHILD | WS_CAPTION +CAPTION "File Viewer" +FONT 8, "MS Sans Serif" +BEGIN + GROUPBOX "Converters",IDC_STATIC,4,7,208,139 + CONTROL "High-ASCII &text",IDC_PVIEW_HITEXT,"Button", + BS_AUTOCHECKBOX | WS_TABSTOP,12,19,88,10 + CONTROL "&CP/M text",IDC_PVIEW_CPMTEXT,"Button",BS_AUTOCHECKBOX | + WS_TABSTOP,12,30,92,10 + CONTROL "&Pascal text",IDC_PVIEW_PASCALTEXT,"Button", + BS_AUTOCHECKBOX | WS_TABSTOP,12,41,87,10 + CONTROL "Pascal &code",IDC_PVIEW_PASCALCODE,"Button", + BS_AUTOCHECKBOX | WS_TABSTOP,12,52,89,10 + CONTROL "&Applesoft BASIC",IDC_PVIEW_APPLESOFT,"Button", + BS_AUTOCHECKBOX | WS_TABSTOP,12,63,88,10 + CONTROL "&Integer BASIC",IDC_PVIEW_INTEGER,"Button", + BS_AUTOCHECKBOX | WS_TABSTOP,12,74,88,10 + CONTROL "Assembly source",IDC_PVIEW_SCASSEM,"Button", + BS_AUTOCHECKBOX | WS_TABSTOP,12,85,94,10 + CONTROL "ProDOS folders",IDC_PVIEW_PRODOSFOLDER,"Button", + BS_AUTOCHECKBOX | WS_TABSTOP,12,96,88,10 + CONTROL "Disassemble code",IDC_PVIEW_DISASM,"Button", + BS_AUTOCHECKBOX | WS_TABSTOP,12,107,91,10 + CONTROL "Resource forks",IDC_PVIEW_RESOURCES,"Button", + BS_AUTOCHECKBOX | WS_TABSTOP,12,118,90,10 + CONTROL "GWP (Teach, AWGS)",IDC_PVIEW_GWP,"Button", + BS_AUTOCHECKBOX | WS_TABSTOP,112,19,92,10 + CONTROL "8-bit word processor",IDC_PVIEW_TEXT8,"Button", + BS_AUTOCHECKBOX | WS_TABSTOP,112,30,94,10 + CONTROL "AppleWorks &WP",IDC_PVIEW_AWP,"Button",BS_AUTOCHECKBOX | + WS_TABSTOP,112,41,88,10 + CONTROL "AppleWorks &DB",IDC_PVIEW_ADB,"Button",BS_AUTOCHECKBOX | + WS_TABSTOP,112,52,94,10 + CONTROL "AppleWorks &SS",IDC_PVIEW_ASP,"Button",BS_AUTOCHECKBOX | + WS_TABSTOP,112,63,94,10 + CONTROL "&Hi-Res images",IDC_PVIEW_HIRES,"Button", + BS_AUTOCHECKBOX | WS_TABSTOP,112,74,90,10 + CONTROL "&Double Hi-Res images",IDC_PVIEW_DHR,"Button", + BS_AUTOCHECKBOX | WS_TABSTOP,112,85,86,10 + CONTROL "&Super Hi-Res images",IDC_PVIEW_SHR,"Button", + BS_AUTOCHECKBOX | WS_TABSTOP,112,96,89,10 + CONTROL "Print Shop graphics",IDC_PVIEW_PRINTSHOP,"Button", + BS_AUTOCHECKBOX | WS_TABSTOP,112,107,77,10 + CONTROL "MacPaint images",IDC_PVIEW_MACPAINT,"Button", + BS_AUTOCHECKBOX | WS_TABSTOP,112,118,93,10 + CONTROL "&Relax type-checking on graphics",IDC_PVIEW_RELAX_GFX, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,50,131,116,10 + GROUPBOX "Conversion options",IDC_STATIC,4,150,208,90 + CONTROL "&Scroll horizontally instead of wrapping words", + IDC_PVIEW_NOWRAP_TEXT,"Button",BS_AUTOCHECKBOX | + WS_TABSTOP,12,162,192,10 + CONTROL "Highlight &hex dump columns (small files)", + IDC_PVIEW_BOLD_HEXDUMP,"Button",BS_AUTOCHECKBOX | + WS_TABSTOP,12,173,156,10 + CONTROL "Prefer syntax &highlighting on BASIC programs", + IDC_PVIEW_BOLD_BASIC,"Button",BS_AUTOCHECKBOX | + WS_TABSTOP,12,184,187,10 + CONTROL "Disassemble BRK/COP as single-byte instructions", + IDC_PVIEW_DISASM_ONEBYTEBRKCOP,"Button",BS_AUTOCHECKBOX | + WS_TABSTOP,12,195,189,10 + CONTROL "Prefer &B&&W for hi-res images",IDC_PVIEW_HIRES_BW, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,206,148,10 + LTEXT "Preferred DHR mode:",IDC_STATIC,12,223,69,8 + COMBOBOX IDC_PVIEW_DHR_CONV_COMBO,84,220,118,48,CBS_DROPDOWNLIST | + CBS_SORT | WS_VSCROLL | WS_TABSTOP + LTEXT "Viewer file size &limit:",IDC_STATIC,4,247,62,8 + EDITTEXT IDC_PVIEW_SIZE_EDIT,69,245,40,14,ES_AUTOHSCROLL | + ES_NUMBER + CONTROL "Spin2",IDC_PVIEW_SIZE_SPIN,"msctls_updown32", + UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | + UDS_ARROWKEYS | UDS_NOTHOUSANDS,112,247,11,14 + LTEXT "KBytes",IDC_STATIC,110,247,36,8 +END + +IDD_DISKEDIT DIALOGEX 0, 0, 458, 197 +STYLE DS_MODALFRAME | DS_3DLOOK | DS_CONTEXTHELP | WS_POPUP | WS_VISIBLE | + WS_CAPTION | WS_SYSMENU +CAPTION "Disk Edit" +FONT 8, "MS Sans Serif", 0, 0, 0x1 +BEGIN + CONTROL "",IDC_DISKEDIT_EDIT,"RICHEDIT",ES_MULTILINE | + ES_AUTOVSCROLL | WS_BORDER | WS_VSCROLL | WS_TABSTOP,4, + 12,384,164,WS_EX_CLIENTEDGE,HIDC_DISKEDIT_EDIT + PUSHBUTTON "&Done",IDC_DISKEDIT_DONE,4,180,50,14,0,0, + HIDC_DISKEDIT_DONE + PUSHBUTTON "&Open File",IDC_DISKEDIT_OPENFILE,60,180,50,14,0,0, + HIDC_DISKEDIT_OPENFILE + EDITTEXT IDC_DISKEDIT_TRACK,397,20,49,14,ES_AUTOHSCROLL,0, + HIDC_DISKEDIT_TRACK + CONTROL "Spin1",IDC_DISKEDIT_TRACKSPIN,"msctls_updown32", + UDS_ALIGNRIGHT | UDS_ARROWKEYS | UDS_NOTHOUSANDS,434,27, + 11,14 + EDITTEXT IDC_DISKEDIT_SECTOR,397,49,49,14,ES_AUTOHSCROLL,0, + HIDC_DISKEDIT_SECTOR + CONTROL "Spin2",IDC_DISKEDIT_SECTORSPIN,"msctls_updown32", + UDS_ALIGNRIGHT | UDS_ARROWKEYS | UDS_NOTHOUSANDS,434,58, + 11,14 + CONTROL "&Hex",IDC_DISKEDIT_HEX,"Button",BS_AUTOCHECKBOX | + WS_TABSTOP,399,68,44,10,0,HIDC_DISKEDIT_HEX + PUSHBUTTON "&Read",IDC_DISKEDIT_DOREAD,399,89,50,14,0,0, + HIDC_DISKEDIT_DOREAD + PUSHBUTTON "&Write",IDC_DISKEDIT_DOWRITE,399,105,50,14,0,0, + HIDC_DISKEDIT_DOWRITE + PUSHBUTTON "Read &Prev",IDC_DISKEDIT_PREV,399,132,50,14,0,0, + HIDC_DISKEDIT_PREV + PUSHBUTTON "Read &Next",IDC_DISKEDIT_NEXT,399,148,50,14,0,0, + HIDC_DISKEDIT_NEXT + LTEXT "Track:",IDC_STEXT_TRACK,396,12,58,8,0,0, + HIDC_STEXT_TRACK + LTEXT "Sector:",IDC_STEXT_SECTOR,396,41,52,8,0,0, + HIDC_STEXT_SECTOR + PUSHBUTTON "Sub &Volume",IDC_DISKEDIT_SUBVOLUME,116,180,50,14,0,0, + HIDC_DISKEDIT_SUBVOLUME + PUSHBUTTON "Help",IDHELP,172,180,50,14,0,0,HIDHELP + LTEXT " 0",IDC_STATIC,32,4,8,8 + LTEXT " 1",IDC_STATIC,48,4,8,8 + LTEXT " 2",IDC_STATIC,64,4,8,8 + LTEXT " 3",IDC_STATIC,80,4,8,8 + LTEXT " 4",IDC_STATIC,96,4,8,8 + LTEXT " 5",IDC_STATIC,112,4,8,8 + LTEXT " 6",IDC_STATIC,128,4,8,8 + LTEXT " 7",IDC_STATIC,144,4,8,8 + LTEXT " 8",IDC_STATIC,160,4,8,8 + LTEXT " 9",IDC_STATIC,176,4,8,8 + LTEXT " a",IDC_STATIC,192,4,8,8 + LTEXT " b",IDC_STATIC,208,4,8,8 + LTEXT " c",IDC_STATIC,224,4,8,8 + LTEXT " d",IDC_STATIC,240,4,8,8 + LTEXT " e",IDC_STATIC,256,4,8,8 + LTEXT " f",IDC_STATIC,272,4,8,8 + COMBOBOX IDC_DISKEDIT_NIBBLE_PARMS,276,182,112,60, + CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP +END + +IDD_DECONF DIALOG DISCARDABLE 0, 0, 188, 173 +STYLE DS_MODALFRAME | DS_CONTEXTHELP | WS_POPUP | WS_VISIBLE | + WS_CLIPCHILDREN | WS_CAPTION | WS_SYSMENU +CAPTION "Disk Image Characteristics" +FONT 8, "MS Sans Serif" +BEGIN + DEFPUSHBUTTON "OK",IDOK,8,152,50,14 + PUSHBUTTON "Cancel",IDCANCEL,68,152,50,14 + PUSHBUTTON "Help",IDC_DECONF_HELP,128,152,50,14 + LTEXT "File source:",IDC_STATIC,7,10,37,8 + EDITTEXT IDC_DECONF_SOURCE,68,8,112,14,ES_AUTOHSCROLL | + ES_READONLY + LTEXT "File Format:",IDC_STATIC,7,50,54,8 + COMBOBOX IDC_DECONF_FILEFORMAT,68,47,112,30,CBS_DROPDOWNLIST | + WS_DISABLED | WS_VSCROLL | WS_TABSTOP + LTEXT "Physical format:",IDC_STATIC,7,69,56,8 + COMBOBOX IDC_DECONF_PHYSICAL,68,66,112,30,CBS_DROPDOWNLIST | + WS_DISABLED | WS_VSCROLL | WS_TABSTOP + LTEXT "Sector ordering:",IDC_STATIC,7,88,55,8 + COMBOBOX IDC_DECONF_SECTORORDER,68,85,112,53,CBS_DROPDOWNLIST | + WS_VSCROLL | WS_TABSTOP + LTEXT "Filesystem format:",IDC_STATIC,7,107,56,8 + COMBOBOX IDC_DECONF_FSFORMAT,68,104,112,60,CBS_DROPDOWNLIST | + WS_VSCROLL | WS_TABSTOP + CONTROL "blocks",IDC_DECONF_VIEWASBLOCKS,"Button", + BS_AUTORADIOBUTTON,18,135,41,10 + CONTROL "sectors",IDC_DECONF_VIEWASSECTORS,"Button", + BS_AUTORADIOBUTTON,63,135,43,10 + CONTROL "track nibbles",IDC_DECONF_VIEWASNIBBLES,"Button", + BS_AUTORADIOBUTTON,109,135,59,10 + COMBOBOX IDC_DECONF_OUTERFORMAT,68,28,112,49,CBS_DROPDOWNLIST | + WS_DISABLED | WS_VSCROLL | WS_TABSTOP + LTEXT "Outer Format:",IDC_STATIC,7,30,56,8 + LTEXT "View data as...",IDC_DECONF_VIEWAS,7,123,48,8 +END + +IDD_SUBV DIALOG DISCARDABLE 0, 0, 153, 135 +STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU +CAPTION "Select Sub-Volume" +FONT 8, "MS Sans Serif" +BEGIN + DEFPUSHBUTTON "OK",IDOK,21,114,50,14 + PUSHBUTTON "Cancel",IDCANCEL,81,114,50,14 + LISTBOX IDC_SUBV_LIST,7,19,139,87,LBS_USETABSTOPS | + LBS_NOINTEGRALHEIGHT | WS_VSCROLL | WS_TABSTOP + LTEXT "Choose the sub-volume to open:",IDC_STATIC,7,7,103,8 +END + +IDD_DEFILE DIALOG DISCARDABLE 0, 0, 199, 97 +STYLE DS_MODALFRAME | DS_CONTEXTHELP | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Open file on disk image..." +FONT 8, "MS Sans Serif" +BEGIN + LTEXT "Enter the name of the file to open:",IDC_STATIC,7,7,108, + 8 + EDITTEXT IDC_DEFILE_FILENAME,7,19,185,14,ES_AUTOHSCROLL + CONTROL "Open &resource fork instead of data fork", + IDC_DEFILE_RSRC,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,7, + 39,185,10 + DEFPUSHBUTTON "OK",IDOK,45,76,50,14 + PUSHBUTTON "Cancel",IDCANCEL,103,76,50,14 + LTEXT "To open a file in a sub-volume, first open the sub-volume.", + IDC_STATIC,7,59,180,8 +END + +IDD_PREF_FILES DIALOG DISCARDABLE 0, 0, 231, 89 +STYLE DS_CONTEXTHELP | WS_CHILD | WS_DISABLED | WS_CAPTION +CAPTION "Files" +FONT 8, "MS Sans Serif" +BEGIN + LTEXT "Folder for temporary files:",IDC_STATIC,7,13,131,8 + EDITTEXT IDC_PREF_TEMP_FOLDER,7,26,197,14,ES_AUTOHSCROLL + PUSHBUTTON "",IDC_PREF_CHOOSE_TEMP_FOLDER,208,26,16,14,BS_BITMAP | + BS_CENTER | BS_VCENTER + LTEXT "External file viewer extensions (separated with semicolons):", + IDC_STATIC,7,55,217,11 + EDITTEXT IDC_PREF_EXTVIEWER_EXTS,7,68,197,14,ES_AUTOHSCROLL +END + +IDD_CHOOSEDIR DIALOGEX 0, 0, 255, 253 +STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU +EXSTYLE WS_EX_CONTEXTHELP +CAPTION "Choose folder" +FONT 8, "MS Sans Serif", 0, 0, 0x1 +BEGIN + DEFPUSHBUTTON "Select",IDOK,43,232,50,14 + PUSHBUTTON "Cancel",IDCANCEL,103,232,50,14 + PUSHBUTTON "Help",IDHELP,162,232,50,14 + LTEXT "Select the folder to use:",IDC_STATIC,7,7,76,8 + CONTROL "Tree1",IDC_CHOOSEDIR_TREE,"SysTreeView32", + TVS_HASBUTTONS | TVS_HASLINES | TVS_LINESATROOT | + TVS_SHOWSELALWAYS | WS_BORDER | WS_TABSTOP,7,17,241,155, + WS_EX_CLIENTEDGE + EDITTEXT IDC_CHOOSEDIR_PATH,7,174,221,14,ES_AUTOHSCROLL | + ES_READONLY + PUSHBUTTON "IDB_NEW_FOLDER",IDC_CHOOSEDIR_NEW_FOLDER,232,174,16,14, + BS_BITMAP | BS_CENTER | BS_VCENTER + LTEXT "Or type the folder name and hit ""Expand Tree"":", + IDC_STATIC,7,198,149,8 + EDITTEXT IDC_CHOOSEDIR_PATHEDIT,7,208,183,14,ES_AUTOHSCROLL + PUSHBUTTON "Expand Tree",IDC_CHOOSEDIR_EXPAND_TREE,198,208,50,14 +END + +IDD_NEWFOLDER DIALOG DISCARDABLE 0, 0, 251, 69 +STYLE DS_MODALFRAME | DS_CONTEXTHELP | WS_POPUP | WS_VISIBLE | WS_CAPTION | + WS_SYSMENU +CAPTION "Create New Folder" +FONT 8, "MS Sans Serif" +BEGIN + EDITTEXT IDC_NEWFOLDER_NAME,6,47,177,14,ES_AUTOHSCROLL + DEFPUSHBUTTON "OK",IDOK,193,7,50,14 + PUSHBUTTON "Cancel",IDCANCEL,193,24,50,14 + LTEXT "Current folder:",IDC_STATIC,6,7,177,8 + EDITTEXT IDC_NEWFOLDER_CURDIR,6,16,177,14,ES_AUTOHSCROLL | + ES_READONLY + LTEXT "New folder name (will be created in current folder):", + IDC_STATIC,6,37,177,8 +END + +IDD_EXTRACT_FILES DIALOG DISCARDABLE 0, 0, 299, 242 +STYLE DS_MODALFRAME | DS_CONTEXTHELP | WS_POPUP | WS_VISIBLE | WS_CAPTION | + WS_SYSMENU +CAPTION "Extract Files" +FONT 8, "MS Sans Serif" +BEGIN + DEFPUSHBUTTON "Extract",IDOK,68,220,50,14 + PUSHBUTTON "Cancel",IDCANCEL,124,220,50,14 + PUSHBUTTON "Help",IDHELP,180,220,50,14 + LTEXT "Folder where files will be extracted:",IDC_STATIC,8,8, + 143,8 + EDITTEXT IDC_EXT_PATH,8,18,263,14,ES_AUTOHSCROLL | WS_GROUP + PUSHBUTTON "",IDC_EXT_CHOOSE_FOLDER,276,18,16,14,BS_BITMAP | + BS_CENTER | BS_VCENTER | WS_GROUP + GROUPBOX "Files to extract",IDC_STATIC,8,40,140,35 + CONTROL ">Extract 65536 selected files<",IDC_EXT_SELECTED,"Button", + BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,16,50,124,10 + CONTROL "Extract &all files",IDC_EXT_ALL,"Button", + BS_AUTORADIOBUTTON | WS_TABSTOP,16,61,124,10 + GROUPBOX "Parts to extract",IDC_STATIC,152,40,140,47,WS_GROUP + CONTROL "&Data forks",IDC_EXT_DATAFORK,"Button",BS_AUTOCHECKBOX | + WS_GROUP | WS_TABSTOP,160,50,128,10 + CONTROL "&Resource forks",IDC_EXT_RSRCFORK,"Button", + BS_AUTOCHECKBOX | WS_GROUP | WS_TABSTOP,160,61,128,10 + CONTROL "Disk &images",IDC_EXT_DISKIMAGE,"Button", + BS_AUTOCHECKBOX | WS_TABSTOP,160,72,128,10 + GROUPBOX "Format conversion",IDC_STATIC,8,77,140,37 + CONTROL "&Convert to non-Apple II formats",IDC_EXT_REFORMAT, + "Button",BS_AUTOCHECKBOX | WS_GROUP | WS_TABSTOP,16,88, + 124,10 + CONTROL "Extract disks as .&2MG",IDC_EXT_DISK_2MG,"Button", + BS_AUTOCHECKBOX | WS_TABSTOP,16,99,124,10 + GROUPBOX "Filenames",IDC_STATIC,152,98,140,40 + CONTROL "Add file attribute &preservation",IDC_EXT_ADD_PRESERVE, + "Button",BS_AUTOCHECKBOX | WS_GROUP | WS_TABSTOP,160,110, + 128,10 + CONTROL "Add type &extension",IDC_EXT_ADD_EXTEN,"Button", + BS_AUTOCHECKBOX | WS_GROUP | WS_TABSTOP,160,122,128,10 + GROUPBOX "Text conversion",IDC_STATIC,8,117,140,70,WS_GROUP + CONTROL "&Don't convert text files",IDC_EXT_CONVEOLNONE,"Button", + BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,16,127,124,10 + CONTROL "Convert text files by file type",IDC_EXT_CONVEOLTYPE, + "Button",BS_AUTORADIOBUTTON | WS_TABSTOP,16,138,126,10 + CONTROL "Auto-detect && &convert files with text", + IDC_EXT_CONVEOLTEXT,"Button",BS_AUTORADIOBUTTON | + WS_TABSTOP,16,149,126,10 + CONTROL "Convert &ALL files",IDC_EXT_CONVEOLALL,"Button", + BS_AUTORADIOBUTTON | WS_TABSTOP,16,160,124,10 + CONTROL "Strip ""&high ASCII"" files",IDC_EXT_CONVHIGHASCII, + "Button",BS_AUTOCHECKBOX | WS_GROUP | WS_TABSTOP,16,171, + 124,10 + GROUPBOX "Miscellaneous",IDC_STATIC,152,151,140,36,WS_GROUP + CONTROL "&Strip folder names",IDC_EXT_STRIP_FOLDER,"Button", + BS_AUTOCHECKBOX | WS_GROUP | WS_TABSTOP,160,161,128,10 + CONTROL "&Overwrite existing files",IDC_EXT_OVERWRITE_EXIST, + "Button",BS_AUTOCHECKBOX | WS_GROUP | WS_TABSTOP,160,172, + 128,10 + PUSHBUTTON "Configure to preserve Apple II formats", + IDC_EXT_CONFIG_PRESERVE,8,196,140,16 + PUSHBUTTON "Configure for easy access in Windows", + IDC_EXT_CONFIG_CONVERT,152,196,140,16 +END + +IDD_ACTION_PROGRESS DIALOG DISCARDABLE 0, 0, 276, 111 +STYLE DS_CENTER | WS_POPUP | WS_VISIBLE | WS_CLIPSIBLINGS | WS_BORDER +FONT 8, "MS Sans Serif" +BEGIN + CONTROL "Progress1",IDC_PROG_PROGRESS,"msctls_progress32", + WS_BORDER,7,65,262,14 + PUSHBUTTON "Cancel",IDCANCEL,112,90,50,14 + LTEXT "Now extracting:",IDC_PROG_VERB,7,7,113,8 + LTEXT ">source<",IDC_PROG_ARC_NAME,7,17,262,8 + LTEXT "To:",IDC_PROG_TOFROM,7,36,115,8 + LTEXT ">target<",IDC_PROG_FILE_NAME,7,47,262,8 +END + +IDD_CONFIRM_OVERWRITE DIALOG DISCARDABLE 0, 0, 292, 105 +STYLE DS_MODALFRAME | DS_CONTEXTHELP | WS_POPUP | WS_VISIBLE | WS_CAPTION | + WS_SYSMENU +CAPTION "Confirm Overwrite" +FONT 8, "MS Sans Serif" +BEGIN + PUSHBUTTON "Yes",IDC_OVWR_YES,8,64,50,14 + PUSHBUTTON "Yes to All",IDC_OVWR_YESALL,64,64,50,14 + PUSHBUTTON "No",IDC_OVWR_NO,120,64,50,14 + PUSHBUTTON "No to All",IDC_OVWR_NOALL,176,64,50,14 + PUSHBUTTON "Rename",IDC_OVWR_RENAME,232,64,50,14 + PUSHBUTTON "Cancel",IDCANCEL,120,84,50,14 + LTEXT "Replace file:",IDC_STATIC,8,8,40,8 + LTEXT ">existing file name<",IDC_OVWR_EXIST_NAME,52,8,228,8 + LTEXT ">existing file info<",IDC_OVWR_EXIST_INFO,52,20,228,8 + LTEXT "With file:",IDC_STATIC,8,36,28,8 + LTEXT ">new file name<",IDC_OVWR_NEW_NAME,52,36,228,8 + LTEXT ">new file info<",IDC_OVWR_NEW_INFO,52,48,228,8 +END + +IDD_RENAME_OVERWRITE DIALOG DISCARDABLE 0, 0, 316, 124 +STYLE DS_MODALFRAME | DS_CONTEXTHELP | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Rename File" +FONT 8, "MS Sans Serif" +BEGIN + DEFPUSHBUTTON "OK",IDOK,103,103,50,14 + PUSHBUTTON "Cancel",IDCANCEL,162,103,50,14 + LTEXT "Original name:",IDC_STATIC,8,8,46,8 + LTEXT ">original name<",IDC_RENOVWR_SOURCE_NAME,8,20,301,8 + EDITTEXT IDC_RENOVWR_ORIG_NAME,8,48,302,14,ES_AUTOHSCROLL | + ES_READONLY + LTEXT "Full pathname:",IDC_STATIC,8,36,47,8 + LTEXT "New pathname:",IDC_STATIC,8,68,51,8 + EDITTEXT IDC_RENOVWR_NEW_NAME,8,80,302,14,ES_AUTOHSCROLL +END + +IDD_ADD_FILES DIALOGEX 0, 0, 292, 231 +STYLE DS_3DLOOK | DS_CONTROL | DS_CONTEXTHELP | WS_CHILD | WS_VISIBLE | + WS_CLIPSIBLINGS +FONT 8, "MS Sans Serif", 0, 0, 0x1 +BEGIN + LTEXT "",1119,0,0,291,87,NOT WS_GROUP,WS_EX_STATICEDGE + PUSHBUTTON "&Accept",IDC_SELECT_ACCEPT,241,88,50,14 + PUSHBUTTON "Cancel",IDCANCEL,241,106,50,14 + PUSHBUTTON "Help",IDHELP,241,124,50,14 + GROUPBOX "File attribute preservation",IDC_ADDFILES_STATIC1,4,112, + 169,47 + CONTROL "&Ignore file attribute preservation tags", + IDC_ADDFILES_NOPRESERVE,"Button",BS_AUTORADIOBUTTON | + WS_GROUP | WS_TABSTOP,8,122,157,10 + CONTROL "&Use file attribute preservation tags", + IDC_ADDFILES_PRESERVE,"Button",BS_AUTORADIOBUTTON | + WS_TABSTOP,8,133,152,10 + CONTROL "Use tags and &guess type from extension", + IDC_ADDFILES_PRESERVEPLUS,"Button",BS_AUTORADIOBUTTON | + WS_TABSTOP,8,144,150,10 + GROUPBOX "Text conversion",IDC_ADDFILES_STATIC4,4,168,169,56 + CONTROL "&Don't convert text files",IDC_ADDFILES_CONVEOLNONE, + "Button",BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,8, + 178,146,10 + CONTROL "Convert text files by file type", + IDC_ADDFILES_CONVEOLTYPE,"Button",BS_AUTORADIOBUTTON | + WS_TABSTOP,8,189,153,10 + CONTROL "Auto-detect && &convert files with text", + IDC_ADDFILES_CONVEOLTEXT,"Button",BS_AUTORADIOBUTTON | + WS_TABSTOP,8,200,148,10 + CONTROL "Convert &ALL files",IDC_ADDFILES_CONVEOLALL,"Button", + BS_AUTORADIOBUTTON | WS_TABSTOP,8,211,141,10 + GROUPBOX "Miscellaneous",IDC_ADDFILES_STATIC2,176,145,115,47 + CONTROL "&Include subfolders",IDC_ADDFILES_INCLUDE_SUBFOLDERS, + "Button",BS_AUTOCHECKBOX | WS_GROUP | WS_TABSTOP,180,155, + 88,10 + CONTROL "&Strip folder names",IDC_ADDFILES_STRIP_FOLDER,"Button", + BS_AUTOCHECKBOX | WS_TABSTOP,180,166,88,10 + CONTROL "&Overwrite existing files",IDC_ADDFILES_OVERWRITE, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,180,177,85,10 + LTEXT "Storage prefix (optional):",IDC_ADDFILES_STATIC3,177, + 198,77,8 + EDITTEXT IDC_ADDFILES_PREFIX,176,210,102,14,ES_AUTOHSCROLL +END + +IDD_USE_SELECTION DIALOG DISCARDABLE 0, 0, 139, 79 +STYLE DS_MODALFRAME | DS_CONTEXTHELP | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Action Files" +FONT 8, "MS Sans Serif" +BEGIN + DEFPUSHBUTTON "Action",IDOK,16,58,50,14,WS_GROUP + PUSHBUTTON "Cancel",IDCANCEL,73,58,50,14 + GROUPBOX "Files",IDC_STATIC,7,7,125,40 + CONTROL ">Action 65536 selected files<",IDC_USE_SELECTED,"Button", + BS_AUTORADIOBUTTON | WS_GROUP,14,19,113,10 + CONTROL "Action &all files",IDC_USE_ALL,"Button", + BS_AUTORADIOBUTTON,14,30,113,10 +END + +IDD_RENAME_ENTRY DIALOG DISCARDABLE 0, 0, 284, 118 +STYLE DS_MODALFRAME | DS_CONTEXTHELP | WS_POPUP | WS_VISIBLE | WS_CAPTION | + WS_SYSMENU +CAPTION "Rename" +FONT 8, "MS Sans Serif" +BEGIN + DEFPUSHBUTTON "OK",IDOK,34,97,50,14 + PUSHBUTTON "Skip",IDC_RENAME_SKIP,89,97,50,14 + PUSHBUTTON "Cancel",IDCANCEL,144,97,50,14 + PUSHBUTTON "Help",IDHELP,199,97,50,14 + LTEXT "Current name:",IDC_STATIC,7,7,126,8 + EDITTEXT IDC_RENAME_OLD,7,18,270,14,ES_AUTOHSCROLL | ES_READONLY + LTEXT "New name:",IDC_STATIC,7,42,125,8 + EDITTEXT IDC_RENAME_NEW,7,52,270,14,ES_AUTOHSCROLL + LTEXT "Path separator character:",IDC_STATIC,7,76,81,8 + EDITTEXT IDC_RENAME_PATHSEP,91,74,11,14,ES_AUTOHSCROLL | NOT + WS_TABSTOP +END + +IDD_COMMENT_EDIT DIALOG DISCARDABLE 0, 0, 362, 151 +STYLE DS_MODALFRAME | DS_CONTEXTHELP | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Edit Comment" +FONT 8, "MS Sans Serif" +BEGIN + EDITTEXT IDC_COMMENT_EDIT,7,7,290,138,ES_MULTILINE | + ES_AUTOVSCROLL | ES_WANTRETURN | WS_VSCROLL + DEFPUSHBUTTON "OK",IDOK,305,7,50,14 + PUSHBUTTON "Cancel",IDCANCEL,305,25,50,14 + PUSHBUTTON "Help",IDHELP,305,43,50,14 + PUSHBUTTON "Delete",IDC_COMMENT_DELETE,305,71,50,14 +END + +IDD_RECOMPRESS_OPTS DIALOG DISCARDABLE 0, 0, 171, 149 +STYLE DS_MODALFRAME | DS_CONTEXTHELP | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Recompress Files" +FONT 8, "MS Sans Serif" +BEGIN + DEFPUSHBUTTON "Action",IDOK,32,128,50,14,WS_GROUP + PUSHBUTTON "Cancel",IDCANCEL,88,128,50,14 + GROUPBOX "Files",IDC_STATIC,7,7,157,40 + CONTROL ">Recompress 65536 selected files<",IDC_USE_SELECTED, + "Button",BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,14, + 19,130,10 + CONTROL "Action &all files",IDC_USE_ALL,"Button", + BS_AUTORADIOBUTTON | WS_TABSTOP,14,30,113,10 + COMBOBOX IDC_RECOMP_COMP,7,65,157,55,CBS_DROPDOWNLIST | + WS_VSCROLL | WS_TABSTOP + LTEXT "New compression type:",IDC_STATIC,7,53,157,8 + LTEXT "IMPORTANT: for broad compatibility, use only Dynamic LZW/2 compression. Other formats will not work with all software, and ""Deflate"" cannot be unpacked on an Apple II.", + IDC_STATIC,7,87,157,35 +END + +IDD_PRINT_CANCEL DIALOG DISCARDABLE 0, 0, 116, 46 +STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION +CAPTION "Printing..." +FONT 8, "MS Sans Serif" +BEGIN + PUSHBUTTON "Cancel",IDCANCEL,33,25,50,14 + LTEXT "Sending file list to the printer.",IDC_STATIC,7,7,102,8 +END + +IDD_ASSOCIATIONS DIALOG DISCARDABLE 0, 0, 258, 281 +STYLE DS_MODALFRAME | DS_CONTEXTHELP | WS_POPUP | WS_VISIBLE | WS_CAPTION | + WS_SYSMENU +CAPTION "CiderPress Associations" +FONT 8, "MS Sans Serif" +BEGIN + DEFPUSHBUTTON "OK",IDOK,201,70,50,14 + PUSHBUTTON "Cancel",IDCANCEL,201,88,50,14 + PUSHBUTTON "Help",IDHELP,201,106,50,14 + LTEXT "Place a checkmark next to the file extensions that you want associated with CiderPress. Opening files with these extensions will cause CiderPress to be launched automatically.", + IDC_STATIC,7,7,244,27 + LTEXT "Press OK in this screen to accept the changes, and hit OK or Apply in the Preferences screen to apply them.", + IDC_STATIC,7,36,244,21 + LTEXT "Current file associations:",IDC_STATIC,7,59,244,8 + CONTROL "List4",IDC_ASSOCIATION_LIST,"SysListView32",LVS_REPORT | + LVS_SINGLESEL | LVS_NOSORTHEADER | WS_BORDER | + WS_TABSTOP,7,70,185,204 +END + +IDD_REGISTRATION DIALOG DISCARDABLE 0, 0, 233, 191 +STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Registration Info" +FONT 8, "MS Sans Serif" +BEGIN + DEFPUSHBUTTON "OK",IDOK,37,170,50,14 + PUSHBUTTON "Cancel",IDCANCEL,92,170,50,14 + PUSHBUTTON "Help",IDHELP,147,170,50,14 + LTEXT "Please enter your registration information. Fill in all fields exactly as they appear on the registration letter. Copying and pasting directly from the registration letter is the easiest way to do this.", + IDC_STATIC,7,7,219,27 + LTEXT "Your name:",IDC_STATIC,7,70,122,8 + EDITTEXT IDC_REGENTER_USER,7,81,181,14,ES_AUTOHSCROLL + LTEXT "Your company:",IDC_STATIC,7,102,119,8 + EDITTEXT IDC_REGENTER_COMPANY,7,113,181,14,ES_AUTOHSCROLL + LTEXT "Registration key:",IDC_STATIC,7,134,128,8 + EDITTEXT IDC_REGENTER_REG,7,145,180,14,ES_AUTOHSCROLL + LTEXT "When you have entered the data, make sure the checksum values match those in the registration letter. If they don't, re-check what you have entered. Punctuation and capitalization are important.", + IDC_STATIC,7,37,219,25 + LTEXT "Checksum",IDC_STATIC,192,70,34,8 + LTEXT "CCCC",IDC_REGENTER_USERCRC,201,85,25,8 + LTEXT "CCCC",IDC_REGENTER_COMPCRC,201,117,25,8 + LTEXT "CCCC",IDC_REGENTER_REGCRC,201,149,25,8 +END + +IDD_DISKCONV DIALOG DISCARDABLE 0, 0, 174, 223 +STYLE DS_MODALFRAME | DS_CONTEXTHELP | WS_POPUP | WS_VISIBLE | WS_CAPTION | + WS_SYSMENU +CAPTION "Convert Disk Image" +FONT 8, "MS Sans Serif" +BEGIN + DEFPUSHBUTTON "OK",IDOK,7,202,50,14 + PUSHBUTTON "Cancel",IDCANCEL,62,202,50,14 + PUSHBUTTON "Help",IDHELP,117,202,50,14 + LTEXT "Image size:",IDC_IMAGE_SIZE_TEXT,7,7,36,8 + LTEXT ">800K floppy<",IDC_IMAGE_TYPE,47,7,120,8 + GROUPBOX "Convert to:",IDC_STATIC,7,20,160,157 + CONTROL "Unadorned DOS-order (.DO)",IDC_DISKCONV_DOS,"Button", + BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,14,31,146,10 + CONTROL "With 2MG header (.2MG)",IDC_DISKCONV_DOS2MG,"Button", + BS_AUTORADIOBUTTON,27,43,133,10 + CONTROL "Unadorned ProDOS-order (.PO)",IDC_DISKCONV_PRODOS, + "Button",BS_AUTORADIOBUTTON,14,55,146,10 + CONTROL "With 2MG header (.2MG)",IDC_DISKCONV_PRODOS2MG,"Button", + BS_AUTORADIOBUTTON,27,67,133,10 + CONTROL "Unadorned nibble (.NIB)",IDC_DISKCONV_NIB,"Button", + BS_AUTORADIOBUTTON,14,79,146,10 + CONTROL "With 2MG header (.2MG)",IDC_DISKCONV_NIB2MG,"Button", + BS_AUTORADIOBUTTON,27,91,133,10 + CONTROL "Unadorned 13-sector disk (.D13)",IDC_DISKCONV_D13, + "Button",BS_AUTORADIOBUTTON,14,103,146,10 + CONTROL "DiskCopy 4.2 disk image (.DSK)",IDC_DISKCONV_DC42, + "Button",BS_AUTORADIOBUTTON,14,115,146,10 + CONTROL "ShrinkIt disk archve (.SDK)",IDC_DISKCONV_SDK,"Button", + BS_AUTORADIOBUTTON,14,127,146,10 + CONTROL "TrackStar image (.APP)",IDC_DISKCONV_TRACKSTAR,"Button", + BS_AUTORADIOBUTTON,14,139,146,10 + CONTROL """Sim //e"" virtual hard drive (.HDV)",IDC_DISKCONV_HDV, + "Button",BS_AUTORADIOBUTTON,14,151,146,10 + CONTROL "DDD Pro (.DDD)",IDC_DISKCONV_DDD,"Button", + BS_AUTORADIOBUTTON,14,163,146,10 + CONTROL "Compress with gzip (.gz)",IDC_DISKCONV_GZIP,"Button", + BS_AUTOCHECKBOX | WS_GROUP | WS_TABSTOP,7,184,91,10 +END + +IDD_DONEOPEN DIALOG DISCARDABLE 0, 0, 119, 55 +STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Success" +FONT 8, "MS Sans Serif" +BEGIN + DEFPUSHBUTTON "Done",IDCANCEL,7,34,50,14 + PUSHBUTTON "&Open Image",IDOK,62,34,50,14 + LTEXT "Would you like to open the disk image you just created?", + IDC_STATIC,7,7,105,25 +END + +IDD_PROPS_EDIT DIALOGEX 0, 0, 239, 157 +STYLE DS_MODALFRAME | DS_CONTEXTHELP | WS_POPUP | WS_VISIBLE | WS_CAPTION | + WS_SYSMENU +CAPTION "Edit Attributes" +FONT 8, "MS Sans Serif", 0, 0, 0x1 +BEGIN + DEFPUSHBUTTON "OK",IDOK,38,135,50,14 + PUSHBUTTON "Cancel",IDCANCEL,94,135,50,14 + PUSHBUTTON "Help",IDHELP,150,135,50,14 + RTEXT "Pathname:",IDC_STATIC,7,9,65,8 + EDITTEXT IDC_PROPS_PATHNAME,79,7,153,14,ES_AUTOHSCROLL | + ES_READONLY + RTEXT "Creation date:",IDC_STATIC,7,26,65,8 + LTEXT ">create date/time<",IDC_PROPS_CREATEWHEN,79,26,153,8 + RTEXT "Modification date:",IDC_STATIC,7,37,65,8 + LTEXT ">mod date/time<",IDC_PROPS_MODWHEN,79,37,153,8 + RTEXT "File type:",IDC_STATIC,7,53,65,8 + COMBOBOX IDC_PROPS_FILETYPE,79,50,52,105,CBS_DROPDOWNLIST | + WS_VSCROLL | WS_TABSTOP + RTEXT "Aux type (hex):",IDC_STATIC,7,69,65,8 + EDITTEXT IDC_PROPS_AUXTYPE,79,67,52,14,ES_AUTOHSCROLL + RTEXT "Type description:",IDC_STATIC,7,86,65,8 + LTEXT ">type description<",IDC_PROPS_TYPEDESCR,79,86,153,8 + RTEXT "Access:",IDC_STATIC,7,102,65,8 + CONTROL "Read",IDC_PROPS_ACCESS_R,"Button",BS_AUTOCHECKBOX | + WS_TABSTOP,79,102,33,10 + CONTROL "Backup",IDC_PROPS_ACCESS_B,"Button",BS_AUTOCHECKBOX | + WS_TABSTOP,123,102,41,10 + CONTROL "Invisible",IDC_PROPS_ACCESS_I,"Button",BS_AUTOCHECKBOX | + WS_TABSTOP,175,102,41,10 + CONTROL "Write",IDC_PROPS_ACCESS_W,"Button",BS_AUTOCHECKBOX | + WS_TABSTOP,79,115,33,10 + CONTROL "Rename",IDC_PROPS_ACCESS_N,"Button",BS_AUTOCHECKBOX | + WS_TABSTOP,124,115,43,10 + CONTROL "Delete",IDC_PROPS_ACCESS_D,"Button",BS_AUTOCHECKBOX | + WS_TABSTOP,176,115,37,10 + EDITTEXT IDC_PROPS_HFS_FILETYPE,192,49,40,14,ES_AUTOHSCROLL + EDITTEXT IDC_PROPS_HFS_AUXTYPE,192,67,40,14,ES_AUTOHSCROLL + CONTROL "HFS type:",IDC_PROPS_HFS_MODE,"Button",BS_AUTOCHECKBOX | + WS_TABSTOP,145,51,45,10 + LTEXT "Creator:",IDC_PROPS_HFS_LABEL,156,69,34,8,0,WS_EX_RIGHT +END + +IDD_CONVFILE_OPTS DIALOG DISCARDABLE 0, 0, 171, 93 +STYLE DS_MODALFRAME | DS_CONTEXTHELP | WS_POPUP | WS_VISIBLE | WS_CAPTION | + WS_SYSMENU +CAPTION "Convert to file archive" +FONT 8, "MS Sans Serif" +BEGIN + DEFPUSHBUTTON "Action",IDOK,33,72,50,14,WS_GROUP + PUSHBUTTON "Cancel",IDCANCEL,89,72,50,14 + GROUPBOX "Files",IDC_STATIC,7,7,157,40 + CONTROL ">Convert 65536 selected files<",IDC_USE_SELECTED,"Button", + BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,14,19,130,10 + CONTROL "Convert &all files",IDC_USE_ALL,"Button", + BS_AUTORADIOBUTTON | WS_TABSTOP,14,30,113,10 + CONTROL "Preserve &empty folders",IDC_CONVFILE_PRESERVEDIR, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,7,55,157,10 +END + +IDD_CONVDISK_OPTS DIALOG DISCARDABLE 0, 0, 175, 225 +STYLE DS_MODALFRAME | DS_CONTEXTHELP | WS_POPUP | WS_VISIBLE | WS_CAPTION | + WS_SYSMENU +CAPTION "Convert to ProDOS disk" +FONT 8, "MS Sans Serif" +BEGIN + DEFPUSHBUTTON "Action",IDOK,34,203,50,14,WS_GROUP + PUSHBUTTON "Cancel",IDCANCEL,90,203,50,14 + GROUPBOX "Files",IDC_STATIC,7,7,161,40 + CONTROL ">Convert 65536 selected files<",IDC_USE_SELECTED,"Button", + BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,14,19,130,10 + CONTROL "Convert &all files",IDC_USE_ALL,"Button", + BS_AUTORADIOBUTTON | WS_TABSTOP,14,30,113,10 + GROUPBOX "New disk size",IDC_STATIC,7,52,161,103,WS_GROUP + CONTROL "140KB (5.25"" floppy)",IDC_CONVDISK_140K,"Button", + BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,14,63,148,10 + CONTROL "800KB (3.5"" floppy)",IDC_CONVDISK_800K,"Button", + BS_AUTORADIOBUTTON | WS_TABSTOP,14,74,148,10 + CONTROL "1.4MB (3.5"" PC floppy)",IDC_CONVDISK_1440K,"Button", + BS_AUTORADIOBUTTON | WS_TABSTOP,14,85,88,10 + CONTROL "5MB",IDC_CONVDISK_5MB,"Button",BS_AUTORADIOBUTTON | + WS_TABSTOP,14,96,148,10 + CONTROL "16MB",IDC_CONVDISK_16MB,"Button",BS_AUTORADIOBUTTON | + WS_TABSTOP,14,107,148,10 + CONTROL "20MB (same as 20MB floptical)",IDC_CONVDISK_20MB,"Button", + BS_AUTORADIOBUTTON | WS_TABSTOP,14,118,148,10 + CONTROL "32MB (largest ProDOS volume)",IDC_CONVDISK_32MB,"Button", + BS_AUTORADIOBUTTON | WS_TABSTOP,14,129,148,10 + CONTROL "Specify size:",IDC_CONVDISK_SPECIFY,"Button", + BS_AUTORADIOBUTTON | WS_TABSTOP,14,140,55,10 + EDITTEXT IDC_CONVDISK_SPECIFY_EDIT,72,139,43,14,ES_AUTOHSCROLL | + ES_NUMBER | WS_GROUP + LTEXT ">Space required: 1000KB<",IDC_CONVDISK_SPACEREQ,7,161, + 96,8 + PUSHBUTTON "Compute",IDC_CONVDISK_COMPUTE,118,158,50,14 + LTEXT "ProDOS volume name:",IDC_STATIC,7,182,73,8 + EDITTEXT IDC_CONVDISK_VOLNAME,87,180,81,14,ES_AUTOHSCROLL + LTEXT "blocks",IDC_STATIC,118,140,22,8 +END + +IDD_BULKCONV DIALOG DISCARDABLE 0, 0, 237, 57 +STYLE DS_MODALFRAME | DS_3DLOOK | WS_POPUP | WS_VISIBLE | WS_CAPTION +CAPTION "Bulk Conversion Progress" +FONT 8, "MS Sans Serif" +BEGIN + PUSHBUTTON "Cancel",IDCANCEL,93,36,50,14 + LTEXT "Now converting:",IDC_STATIC,7,7,223,8 + LTEXT ">pathname<",IDC_BULKCONV_PATHNAME,7,18,223,8 +END + +IDD_OPENVOLUMEDLG DIALOG DISCARDABLE 0, 0, 300, 166 +STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Select Volume" +FONT 8, "MS Sans Serif" +BEGIN + DEFPUSHBUTTON "OK",IDOK,7,145,50,14 + PUSHBUTTON "Cancel",IDCANCEL,64,145,50,14 + PUSHBUTTON "Help",IDHELP,120,145,50,14 + LTEXT "Show:",IDC_STATIC,7,9,27,8 + COMBOBOX IDC_VOLUME_FILTER,34,7,151,45,CBS_DROPDOWNLIST | + WS_VSCROLL | WS_TABSTOP + CONTROL "List1",IDC_VOLUME_LIST,"SysListView32",LVS_REPORT | + LVS_SINGLESEL | LVS_SHOWSELALWAYS | LVS_NOSORTHEADER | + WS_BORDER | WS_TABSTOP,7,25,286,100 + CONTROL "Open as read-only (writing to the volume will be disabled)", + IDC_OPENVOL_READONLY,"Button",BS_AUTOCHECKBOX | + WS_TABSTOP,7,131,242,10 +END + +IDD_VOLUMECOPYPROG DIALOG DISCARDABLE 0, 0, 276, 111 +STYLE DS_MODALFRAME | DS_3DLOOK | WS_POPUP | WS_VISIBLE | WS_CAPTION +CAPTION "Volume Copy Progress" +FONT 8, "MS Sans Serif" +BEGIN + PUSHBUTTON "Cancel",IDCANCEL,112,90,50,14 + LTEXT "Copy from:",IDC_STATIC,7,7,79,8 + LTEXT "To:",IDC_STATIC,7,36,79,8 + LTEXT ">path-or-vol<",IDC_VOLUMECOPYPROG_FROM,7,17,262,8 + LTEXT ">path-or-vol<",IDC_VOLUMECOPYPROG_TO,7,47,245,8 + CONTROL "Progress1",IDC_VOLUMECOPYPROG_PROGRESS, + "msctls_progress32",PBS_SMOOTH | WS_BORDER,7,65,262,14 +END + +IDD_DISKEDIT_OPENWHICH DIALOG DISCARDABLE 0, 0, 134, 103 +STYLE DS_MODALFRAME | DS_3DLOOK | WS_POPUP | WS_VISIBLE +FONT 8, "MS Sans Serif" +BEGIN + PUSHBUTTON "Open disk image file",IDC_DEOW_FILE,7,7,120,14 + PUSHBUTTON "Open logical or physical volume",IDC_DEOW_VOLUME,7,32, + 120,14 + PUSHBUTTON "Open current archive",IDC_DEOW_CURRENT,7,57,120,14 + PUSHBUTTON "Cancel",IDCANCEL,42,82,50,14 +END + +IDD_VOLUMECOPYSEL DIALOG DISCARDABLE 0, 0, 386, 138 +STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Volume Copy" +FONT 8, "MS Sans Serif" +BEGIN + DEFPUSHBUTTON "Done",IDOK,316,117,63,14 + LTEXT "Choose volume or sub-volume to copy:",IDC_STATIC,7,7, + 293,8 + CONTROL "List2",IDC_VOLUMECOPYSEL_LIST,"SysListView32", + LVS_REPORT | LVS_SINGLESEL | LVS_SHOWSELALWAYS | + LVS_NOSORTHEADER | WS_BORDER | WS_TABSTOP,7,22,297,109 + PUSHBUTTON "Copy &to file",IDC_VOLUEMCOPYSEL_TOFILE,316,22,63,14 + PUSHBUTTON "Load &from file",IDC_VOLUEMCOPYSEL_FROMFILE,316,42,63, + 14 + PUSHBUTTON "Help",IDHELP,316,97,63,14 +END + +IDD_FORMATTING DIALOG DISCARDABLE 0, 0, 146, 38 +STYLE DS_MODALFRAME | DS_3DLOOK | WS_POPUP | WS_VISIBLE +FONT 8, "MS Sans Serif" +BEGIN + CTEXT "Preparing disk image, please wait...",IDC_STATIC,7,15, + 132,8 +END + +IDD_CREATEIMAGE DIALOG DISCARDABLE 0, 0, 342, 222 +STYLE DS_MODALFRAME | DS_3DLOOK | DS_CONTEXTHELP | WS_POPUP | WS_VISIBLE | + WS_CAPTION | WS_SYSMENU +CAPTION "Create Disk Image" +FONT 8, "MS Sans Serif" +BEGIN + DEFPUSHBUTTON "OK",IDOK,88,201,50,14 + PUSHBUTTON "Cancel",IDCANCEL,145,201,50,14 + PUSHBUTTON "Help",IDHELP,202,201,50,14 + GROUPBOX "Filesystem",IDC_STATIC,7,7,160,80 + CONTROL "DOS 3.2 (13-sector)",IDC_CREATEFS_DOS32,"Button", + BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,14,18,86,10 + CONTROL "DOS 3.3",IDC_CREATEFS_DOS33,"Button",BS_AUTORADIOBUTTON | + WS_TABSTOP,14,29,86,10 + CONTROL "ProDOS",IDC_CREATEFS_PRODOS,"Button",BS_AUTORADIOBUTTON | + WS_TABSTOP,14,40,86,10 + CONTROL "Pascal",IDC_CREATEFS_PASCAL,"Button",BS_AUTORADIOBUTTON | + WS_TABSTOP,14,51,86,10 + CONTROL "HFS",IDC_CREATEFS_HFS,"Button",BS_AUTORADIOBUTTON,14,62, + 86,10 + CONTROL "Blank",IDC_CREATEFS_BLANK,"Button",BS_AUTORADIOBUTTON | + WS_TABSTOP,14,73,86,10 + GROUPBOX "New disk size",IDC_STATIC,7,93,160,104,WS_GROUP + CONTROL "140KB (5.25"" floppy)",IDC_CONVDISK_140K,"Button", + BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,14,104,148,10 + CONTROL "800KB (3.5"" floppy)",IDC_CONVDISK_800K,"Button", + BS_AUTORADIOBUTTON | WS_TABSTOP,14,115,148,10 + CONTROL "1.4MB (3.5"" PC floppy)",IDC_CONVDISK_1440K,"Button", + BS_AUTORADIOBUTTON | WS_TABSTOP,14,126,88,10 + CONTROL "5MB",IDC_CONVDISK_5MB,"Button",BS_AUTORADIOBUTTON | + WS_TABSTOP,14,137,148,10 + CONTROL "16MB",IDC_CONVDISK_16MB,"Button",BS_AUTORADIOBUTTON | + WS_TABSTOP,14,148,148,10 + CONTROL "20MB (same as 20MB floptical)",IDC_CONVDISK_20MB,"Button", + BS_AUTORADIOBUTTON | WS_TABSTOP,14,159,148,10 + CONTROL "32MB (largest ProDOS volume)",IDC_CONVDISK_32MB,"Button", + BS_AUTORADIOBUTTON | WS_TABSTOP,14,170,148,10 + CONTROL "Specify size:",IDC_CONVDISK_SPECIFY,"Button", + BS_AUTORADIOBUTTON | WS_TABSTOP,14,181,55,10 + EDITTEXT IDC_CONVDISK_SPECIFY_EDIT,72,180,43,14,ES_AUTOHSCROLL | + ES_NUMBER | WS_GROUP + LTEXT "blocks",IDC_STATIC,118,181,22,8 + GROUPBOX "DOS options",IDC_STATIC,175,7,160,46 + CONTROL "Allocate DOS tracks",IDC_CREATEFSDOS_ALLOCDOS,"Button", + BS_AUTOCHECKBOX | WS_GROUP | WS_TABSTOP,184,20,143,10 + LTEXT "Disk volume:",IDC_STATIC,184,33,46,8 + EDITTEXT IDC_CREATEFSDOS_VOLNUM,235,32,40,14,ES_AUTOHSCROLL | + ES_NUMBER + GROUPBOX "ProDOS options",IDC_STATIC,175,56,160,45 + LTEXT "ProDOS volume name (15 chars):",IDC_STATIC,184,69,143,8 + EDITTEXT IDC_CREATEFSPRODOS_VOLNAME,184,80,115,14,ES_AUTOHSCROLL + GROUPBOX "Pascal options",IDC_STATIC,175,104,160,45 + LTEXT "Pascal volume name (7 chars):",IDC_STATIC,184,116,145,8 + EDITTEXT IDC_CREATEFSPASCAL_VOLNAME,184,127,115,14,ES_UPPERCASE | + ES_AUTOHSCROLL + GROUPBOX "HFS options",IDC_STATIC,175,152,160,45 + EDITTEXT IDC_CREATEFSHFS_VOLNAME,184,178,145,14,ES_AUTOHSCROLL + LTEXT "HFS volume name (27 chars):",IDC_STATIC,184,166,145,8 +END + +IDD_CHOOSE_ADD_TARGET DIALOG DISCARDABLE 0, 0, 220, 279 +STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU +CAPTION "Select location" +FONT 8, "MS Sans Serif" +BEGIN + DEFPUSHBUTTON "OK",IDOK,26,258,50,14 + PUSHBUTTON "Cancel",IDCANCEL,85,258,50,14 + PUSHBUTTON "Help",IDHELP,144,258,50,14 + CONTROL "Tree1",IDC_ADD_TARGET_TREE,"SysTreeView32", + TVS_HASBUTTONS | TVS_HASLINES | TVS_LINESATROOT | + TVS_DISABLEDRAGDROP | TVS_SHOWSELALWAYS | WS_BORDER | + WS_TABSTOP,7,20,206,209 + LTEXT "Select location where files will be added:",IDC_STATIC, + 7,7,194,8 + LTEXT "Tip: you can skip this step on ProDOS disks by selecting a directory from the file list before using ""add files"".", + IDC_STATIC,7,235,206,17 +END + +IDD_ARCHIVEINFO_NUFX DIALOGEX 0, 0, 320, 127 +STYLE DS_MODALFRAME | DS_3DLOOK | WS_POPUP | WS_VISIBLE | WS_CAPTION | + WS_SYSMENU +CAPTION "NuFX (ShrinkIt) Archive" +FONT 8, "MS Sans Serif", 0, 0, 0x1 +BEGIN + DEFPUSHBUTTON "Done",IDOK,164,106,50,14 + LTEXT "Filename:",IDC_STATIC,7,18,68,8,0,WS_EX_RIGHT + LTEXT "Format:",IDC_STATIC,7,29,68,8,0,WS_EX_RIGHT + LTEXT "Master Version:",IDC_STATIC,7,51,68,8,0,WS_EX_RIGHT + LTEXT "Junk Skipped:",IDC_STATIC,7,84,68,8,0,WS_EX_RIGHT + LTEXT "Created:",IDC_STATIC,7,62,68,8,0,WS_EX_RIGHT + LTEXT "Modified:",IDC_STATIC,7,73,68,8,0,WS_EX_RIGHT + LTEXT "Records:",IDC_STATIC,7,40,68,8,0,WS_EX_RIGHT + GROUPBOX "Archive Info",IDC_STATIC,6,7,307,92 + LTEXT "",IDC_AI_FILENAME,84,18,218,8 + LTEXT "",IDC_AINUFX_FORMAT,84,29,218,8 + LTEXT "",IDC_AINUFX_RECORDS,84,40,218,8 + LTEXT "",IDC_AINUFX_MASTERVERSION,84,51,218,8 + LTEXT "",IDC_AINUFX_CREATEWHEN,84,62,218,8 + LTEXT "",IDC_AINUFX_MODIFYWHEN,84,73,218,8 + LTEXT "",IDC_AINUFX_JUNKSKIPPED,84,84,218,8 + PUSHBUTTON "Help",IDHELP,105,106,50,14 +END + +IDD_ARCHIVEINFO_DISK DIALOGEX 0, 0, 320, 250 +STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Disk Image Info" +FONT 8, "MS Sans Serif", 0, 0, 0x1 +BEGIN + DEFPUSHBUTTON "Done",IDOK,164,229,50,14 + LTEXT "Filename:",IDC_STATIC,7,18,68,8,0,WS_EX_RIGHT + GROUPBOX "File Characteristics",IDC_STATIC,6,7,307,59 + LTEXT "Outer Format:",IDC_STATIC,7,29,68,8,0,WS_EX_RIGHT + LTEXT "File Format:",IDC_STATIC,7,40,68,8,0,WS_EX_RIGHT + LTEXT "Physical Format:",IDC_STATIC,7,51,68,8,0,WS_EX_RIGHT + LTEXT "Sector Ordering:",IDC_STATIC,7,95,68,8,0,WS_EX_RIGHT + LTEXT "Filesystem Format:",IDC_STATIC,7,106,68,8,0,WS_EX_RIGHT + LTEXT "Sub-Volume:",IDC_STATIC,7,81,68,8,0,WS_EX_RIGHT + COMBOBOX IDC_AIDISK_SUBVOLSEL,85,79,221,58,CBS_DROPDOWNLIST | + WS_VSCROLL | WS_TABSTOP + LTEXT "Files+Directories:",IDC_STATIC,7,117,68,8,0,WS_EX_RIGHT + LTEXT "Storage Capacity:",IDC_STATIC,7,128,68,8,0,WS_EX_RIGHT + LTEXT "Free Space:",IDC_STATIC,7,139,68,8,0,WS_EX_RIGHT + LTEXT "Damaged?",IDC_STATIC,7,161,68,8,0,WS_EX_RIGHT + LTEXT "Notes:",IDC_STATIC,7,176,68,8,0,WS_EX_RIGHT + EDITTEXT IDC_AIDISK_NOTES,85,175,221,39,ES_MULTILINE | + ES_AUTOVSCROLL | ES_AUTOHSCROLL | ES_READONLY | + WS_VSCROLL | WS_HSCROLL + GROUPBOX "Disk Characteristics",IDC_STATIC,6,68,307,153 + LTEXT "",IDC_AI_FILENAME,85,18,221,8 + LTEXT "",IDC_AIDISK_OUTERFORMAT,85,29,221,8 + LTEXT "",IDC_AIDISK_FILEFORMAT,85,40,221,8 + LTEXT "",IDC_AIDISK_PHYSICALFORMAT,85,51,221, + 8 + LTEXT "",IDC_AIDISK_SECTORORDER,85,95,221,8 + LTEXT "",IDC_AIDISK_FSFORMAT,85,106,221,8 + LTEXT "",IDC_AIDISK_FILECOUNT,85,117,221,8 + LTEXT "",IDC_AIDISK_CAPACITY,85,128,221,8 + LTEXT "",IDC_AIDISK_FREESPACE,85,139,221,8 + LTEXT "",IDC_AIDISK_DAMAGED,85,161,221,8 + LTEXT "Writeable Format?",IDC_STATIC,7,150,68,8,0,WS_EX_RIGHT + LTEXT "",IDC_AIDISK_WRITEABLE,85,150,221,8 + PUSHBUTTON "Help",IDHELP,104,229,50,14 +END + +IDD_ARCHIVEINFO_BNY DIALOGEX 0, 0, 320, 74 +STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Binary II Archive" +FONT 8, "MS Sans Serif", 0, 0, 0x1 +BEGIN + DEFPUSHBUTTON "Done",IDOK,164,53,50,14 + LTEXT "Filename:",IDC_STATIC,7,18,67,8,0,WS_EX_RIGHT + LTEXT "Records:",IDC_STATIC,7,29,67,8,0,WS_EX_RIGHT + GROUPBOX "Archive Info",IDC_STATIC,6,7,307,36 + LTEXT "",IDC_AI_FILENAME,84,18,218,8 + LTEXT "",IDC_AIBNY_RECORDS,84,29,218,8 + PUSHBUTTON "Help",IDHELP,105,53,50,14 +END + +IDD_PREF_DISKIMAGE DIALOG DISCARDABLE 0, 0, 219, 105 +STYLE DS_CONTEXTHELP | WS_CHILD | WS_DISABLED | WS_CAPTION +CAPTION "Disk Images" +FONT 8, "MS Sans Serif" +BEGIN + GROUPBOX "General",IDC_STATIC,7,7,205,48 + CONTROL "&Confirm disk image format",IDC_PDISK_CONFIRM_FORMAT, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,15,18,185,10 + CONTROL "Default to read-only when opening volumes", + IDC_PDISK_OPENVOL_RO,"Button",BS_AUTOCHECKBOX | + WS_TABSTOP,15,29,185,10 + CONTROL "Allow write access to physical disk 0 (not recommended)", + IDC_PDISK_OPENVOL_PHYS0,"Button",BS_AUTOCHECKBOX | + WS_TABSTOP,15,40,192,10 + GROUPBOX "ProDOS",IDC_STATIC,7,61,205,38 + CONTROL "Allow &lower-case letters and spaces in filenames", + IDC_PDISK_PRODOS_ALLOWLOWER,"Button",BS_AUTOCHECKBOX | + WS_TABSTOP,15,72,186,10 + CONTROL "Use ""sparse"" allocation for empty blocks", + IDC_PDISK_PRODOS_USESPARSE,"Button",BS_AUTOCHECKBOX | + WS_TABSTOP,15,83,186,10 +END + +IDD_CREATE_SUBDIR DIALOG DISCARDABLE 0, 0, 284, 98 +STYLE DS_MODALFRAME | DS_CONTEXTHELP | WS_POPUP | WS_VISIBLE | WS_CAPTION | + WS_SYSMENU +CAPTION "Create Subdirectory" +FONT 8, "MS Sans Serif" +BEGIN + DEFPUSHBUTTON "OK",IDOK,87,77,50,14 + PUSHBUTTON "Cancel",IDCANCEL,145,77,50,14 + LTEXT "Parent directory:",IDC_STATIC,7,7,150,8 + EDITTEXT IDC_CREATESUBDIR_BASE,7,18,270,14,ES_AUTOHSCROLL | + ES_READONLY + LTEXT "New subdirectory name:",IDC_STATIC,7,42,151,8 + EDITTEXT IDC_CREATESUBDIR_NEW,7,52,270,14,ES_AUTOHSCROLL +END + +IDD_RENAME_VOLUME DIALOG DISCARDABLE 0, 0, 218, 172 +STYLE DS_MODALFRAME | DS_CONTEXTHELP | WS_POPUP | WS_VISIBLE | WS_CAPTION | + WS_SYSMENU +CAPTION "Rename volume" +FONT 8, "MS Sans Serif" +BEGIN + DEFPUSHBUTTON "OK",IDOK,26,151,50,14 + PUSHBUTTON "Cancel",IDCANCEL,83,151,50,14 + LTEXT "Select volume to rename:",IDC_STATIC,7,7,175,8 + CONTROL "Tree1",IDC_RENAMEVOL_TREE,"SysTreeView32", + TVS_HASBUTTONS | TVS_HASLINES | TVS_LINESATROOT | + TVS_DISABLEDRAGDROP | TVS_SHOWSELALWAYS | WS_BORDER | + WS_TABSTOP,7,19,204,87 + LTEXT "New name:",IDC_STATIC,7,116,167,8 + EDITTEXT IDC_RENAMEVOL_NEW,7,128,204,14,ES_AUTOHSCROLL + PUSHBUTTON "Help",IDHELP,140,151,50,14 +END + +IDD_EOLSCAN DIALOG DISCARDABLE 0, 0, 173, 106 +STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "EOL Scanner" +FONT 8, "MS Sans Serif" +BEGIN + DEFPUSHBUTTON "OK",IDOK,33,85,50,14 + PUSHBUTTON "Help",IDHELP,90,85,50,14 + LTEXT "Results (click ""Help"" for explanation):",IDC_STATIC,7, + 7,159,8 + RTEXT ">cr<",IDC_EOLSCAN_CR,7,45,61,8 + LTEXT "carriage returns (0x0d)",IDC_STATIC,72,45,94,8 + RTEXT ">lf<",IDC_EOLSCAN_LF,7,56,61,8 + LTEXT "line feeds (0x0a)",IDC_STATIC,72,56,94,8 + RTEXT ">crlf<",IDC_EOLSCAN_CRLF,7,67,61,8 + LTEXT "CRLFs (0x0d0a)",IDC_STATIC,72,67,94,8 + RTEXT ">chars<",IDC_EOLSCAN_CHARS,7,23,61,8 + LTEXT "characters",IDC_STATIC,72,23,94,8 + RTEXT ">high-ascii<",IDC_EOLSCAN_HIGHASCII,7,34,61,8 + LTEXT "high-ASCII characters",IDC_STATIC,72,34,94,8 +END + +IDD_TWOIMG_PROPS DIALOG DISCARDABLE 0, 0, 220, 198 +STYLE DS_MODALFRAME | DS_3DLOOK | DS_CONTEXTHELP | WS_POPUP | WS_VISIBLE | + WS_CAPTION | WS_SYSMENU +CAPTION "2MG Disk Image Properties" +FONT 8, "MS Sans Serif" +BEGIN + DEFPUSHBUTTON "Save",IDOK,56,176,50,14 + PUSHBUTTON "Cancel",IDCANCEL,113,176,50,14 + GROUPBOX "Header fields:",IDC_STATIC,7,7,206,56 + RTEXT "Creator:",IDC_STATIC,9,18,50,8 + LTEXT ">creator<",IDC_TWOIMG_CREATOR,65,18,139,8 + RTEXT "Version:",IDC_STATIC,9,29,50,8 + LTEXT ">version<",IDC_TWOIMG_VERSION,65,29,140,8 + RTEXT "Image format:",IDC_STATIC,9,40,50,8 + LTEXT ">format<",IDC_TWOIMG_FORMAT,65,40,140,8 + RTEXT "Blocks:",IDC_STATIC,9,51,50,8 + LTEXT ">blocks<",IDC_TWOIMG_BLOCKS,65,51,140,8 + CONTROL "Locked",IDC_TWOIMG_LOCKED,"Button",BS_AUTOCHECKBOX | + WS_TABSTOP,7,68,120,10 + CONTROL "Specify disk volume number",IDC_TWOIMG_DOSVOLSET,"Button", + BS_AUTOCHECKBOX | WS_TABSTOP,7,79,118,10 + LTEXT "Volume number:",IDC_STATIC,7,92,52,8 + EDITTEXT IDC_TWOIMG_DOSVOLNUM,65,91,40,14,ES_AUTOHSCROLL | + ES_NUMBER + LTEXT "Comment:",IDC_STATIC,7,110,52,8 + EDITTEXT IDC_TWOIMG_COMMENT,7,121,206,48,ES_MULTILINE | + ES_AUTOHSCROLL | ES_WANTRETURN | WS_VSCROLL | WS_HSCROLL +END + +IDD_LOADING DIALOG DISCARDABLE 0, 0, 146, 38 +STYLE DS_MODALFRAME | DS_3DLOOK | WS_POPUP | WS_VISIBLE +FONT 8, "MS Sans Serif" +BEGIN + CTEXT "Loading data, please wait...",IDC_STATIC,7,15,132,8 +END + +IDD_IMPORTCASSETTE DIALOG DISCARDABLE 0, 0, 355, 157 +STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Import cassette WAV" +FONT 8, "MS Sans Serif" +BEGIN + DEFPUSHBUTTON "Import",IDC_IMPORT_CHUNK,81,136,50,14 + PUSHBUTTON "Done",IDCANCEL,136,136,50,14 + PUSHBUTTON "Help",IDHELP,191,136,50,14 + LTEXT "Input file:",IDC_STATIC,7,7,30,8 + LTEXT ">input-file<",IDC_CASSETTE_INPUT,43,7,305,8 + LTEXT "Algorithm:",IDC_STATIC,7,21,34,8 + COMBOBOX IDC_CASSETTE_ALG,43,19,173,63,CBS_DROPDOWNLIST | + WS_VSCROLL | WS_TABSTOP + LTEXT "Select the chunk to import:",IDC_STATIC,7,40,180,8 + CONTROL "List2",IDC_CASSETTE_LIST,"SysListView32",LVS_REPORT | + LVS_SINGLESEL | LVS_SHOWSELALWAYS | LVS_NOSORTHEADER | + WS_BORDER | WS_TABSTOP,7,51,341,75 +END + +IDD_CASSIMPTARGET DIALOG DISCARDABLE 0, 0, 186, 127 +STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION +CAPTION "Import..." +FONT 8, "MS Sans Serif" +BEGIN + DEFPUSHBUTTON "OK",IDOK,39,106,50,14 + PUSHBUTTON "Cancel",IDCANCEL,96,106,50,14 + LTEXT "Name of file to create?",IDC_STATIC,7,7,113,8 + EDITTEXT IDC_CASSIMPTARG_FILENAME,7,18,172,14,ES_AUTOHSCROLL + GROUPBOX "Static",IDC_STATIC,7,36,172,60 + CONTROL "Applesoft BASIC",IDC_CASSIMPTARG_BAS,"Button", + BS_AUTORADIOBUTTON | WS_GROUP,13,47,87,10 + CONTROL "Integer BASIC",IDC_CASSIMPTARG_INT,"Button", + BS_AUTORADIOBUTTON,13,58,94,10 + CONTROL "Binary",IDC_CASSIMPTARG_BIN,"Button",BS_AUTORADIOBUTTON, + 13,69,77,10 + LTEXT "Start address (hex):",IDC_STATIC,25,80,63,8 + EDITTEXT IDC_CASSIMPTARG_BINADDR,93,78,31,14,ES_AUTOHSCROLL + LTEXT ".XXXX",IDC_CASSIMPTARG_RANGE,126,80,29,8 +END + +IDD_ADD_CLASH DIALOG DISCARDABLE 0, 0, 307, 113 +STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION +CAPTION "Filename Clash" +FONT 8, "MS Sans Serif" +BEGIN + LTEXT "A file being added to the archive has the same name as another file being added. This can happen when directory names or file attribute preservations strings are stripped from Windows filenames.", + IDC_STATIC,7,7,293,29 + LTEXT "You can rename this file, skip adding it, or cancel the entire operation.", + IDC_STATIC,7,39,290,8 + LTEXT "Windows name:",IDC_STATIC,7,57,58,8 + LTEXT ">windows name<",IDC_CLASH_WINNAME,70,57,230,8 + LTEXT "Storage name:",IDC_STATIC,7,72,58,8 + LTEXT ">storage name<",IDC_CLASH_STORAGENAME,70,72,230,8 + PUSHBUTTON "Rename",IDC_CLASH_RENAME,7,91,50,14 + PUSHBUTTON "Skip",IDC_CLASH_SKIP,61,91,50,14 + PUSHBUTTON "Cancel",IDCANCEL,116,91,50,14 +END + +IDD_ARCHIVEINFO_ACU DIALOGEX 0, 0, 320, 74 +STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "AppleLink ACU Archive" +FONT 8, "MS Sans Serif", 0, 0, 0x1 +BEGIN + DEFPUSHBUTTON "Done",IDOK,164,53,50,14 + LTEXT "Filename:",IDC_STATIC,7,18,68,8,0,WS_EX_RIGHT + LTEXT "Records:",IDC_STATIC,7,29,68,8,0,WS_EX_RIGHT + GROUPBOX "Archive Info",IDC_STATIC,6,7,307,36 + LTEXT "",IDC_AI_FILENAME,84,18,218,8 + LTEXT "",IDC_AIBNY_RECORDS,84,29,218,8 + PUSHBUTTON "Help",IDHELP,105,53,50,14 +END + +IDD_IMPORT_BAS DIALOG DISCARDABLE 0, 0, 247, 118 +STYLE DS_MODALFRAME | DS_3DLOOK | WS_POPUP | WS_VISIBLE | WS_CAPTION | + WS_SYSMENU +CAPTION "Import BAS from text file" +FONT 8, "MS Sans Serif" +BEGIN + DEFPUSHBUTTON "Save",IDOK,44,97,50,14 + PUSHBUTTON "Cancel",IDCANCEL,98,97,50,14 + LTEXT "Results:",IDC_STATIC,7,7,40,8 + EDITTEXT IDC_IMPORT_BAS_RESULTS,7,19,233,42,ES_MULTILINE | + ES_AUTOHSCROLL | ES_READONLY | WS_VSCROLL + LTEXT "Save as:",IDC_STATIC,7,74,34,8 + EDITTEXT IDC_IMPORT_BAS_SAVEAS,39,72,201,14,ES_AUTOHSCROLL + PUSHBUTTON "Help",IDHELP,153,97,50,14 +END + +IDD_PASTE_SPECIAL DIALOG DISCARDABLE 0, 0, 186, 78 +STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU +CAPTION "Paste..." +FONT 8, "MS Sans Serif" +BEGIN + DEFPUSHBUTTON "OK",IDOK,40,57,50,14 + PUSHBUTTON "Cancel",IDCANCEL,95,57,50,14 + LTEXT "Choose how you would like to handle filenames:", + IDC_PASTE_SPECIAL_COUNT,7,7,172,8 + CONTROL "Keep full pathnames",IDC_PASTE_SPECIAL_PATHS,"Button", + BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,18,22,161,10 + CONTROL "Strip paths, keep filename only", + IDC_PASTE_SPECIAL_NOPATHS,"Button",BS_AUTORADIOBUTTON | + WS_TABSTOP,18,34,161,10 +END + +IDD_PROGRESS_COUNTER DIALOG DISCARDABLE 0, 0, 186, 57 +STYLE DS_MODALFRAME | DS_3DLOOK | WS_POPUP | WS_VISIBLE +FONT 8, "MS Sans Serif" +BEGIN + PUSHBUTTON "Cancel",IDCANCEL,67,36,50,14 + CTEXT ">descr<",IDC_PROGRESS_COUNTER_DESC,7,7,172,8 + CTEXT ">count<",IDC_PROGRESS_COUNTER_COUNT,7,19,172,8 +END + + +///////////////////////////////////////////////////////////////////////////// +// +// Bitmap +// + +IDB_FSLOGO BITMAP DISCARDABLE "Graphics\\fslogo.bmp" +IDR_TOOLBAR1 BITMAP DISCARDABLE "Graphics\\toolbar1.bmp" +IDB_LIST_PICS BITMAP DISCARDABLE "Graphics\\list-pics.bmp" +IDB_HDRBAR BITMAP DISCARDABLE "Graphics\\hdrbar.bmp" +IDB_NEW_FOLDER BITMAP DISCARDABLE "graphics\\NewFolder.bmp" +IDB_CHOOSE_FOLDER BITMAP DISCARDABLE "Graphics\\ChooseFolder.bmp" +IDB_VOL_PICS BITMAP DISCARDABLE "graphics\\vol_pics.bmp" +IDB_TREE_PICS BITMAP DISCARDABLE "graphics\\tree_pics.bmp" + +///////////////////////////////////////////////////////////////////////////// +// +// Toolbar +// + +IDR_TOOLBAR1 TOOLBAR DISCARDABLE 24, 23 +BEGIN + BUTTON IDM_FILE_OPEN + BUTTON IDM_FILE_OPEN_VOLUME + BUTTON IDM_FILE_NEW_ARCHIVE + BUTTON IDM_TOOLS_IMAGECREATOR + BUTTON IDM_FILE_PRINT + SEPARATOR + BUTTON IDM_ACTIONS_ADD_FILES + BUTTON IDM_ACTIONS_ADD_DISKS + BUTTON IDM_ACTIONS_VIEW + BUTTON IDM_ACTIONS_EXTRACT + BUTTON IDM_ACTIONS_TEST + BUTTON IDM_ACTIONS_RENAME + BUTTON IDM_ACTIONS_DELETE + BUTTON IDM_ACTIONS_RECOMPRESS + BUTTON IDM_ACTIONS_EDIT_COMMENT + SEPARATOR + BUTTON IDM_TOOLS_DISKEDIT + BUTTON IDM_TOOLS_DISKCONV + BUTTON IDM_TOOLS_VOLUMECOPIER_VOLUME + BUTTON IDM_TOOLS_SST_MERGE +END + + +#ifndef _MAC +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +VS_VERSION_INFO VERSIONINFO + FILEVERSION 2,4,6,0 + PRODUCTVERSION 2,4,6,0 + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS 0x40004L + FILETYPE 0x1L + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "Comments", "The end is nigh.\0" + VALUE "CompanyName", "FaddenSoft, LLC\0" + VALUE "FileDescription", "CiderPress\0" + VALUE "FileVersion", "2, 4, 6, 0\0" + VALUE "InternalName", "CiderPress\0" + VALUE "LegalCopyright", "Copyright © 2007 FaddenSoft, LLC\0" + VALUE "LegalTrademarks", "\0" + VALUE "OriginalFilename", "CiderPress.exe\0" + VALUE "PrivateBuild", "\0" + VALUE "ProductName", "CiderPress\0" + VALUE "ProductVersion", "2, 4, 6, 0\0" + VALUE "SpecialBuild", "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END +END + +#endif // !_MAC + + +///////////////////////////////////////////////////////////////////////////// +// +// DESIGNINFO +// + +#ifdef APSTUDIO_INVOKED +GUIDELINES DESIGNINFO DISCARDABLE +BEGIN + IDD_ABOUTDLG, DIALOG + BEGIN + RIGHTMARGIN, 205 + VERTGUIDE, 7 + VERTGUIDE, 16 + VERTGUIDE, 77 + BOTTOMMARGIN, 146 + HORZGUIDE, 65 + END + + IDD_PREF_GENERAL, DIALOG + BEGIN + LEFTMARGIN, 4 + RIGHTMARGIN, 259 + VERTGUIDE, 14 + VERTGUIDE, 96 + VERTGUIDE, 104 + TOPMARGIN, 7 + BOTTOMMARGIN, 173 + END + + IDD_PREF_COMPRESSION, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 205 + VERTGUIDE, 22 + VERTGUIDE, 114 + VERTGUIDE, 124 + TOPMARGIN, 7 + BOTTOMMARGIN, 205 + END + + IDD_FILE_VIEWER, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 485 + VERTGUIDE, 8 + VERTGUIDE, 482 + TOPMARGIN, 7 + HORZGUIDE, 7 + HORZGUIDE, 155 + HORZGUIDE, 238 + END + + IDD_PREF_FVIEW, DIALOG + BEGIN + LEFTMARGIN, 4 + RIGHTMARGIN, 212 + VERTGUIDE, 12 + VERTGUIDE, 50 + VERTGUIDE, 112 + TOPMARGIN, 7 + BOTTOMMARGIN, 261 + END + + IDD_DISKEDIT, DIALOG + BEGIN + LEFTMARGIN, 4 + RIGHTMARGIN, 451 + VERTGUIDE, 388 + TOPMARGIN, 7 + BOTTOMMARGIN, 194 + END + + IDD_DECONF, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 180 + VERTGUIDE, 68 + TOPMARGIN, 7 + BOTTOMMARGIN, 166 + END + + IDD_SUBV, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 146 + TOPMARGIN, 7 + BOTTOMMARGIN, 128 + END + + IDD_DEFILE, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 192 + TOPMARGIN, 7 + BOTTOMMARGIN, 90 + END + + IDD_PREF_FILES, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 224 + VERTGUIDE, 204 + TOPMARGIN, 7 + BOTTOMMARGIN, 82 + END + + IDD_CHOOSEDIR, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 248 + TOPMARGIN, 7 + BOTTOMMARGIN, 246 + END + + IDD_NEWFOLDER, DIALOG + BEGIN + LEFTMARGIN, 6 + RIGHTMARGIN, 243 + TOPMARGIN, 7 + BOTTOMMARGIN, 61 + END + + IDD_EXTRACT_FILES, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 292 + VERTGUIDE, 16 + TOPMARGIN, 7 + BOTTOMMARGIN, 235 + END + + IDD_ACTION_PROGRESS, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 269 + TOPMARGIN, 7 + BOTTOMMARGIN, 104 + END + + IDD_CONFIRM_OVERWRITE, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 285 + TOPMARGIN, 7 + BOTTOMMARGIN, 98 + END + + IDD_RENAME_OVERWRITE, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 309 + TOPMARGIN, 7 + BOTTOMMARGIN, 117 + END + + "IDD_ADD_FILES", DIALOG + BEGIN + VERTGUIDE, 8 + BOTTOMMARGIN, 224 + END + + IDD_USE_SELECTION, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 132 + TOPMARGIN, 7 + BOTTOMMARGIN, 72 + END + + IDD_RENAME_ENTRY, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 277 + TOPMARGIN, 7 + BOTTOMMARGIN, 111 + END + + IDD_COMMENT_EDIT, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 355 + TOPMARGIN, 7 + BOTTOMMARGIN, 144 + END + + IDD_RECOMPRESS_OPTS, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 164 + TOPMARGIN, 7 + BOTTOMMARGIN, 142 + END + + IDD_PRINT_CANCEL, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 109 + TOPMARGIN, 7 + BOTTOMMARGIN, 39 + END + + IDD_ASSOCIATIONS, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 251 + TOPMARGIN, 7 + BOTTOMMARGIN, 274 + HORZGUIDE, 70 + END + + IDD_REGISTRATION, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 226 + VERTGUIDE, 201 + TOPMARGIN, 7 + BOTTOMMARGIN, 184 + END + + IDD_DISKCONV, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 167 + VERTGUIDE, 14 + VERTGUIDE, 27 + VERTGUIDE, 160 + TOPMARGIN, 7 + BOTTOMMARGIN, 216 + END + + IDD_DONEOPEN, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 112 + TOPMARGIN, 7 + BOTTOMMARGIN, 48 + END + + IDD_PROPS_EDIT, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 232 + VERTGUIDE, 72 + VERTGUIDE, 79 + VERTGUIDE, 124 + VERTGUIDE, 176 + VERTGUIDE, 190 + TOPMARGIN, 7 + BOTTOMMARGIN, 150 + HORZGUIDE, 7 + HORZGUIDE, 91 + HORZGUIDE, 104 + END + + IDD_CONVFILE_OPTS, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 164 + VERTGUIDE, 14 + TOPMARGIN, 7 + BOTTOMMARGIN, 86 + END + + IDD_CONVDISK_OPTS, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 168 + VERTGUIDE, 14 + VERTGUIDE, 162 + TOPMARGIN, 7 + BOTTOMMARGIN, 218 + HORZGUIDE, 170 + END + + IDD_BULKCONV, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 230 + TOPMARGIN, 7 + BOTTOMMARGIN, 50 + END + + IDD_OPENVOLUMEDLG, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 293 + VERTGUIDE, 34 + TOPMARGIN, 7 + BOTTOMMARGIN, 159 + END + + IDD_VOLUMECOPYPROG, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 269 + VERTGUIDE, 86 + TOPMARGIN, 7 + BOTTOMMARGIN, 104 + END + + IDD_DISKEDIT_OPENWHICH, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 127 + TOPMARGIN, 7 + BOTTOMMARGIN, 96 + END + + IDD_VOLUMECOPYSEL, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 379 + VERTGUIDE, 288 + VERTGUIDE, 300 + TOPMARGIN, 7 + BOTTOMMARGIN, 131 + HORZGUIDE, 22 + END + + IDD_FORMATTING, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 139 + TOPMARGIN, 7 + BOTTOMMARGIN, 31 + END + + IDD_CREATEIMAGE, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 335 + VERTGUIDE, 14 + VERTGUIDE, 100 + VERTGUIDE, 167 + VERTGUIDE, 175 + VERTGUIDE, 184 + TOPMARGIN, 7 + BOTTOMMARGIN, 215 + END + + IDD_CHOOSE_ADD_TARGET, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 213 + TOPMARGIN, 7 + BOTTOMMARGIN, 272 + END + + IDD_ARCHIVEINFO_NUFX, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 313 + VERTGUIDE, 75 + VERTGUIDE, 84 + VERTGUIDE, 302 + TOPMARGIN, 7 + BOTTOMMARGIN, 120 + END + + IDD_ARCHIVEINFO_DISK, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 313 + VERTGUIDE, 75 + VERTGUIDE, 85 + VERTGUIDE, 306 + TOPMARGIN, 7 + BOTTOMMARGIN, 243 + END + + IDD_ARCHIVEINFO_BNY, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 313 + VERTGUIDE, 74 + VERTGUIDE, 84 + VERTGUIDE, 302 + TOPMARGIN, 7 + BOTTOMMARGIN, 67 + END + + IDD_PREF_DISKIMAGE, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 212 + VERTGUIDE, 15 + TOPMARGIN, 7 + BOTTOMMARGIN, 98 + END + + IDD_CREATE_SUBDIR, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 277 + TOPMARGIN, 7 + BOTTOMMARGIN, 91 + END + + IDD_RENAME_VOLUME, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 211 + TOPMARGIN, 7 + BOTTOMMARGIN, 165 + END + + IDD_EOLSCAN, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 166 + VERTGUIDE, 68 + VERTGUIDE, 72 + TOPMARGIN, 7 + BOTTOMMARGIN, 99 + END + + IDD_TWOIMG_PROPS, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 213 + VERTGUIDE, 59 + VERTGUIDE, 65 + TOPMARGIN, 7 + BOTTOMMARGIN, 191 + END + + IDD_LOADING, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 139 + TOPMARGIN, 7 + BOTTOMMARGIN, 31 + END + + IDD_IMPORTCASSETTE, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 348 + VERTGUIDE, 43 + TOPMARGIN, 7 + BOTTOMMARGIN, 150 + END + + IDD_CASSIMPTARGET, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 179 + VERTGUIDE, 13 + TOPMARGIN, 7 + BOTTOMMARGIN, 120 + END + + IDD_ADD_CLASH, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 300 + TOPMARGIN, 7 + BOTTOMMARGIN, 106 + END + + IDD_ARCHIVEINFO_ACU, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 313 + VERTGUIDE, 74 + VERTGUIDE, 84 + TOPMARGIN, 7 + BOTTOMMARGIN, 67 + END + + IDD_IMPORT_BAS, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 240 + TOPMARGIN, 7 + BOTTOMMARGIN, 111 + END + + IDD_PASTE_SPECIAL, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 179 + VERTGUIDE, 18 + TOPMARGIN, 7 + BOTTOMMARGIN, 71 + END + + IDD_PROGRESS_COUNTER, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 179 + TOPMARGIN, 7 + BOTTOMMARGIN, 50 + END +END +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Dialog Info +// + +IDD_PREF_FVIEW DLGINIT +BEGIN + IDC_PVIEW_DHR_CONV_COMBO, 0x403, 14, 0 +0x6c42, 0x6361, 0x206b, 0x2026, 0x6857, 0x7469, 0x0065, + IDC_PVIEW_DHR_CONV_COMBO, 0x403, 28, 0 +0x614c, 0x6374, 0x6568, 0x2064, 0x6f63, 0x6f6c, 0x2072, 0x7228, 0x6365, +0x6d6f, 0x656d, 0x646e, 0x6465, 0x0029, + IDC_PVIEW_DHR_CONV_COMBO, 0x403, 11, 0 +0x6953, 0x706d, 0x656c, 0x3120, 0x3034, "\000" + IDC_PVIEW_DHR_CONV_COMBO, 0x403, 15, 0 +0x6c53, 0x6469, 0x6e69, 0x2067, 0x6977, 0x646e, 0x776f, "\000" + 0 +END + +IDD_OPENVOLUMEDLG DLGINIT +BEGIN + IDC_VOLUME_FILTER, 0x403, 26, 0 +0x6f42, 0x6874, 0x6c20, 0x676f, 0x6369, 0x6c61, 0x6120, 0x646e, 0x7020, +0x7968, 0x6973, 0x6163, 0x006c, + IDC_VOLUME_FILTER, 0x403, 16, 0 +0x6f4c, 0x6967, 0x6163, 0x206c, 0x6f76, 0x756c, 0x656d, 0x0073, + IDC_VOLUME_FILTER, 0x403, 15, 0 +0x6850, 0x7379, 0x6369, 0x6c61, 0x6420, 0x7369, 0x736b, "\000" + 0 +END + +IDD_IMPORTCASSETTE DLGINIT +BEGIN + IDC_CASSETTE_ALG, 0x403, 14, 0 +0x655a, 0x6f72, 0x4320, 0x6f72, 0x7373, 0x6e69, 0x0067, + IDC_CASSETTE_ALG, 0x403, 27, 0 +0x6550, 0x6b61, 0x742d, 0x2d6f, 0x6550, 0x6b61, 0x5720, 0x6469, 0x6874, +0x2820, 0x6853, 0x7261, 0x2970, "\000" + IDC_CASSETTE_ALG, 0x403, 27, 0 +0x6550, 0x6b61, 0x742d, 0x2d6f, 0x6550, 0x6b61, 0x5720, 0x6469, 0x6874, +0x2820, 0x6f52, 0x6e75, 0x2964, "\000" + IDC_CASSETTE_ALG, 0x403, 29, 0 +0x6550, 0x6b61, 0x742d, 0x2d6f, 0x6550, 0x6b61, 0x5720, 0x6469, 0x6874, +0x2820, 0x6853, 0x6c61, 0x6f6c, 0x2977, "\000" + 0 +END + + +///////////////////////////////////////////////////////////////////////////// +// +// String Table +// + +STRINGTABLE DISCARDABLE +BEGIN + IDM_FILE_NEW_ARCHIVE "Create a new NuFX (ShrinkIt) archive\nNew archive (Ctrl-N)" + IDM_FILE_OPEN "Open an archive or disk image\nOpen (Ctrl-O)" + IDM_FILE_CLOSE "Close current archive or disk image\nClose (Ctrl-W)" + IDM_FILE_PRINT "Print the current list of files\nPrint (Ctrl-P)" + IDM_ACTIONS_ADD_FILES "Add files to the current archive\nAdd files" + IDM_FILE_EXIT "Exit CiderPress\nExit" + IDM_ACTIONS_ADD_DISKS "Add files as disk images\nAdd disk image" + IDM_ACTIONS_EXTRACT "Extract one or more files\nExtract" + IDM_ACTIONS_VIEW "View the contents of the selected files\nView (Tab)" + IDM_ACTIONS_TEST "Test the integrity of files\nTest" + IDM_ACTIONS_DELETE "Delete files from the archive\nDelete (DEL)" + IDM_EDIT_SELECT_ALL "Select all files in an archive" + IDM_ACTIONS_INVERT_SELECTION + "Select un-selected items, and un-select selected items" +END + +STRINGTABLE DISCARDABLE +BEGIN + IDM_EDIT_PREFERENCES "Set application preferences" + IDM_HELP_CONTENTS "Table of contents for on-line help" + IDM_HELP_ORDERING "How to register this product" + IDM_HELP_ABOUT "About this product\nAbout" + IDM_ACTIONS_RECOMPRESS "Re-compress one or more files\nRecompress" + IDM_SORT_PATHNAME "Sort the list by pathname" + IDM_SORT_TYPE "Sort list by file type" + IDM_SORT_AUXTYPE "Sort by auxtype" + IDM_SORT_MODDATE "Sort by modification date" + IDM_SORT_FORMAT "Sort by compression format" + IDM_SORT_SIZE "Sort by uncompressed size" + IDM_SORT_RATIO "Sort by compression ratio" +END + +STRINGTABLE DISCARDABLE +BEGIN + IDM_SORT_PACKED "Sort by compressed size" + IDM_SORT_ACCESS "Sort by access flags" + IDM_SORT_ORIGINAL "Show the files in the order in which they appear in the archive" + IDM_CONVERT_TOBSC "Convert a file to BinSCII format" + IDM_CONVERT_FROMBSC "Convert BinSCII files" + ID_INDICATOR_COMPLETE "100%" + IDM_TOOLS_DISKEDIT "View disk images at the sector/block level\nDisk Sector Viewer" + IDM_HELP_WEBSITE "Visit the CiderPress web site\nGo to web site" +END + +STRINGTABLE DISCARDABLE +BEGIN + IDM_EDIT_INVERT_SELECTION "Invert selection" + IDM_ACTIONS_RENAME "Rename a file\nRename" + IDM_ACTIONS_EDIT_COMMENT "Add, update, or delete a comment\nEdit comment" + IDM_TOOLS_DISKCONV "Convert disk image formats\nDisk Image Converter" + IDM_TOOLS_SST_MERGE "Combine two SST images into one NIB image\nMerge SST Images" + IDM_ACTIONS_OPENASDISK "Open the selected file as a disk image in a new window\nOpen as disk image" +END + +STRINGTABLE DISCARDABLE +BEGIN + IDS_READONLY "(read only)" + IDS_FAILED "Failed" + IDS_ERROR "Error" + IDS_NOT_ALLOWED "Not allowed" + IDS_WARNING "Warning" + IDS_CANCELLED "Cancelled" +END + +STRINGTABLE DISCARDABLE +BEGIN + IDS_SELECTED_COUNT "1 file selected" + IDS_SELECTED_COUNTS_FMT "%d files selected" + IDS_BLOCK "Block:" + IDS_DEFILE_FIND_FAILED "Unable to open %s: file not found." +END + +STRINGTABLE DISCARDABLE +BEGIN + IDS_DEFILE_OPEN_FAILED "Unable to open %s: %s." + IDS_DISKEDIT_NOREADTS "Unable to read track %d sector %d." + IDS_DISKEDIT_NOREADBLOCK "Unable to read block %d." + IDS_DISKEDIT_FIRDFAILED "Unable to read from file: %s." + IDS_EXT_SELECTED_COUNT "Extract 1 selected file" + IDS_EXT_SELECTED_COUNTS_FMT "Extract %d selected files" + IDS_INDIC_RSRC " " + IDS_INDIC_DISK " " +END + +STRINGTABLE DISCARDABLE +BEGIN + IDS_INDIC_COMMENT " " + IDS_INDIC_DATA "" + IDS_NOW_ADDING "Now adding:" + IDS_ADDING_AS "As:" + IDS_DEL_SELECTED_COUNTS_FMT "Delete %d selected files" + IDS_DEL_SELECTED_COUNT "Delete 1 selected file" + IDS_DEL_ALL_FILES "Delete all files" + IDS_DEL_OK "Delete" + IDS_DEL_TITLE "Delete Files" + IDS_TEST_SELECTED_COUNTS_FMT "Test %d selected files" + IDS_TEST_SELECTED_COUNT "Test 1 selected file" + IDS_TEST_ALL_FILES "Test all files" + IDS_TEST_OK "Test" + IDS_TEST_TITLE "Test Files" + IDS_NOW_TESTING "Now testing:" + IDS_MB_APP_NAME "CiderPress" +END + +STRINGTABLE DISCARDABLE +BEGIN + AFX_IDS_IDLEMESSAGE "Ready" +END + +STRINGTABLE DISCARDABLE +BEGIN + IDS_NO_COMMENT_ADD "This entry does not have a comment. Did you want to add one?" + IDS_EDIT_COMMENT "Edit Comment" + IDS_DEL_COMMENT_OK "Okay to delete comment?" + IDS_RECOMP_SELECTED_COUNT "Recompress 1 selected file" + IDS_RECOMP_SELECTED_COUNTS_FMT "Recompress %d selected files" + IDS_RECOMP_ALL_FILES "Recompress all files" + IDS_RECOMP_OK "Go" + IDS_RECOMP_TITLE "Recompress Files" + IDS_NOW_EXPANDING "Now expanding:" + IDS_NOW_COMPRESSING "Now compressing:" + IDS_PRINT_CL_JOB_TITLE "CiderPress File Listing" + IDS_REG_EXPIRED "The evaluation period has expired. To continue using CiderPress, you will need to register your copy." + IDS_REG_FAILURE "Unable to access system registry. Reboot and try again." + IDS_REG_INVALID "Your registration key appears to be invalid. Please re-enter your registration data." + IDS_REG_EVAL_REM "Evaluation period ends in %d days." + IDS_REG_BAD_ENTRY "The information you have entered does not appear to be correct.\r\n\r\nPlease make sure that all values are entered exactly as they appear in the confirmation message." +END + +STRINGTABLE DISCARDABLE +BEGIN + IDS_OPEN_AS_NUFX "The file could not be opened as a disk image. However, it appears to be a valid NuFX file archive. Open it?" + IDS_ABOUT_UNREGISTERED "Unregistered - register today!" + IDS_CDESC_140K "140K (5.25"" floppy)" + IDS_CDESC_800K "800K (3.5"" floppy)" + IDS_CDESC_BLOCKS "%ldK (block image)" + IDS_CDEC_140K_13 "13-sector disk (5.25"" floppy)" + IDS_BAD_SST_IMAGE "This image doesn't quite look right. Make sure that:\n - The disk image was created by SST.\n - The image has the correct extension for its sector ordering.\n - You selected side 1 first and side 2 second.\n\nContinue anyway?" + IDS_NIBBLE_TO_SECTOR_WARNING + "Moving from a nibble format to a sector format can cause some data to be lost, especially on copy-protected disks.\n\nContinue anyway?" + IDS_CDEC_RAWNIB "35 tracks (5.25"" floppy)" + IDS_DISKEDITMSG_EMPTY "Empty file -- no storage allocated." + IDS_DISKEDITMSG_SPARSE "Index %ld is sparse (zero-filled)." + IDS_DISKEDITMSG_BADSECTOR "Unable to read sector." + IDS_DISKEDITMSG_BADBLOCK "Unable to read block." + IDS_DISKEDITMSG_BADTRACK "Unable to read track." + IDS_CONVFILE_SELECTED_COUNT "Convert 1 selected file" + IDS_CONVFILE_SELECTED_COUNTS_FMT "Convert %d selected files" +END + +STRINGTABLE DISCARDABLE +BEGIN + IDM_ACTIONS_EDIT_PROPS "Edit file type and access permissions\nEdit file attributes" + IDM_ACTIONS_CONV_DISK "Convert part or all of this archive to a ProDOS disk image\nConvert to disk image" + IDM_ACTIONS_CONV_FILE "Extract one or more files and put them in a ShrinkIt file archive\nConvert to file archive" + IDM_TOOLS_BULKDISKCONV "Convert several disk images to a different format\nBulk disk image converter" + IDM_FILE_OPEN_VOLUME "Open a disk volume (e.g. CD-ROM, CFFA card, or 1.44MB ProDOS floppy)\nOpen volume (Ctrl-Shift-O)" + IDM_TOOLS_IMAGECREATOR "Create empty disk image\nCreate disk image" + IDM_TOOLS_VOLUMECOPIER_FILE + "Copy a Windows volume (e.g. floppy disk) to or from a file\nVolume copier (open file)" + IDM_TOOLS_VOLUMECOPIER_VOLUME + "Copy a Windows volume (e.g. floppy disk) to or from a file\nVolume copier (open volume)" + IDM_FILE_ARCHIVEINFO "Show detailed information on this archive or disk image\nArchive Info (Ctrl-I)" +END + +STRINGTABLE DISCARDABLE +BEGIN + IDS_CONVFILE_ALL_FILES "Convert all files" + IDS_CONVFILE_OK "Go" + IDS_CONVFILE_TITLE "Convert to file archive" + IDS_CONVDISK_SELECTED_COUNT "Convert 1 selected file" + IDS_CONVDISK_SELECTED_COUNTS_FMT "Convert %d selected files" + IDS_CONVDISK_ALL_FILES "Convert all files" + IDS_CONVDISK_OK "Go" + IDS_CONVDISK_TITLE "Convert to disk archive" + IDS_CONVDISK_SPACEREQ "Space required: %s" + IDS_CDESC_40TRACK "40-track image (5.25"" floppy)" + IDS_TRACKSTAR_TO_OTHER_WARNING + "TrackStar images are stored as 40-track variable-length nibble images. No other supported format is directly compatible, so images are converted as formatted 35-track 16-sector disks. This probably won't work well for copy-protected disks.\n\nContinue anyway?" + IDS_DIFFERENT_NIBBLE_WARNING + "You are converting an image between formats that use different nibble track lengths. A direct conversion isn't possible, so the image will be converted as a formatted 16-sector disk. This will likely cause problems for copy-protected disks.\n\nContinue anyway?" + IDS_VOLUME_NO_REMOTE "Network volumes are not supported." + IDS_VOLUME_NO_CDROM "CD-ROM drives are not currently supported." + IDS_VOLUME_NO_RAMDISK "RAM disks are not supported." + IDS_VOLUME_NO_GENERIC "That type of drive is not supported." +END + +STRINGTABLE DISCARDABLE +BEGIN + IDS_VOLUME_NO_CDRIVE "For safety, access to C:\\ is not allowed." + IDS_VOLUME_SELECT_ONE "Please select the volume to access." + IDS_OPERATION_CANCELLED "Operation cancelled." + IDS_NOT_READY "Not ready" + IDS_ASPI_NOT_LOADED "ASPI driver not loaded" + IDS_NOW_DELETING "Now deleting:" + IDS_VALID_FILENAME_PRODOS + "Valid ProDOS filenames are 1-15 characters long, start with a letter, and use only letters numbers, spaces, and '.'. If the ""allow lower case"" preference is disabled, lower case letters are converted to upper case, and space becomes '.'." + IDS_VALID_FILENAME_DOS "Valid DOS file names are 1-30 characters long, and use upper case letters, numbers, spaces, and symbols other than ','. Trailing spaces are not allowed." + IDS_VALID_FILENAME_PASCAL + "Valid Pascal file names are 1-15 characters long, and use upper case letters, numbers, and symbols other than '$=?,]#:'." + IDS_VALID_VOLNAME_PRODOS + "Valid ProDOS volume names are 1-15 characters long, begin with a letter, and have only letters, numbers, and '.'. If the ""allow lower case"" preference is disabled, lower case letters will be converted to upper case. Trailing spaces are not allowed." + IDS_VALID_VOLNAME_DOS "DOS volume numbers must be between 1 and 254." + IDS_VALID_VOLNAME_PASCAL + "Valid Pascal volume names are 1-7 characters long and use upper case letters, numbers, and symbols other than '$=?,]#:'." + IDS_CLIPBOARD_REGFAILED "ERROR: unable to register clipboard format." + IDS_CLIPBOARD_OPENFAILED "ERROR: unable to open clipboard." + IDS_CLIPBOARD_NOITEMS "Nothing copied to clipboard." + IDS_CLIPBOARD_ALLOCFAILED "Unable to allocate memory for clipboard data." +END + +STRINGTABLE DISCARDABLE +BEGIN + IDM_ACTIONS_CREATE_SUBDIR + "Create a new subdirectory on a ProDOS disk\nCreate subdirectory" + IDM_ACTIONS_RENAME_VOLUME + "Change a disk's volume name or number\nRename volume" + IDM_TOOLS_EOLSCANNER "Scans files for end-of-line markers\nEOL scanner" + IDM_FILE_FLUSH "Save any unwritten data to disk\nSave changes" + IDM_FILE_SAVE "Flush any pending changes to disk.\nSave (Ctrl-S)" + IDM_EDIT_COPY "Copy the selected files to the clipboard\nCopy" + IDM_EDIT_PASTE "Paste files from the clipboard" + IDM_TOOLS_TWOMGPROPS "Edit the properties of a .2MG disk image.\n2MG image editor" + IDM_TOOLS_TWOIMGPROPS "Edit header fields of a 2MG disk image\n2MG properties editor" + IDM_ACTIONS_CONV_TOWAV "Convert a BASIC or BIN file to Apple II cassette format\nConvert to WAV" + IDM_ACTIONS_CONV_FROMWAV + "Convert a WAV file recording of an Apple II cassette into an Apple II file\nImport file from WAV" +END + +STRINGTABLE DISCARDABLE +BEGIN + IDS_CLIPBOARD_NOTFOUND "No CiderPress data found in the clipboard." + IDS_CLIPBOARD_READFAILURE "Failed while reading data from the clipboard." + IDS_CLIPBOARD_WRITEFAILURE "Failed while copying data to the clipboard." + IDS_CLIPBOARD_WIN9XMAX "In Win98/ME the clipboard is limited to %ldMB of data. Your request includes %.3fMB. Please select less data." + IDS_PRINTER_NOT_USABLE "Attempting to print to the requested printer failed." + IDS_NLIST_DATA_FAILED "Unable to load NList.Data from '%s' or '%s'." + IDS_PROPS_DOS_TYPE_CHANGE + "Changing the type of a DOS 3.2/3.3 file to BIN, INT, or BAS can cause problems. Consult the ""help"" file for details. Do you wish to continue?" + IDS_FDI_TO_OTHER_WARNING + "FDI images are stored as variable-length nibble images. No other supported format is directly compatible, so images are converted to formatted 35-track 16-sector disks. This probably won't work well for copy-protected disks.\n\nContinue anyway?" + IDS_VALID_VOLNAME_HFS "Valid HFS volume names are 1-27 characters long, and may not contain a colon (:)." + IDS_VALID_FILENAME_HFS "Valid HFS filenames are 1-31 characters long, and do not contain colons (':')." + IDS_PASTE_SPECIAL_COUNT "%d files in clipboard" +END + +STRINGTABLE DISCARDABLE +BEGIN + IDM_ACTIONS_IMPORT_BAS "Import Applesoft BASIC program from text file" + IDM_FILE_REOPEN "Re-open the existing archive or disk image\nReopen" + IDM_EDIT_FIND "Search for an entry in the file list" + IDM_EDIT_PASTE_SPECIAL "Choose how files on the clipboard will be pasted" +END + +STRINGTABLE DISCARDABLE +BEGIN + ID_EDIT_FIND "Search for a " +END + +#endif // English (U.S.) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/app/Clipboard.cpp b/app/Clipboard.cpp new file mode 100644 index 0000000..57235fe --- /dev/null +++ b/app/Clipboard.cpp @@ -0,0 +1,1079 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Handle clipboard functions (copy, paste). + */ +#include "StdAfx.h" +#include "Main.h" +#include "PasteSpecialDialog.h" + + +static const char* kClipboardFmtName = "faddenSoft:CiderPress:v1"; +const int kClipVersion = 1; // should match "vN" in fmt name +const unsigned short kEntrySignature = 0x4350; + +/* + * Win98 is quietly dying on large (20MB-ish) copies. Everything + * runs along nicely and then, the next time we try to interact with + * Windows, the entire system locks up and must be Ctrl-Alt-Deleted. + * + * In tests, it blew up on 16762187 but not 16682322, both of which + * are shy of the 16MB mark (the former by about 15K). This includes the + * text version, which is potentially copied multiple times. Windows doesn't + * create the additional stuff (CF_OEMTEXT and CF_LOCALE; Win2K also creates + * CF_UNICODETEXT) until after the clipboard is closed. We can open & close + * the clipboard to get an exact count, or just multiply by a small integer + * to get a reasonable estimate (1x for alternate text, 2x for UNICODE). + * + * Microsoft Excel limits its clipboard to 4MB on small systems, and 8MB + * on systems with at least 64MB of physical memory. My guess is they + * haven't really tried larger values. + * + * The description of GlobalAlloc suggests that it gets a little weird + * when you go above 4MB, which makes me a little nervous about using + * anything larger. However, it seems to work very reliably until you + * cross 16MB, at which point it seizes up 100% of the time. It's possible + * we're stomping on stuff and just getting lucky, but it's too-reliably + * successful and too-reliably failing for me to believe that. + */ +const long kWin98ClipboardMax = 16 * 1024 * 1024; +const long kWin98NeutralZone = 512; // extra padding +const int kClipTextMult = 4; // CF_OEMTEXT, CF_LOCALE, CF_UNICODETEXT*2 + +/* + * File collection header. + */ +typedef struct FileCollection { + unsigned short version; // currently 1 + unsigned short dataOffset; // offset to start of data + unsigned long length; // total length; + unsigned long count; // #of entries +} FileCollection; + +/* what kind of entry is this */ +typedef enum EntryKind { + kEntryKindUnknown = 0, + kEntryKindFileDataFork, + kEntryKindFileRsrcFork, + kEntryKindFileBothForks, + kEntryKindDirectory, + kEntryKindDiskImage, +} EntryKind; + +/* + * One of these per entry in the collection. + * + * The next file starts at (start + dataOffset + dataLen + rsrcLen + cmmtLen). + */ +typedef struct FileCollectionEntry { + unsigned short signature; // let's be paranoid + unsigned short dataOffset; // offset to start of data + unsigned short fileNameLen; // len of filename + unsigned long dataLen; // len of data fork + unsigned long rsrcLen; // len of rsrc fork + unsigned long cmmtLen; // len of comments + unsigned long fileType; + unsigned long auxType; + time_t createWhen; + time_t modWhen; + unsigned char access; // ProDOS access flags + unsigned char entryKind; // GenericArchive::FileDetails::FileKind + unsigned char sourceFS; // DiskImgLib::DiskImg::FSFormat + unsigned char fssep; // filesystem separator char, e.g. ':' + + /* data comes next: filename, then data, then resource, then comment */ +} FileCollectionEntry; + + +/* + * ========================================================================== + * Copy + * ========================================================================== + */ + +/* + * Copy data to the clipboard. + */ +void +MainWindow::OnEditCopy(void) +{ + CString errStr, fileList; + SelectionSet selSet; + UINT myFormat; + bool isOpen = false; + HGLOBAL hGlobal; + LPVOID pGlobal; + unsigned char* buf = nil; + long bufLen = -1; + + /* associate a number with the format name */ + myFormat = RegisterClipboardFormat(kClipboardFmtName); + if (myFormat == 0) { + errStr.LoadString(IDS_CLIPBOARD_REGFAILED); + ShowFailureMsg(this, errStr, IDS_FAILED); + goto bail; + } + WMSG1("myFormat = %u\n", myFormat); + + /* open & empty the clipboard, even if we fail later */ + if (OpenClipboard() == false) { + errStr.LoadString(IDS_CLIPBOARD_OPENFAILED); + ShowFailureMsg(this, errStr, IDS_FAILED); + goto bail; + } + isOpen = true; + EmptyClipboard(); + + /* + * Create a selection set with the entries. + * + * Strictly speaking we don't need the directories, since we recreate + * them as needed. However, storing them explicitly will allow us + * to preserve empty subdirs. + */ + selSet.CreateFromSelection(fpContentList, + GenericEntry::kAnyThread | GenericEntry::kAllowDirectory); + if (selSet.GetNumEntries() == 0) { + errStr.LoadString(IDS_CLIPBOARD_NOITEMS); + MessageBox(errStr, "No match", MB_OK | MB_ICONEXCLAMATION); + goto bail; + } + + /* + * Make a big string with a file listing. + */ + fileList = CreateFileList(&selSet); + + /* + * Add the string to the clipboard. The clipboard will own the memory we + * allocate. + */ + hGlobal = ::GlobalAlloc(GHND | GMEM_SHARE, fileList.GetLength() +1); + if (hGlobal == nil) { + WMSG1("Failed allocating %ld bytes\n", fileList.GetLength() +1); + errStr.LoadString(IDS_CLIPBOARD_ALLOCFAILED); + ShowFailureMsg(this, errStr, IDS_FAILED); + goto bail; + } + WMSG1(" Allocated %ld bytes for file list on clipboard\n", + fileList.GetLength() +1); + pGlobal = ::GlobalLock(hGlobal); + ASSERT(pGlobal != nil); + strcpy((char*) pGlobal, fileList); + ::GlobalUnlock(hGlobal); + + SetClipboardData(CF_TEXT, hGlobal); + + /* + * Create a (potentially very large) buffer with the contents of the + * files in it. This may fail for any number of reasons. + */ + hGlobal = CreateFileCollection(&selSet); + if (hGlobal != nil) { + SetClipboardData(myFormat, hGlobal); + // beep annoys me on copy + //SuccessBeep(); + } + +bail: + CloseClipboard(); +} +void +MainWindow::OnUpdateEditCopy(CCmdUI* pCmdUI) +{ + pCmdUI->Enable(fpContentList != nil && + fpContentList->GetSelectedCount() > 0); +} + +/* + * Create a list of selected files. + * + * The returned string uses tab-delineated fields with CSV-style quoting + * around the filename (so that double quotes in the filename don't confuse + * applications like MS Excel). + */ +CString +MainWindow::CreateFileList(SelectionSet* pSelSet) +{ + SelectionEntry* pSelEntry; + GenericEntry* pEntry; + CString tmpStr, fullStr; + char fileTypeBuf[ContentList::kFileTypeBufLen]; + char auxTypeBuf[ContentList::kAuxTypeBufLen]; + CString fileName, subVol, fileType, auxType, modDate, format, length; + + pSelEntry = pSelSet->IterNext(); + while (pSelEntry != nil) { + pEntry = pSelEntry->GetEntry(); + ASSERT(pEntry != nil); + + fileName = DblDblQuote(pEntry->GetPathName()); + subVol = pEntry->GetSubVolName(); + ContentList::MakeFileTypeDisplayString(pEntry, fileTypeBuf); + fileType = DblDblQuote(fileTypeBuf); // Mac HFS types might have '"'? + ContentList::MakeAuxTypeDisplayString(pEntry, auxTypeBuf); + auxType = DblDblQuote(auxTypeBuf); + FormatDate(pEntry->GetModWhen(), &modDate); + format = pEntry->GetFormatStr(); + length.Format("%I64d", (LONGLONG) pEntry->GetUncompressedLen()); + + tmpStr.Format("\"%s\"\t%s\t\"%s\"\t\"%s\"\t%s\t%s\t%s\r\n", + fileName, subVol, fileType, auxType, modDate, format, length); + fullStr += tmpStr; + + pSelEntry = pSelSet->IterNext(); + } + + return fullStr; +} + +/* + * Double-up all double quotes. + */ +/*static*/ CString +MainWindow::DblDblQuote(const char* str) +{ + CString result; + char* buf; + + buf = result.GetBuffer(strlen(str) * 2 +1); + while (*str != '\0') { + if (*str == '"') { + *buf++ = *str; + *buf++ = *str; + } else { + *buf++ = *str; + } + str++; + } + *buf = *str; + + result.ReleaseBuffer(); + + return result; +} + + +/* + * Compute the size of everything currently on the clipboard. + */ +long +MainWindow::GetClipboardContentLen(void) +{ + long len = 0; + UINT format = 0; + HGLOBAL hGlobal; + + while ((format = EnumClipboardFormats(format)) != 0) { + hGlobal = GetClipboardData(format); + ASSERT(hGlobal != nil); + len += GlobalSize(hGlobal); + } + + return len; +} + +/* + * Create the file collection. + */ +HGLOBAL +MainWindow::CreateFileCollection(SelectionSet* pSelSet) +{ + SelectionEntry* pSelEntry; + GenericEntry* pEntry; + HGLOBAL hGlobal = nil; + HGLOBAL hResult = nil; + LPVOID pGlobal; + long totalLength, numFiles; + long priorLength; + + /* get len of text version(s), with kluge to avoid close & reopen */ + priorLength = GetClipboardContentLen() * kClipTextMult; + /* add some padding -- textmult doesn't work for fixed-size CF_LOCALE */ + priorLength += kWin98NeutralZone; + + totalLength = sizeof(FileCollection); + numFiles = 0; + + /* + * Compute the amount of space required to hold it all. + */ + pSelSet->IterReset(); + pSelEntry = pSelSet->IterNext(); + while (pSelEntry != nil) { + pEntry = pSelEntry->GetEntry(); + ASSERT(pEntry != nil); + + //WMSG1("+++ Examining '%s'\n", pEntry->GetDisplayName()); + + if (pEntry->GetRecordKind() != GenericEntry::kRecordKindVolumeDir) { + totalLength += sizeof(FileCollectionEntry); + totalLength += strlen(pEntry->GetPathName()) +1; + numFiles++; + if (pEntry->GetRecordKind() != GenericEntry::kRecordKindDirectory) { + totalLength += (long) pEntry->GetDataForkLen(); + totalLength += (long) pEntry->GetRsrcForkLen(); + } + } + + if (totalLength < 0) { + DebugBreak(); + WMSG0("Overflow\n"); // pretty hard to do right now! + return nil; + } + + pSelEntry = pSelSet->IterNext(); + } + +#if 0 + { + CString msg; + msg.Format("totalLength is %ld+%ld = %ld", + totalLength, priorLength, totalLength+priorLength); + if (MessageBox(msg, nil, MB_OKCANCEL) == IDCANCEL) + goto bail; + } +#endif + + WMSG3("Total length required is %ld + %ld = %ld\n", + totalLength, priorLength, totalLength+priorLength); + if (IsWin9x() && totalLength+priorLength >= kWin98ClipboardMax) { + CString errMsg; + errMsg.Format(IDS_CLIPBOARD_WIN9XMAX, + kWin98ClipboardMax / (1024*1024), + ((float) (totalLength+priorLength)) / (1024.0*1024.0)); + ShowFailureMsg(this, errMsg, IDS_MB_APP_NAME); + goto bail; + } + + /* + * Create a big buffer to hold it all. + */ + hGlobal = ::GlobalAlloc(GHND | GMEM_SHARE, totalLength); + if (hGlobal == nil) { + CString errMsg; + errMsg.Format("ERROR: unable to allocate %ld bytes for copy", + totalLength); + WMSG1("%s\n", (const char*) errMsg); + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + pGlobal = ::GlobalLock(hGlobal); + + ASSERT(pGlobal != nil); + ASSERT(GlobalSize(hGlobal) >= (DWORD) totalLength); + WMSG3("hGlobal=0x%08lx pGlobal=0x%08lx size=%ld\n", + (long) hGlobal, (long) pGlobal, GlobalSize(hGlobal)); + + /* + * Set up a progress dialog to track it. + */ + ASSERT(fpActionProgress == nil); + fpActionProgress = new ActionProgressDialog; + fpActionProgress->Create(ActionProgressDialog::kActionExtract, this); + fpActionProgress->SetFileName("Clipboard"); + + /* + * Extract the data into the buffer. + */ + long remainingLen; + void* buf; + + remainingLen = totalLength - sizeof(FileCollection); + buf = (unsigned char*) pGlobal + sizeof(FileCollection); + pSelSet->IterReset(); + pSelEntry = pSelSet->IterNext(); + while (pSelEntry != nil) { + CString errStr; + + pEntry = pSelEntry->GetEntry(); + ASSERT(pEntry != nil); + + fpActionProgress->SetArcName(pEntry->GetDisplayName()); + + errStr = CopyToCollection(pEntry, &buf, &remainingLen); + if (!errStr.IsEmpty()) { + ShowFailureMsg(fpActionProgress, errStr, IDS_MB_APP_NAME); + goto bail; + } + //WMSG1("remainingLen now %ld\n", remainingLen); + + pSelEntry = pSelSet->IterNext(); + } + + ASSERT(remainingLen == 0); + ASSERT(buf == (unsigned char*) pGlobal + totalLength); + + /* + * Write the header. + */ + FileCollection fileColl; + fileColl.version = kClipVersion; + fileColl.dataOffset = sizeof(FileCollection); + fileColl.length = totalLength; + fileColl.count = numFiles; + memcpy(pGlobal, &fileColl, sizeof(fileColl)); + + /* + * Success! + */ + ::GlobalUnlock(hGlobal); + + hResult = hGlobal; + hGlobal = nil; + +bail: + if (hGlobal != nil) { + ASSERT(hResult == nil); + ::GlobalUnlock(hGlobal); + ::GlobalFree(hGlobal); + } + if (fpActionProgress != nil) { + fpActionProgress->Cleanup(this); + fpActionProgress = nil; + } + return hResult; +} + +/* + * Copy the contents of the file referred to by "pEntry" into the buffer + * "*pBuf", which has "*pBufLen" bytes in it. + * + * The call fails if "*pBufLen" isn't large enough. + * + * Returns an empty string on success, or an error message on failure. + * On success, "*pBuf" will be advanced past the data added, and "*pBufLen" + * will be reduced by the amount of data copied into "buf". + */ +CString +MainWindow::CopyToCollection(GenericEntry* pEntry, void** pBuf, long* pBufLen) +{ + FileCollectionEntry collEnt; + CString errStr, dummyStr; + unsigned char* buf = (unsigned char*) *pBuf; + long remLen = *pBufLen; + + errStr.LoadString(IDS_CLIPBOARD_WRITEFAILURE); + + if (pEntry->GetRecordKind() == GenericEntry::kRecordKindVolumeDir) { + WMSG0("Not copying volume dir to collection\n"); + return ""; + } + + if (remLen < sizeof(collEnt)) { + ASSERT(false); + return errStr; + } + + GenericArchive::FileDetails::FileKind entryKind; + if (pEntry->GetRecordKind() == GenericEntry::kRecordKindDirectory) + entryKind = GenericArchive::FileDetails::kFileKindDirectory; + else if (pEntry->GetHasDataFork() && pEntry->GetHasRsrcFork()) + entryKind = GenericArchive::FileDetails::kFileKindBothForks; + else if (pEntry->GetHasDataFork()) + entryKind = GenericArchive::FileDetails::kFileKindDataFork; + else if (pEntry->GetHasRsrcFork()) + entryKind = GenericArchive::FileDetails::kFileKindRsrcFork; + else if (pEntry->GetHasDiskImage()) + entryKind = GenericArchive::FileDetails::kFileKindDiskImage; + else { + ASSERT(false); + return errStr; + } + ASSERT((int) entryKind >= 0 && (int) entryKind <= 255); + + memset(&collEnt, 0x99, sizeof(collEnt)); + collEnt.signature = kEntrySignature; + collEnt.dataOffset = sizeof(collEnt); + collEnt.fileNameLen = strlen(pEntry->GetPathName()) +1; + if (pEntry->GetRecordKind() == GenericEntry::kRecordKindDirectory) { + collEnt.dataLen = collEnt.rsrcLen = collEnt.cmmtLen = 0; + } else { + collEnt.dataLen = (unsigned long) pEntry->GetDataForkLen(); + collEnt.rsrcLen = (unsigned long) pEntry->GetRsrcForkLen(); + collEnt.cmmtLen = 0; // CMMT FIX -- length unknown?? + } + collEnt.fileType = pEntry->GetFileType(); + collEnt.auxType = pEntry->GetAuxType(); + collEnt.createWhen = pEntry->GetCreateWhen(); + collEnt.modWhen = pEntry->GetModWhen(); + collEnt.access = (unsigned char) pEntry->GetAccess(); + collEnt.entryKind = (unsigned char) entryKind; + collEnt.sourceFS = pEntry->GetSourceFS(); + collEnt.fssep = pEntry->GetFssep(); + + /* verify there's enough space to hold everything */ + if ((unsigned long) remLen < collEnt.fileNameLen + + collEnt.dataLen + collEnt.rsrcLen + collEnt.cmmtLen) + { + ASSERT(false); + return errStr; + } + + memcpy(buf, &collEnt, sizeof(collEnt)); + buf += sizeof(collEnt); + remLen -= sizeof(collEnt); + + /* copy string with terminating null */ + memcpy(buf, pEntry->GetPathName(), collEnt.fileNameLen); + buf += collEnt.fileNameLen; + remLen -= collEnt.fileNameLen; + + /* + * Extract data forks, resource forks, and disk images as appropriate. + */ + char* bufCopy; + long lenCopy; + int result, which; + + if (pEntry->GetRecordKind() == GenericEntry::kRecordKindDirectory) { + ASSERT(collEnt.dataLen == 0); + ASSERT(collEnt.rsrcLen == 0); + ASSERT(collEnt.cmmtLen == 0); + ASSERT(!pEntry->GetHasRsrcFork()); + } else if (pEntry->GetHasDataFork() || pEntry->GetHasDiskImage()) { + bufCopy = (char*) buf; + lenCopy = remLen; + if (pEntry->GetHasDiskImage()) + which = GenericEntry::kDiskImageThread; + else + which = GenericEntry::kDataThread; + + result = pEntry->ExtractThreadToBuffer(which, &bufCopy, &lenCopy, + &dummyStr); + if (result == IDCANCEL) { + errStr.LoadString(IDS_CANCELLED); + return errStr; + } else if (result != IDOK) { + WMSG0("ExtractThreadToBuffer (data) failed\n"); + return errStr; + } + + ASSERT(lenCopy == (long) collEnt.dataLen); + buf += collEnt.dataLen; + remLen -= collEnt.dataLen; + } else { + ASSERT(collEnt.dataLen == 0); + } + + if (pEntry->GetHasRsrcFork()) { + bufCopy = (char*) buf; + lenCopy = remLen; + which = GenericEntry::kRsrcThread; + + result = pEntry->ExtractThreadToBuffer(which, &bufCopy, &lenCopy, + &dummyStr); + if (result == IDCANCEL) { + errStr.LoadString(IDS_CANCELLED); + return errStr; + } else if (result != IDOK) { + WMSG0("ExtractThreadToBuffer (rsrc) failed\n"); + return errStr; + } + + ASSERT(lenCopy == (long) collEnt.rsrcLen); + buf += collEnt.rsrcLen; + remLen -= collEnt.rsrcLen; + } + + if (pEntry->GetHasComment()) { +#if 0 // CMMT FIX + bufCopy = (char*) buf; + lenCopy = remLen; + which = GenericEntry::kCommentThread; + + result = pEntry->ExtractThreadToBuffer(which, &bufCopy, &lenCopy); + if (result == IDCANCEL) { + errStr.LoadString(IDS_CANCELLED); + return errStr; + } else if (result != IDOK) + return errStr; + + ASSERT(lenCopy == (long) collEnt.cmmtLen); + buf += collEnt.cmmtLen; + remLen -= collEnt.cmmtLen; +#else + ASSERT(collEnt.cmmtLen == 0); +#endif + } + + *pBuf = buf; + *pBufLen = remLen; + return ""; +} + + +/* + * ========================================================================== + * Paste + * ========================================================================== + */ + +/* + * Paste data from the clipboard, using the configured defaults. + */ +void +MainWindow::OnEditPaste(void) +{ + bool pasteJunkPaths = fPreferences.GetPrefBool(kPrPasteJunkPaths); + + DoPaste(pasteJunkPaths); +} +void +MainWindow::OnUpdateEditPaste(CCmdUI* pCmdUI) +{ + bool dataAvailable = false; + UINT myFormat; + + myFormat = RegisterClipboardFormat(kClipboardFmtName); + if (myFormat != 0 && IsClipboardFormatAvailable(myFormat)) + dataAvailable = true; + + pCmdUI->Enable(fpContentList != nil && !fpOpenArchive->IsReadOnly() && + dataAvailable); +} + +/* + * Paste data from the clipboard, giving the user the opportunity to select + * how the files are handled. + */ +void +MainWindow::OnEditPasteSpecial(void) +{ + PasteSpecialDialog dlg; + bool pasteJunkPaths = fPreferences.GetPrefBool(kPrPasteJunkPaths); + + // invert the meaning, so non-default mode is default in dialog + if (pasteJunkPaths) + dlg.fPasteHow = PasteSpecialDialog::kPastePaths; + else + dlg.fPasteHow = PasteSpecialDialog::kPasteNoPaths; + if (dlg.DoModal() != IDOK) + return; + + switch (dlg.fPasteHow) { + case PasteSpecialDialog::kPastePaths: + pasteJunkPaths = false; + break; + case PasteSpecialDialog::kPasteNoPaths: + pasteJunkPaths = true; + break; + default: + assert(false); + break; + } + + DoPaste(pasteJunkPaths); +} +void +MainWindow::OnUpdateEditPasteSpecial(CCmdUI* pCmdUI) +{ + OnUpdateEditPaste(pCmdUI); +} + +/* + * Do some prep work and then call ProcessClipboard to copy files in. + */ +void +MainWindow::DoPaste(bool pasteJunkPaths) +{ + CString errStr, buildStr; + UINT format = 0; + UINT myFormat; + bool isOpen = false; + + if (fpContentList == nil || fpOpenArchive->IsReadOnly()) { + ASSERT(false); + return; + } + + myFormat = RegisterClipboardFormat(kClipboardFmtName); + if (myFormat == 0) { + errStr.LoadString(IDS_CLIPBOARD_REGFAILED); + ShowFailureMsg(this, errStr, IDS_FAILED); + goto bail; + } + WMSG1("myFormat = %u\n", myFormat); + + if (OpenClipboard() == false) { + errStr.LoadString(IDS_CLIPBOARD_OPENFAILED); + ShowFailureMsg(this, errStr, IDS_FAILED); + goto bail;; + } + isOpen = true; + + WMSG1("Found %d clipboard formats\n", CountClipboardFormats()); + while ((format = EnumClipboardFormats(format)) != 0) { + CString tmpStr; + tmpStr.Format(" %u", format); + buildStr += tmpStr; + } + WMSG1(" %s\n", buildStr); + +#if 0 + if (IsClipboardFormatAvailable(CF_HDROP)) { + errStr.LoadString(IDS_CLIPBOARD_NO_HDROP); + ShowFailureMsg(this, errStr, IDS_FAILED); + goto bail; + } +#endif + + /* impossible unless OnUpdateEditPaste was bypassed */ + if (!IsClipboardFormatAvailable(myFormat)) { + errStr.LoadString(IDS_CLIPBOARD_NOTFOUND); + ShowFailureMsg(this, errStr, IDS_FAILED); + goto bail; + } + + WMSG1("+++ total data on clipboard: %ld bytes\n", + GetClipboardContentLen()); + + HGLOBAL hGlobal; + LPVOID pGlobal; + + hGlobal = GetClipboardData(myFormat); + if (hGlobal == nil) { + ASSERT(false); + goto bail; + } + pGlobal = GlobalLock(hGlobal); + ASSERT(pGlobal != nil); + errStr = ProcessClipboard(pGlobal, GlobalSize(hGlobal), pasteJunkPaths); + fpContentList->Reload(); + + if (!errStr.IsEmpty()) + ShowFailureMsg(this, errStr, IDS_FAILED); + else + SuccessBeep(); + + GlobalUnlock(hGlobal); + +bail: + if (isOpen) + CloseClipboard(); +} + +/* + * Process the data in the clipboard. + * + * Returns an empty string on success, or an error message on failure. + */ +CString +MainWindow::ProcessClipboard(const void* vbuf, long bufLen, bool pasteJunkPaths) +{ + FileCollection fileColl; + CString errMsg, storagePrefix; + const unsigned char* buf = (const unsigned char*) vbuf; + DiskImgLib::A2File* pTargetSubdir = nil; + XferFileOptions xferOpts; + bool xferPrepped = false; + + /* set a standard error message */ + errMsg.LoadString(IDS_CLIPBOARD_READFAILURE); + + /* + * Pull the header out. + */ + if (bufLen < sizeof(fileColl)) { + WMSG0("Clipboard contents too small!\n"); + goto bail; + } + memcpy(&fileColl, buf, sizeof(fileColl)); + + /* + * Verify the length. Win98 seems to like to round things up to 16-byte + * boundaries, which screws up our "bufLen > 0" while condition below. + */ + if ((long) fileColl.length > bufLen) { + WMSG2("GLITCH: stored len=%ld, clip len=%ld\n", + fileColl.length, bufLen); + goto bail; + } + if (bufLen > (long) fileColl.length) { + /* trim off extra */ + WMSG2("NOTE: Windows reports excess length (%ld vs %ld)\n", + fileColl.length, bufLen); + bufLen = fileColl.length; + } + + buf += sizeof(fileColl); + bufLen -= sizeof(fileColl); + + WMSG4("FileCollection found: vers=%d off=%d len=%ld count=%ld\n", + fileColl.version, fileColl.dataOffset, fileColl.length, + fileColl.count); + if (fileColl.count == 0) { + /* nothing to do? */ + ASSERT(false); + return errMsg; + } + + /* + * Figure out where we want to put the files. For a disk archive + * this can be complicated. + * + * The target DiskFS (which could be a sub-volume) gets tucked into + * the xferOpts. + */ + if (fpOpenArchive->GetArchiveKind() == GenericArchive::kArchiveDiskImage) { + if (!ChooseAddTarget(&pTargetSubdir, &xferOpts.fpTargetFS)) + return ""; + } + fpOpenArchive->XferPrepare(&xferOpts); + xferPrepped = true; + + if (pTargetSubdir != nil) { + storagePrefix = pTargetSubdir->GetPathName(); + WMSG1("--- using storagePrefix '%s'\n", (const char*) storagePrefix); + } + + /* + * Set up a progress dialog to track it. + */ + ASSERT(fpActionProgress == nil); + fpActionProgress = new ActionProgressDialog; + fpActionProgress->Create(ActionProgressDialog::kActionAdd, this); + fpActionProgress->SetArcName("Clipboard data"); + + /* + * Loop over all files. + */ + WMSG1("+++ Starting paste, bufLen=%ld\n", bufLen); + while (bufLen > 0) { + FileCollectionEntry collEnt; + CString fileName, processErrStr; + + /* read the entry info */ + if (bufLen < sizeof(collEnt)) { + WMSG2("GLITCH: bufLen=%ld, sizeof(collEnt)=%d\n", + bufLen, sizeof(collEnt)); + ASSERT(false); + goto bail; + } + memcpy(&collEnt, buf, sizeof(collEnt)); + if (collEnt.signature != kEntrySignature) { + ASSERT(false); + goto bail; + } + + /* advance to the start of data */ + if (bufLen < collEnt.dataOffset) { + ASSERT(false); + goto bail; + } + buf += collEnt.dataOffset; + bufLen -= collEnt.dataOffset; + + /* extract the filename */ + if (bufLen < collEnt.fileNameLen) { + ASSERT(false); + goto bail; + } + fileName = buf; + buf += collEnt.fileNameLen; + bufLen -= collEnt.fileNameLen; + + //WMSG1("+++ pasting '%s'\n", fileName); + + /* strip the path (if requested) and prepend the storage prefix */ + ASSERT(fileName.GetLength() == collEnt.fileNameLen -1); + if (pasteJunkPaths && collEnt.fssep != '\0') { + int idx; + idx = fileName.ReverseFind(collEnt.fssep); + if (idx >= 0) + fileName = fileName.Right(fileName.GetLength() - idx -1); + } + if (!storagePrefix.IsEmpty()) { + CString tmpStr, tmpFileName; + tmpFileName = fileName; + if (collEnt.fssep == '\0') { + tmpFileName.Replace(':', '_'); // strip any ':'s in the name + collEnt.fssep = ':'; // define an fssep + } + + tmpStr = storagePrefix; + + /* storagePrefix fssep is always ':'; change it to match */ + if (collEnt.fssep != ':') + tmpStr.Replace(':', collEnt.fssep); + + tmpStr += collEnt.fssep; + tmpStr += tmpFileName; + fileName = tmpStr; + + } + fpActionProgress->SetFileName(fileName); + + /* make sure the data is there */ + if (bufLen < (long) (collEnt.dataLen + collEnt.rsrcLen + collEnt.cmmtLen)) + { + ASSERT(false); + goto bail; + } + + /* + * Process the entry. + * + * If the user hits "cancel" in the progress dialog we'll get thrown + * back out. For the time being I'm just treating it like any other + * failure. + */ + processErrStr = ProcessClipboardEntry(&collEnt, fileName, buf, bufLen); + if (!processErrStr.IsEmpty()) { + errMsg.Format("Unable to paste '%s': %s.", + (const char*) fileName, (const char*) processErrStr); + goto bail; + } + + buf += collEnt.dataLen + collEnt.rsrcLen + collEnt.cmmtLen; + bufLen -= collEnt.dataLen + collEnt.rsrcLen + collEnt.cmmtLen; + } + + ASSERT(bufLen == 0); + errMsg = ""; + +bail: + if (xferPrepped) { + if (errMsg.IsEmpty()) + fpOpenArchive->XferFinish(this); + else + fpOpenArchive->XferAbort(this); + } + if (fpActionProgress != nil) { + fpActionProgress->Cleanup(this); + fpActionProgress = nil; + } + return errMsg; +} + +/* + * Process a single clipboard entry. + * + * On entry, "buf" points to the start of the first chunk of data (either + * data fork or resource fork). If the file has empty forks or is a + * subdirectory, then "buf" is actually pointing at the start of the + * next entry. + */ +CString +MainWindow::ProcessClipboardEntry(const FileCollectionEntry* pCollEnt, + const char* pathName, const unsigned char* buf, long remLen) +{ + GenericArchive::FileDetails::FileKind entryKind; + GenericArchive::FileDetails details; + unsigned char* dataBuf = nil; + unsigned char* rsrcBuf = nil; + long dataLen, rsrcLen, cmmtLen; + CString errMsg; + + entryKind = (GenericArchive::FileDetails::FileKind) pCollEnt->entryKind; + WMSG2(" Processing '%s' (%d)\n", pathName, entryKind); + + details.entryKind = entryKind; + details.origName = "Clipboard"; + details.storageName = pathName; + details.fileSysFmt = (DiskImg::FSFormat) pCollEnt->sourceFS; + details.fileSysInfo = pCollEnt->fssep; + details.access = pCollEnt->access; + details.fileType = pCollEnt->fileType; + details.extraType = pCollEnt->auxType; + GenericArchive::UNIXTimeToDateTime(&pCollEnt->createWhen, + &details.createWhen); + GenericArchive::UNIXTimeToDateTime(&pCollEnt->modWhen, + &details.modWhen); + time_t now = time(nil); + GenericArchive::UNIXTimeToDateTime(&now, &details.archiveWhen); + + /* + * Because of the way XferFile works, we need to make a copy of + * the data. (For NufxLib, it's going to gather up all of the + * data and flush it all at once, so it needs to own the memory.) + * + * Ideally we'd use a different interface that didn't require a + * data copy -- NufxLib can do it that way as well -- but it's + * not worth maintaining two separate interfaces. + * + * This approach does allow the xfer code to handle DOS high-ASCII + * text conversions in place, though. If we didn't do it this way + * we'd have to make a copy in the xfer code to avoid contaminating + * the clipboard data. That would be more efficient, but probably + * a bit messier. + * + * The stuff below figures out which forks we're expected to have based + * on the file type info. This helps us distinguish between a file + * with a zero-length fork and a file without that kind of fork. + */ + bool hasData = false; + bool hasRsrc = false; + if (details.entryKind == GenericArchive::FileDetails::kFileKindDataFork) { + hasData = true; + details.storageType = kNuStorageSeedling; + } else if (details.entryKind == GenericArchive::FileDetails::kFileKindRsrcFork) { + hasRsrc = true; + details.storageType = kNuStorageExtended; + } else if (details.entryKind == GenericArchive::FileDetails::kFileKindBothForks) { + hasData = hasRsrc = true; + details.storageType = kNuStorageExtended; + } else if (details.entryKind == GenericArchive::FileDetails::kFileKindDiskImage) { + hasData = true; + details.storageType = kNuStorageSeedling; + } else if (details.entryKind == GenericArchive::FileDetails::kFileKindDirectory) { + details.storageType = kNuStorageDirectory; + } else { + ASSERT(false); + return "Internal error."; + } + + if (hasData) { + if (pCollEnt->dataLen == 0) { + dataBuf = new unsigned char[1]; + dataLen = 0; + } else { + dataLen = pCollEnt->dataLen; + dataBuf = new unsigned char[dataLen]; + if (dataBuf == nil) + return "memory allocation failed."; + memcpy(dataBuf, buf, dataLen); + buf += dataLen; + remLen -= dataLen; + } + } else { + ASSERT(dataBuf == nil); + dataLen = -1; + } + + if (hasRsrc) { + if (pCollEnt->rsrcLen == 0) { + rsrcBuf = new unsigned char[1]; + rsrcLen = 0; + } else { + rsrcLen = pCollEnt->rsrcLen; + rsrcBuf = new unsigned char[rsrcLen]; + if (rsrcBuf == nil) + return "Memory allocation failed."; + memcpy(rsrcBuf, buf, rsrcLen); + buf += rsrcLen; + remLen -= rsrcLen; + } + } else { + ASSERT(rsrcBuf == nil); + rsrcLen = -1; + } + + if (pCollEnt->cmmtLen > 0) { + cmmtLen = pCollEnt->cmmtLen; + /* CMMT FIX -- not supported by XferFile */ + } + + ASSERT(remLen >= 0); + + errMsg = fpOpenArchive->XferFile(&details, &dataBuf, dataLen, + &rsrcBuf, rsrcLen); + delete[] dataBuf; + delete[] rsrcBuf; + dataBuf = rsrcBuf = nil; + + return errMsg; +} diff --git a/app/ConfirmOverwriteDialog.cpp b/app/ConfirmOverwriteDialog.cpp new file mode 100644 index 0000000..e5d910a --- /dev/null +++ b/app/ConfirmOverwriteDialog.cpp @@ -0,0 +1,173 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Support for ConfirmOverwriteDialog and RenameOverwriteDialog classes. + */ +#include "stdafx.h" +#include "ConfirmOverwriteDialog.h" +#include "GenericArchive.h" +#include + + +/* + * ========================================================================== + * RenameOverwriteDialog + * ========================================================================== + */ + +BEGIN_MESSAGE_MAP(RenameOverwriteDialog, CDialog) + ON_WM_HELPINFO() +END_MESSAGE_MAP() + +/* + * Init static text fields. + */ +BOOL +RenameOverwriteDialog::OnInitDialog(void) +{ + CWnd* pWnd; + + pWnd = GetDlgItem(IDC_RENOVWR_SOURCE_NAME); + ASSERT(pWnd != nil); + pWnd->SetWindowText(fNewFileSource); + + return CDialog::OnInitDialog(); +} + +/* + * Convert values. + */ +void +RenameOverwriteDialog::DoDataExchange(CDataExchange* pDX) +{ + DDX_Text(pDX, IDC_RENOVWR_ORIG_NAME, fExistingFile); + DDX_Text(pDX, IDC_RENOVWR_NEW_NAME, fNewName); + + /* validate the path field */ + if (pDX->m_bSaveAndValidate) { + if (fNewName.IsEmpty()) { + MessageBox("You must specify a new name.", + "CiderPress", MB_OK); + pDX->Fail(); + } + + // we *could* try to validate the path here... + } +} + +BOOL +RenameOverwriteDialog::OnHelpInfo(HELPINFO* lpHelpInfo) +{ + WinHelp((DWORD) lpHelpInfo->iCtrlId, HELP_CONTEXTPOPUP); + return TRUE; // yes, we handled it +} + + +/* + * ========================================================================== + * ConfirmOverwriteDialog + * ========================================================================== + */ + +BEGIN_MESSAGE_MAP(ConfirmOverwriteDialog, CDialog) + ON_BN_CLICKED(IDC_OVWR_YES, OnYes) + ON_BN_CLICKED(IDC_OVWR_YESALL, OnYesToAll) + ON_BN_CLICKED(IDC_OVWR_NO, OnNo) + ON_BN_CLICKED(IDC_OVWR_NOALL, OnNoToAll) + ON_BN_CLICKED(IDC_OVWR_RENAME, OnRename) + ON_WM_HELPINFO() +END_MESSAGE_MAP() + + +/* + * Replace some static text fields. + */ +BOOL +ConfirmOverwriteDialog::OnInitDialog(void) +{ + CWnd* pWnd; + CString tmpStr, dateStr; + + pWnd = GetDlgItem(IDC_OVWR_EXIST_NAME); + ASSERT(pWnd != nil); + pWnd->SetWindowText(fExistingFile); + + pWnd = GetDlgItem(IDC_OVWR_EXIST_INFO); + ASSERT(pWnd != nil); + FormatDate(fExistingFileModWhen, &dateStr); + tmpStr.Format("Modified %s", dateStr); + pWnd->SetWindowText(tmpStr); + + pWnd = GetDlgItem(IDC_OVWR_NEW_NAME); + ASSERT(pWnd != nil); + pWnd->SetWindowText(fNewFileSource); + + pWnd = GetDlgItem(IDC_OVWR_NEW_INFO); + ASSERT(pWnd != nil); + FormatDate(fNewFileModWhen, &dateStr); + tmpStr.Format("Modified %s", dateStr); + pWnd->SetWindowText(tmpStr); + + pWnd = GetDlgItem(IDC_OVWR_RENAME); + ASSERT(pWnd != nil); + pWnd->EnableWindow(fAllowRename); + + return CDialog::OnInitDialog(); +} + +/* + * Handle a click on the question-mark button. + */ +BOOL +ConfirmOverwriteDialog::OnHelpInfo(HELPINFO* lpHelpInfo) +{ + WinHelp((DWORD) lpHelpInfo->iCtrlId, HELP_CONTEXTPOPUP); + return TRUE; // yes, we handled it +} + +/* + * One of the buttons was hit. + */ +void +ConfirmOverwriteDialog::OnYes(void) +{ + fResultOverwrite = true; + CDialog::OnOK(); +} +void +ConfirmOverwriteDialog::OnYesToAll(void) +{ + fResultOverwrite = true; + fResultApplyToAll = true; + CDialog::OnOK(); +} +void +ConfirmOverwriteDialog::OnNo(void) +{ + //fResultOverwrite = false; + CDialog::OnOK(); +} +void +ConfirmOverwriteDialog::OnNoToAll(void) +{ + //fResultOverwrite = true; + fResultApplyToAll = true; + CDialog::OnOK(); +} +void +ConfirmOverwriteDialog::OnRename(void) +{ + RenameOverwriteDialog dlg; + + dlg.fNewFileSource = fNewFileSource; + dlg.fExistingFile = fExistingFile; + dlg.fNewName = fExistingFile; + if (dlg.DoModal() == IDOK) { + fExistingFile = dlg.fNewName; + fResultRename = true; + CDialog::OnOK(); + } +} diff --git a/app/ConfirmOverwriteDialog.h b/app/ConfirmOverwriteDialog.h new file mode 100644 index 0000000..8de5e96 --- /dev/null +++ b/app/ConfirmOverwriteDialog.h @@ -0,0 +1,93 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Ask for confirmation before overwriting a file. + */ +#ifndef __CONFIRMOVERWRITEDIALOG__ +#define __CONFIRMOVERWRITEDIALOG__ + +#include "resource.h" + +/* + * Accept or reject overwriting an existing file or archive record. + */ +class ConfirmOverwriteDialog : public CDialog { +public: + ConfirmOverwriteDialog(CWnd* pParentWnd = nil) : + CDialog(IDD_CONFIRM_OVERWRITE, pParentWnd) + { + fResultOverwrite = false; + fResultApplyToAll = false; + fResultRename = false; + fAllowRename = true; + fNewFileModWhen = -1; + fExistingFileModWhen = -1; + } + ~ConfirmOverwriteDialog(void) {} + + // name of file in archive (during extraction) or disk (for add) + CString fNewFileSource; + time_t fNewFileModWhen; + + // full path of file being extracted onto (or record name for add) + CString fExistingFile; + time_t fExistingFileModWhen; + + // result flags: yes/no/yes-all/no-all + bool fResultOverwrite; + bool fResultApplyToAll; + // if this flag is set, try again with updated "fExistingFile" value + bool fResultRename; + // set this to enable the "Rename" button + bool fAllowRename; + +private: + afx_msg void OnYes(void); + afx_msg void OnYesToAll(void); + afx_msg void OnNo(void); + afx_msg void OnNoToAll(void); + afx_msg void OnRename(void); + afx_msg BOOL OnHelpInfo(HELPINFO* lpHelpInfo); + + virtual BOOL OnInitDialog(void); + + DECLARE_MESSAGE_MAP() +}; + +/* + * Allow the user to rename a file being added or extracted, rather than + * overwriting an existing file. ConfirmOverwriteDialog creates one of these + * when the "rename" button is clicked on. + * + * The names of the fields here correspond directly to those in + * ConfirmOverwriteDialog. + */ +class RenameOverwriteDialog : public CDialog { +public: + RenameOverwriteDialog(CWnd* pParentWnd = nil) : + CDialog(IDD_RENAME_OVERWRITE, pParentWnd) + {} + ~RenameOverwriteDialog(void) {} + + // name of file on source medium + CString fNewFileSource; + + // converted name, which already exists in destination medium + CString fExistingFile; + + // result: what the user has renamed it to + CString fNewName; + +private: + virtual BOOL OnInitDialog(void); + virtual void DoDataExchange(CDataExchange* pDX); + + afx_msg BOOL OnHelpInfo(HELPINFO* lpHelpInfo); + + DECLARE_MESSAGE_MAP() +}; + +#endif /*__CONFIRMOVERWRITEDIALOG__*/ \ No newline at end of file diff --git a/app/ContentList.cpp b/app/ContentList.cpp new file mode 100644 index 0000000..903bea1 --- /dev/null +++ b/app/ContentList.cpp @@ -0,0 +1,1150 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Implementation of list control showing archive contents. + */ +#include "stdafx.h" +#include "Main.h" +#include "ContentList.h" + +const LPARAM kDescendingFlag = 0x0100; + + +BEGIN_MESSAGE_MAP(ContentList, CListCtrl) + ON_WM_CREATE() + ON_WM_DESTROY() + ON_WM_SYSCOLORCHANGE() + //ON_WM_MOUSEWHEEL() + ON_NOTIFY_REFLECT(NM_DBLCLK, OnDoubleClick) + ON_NOTIFY_REFLECT(NM_RCLICK, OnRightClick) + ON_NOTIFY_REFLECT(LVN_COLUMNCLICK, OnColumnClick) + ON_NOTIFY_REFLECT(LVN_GETDISPINFO, OnGetDispInfo) +END_MESSAGE_MAP() + +#if 0 +afx_msg BOOL +ContentList::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt) +{ + WMSG0("MOUSE WHEEL\n"); + return CWnd::OnMouseWheel(nFlags, zDelta, pt); +// return TRUE; +} +#endif + + +/* + * Put the window into "report" mode, and add a client edge since we're not + * using one on the frame window. + */ +BOOL +ContentList::PreCreateWindow(CREATESTRUCT& cs) +{ + if (!CListCtrl::PreCreateWindow(cs)) + return FALSE; + + cs.style &= ~LVS_TYPEMASK; + cs.style |= LVS_REPORT; + cs.style |= LVS_SHOWSELALWAYS; + cs.dwExStyle |= WS_EX_CLIENTEDGE; + + return TRUE; +} + +/* + * Auto-cleanup the object. + */ +void +ContentList::PostNcDestroy(void) +{ + WMSG0("ContentList PostNcDestroy\n"); + delete this; +} + +static inline int +MaxVal(int a, int b) +{ + return a > b ? a : b; +} + +/* + * Create and populate list control. + */ +int +ContentList::OnCreate(LPCREATESTRUCT lpcs) +{ + CString colHdrs[kNumVisibleColumns] = { + "Pathname", "Type", "Aux", "Mod Date", + "Format", "Size", "Ratio", "Packed", "Access" + }; // these should come from string table, not hard-coded + static int colFmt[kNumVisibleColumns] = { + LVCFMT_LEFT, LVCFMT_LEFT, LVCFMT_LEFT, LVCFMT_LEFT, + LVCFMT_LEFT, LVCFMT_RIGHT, LVCFMT_RIGHT, LVCFMT_RIGHT, LVCFMT_LEFT + }; + + if (CListCtrl::OnCreate(lpcs) == -1) + return -1; + + /* + * Create all of the columns with an initial width of 1, then set + * them to the correct values with NewColumnWidths() (which handles + * defaulted values). + */ + for (int i = 0; i < kNumVisibleColumns; i++) + InsertColumn(i, colHdrs[i], colFmt[i], 1); + NewColumnWidths(); + + /* add images for list; this MUST be loaded before header images */ + LoadListImages(); + SetImageList(&fListImageList, LVSIL_SMALL); + + /* add our up/down arrow bitmaps */ + LoadHeaderImages(); + CHeaderCtrl* pHeader = GetHeaderCtrl(); + if (pHeader == nil) + WMSG0("GLITCH: couldn't get header ctrl\n"); + ASSERT(pHeader != NULL); + pHeader->SetImageList(&fHdrImageList); + + /* load the data and sort it */ + if (LoadData() != 0) { + MessageBox("Not all entries were loaded.", "Error", + MB_OK | MB_ICONSTOP); + /* keep going with what we've got; the error only affects display */ + } + NewSortOrder(); + + /* grab the focus so we get keyboard and mouse wheel messages */ + SetFocus(); + + /* highlight/select entire line, not just filename */ + ListView_SetExtendedListViewStyleEx(m_hWnd, + LVS_EX_FULLROWSELECT, LVS_EX_FULLROWSELECT); + + return 0; +} + +/* + * If we're being shut down, save off the column width info before the window + * gets destroyed. + */ +void +ContentList::OnDestroy(void) +{ + WMSG0("ContentList OnDestroy\n"); + + ExportColumnWidths(); + CListCtrl::OnDestroy(); +} + +/* + * The system colors are changing; delete the image list and re-load it. + */ +void +ContentList::OnSysColorChange(void) +{ + fHdrImageList.DeleteImageList(); + LoadHeaderImages(); +} + +/* + * They've clicked on a header. Figure out what kind of sort order we want + * to use. + */ +void +ContentList::OnColumnClick(NMHDR* pnmh, LRESULT* pResult) +{ + NM_LISTVIEW* pnmlv = (NM_LISTVIEW*) pnmh; + + WMSG0("OnColumnClick!!\n"); + + if (fpLayout->GetSortColumn() == pnmlv->iSubItem) + fpLayout->SetAscending(!fpLayout->GetAscending()); + else { + fpLayout->SetSortColumn(pnmlv->iSubItem); + fpLayout->SetAscending(true); + } + + NewSortOrder(); + *pResult = 0; +} + +/* + * Copy the current column widths out to the Preferences object. + */ +void +ContentList::ExportColumnWidths(void) +{ + //WMSG0("ExportColumnWidths\n"); + for (int i = 0; i < kNumVisibleColumns; i++) + fpLayout->SetColumnWidth(i, GetColumnWidth(i)); +} + +/* + * Call this when the column widths are changed programmatically (e.g. by + * the preferences page enabling or disabling columns). + * + * We want to set any defaulted entries to actual values so that, if the + * font properties change, column A doesn't resize when column B is tweaked + * in the Preferences dialog. (If it's still set to "default", then when + * we say "update all widths" the defaultedness will be re-evaluated.) + */ +void +ContentList::NewColumnWidths(void) +{ + for (int i = 0; i < kNumVisibleColumns; i++) { + int width = fpLayout->GetColumnWidth(i); + if (width == ColumnLayout::kWidthDefaulted) { + width = GetDefaultWidth(i); + WMSG2("Defaulting width %d to %d\n", i, width); + fpLayout->SetColumnWidth(i, width); + } + SetColumnWidth(i, width); + } +} + +#if 0 // replaced by GenericArchive reload flag +/* + * If we're in the middle of an update, invalidate the contents of the list + * so that we don't try to redraw from underlying storage that is no longer + * there. + * + * If we call DeleteAllItems the list will immediately blank itself. This + * rather sucks. Instead, we just mark it as invalid and have the "virtual" + * list goodies return empty strings. If the window has to redraw it won't + * do so properly, but most of the time it looks good and it beats flashing + * blank or crashing. + */ +void +ContentList::Invalidate(void) +{ + fInvalid = true; +} +#endif + +/* + * The archive contents have changed. Reload the list from the + * GenericArchive. + * + * Reloading causes the current selection and view position to be lost. This + * is sort of annoying if all we did is add a comment, so we try to save the + * selection and reapply it. To do this correctly we need some sort of + * unique identifier so we can spot the records that have come back. + * + * Nothing in GenericArchive should be considered valid at this point. + */ +void +ContentList::Reload(bool saveSelection) +{ + WMSG0("Reloading ContentList\n"); + CWaitCursor waitc; + +// fInvalid = false; + fpArchive->ClearReloadFlag(); + + long* savedSel = nil; + long selCount = 0; + + if (saveSelection) { + /* get the serials for the current selection (if any) */ + savedSel = GetSelectionSerials(&selCount); + } + + /* get the item that's currently at the top of the page */ + int top = GetTopIndex(); + int bottom = top + GetCountPerPage() -1; + + /* reload the list */ + LoadData(); + NewSortOrder(); + + if (savedSel != nil) { + /* restore the selection */ + RestoreSelection(savedSel, selCount); + delete[] savedSel; + } + + /* try to put us back in the same place */ + EnsureVisible(bottom, false); + EnsureVisible(top, false); +} + +#if 1 +/* + * Get the "selection serials" from the list of selected items. + * + * The caller is responsible for delete[]ing the return value. + */ +long* +ContentList::GetSelectionSerials(long* pSelCount) +{ + long* savedSel = nil; + long maxCount; + + maxCount = GetSelectedCount(); + WMSG1("GetSelectionSerials (maxCount=%d)\n", maxCount); + + if (maxCount > 0) { + savedSel = new long[maxCount]; + int idx = 0; + + POSITION posn; + posn = GetFirstSelectedItemPosition(); + ASSERT(posn != nil); + if (posn == nil) + return nil; + while (posn != nil) { + int num = GetNextSelectedItem(posn); + GenericEntry* pEntry = (GenericEntry*) GetItemData(num); + + if (idx == maxCount) { + ASSERT(false); + break; + } + savedSel[idx++] = pEntry->GetSelectionSerial(); + } + + ASSERT(idx == maxCount); + } + + *pSelCount = maxCount; + return savedSel; +} + +/* + * Restore the selection from the "savedSel" list. + */ +void +ContentList::RestoreSelection(const long* savedSel, long selCount) +{ + WMSG1("RestoreSelection (selCount=%d)\n", selCount); + if (savedSel == nil) + return; + + int i, j; + + for (i = GetItemCount()-1; i >= 0; i--) { + GenericEntry* pEntry = (GenericEntry*) GetItemData(i); + + for (j = 0; j < selCount; j++) { + if (pEntry->GetSelectionSerial() == savedSel[j] && + pEntry->GetSelectionSerial() != -1) + { + /* match! */ + if (SetItemState(i, LVIS_SELECTED, LVIS_SELECTED) == FALSE) { + WMSG1("WHOA: unable to set selected on item=%d\n", i); + } + break; + } + } + } +} +#endif + + +/* + * Call this when the sort order changes. + */ +void +ContentList::NewSortOrder(void) +{ + CWaitCursor wait; // automatically changes mouse to hourglass + int column; + + column = fpLayout->GetSortColumn(); + if (!fpLayout->GetAscending()) + column |= kDescendingFlag; + + SetSortIcon(); + SortItems(CompareFunc, column); +} + +/* + * Get the file type display string. + * + * "buf" must be able to hold at least 4 characters plus the NUL (i.e. 5). + * Use kFileTypeBufLen. + */ +/*static*/ void +ContentList::MakeFileTypeDisplayString(const GenericEntry* pEntry, char* buf) +{ + bool isDir = + pEntry->GetRecordKind() == GenericEntry::kRecordKindVolumeDir || + pEntry->GetRecordKind() == GenericEntry::kRecordKindDirectory; + + if (pEntry->GetSourceFS() == DiskImg::kFormatMacHFS && isDir) { + /* HFS directories don't have types; fake it */ + ::lstrcpy(buf, "DIR/"); + } else if (!(pEntry->GetFileType() >= 0 && pEntry->GetFileType() <= 0xff)) + { + /* oversized type; assume it's HFS */ + char typeBuf[kFileTypeBufLen]; + MakeMacTypeString(pEntry->GetFileType(), typeBuf); + + switch (pEntry->GetRecordKind()) { + case GenericEntry::kRecordKindFile: + ::lstrcpy(buf, typeBuf); + break; + case GenericEntry::kRecordKindForkedFile: + ::sprintf(buf, "%s+", typeBuf); + break; + case GenericEntry::kRecordKindUnknown: + // shouldn't happen + ::sprintf(buf, "%s-", typeBuf); + break; + case GenericEntry::kRecordKindVolumeDir: + case GenericEntry::kRecordKindDirectory: + case GenericEntry::kRecordKindDisk: + default: + ASSERT(FALSE); + ::lstrcpy(buf, "!!!"); + break; + } + } else { + /* typical ProDOS-style stuff */ + switch (pEntry->GetRecordKind()) { + case GenericEntry::kRecordKindVolumeDir: + case GenericEntry::kRecordKindDirectory: + ::sprintf(buf, "%s/", pEntry->GetFileTypeString()); + break; + case GenericEntry::kRecordKindFile: + ::sprintf(buf, "%s", pEntry->GetFileTypeString()); + break; + case GenericEntry::kRecordKindForkedFile: + ::sprintf(buf, "%s+", pEntry->GetFileTypeString()); + break; + case GenericEntry::kRecordKindDisk: + ::lstrcpy(buf, "Disk"); + break; + case GenericEntry::kRecordKindUnknown: + // usually a GSHK-archived empty data file does this + ::sprintf(buf, "%s-", pEntry->GetFileTypeString()); + break; + default: + ASSERT(FALSE); + ::lstrcpy(buf, "!!!"); + break; + } + } +} + +/* + * Convert an HFS file/creator type into a string. + * + * "buf" must be able to hold at least 4 characters plus the NUL. Use + * kFileTypeBufLen. + */ +/*static*/ void +ContentList::MakeMacTypeString(unsigned long val, char* buf) +{ + /* expand longword with ASCII type bytes */ + buf[0] = (unsigned char) (val >> 24); + buf[1] = (unsigned char) (val >> 16); + buf[2] = (unsigned char) (val >> 8); + buf[3] = (unsigned char) val; + buf[4] = '\0'; + + /* sanitize */ + while (*buf != '\0') { + *buf = DiskImg::MacToASCII(*buf); + buf++; + } +} + +/* + * Get the aux type display string. + * + * "buf" must be able to hold at least 5 characters plus the NUL (i.e. 6). + * Use kFileTypeBufLen. + */ +/*static*/ void +ContentList::MakeAuxTypeDisplayString(const GenericEntry* pEntry, char* buf) +{ + bool isDir = + pEntry->GetRecordKind() == GenericEntry::kRecordKindVolumeDir || + pEntry->GetRecordKind() == GenericEntry::kRecordKindDirectory; + + if (pEntry->GetSourceFS() == DiskImg::kFormatMacHFS && isDir) { + /* HFS directories don't have types; fake it */ + ::lstrcpy(buf, " "); + } else if (!(pEntry->GetFileType() >= 0 && pEntry->GetFileType() <= 0xff)) + { + /* oversized type; assume it's HFS */ + MakeMacTypeString(pEntry->GetAuxType(), buf); + } else { + if (pEntry->GetRecordKind() == GenericEntry::kRecordKindDisk) + ::sprintf(buf, "%dk", pEntry->GetUncompressedLen() / 1024); + else + ::sprintf(buf, "$%04lX", pEntry->GetAuxType()); + } +} + + +/* + * Generate the funky ratio display string. While we're at it, return a + * numeric value that we can sort on. + * + * "buf" must be able to hold at least 6 chars plus the NULL. + */ +void +ContentList::MakeRatioDisplayString(const GenericEntry* pEntry, char* buf, + int* pPerc) +{ + LONGLONG totalLen, totalCompLen; + totalLen = pEntry->GetUncompressedLen(); + totalCompLen = pEntry->GetCompressedLen(); + + if ((!totalLen && totalCompLen) || (totalLen && !totalCompLen)) { + ::lstrcpy(buf, "---"); /* weird */ + *pPerc = -1; + } else if (totalLen < totalCompLen) { + ::lstrcpy(buf, ">100%"); /* compression failed? */ + *pPerc = 101; + } else { + *pPerc = ComputePercent(totalCompLen, totalLen); + ::sprintf(buf, "%d%%", *pPerc); + } +} + + +/* + * Return the value for a particular row and column. + * + * This gets called *a lot* while the list is being drawn, scrolled, etc. + * Don't do anything too expensive. + */ +void +ContentList::OnGetDispInfo(NMHDR* pnmh, LRESULT* pResult) +{ + //static const char kAccessBits[] = "DNB IWR"; + static const char kAccessBits[] = "dnb iwr"; + LV_DISPINFO* plvdi = (LV_DISPINFO*) pnmh; + CString str; + + if (fpArchive->GetReloadFlag()) { + ::lstrcpy(plvdi->item.pszText, ""); + *pResult = 0; + return; + } + + //WMSG0("OnGetDispInfo\n"); + + if (plvdi->item.mask & LVIF_TEXT) { + GenericEntry* pEntry = (GenericEntry*) plvdi->item.lParam; + //GenericEntry* pEntry = fpArchive->GetEntry(plvdi->item.iItem); + + switch (plvdi->item.iSubItem) { + case 0: // pathname + if ((int)strlen(pEntry->GetDisplayName()) > plvdi->item.cchTextMax) { + // looks like current limit is 264 chars, which we could hit + ::strncpy(plvdi->item.pszText, pEntry->GetDisplayName(), + plvdi->item.cchTextMax); + plvdi->item.pszText[plvdi->item.cchTextMax-1] = '\0'; + } else { + ::lstrcpy(plvdi->item.pszText, pEntry->GetDisplayName()); + } + + /* + * Sanitize the string. This is really only necessary for + * HFS, which has 8-bit "Macintosh Roman" filenames. The Win32 + * controls can deal with it, but it looks better if we massage + * it a little. + */ + { + unsigned char* str = (unsigned char*) plvdi->item.pszText; + + while (*str != '\0') { + *str = DiskImg::MacToASCII(*str); + str++; + } + } + break; + case 1: // type + MakeFileTypeDisplayString(pEntry, plvdi->item.pszText); + break; + case 2: // auxtype + MakeAuxTypeDisplayString(pEntry, plvdi->item.pszText); + break; + case 3: // mod date + { + CString modDate; + FormatDate(pEntry->GetModWhen(), &modDate); + ::lstrcpy(plvdi->item.pszText, (LPCTSTR) modDate); + } + break; + case 4: // format + ASSERT(pEntry->GetFormatStr() != nil); + ::lstrcpy(plvdi->item.pszText, pEntry->GetFormatStr()); + break; + case 5: // size + ::sprintf(plvdi->item.pszText, "%ld", pEntry->GetUncompressedLen()); + break; + case 6: // ratio + int crud; + MakeRatioDisplayString(pEntry, plvdi->item.pszText, &crud); + break; + case 7: // packed + ::sprintf(plvdi->item.pszText, "%ld", pEntry->GetCompressedLen()); + break; + case 8: // access + char bitLabels[sizeof(kAccessBits)]; + int i, j, mask; + + for (i = 0, j = 0, mask = 0x80; i < 8; i++, mask >>= 1) { + if (pEntry->GetAccess() & mask) + bitLabels[j++] = kAccessBits[i]; + } + bitLabels[j] = '\0'; + ASSERT(j < sizeof(bitLabels)); + //::sprintf(plvdi->item.pszText, "0x%02x", pEntry->GetAccess()); + ::lstrcpy(plvdi->item.pszText, bitLabels); + break; + case 9: // NuRecordIdx [hidden] + break; + default: + ASSERT(false); + break; + } + } + + //if (plvdi->item.mask & LVIF_IMAGE) { + // WMSG2("IMAGE req item=%d subitem=%d\n", + // plvdi->item.iItem, plvdi->item.iSubItem); + //} + + *pResult = 0; +} + + +/* + * Helper functions for sort routine. + */ +static inline +CompareUnsignedLong(unsigned long u1, unsigned long u2) +{ + if (u1 < u2) + return -1; + else if (u1 > u2) + return 1; + else + return 0; +} +static inline +CompareLONGLONG(LONGLONG u1, LONGLONG u2) +{ + if (u1 < u2) + return -1; + else if (u1 > u2) + return 1; + else + return 0; +} + +/* + * Static comparison function for list sorting. + */ +int CALLBACK +ContentList::CompareFunc(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort) +{ + const GenericEntry* pEntry1 = (const GenericEntry*) lParam1; + const GenericEntry* pEntry2 = (const GenericEntry*) lParam2; + char tmpBuf1[16]; // needs >= 5 for file type compare, and + char tmpBuf2[16]; // >= 7 for ratio string + int result; + + /* for descending order, flip the parameters */ + if (lParamSort & kDescendingFlag) { + const GenericEntry* tmp; + lParamSort &= ~(kDescendingFlag); + tmp = pEntry1; + pEntry1 = pEntry2; + pEntry2 = tmp; + } + + switch (lParamSort) { + case 0: // pathname + result = ::stricmp(pEntry1->GetDisplayName(), pEntry2->GetDisplayName()); + break; + case 1: // file type + MakeFileTypeDisplayString(pEntry1, tmpBuf1); + MakeFileTypeDisplayString(pEntry2, tmpBuf2); + result = ::stricmp(tmpBuf1, tmpBuf2); + if (result != 0) + break; + /* else fall through to case 2 */ + case 2: // aux type + if (pEntry1->GetRecordKind() == GenericEntry::kRecordKindDisk) { + if (pEntry2->GetRecordKind() == GenericEntry::kRecordKindDisk) { + result = pEntry1->GetAuxType() - pEntry2->GetAuxType(); + } else { + result = -1; + } + } else if (pEntry2->GetRecordKind() == GenericEntry::kRecordKindDisk) { + result = 1; + } else { + result = pEntry1->GetAuxType() - pEntry2->GetAuxType(); + } + break; + case 3: // mod date + result = CompareUnsignedLong(pEntry1->GetModWhen(), + pEntry2->GetModWhen()); + break; + case 4: // format + result = ::lstrcmp(pEntry1->GetFormatStr(), pEntry2->GetFormatStr()); + break; + case 5: // size + result = CompareLONGLONG(pEntry1->GetUncompressedLen(), + pEntry2->GetUncompressedLen()); + break; + case 6: // ratio + int perc1, perc2; + MakeRatioDisplayString(pEntry1, tmpBuf1, &perc1); + MakeRatioDisplayString(pEntry2, tmpBuf2, &perc2); + result = perc1 - perc2; + break; + case 7: // packed + result = CompareLONGLONG(pEntry1->GetCompressedLen(), + pEntry2->GetCompressedLen()); + break; + case 8: // access + result = CompareUnsignedLong(pEntry1->GetAccess(), + pEntry2->GetAccess()); + break; + case kNumVisibleColumns: // file-order sort + default: + result = pEntry1->GetIndex() - pEntry2->GetIndex(); + break; + } + + return result; +} + +/* + * Fill the columns with data from the archive entries. We use a "virtual" + * list control to avoid storing everything multiple times. However, we + * still create one item per entry so that the list control will do most + * of the sorting for us (otherwise we have to do the sorting ourselves). + * + * Someday we should probably move to a wholly virtual list view. + */ +int +ContentList::LoadData(void) +{ + GenericEntry* pEntry; + LV_ITEM lvi; + int dirCount = 0; + int idx = 0; + + DeleteAllItems(); // for Reload case + + pEntry = fpArchive->GetEntries(); + while (pEntry != nil) { + pEntry->SetIndex(idx); + + lvi.mask = LVIF_TEXT | LVIF_IMAGE | LVIF_PARAM; + lvi.iItem = idx++; + lvi.iSubItem = 0; + if (pEntry->GetDamaged()) + lvi.iImage = kListIconDamaged; + else if (pEntry->GetSuspicious()) + lvi.iImage = kListIconSuspicious; + else if (pEntry->GetHasNonEmptyComment()) + lvi.iImage = kListIconNonEmptyComment; + else if (pEntry->GetHasComment()) + lvi.iImage = kListIconComment; + else + lvi.iImage = kListIconNone; + lvi.pszText = LPSTR_TEXTCALLBACK; + lvi.lParam = (LPARAM) pEntry; + + if (InsertItem(&lvi) == -1) { + ASSERT(false); + return -1; + } + + pEntry = pEntry->GetNext(); + } + + WMSG3("ContentList got %d entries (%d files + %d unseen directories)\n", + idx, idx - dirCount, dirCount); + return 0; +} + + +/* + * Return the default width for the specified column. + */ +int +ContentList::GetDefaultWidth(int col) +{ + int retval; + + switch (col) { + case 0: // pathname + retval = 200; + break; + case 1: // type (need "$XY" and long HFS types) + //retval = MaxVal(GetStringWidth("XXMMM+"), GetStringWidth("XXType")); + retval = MaxVal(GetStringWidth("XXMMMM+"), GetStringWidth("XXType")); + break; + case 2: // auxtype (hex or long HFS type) + //retval = MaxVal(GetStringWidth("XX$8888"), GetStringWidth("XXAux")); + retval = MaxVal(GetStringWidth("XX$CCCC"), GetStringWidth("XXAux")); + break; + case 3: // mod date + retval = GetStringWidth("XX88-MMM-88 88:88"); + break; + case 4: // format + retval = GetStringWidth("XXUncompr"); + break; + case 5: // uncompressed size + retval = GetStringWidth("XX88888888"); + break; + case 6: // ratio + retval = MaxVal(GetStringWidth("XXRatio"), GetStringWidth("XX100%")); + break; + case 7: // packed + retval = GetStringWidth("XX88888888"); + break; + case 8: // access + retval = MaxVal(GetStringWidth("XXAccess"), GetStringWidth("XXdnbiwr")); + break; + default: + ASSERT(false); + retval = 0; + } + + return retval; +} + + +/* + * Set the up/down sorting arrow as appropriate. + */ +void +ContentList::SetSortIcon(void) +{ + CHeaderCtrl* pHeader = GetHeaderCtrl(); + ASSERT(pHeader != NULL); + HDITEM curItem; + + /* update all column headers */ + for (int i = 0; i < kNumVisibleColumns; i++) { + curItem.mask = HDI_IMAGE | HDI_FORMAT; + pHeader->GetItem(i, &curItem); + + if (fpLayout->GetSortColumn() != i) { + curItem.fmt &= ~(HDF_IMAGE | HDF_BITMAP_ON_RIGHT); + } else { + //WMSG1(" Sorting on %d\n", i); + curItem.fmt |= HDF_IMAGE | HDF_BITMAP_ON_RIGHT; + if (fpLayout->GetAscending()) + curItem.iImage = 0; + else + curItem.iImage = 1; + } + + pHeader->SetItem(i, &curItem); + } +} + + +/* + * Handle a double-click on an item. + * + * The double-click should single-select the item, so we can throw it + * straight into the viewer. However, there are some uses for bulk + * double-clicking. + */ +void +ContentList::OnDoubleClick(NMHDR*, LRESULT* pResult) +{ + /* test */ + DWORD dwPos = ::GetMessagePos(); + CPoint point ((int) LOWORD(dwPos), (int) HIWORD(dwPos)); + ScreenToClient(&point); + + int idx = HitTest(point); + if (idx != -1) { + CString str = GetItemText(idx, 0); + WMSG1("%s was double-clicked\n", str); + } + + ((MainWindow*) ::AfxGetMainWnd())->HandleDoubleClick(); + *pResult = 0; +} + +/* + * Handle a right-click on an item. + * + * -The first item in the menu performs the double-click action on the + * -item clicked on. The rest of the menu is simply a mirror of the items + * -in the "Actions" menu. To make this work, we let the main window handle + * -everything, but save a copy of the index of the menu item that was + * -clicked on. + * + * [We do this differently now?? ++ATM 20040722] + */ +void +ContentList::OnRightClick(NMHDR*, LRESULT* pResult) +{ + DWORD dwPos = ::GetMessagePos(); + CPoint point ((int) LOWORD(dwPos), (int) HIWORD(dwPos)); + ScreenToClient(&point); + +#if 0 + int idx = HitTest(point); + if (idx != -1) { + CString str = GetItemText(idx, 0); + //TRACE1("%s was right-clicked\n", str); + WMSG1("%s was right-clicked\n", str); + + //fRightClickItem = idx; +#else + { +#endif + + CMenu menu; + menu.LoadMenu(IDR_RIGHTCLICKMENU); + CMenu* pContextMenu = menu.GetSubMenu(0); + ClientToScreen(&point); + pContextMenu->TrackPopupMenu(TPM_LEFTALIGN | TPM_LEFTBUTTON | TPM_RIGHTBUTTON, + point.x, point.y, ::AfxGetMainWnd()); + } + *pResult = 0; +} + +/* + * Mark everything as selected. + */ +void +ContentList::SelectAll(void) +{ + int i; + + for (i = GetItemCount()-1; i >= 0; i--) { + if (!SetItemState(i, LVIS_SELECTED, LVIS_SELECTED)) { + WMSG1("Glitch: SetItemState failed on %d\n", i); + } + } +} + +/* + * Toggle the "selected" state flag. + */ +void +ContentList::InvertSelection(void) +{ + int i, oldState; + + for (i = GetItemCount()-1; i >= 0; i--) { + oldState = GetItemState(i, LVIS_SELECTED); + if (!SetItemState(i, oldState ? 0 : LVIS_SELECTED, LVIS_SELECTED)) { + WMSG1("Glitch: SetItemState failed on %d\n", i); + } + } +} + +/* + * Select the contents of any selected subdirs. + * + * We do the selection by prefix matching on the display name. This means + * we do one pass through the list for the contents of a subdir, including + * all of its subdirs. However, the subdirs we select as we're going will + * be indistinguishable from subdirs selected by the user, which could + * result in O(n^2) behavior. + * + * We mark the user's selection with LVIS_CUT, process them all, then go + * back and clear all of the LVIS_CUT flags. Of course, if they select + * the entire archive, we're approach O(n^2) anyway. If efficiency is a + * problem we will need to sort the list, do some work, then sort it back + * the way it was. + * + * This doesn't work for volume directories, because their display name + * isn't quite right. That's okay for now -- we document that we don't + * allow deletion of the volume directory. (We don't currently have a test + * to see if a GenericEntry is a volume dir; might want to add one.) + */ +void +ContentList::SelectSubdirContents(void) +{ + POSITION posn; + posn = GetFirstSelectedItemPosition(); + if (posn == nil) { + WMSG0("SelectSubdirContents: nothing is selected\n"); + return; + } + /* mark all selected items with LVIS_CUT */ + while (posn != nil) { + int num = GetNextSelectedItem(/*ref*/ posn); + SetItemState(num, LVIS_CUT, LVIS_CUT); + } + + /* for each LVIS_CUT entry, select all prefix matches */ + CString prefix; + for (int i = GetItemCount()-1; i >= 0; i--) { + GenericEntry* pEntry = (GenericEntry*) GetItemData(i); + bool origSel; + + origSel = GetItemState(i, LVIS_CUT) != 0; + + if (origSel && + (pEntry->GetRecordKind() == GenericEntry::kRecordKindDirectory || + pEntry->GetRecordKind() == GenericEntry::kRecordKindVolumeDir)) + { + prefix = pEntry->GetDisplayName(); + prefix += pEntry->GetFssep(); + SelectSubdir(prefix); + } + +// if (!SetItemState(i, oldState ? 0 : LVIS_SELECTED, LVIS_SELECTED)) { +// WMSG1("GLITCH: SetItemState failed on %d\n", i); +// } + } + + /* clear the LVIS_CUT flags */ + posn = GetFirstSelectedItemPosition(); + while (posn != nil) { + int num = GetNextSelectedItem(/*ref*/ posn); + SetItemState(num, 0, LVIS_CUT); + } +} + +/* + * Select every entry whose display name has "displayPrefix" as a prefix. + */ +void +ContentList::SelectSubdir(const char* displayPrefix) +{ + WMSG1(" ContentList selecting all in '%s'\n", displayPrefix); + int len = strlen(displayPrefix); + + for (int i = GetItemCount()-1; i >= 0; i--) { + GenericEntry* pEntry = (GenericEntry*) GetItemData(i); + + if (strncasecmp(displayPrefix, pEntry->GetDisplayName(), len) == 0) + SetItemState(i, LVIS_SELECTED, LVIS_SELECTED); + } +} + +/* + * Mark all items as unselected. + */ +void +ContentList::ClearSelection(void) +{ + for (int i = GetItemCount()-1; i >= 0; i--) + SetItemState(i, 0, LVIS_SELECTED); +} + +/* + * Find the next matching entry. We start after the first selected item. + * If we find a matching entry, we clear the current selection and select it. + */ +void +ContentList::FindNext(const char* str, bool down, bool matchCase, + bool wholeWord) +{ + POSITION posn; + int i, num; + bool found = false; + + WMSG4("FindNext '%s' d=%d c=%d w=%d\n", str, down, matchCase, wholeWord); + + posn = GetFirstSelectedItemPosition(); + num = GetNextSelectedItem(/*ref*/ posn); + if (num < 0) { // num will be -1 if nothing is selected + if (down) + num = -1; + else + num = GetItemCount(); + } + + WMSG1(" starting search from entry %d\n", num); + + if (down) { + for (i = num+1; i < GetItemCount(); i++) { + found = CompareFindString(i, str, matchCase, wholeWord); + if (found) + break; + } + if (!found) { // wrap + for (i = 0; i <= num; i++) { + found = CompareFindString(i, str, matchCase, wholeWord); + if (found) + break; + } + } + } else { + for (i = num-1; i >= 0; i--) { + found = CompareFindString(i, str, matchCase, wholeWord); + if (found) + break; + } + if (!found) { // wrap + for (i = GetItemCount()-1; i >= num; i--) { + found = CompareFindString(i, str, matchCase, wholeWord); + if (found) + break; + } + } + } + + if (found) { + WMSG1("Found, i=%d\n", i); + ClearSelection(); + SetItemState(i, LVIS_SELECTED, LVIS_SELECTED); + EnsureVisible(i, false); + } else { + WMSG0("Not found\n"); + MainWindow* pMain = (MainWindow*)::AfxGetMainWnd(); + pMain->FailureBeep(); + } +} + +/* + * Compare "str" against the contents of entry "num". + */ +bool +ContentList::CompareFindString(int num, const char* str, bool matchCase, + bool wholeWord) +{ + GenericEntry* pEntry = (GenericEntry*) GetItemData(num); + char fssep = pEntry->GetFssep(); + char* (*pSubCompare)(const char* str, const char* subStr) = nil; + + if (matchCase) + pSubCompare = strstr; + else + pSubCompare = stristr; + + if (wholeWord) { + const char* src = pEntry->GetDisplayName(); + const char* start = src; + int strLen = strlen(str); + + /* scan forward, looking for a match that starts & ends on fssep */ + while (*start != '\0') { + const char* match; + + match = (*pSubCompare)(start, str); + + if (match == nil) + break; + if ((match == src || *(match-1) == fssep) && + (match[strLen] == '\0' || match[strLen] == fssep)) + { + return true; + } + + start++; + } + } else { + if ((*pSubCompare)(pEntry->GetDisplayName(), str) != nil) + return true; + } + + return false; +} diff --git a/app/ContentList.h b/app/ContentList.h new file mode 100644 index 0000000..63098b8 --- /dev/null +++ b/app/ContentList.h @@ -0,0 +1,136 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Class declaration for a list control showing archive contents. + */ +#ifndef __CONTENT_LIST__ +#define __CONTENT_LIST__ + +#include "GenericArchive.h" +#include "Preferences.h" +#include "Resource.h" +#include +#include + + +/* + * A ListCtrl with headers appropriate for viewing archive contents. + * + * NOTE: this class performs auto-cleanup, and must be allocated on the heap. + * + * We currently use the underlying GenericArchive as our storage for the stuff + * we display. This works great until we change or delete entries from + * GenericArchive. At that point we run the risk of displaying bad pointers. + * + * The GenericArchive has local copies of everything interesting, so the only + * time things go badly for us is when somebody inside GenericArchive calls + * Reload. That frees and reallocates the storage we're pointing to. So, + * GenericArchive maintains a "I have reloaded" flag that we test before we + * draw. + */ +class ContentList: public CListCtrl +{ +public: + ContentList(GenericArchive* pArchive, ColumnLayout* pLayout) { + ASSERT(pArchive != nil); + ASSERT(pLayout != nil); + fpArchive = pArchive; + fpLayout = pLayout; +// fInvalid = false; + //fRightClickItem = -1; + + fpArchive->ClearReloadFlag(); + } + + // call this before updating underlying storage; call Reload to un-inval +// void Invalidate(void); + // reload from underlying storage + void Reload(bool saveSelection = false); + + void NewSortOrder(void); + void NewColumnWidths(void); + void ExportColumnWidths(void); + void SelectAll(void); + void InvertSelection(void); + void ClearSelection(void); + + void SelectSubdirContents(void); + + void FindNext(const char* str, bool down, bool matchCase, bool wholeWord); + bool CompareFindString(int num, const char* str, bool matchCase, + bool wholeWord); + + //int GetRightClickItem(void) const { return fRightClickItem; } + //void ClearRightClickItem(void) { fRightClickItem = -1; } + + enum { kFileTypeBufLen = 5, kAuxTypeBufLen = 6 }; + static void MakeFileTypeDisplayString(const GenericEntry* pEntry, + char* buf); + static void MakeAuxTypeDisplayString(const GenericEntry* pEntry, + char* buf); + +protected: + // overridden functions + virtual BOOL PreCreateWindow(CREATESTRUCT& cs); + virtual void PostNcDestroy(void); + + afx_msg int OnCreate(LPCREATESTRUCT); + afx_msg void OnDestroy(void); + afx_msg void OnSysColorChange(void); + //afx_msg BOOL OnMouseWheel(UINT nFlags, short zDelta, CPoint pt); + afx_msg void OnColumnClick(NMHDR*, LRESULT*); + afx_msg void OnGetDispInfo(NMHDR* pnmh, LRESULT* pResult); + +private: + // Load the header images. Must do this every time the syscolors change. + // (Ideally this would re-map all 3dface colors. Note the current + // implementation relies on the top left pixel color.) + void LoadHeaderImages(void) { + if (!fHdrImageList.Create(IDB_HDRBAR, 16, 1, CLR_DEFAULT)) + WMSG0("GLITCH: header list create failed\n"); + fHdrImageList.SetBkColor(::GetSysColor(COLOR_BTNFACE)); + } + void LoadListImages(void) { + if (!fListImageList.Create(IDB_LIST_PICS, 16, 1, CLR_DEFAULT)) + WMSG0("GLITCH: list image create failed\n"); + fListImageList.SetBkColor(::GetSysColor(COLOR_WINDOW)); + } + enum { // defs for IDB_LIST_PICS + kListIconNone = 0, + kListIconComment = 1, + kListIconNonEmptyComment = 2, + kListIconDamaged = 3, + kListIconSuspicious = 4, + }; + int LoadData(void); + long* GetSelectionSerials(long* pSelCount); + void RestoreSelection(const long* savedSel, long selCount); + + int GetDefaultWidth(int col); + + static void MakeMacTypeString(unsigned long val, char* buf); + static void MakeRatioDisplayString(const GenericEntry* pEntry, char* buf, + int* pPerc); + + void SetSortIcon(void); + static int CALLBACK CompareFunc(LPARAM lParam1, LPARAM lParam2, + LPARAM lParamSort); + + void OnDoubleClick(NMHDR* pnmh, LRESULT* pResult); + void OnRightClick(NMHDR* pnmh, LRESULT* pResult); + void SelectSubdir(const char* displayPrefix); + + CImageList fHdrImageList; + CImageList fListImageList; + GenericArchive* fpArchive; // data we're expected to display + ColumnLayout* fpLayout; +// int fRightClickItem; +// bool fInvalid; + + DECLARE_MESSAGE_MAP() +}; + +#endif /*__CONTENT_LIST__*/ \ No newline at end of file diff --git a/app/ConvDiskOptionsDialog.cpp b/app/ConvDiskOptionsDialog.cpp new file mode 100644 index 0000000..1b594be --- /dev/null +++ b/app/ConvDiskOptionsDialog.cpp @@ -0,0 +1,344 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Support for ConvDiskOptionsDialog. + */ +#include "stdafx.h" +#include "ConvDiskOptionsDialog.h" +#include "NufxArchive.h" +#include "Main.h" +#include "ActionProgressDialog.h" +#include "DiskArchive.h" +#include "NewDiskSize.h" +#include "../diskimg/DiskImgDetail.h" // need ProDOS filename validator + +BEGIN_MESSAGE_MAP(ConvDiskOptionsDialog, CDialog) + ON_WM_HELPINFO() + //ON_COMMAND(IDHELP, OnHelp) + ON_BN_CLICKED(IDC_CONVDISK_COMPUTE, OnCompute) + ON_BN_CLICKED(IDC_USE_SELECTED, ResetSizeControls) + ON_BN_CLICKED(IDC_USE_ALL, ResetSizeControls) + //ON_BN_CLICKED(IDC_CONVDISK_SPARSE, ResetSizeControls) + ON_CONTROL_RANGE(BN_CLICKED, IDC_CONVDISK_140K, IDC_CONVDISK_SPECIFY, + OnRadioChangeRange) +END_MESSAGE_MAP() + + + +const int kProDOSVolNameMax = 15; // longest possible ProDOS volume name + +/* + * Set up our modified version of the "use selection" dialog. + */ +BOOL +ConvDiskOptionsDialog::OnInitDialog(void) +{ + CEdit* pEdit = (CEdit*) GetDlgItem(IDC_CONVDISK_VOLNAME); + ASSERT(pEdit != nil); + pEdit->SetLimitText(kProDOSVolNameMax); + + ResetSizeControls(); + + pEdit = (CEdit*) GetDlgItem(IDC_CONVDISK_SPECIFY_EDIT); + ASSERT(pEdit != nil); + pEdit->SetLimitText(5); // enough for "65535" + pEdit->EnableWindow(FALSE); + + return UseSelectionDialog::OnInitDialog(); +} + +/* + * Convert values. + */ +void +ConvDiskOptionsDialog::DoDataExchange(CDataExchange* pDX) +{ + UINT specifyBlocks = 280; + CString errMsg; + + DDX_Radio(pDX, IDC_CONVDISK_140K, fDiskSizeIdx); + //DDX_Check(pDX, IDC_CONVDISK_ALLOWLOWER, fAllowLower); + //DDX_Check(pDX, IDC_CONVDISK_SPARSE, fSparseAlloc); + DDX_Text(pDX, IDC_CONVDISK_VOLNAME, fVolName); + DDX_Text(pDX, IDC_CONVDISK_SPECIFY_EDIT, specifyBlocks); + + ASSERT(fDiskSizeIdx >= 0 && fDiskSizeIdx < (int)NewDiskSize::GetNumSizeEntries()); + + if (pDX->m_bSaveAndValidate) { + + fNumBlocks = NewDiskSize::GetDiskSizeByIndex(fDiskSizeIdx); + if (fNumBlocks == NewDiskSize::kSpecified) { + fNumBlocks = specifyBlocks; + + // Max is really 65535, but we allow 65536 for creation of volumes + // that can be copied to CFFA cards. + if (specifyBlocks < 16 || specifyBlocks > 65536) + errMsg = "Specify a size of at least 16 blocks and no more" + " than 65536 blocks."; + } + + + if (fVolName.IsEmpty() || fVolName.GetLength() > kProDOSVolNameMax) { + errMsg = "You must specify a volume name 1-15 characters long."; + } else { + if (!IsValidVolumeName_ProDOS(fVolName)) + errMsg.LoadString(IDS_VALID_VOLNAME_PRODOS); + } + } + + if (!errMsg.IsEmpty()) { + CString appName; + appName.LoadString(IDS_MB_APP_NAME); + MessageBox(errMsg, appName, MB_OK); + pDX->Fail(); + } + + UseSelectionDialog::DoDataExchange(pDX); +} + +/* + * When one of the radio buttons is clicked on, update the active status + * and contents of the "specify size" edit box. + */ +void +ConvDiskOptionsDialog::OnRadioChangeRange(UINT nID) +{ + WMSG1("OnChangeRange id=%d\n", nID); + + CButton* pButton = (CButton*) GetDlgItem(IDC_CONVDISK_SPECIFY); + CEdit* pEdit = (CEdit*) GetDlgItem(IDC_CONVDISK_SPECIFY_EDIT); + pEdit->EnableWindow(pButton->GetCheck() == BST_CHECKED); + + NewDiskSize::UpdateSpecifyEdit(this); +} + +/* + * Test a ProDOS filename for validity. + */ +bool +ConvDiskOptionsDialog::IsValidVolumeName_ProDOS(const char* name) +{ + return DiskImgLib::DiskFSProDOS::IsValidVolumeName(name); +} + + +/* + * Enable all size radio buttons and reset the "size required" display. + * + * This should be invoked whenever the convert selection changes, and may be + * called at any time. + */ +void +ConvDiskOptionsDialog::ResetSizeControls(void) +{ + CWnd* pWnd; + CString spaceReq; + + WMSG0("Resetting size controls\n"); + spaceReq.Format(IDS_CONVDISK_SPACEREQ, "(unknown)"); + pWnd = GetDlgItem(IDC_CONVDISK_SPACEREQ); + ASSERT(pWnd != nil); + pWnd->SetWindowText(spaceReq); + +#if 0 + int i; + for (i = 0; i < NELEM(gDiskSizes); i++) { + pWnd = GetDlgItem(gDiskSizes[i].ctrlID); + ASSERT(pWnd != nil); + pWnd->EnableWindow(TRUE); + } +#endif + NewDiskSize::EnableButtons(this); +} + +/* + * Display the space requirements and disable radio button controls that are + * for values that are too small. + * + * Pass in the number of blocks required on a 32MB ProDOS volume. + */ +void +ConvDiskOptionsDialog::LimitSizeControls(long totalBlocks, long blocksUsed) +{ + WMSG2("LimitSizeControls %ld %ld\n", totalBlocks, blocksUsed); + WMSG1("Full volume requires %ld bitmap blocks\n", + NewDiskSize::GetNumBitmapBlocks_ProDOS(totalBlocks)); + + CWnd* pWnd; + long usedWithoutBitmap = + blocksUsed - NewDiskSize::GetNumBitmapBlocks_ProDOS(totalBlocks); + long sizeInK = usedWithoutBitmap / 2; + CString sizeStr, spaceReq; + sizeStr.Format("%dK", sizeInK); + spaceReq.Format(IDS_CONVDISK_SPACEREQ, sizeStr); + + pWnd = GetDlgItem(IDC_CONVDISK_SPACEREQ); + ASSERT(pWnd != nil); + pWnd->SetWindowText(spaceReq); + + NewDiskSize::EnableButtons_ProDOS(this, totalBlocks, blocksUsed); + +#if 0 + bool first = true; + for (int i = 0; i < NELEM(gDiskSizes); i++) { + if (gDiskSizes[i].blocks == -1) + continue; + + CButton* pButton; + pButton = (CButton*) GetDlgItem(gDiskSizes[i].ctrlID); + ASSERT(pButton != nil); + if (usedWithoutBitmap + GetNumBitmapBlocks(gDiskSizes[i].blocks) <= + gDiskSizes[i].blocks) + { + pButton->EnableWindow(TRUE); + if (first) { + pButton->SetCheck(BST_CHECKED); + first = false; + } else { + pButton->SetCheck(BST_UNCHECKED); + } + } else { + pButton->EnableWindow(FALSE); + pButton->SetCheck(BST_UNCHECKED); + } + } +#endif +} + + +/* + * Compute the amount of space required for the files. We use the result to + * disable the controls that can't be used. + * + * We don't need to enable controls here, because the only way to change the + * set of files is by flipping between "all" and "selected", and we can handle + * that separately. + */ +void +ConvDiskOptionsDialog::OnCompute(void) +{ + MainWindow* pMain = (MainWindow*)::AfxGetMainWnd(); + const Preferences* pPreferences = GET_PREFERENCES(); + + if (UpdateData() == FALSE) + return; + + /* + * Create a "selection set" of data forks, resource forks, and + * disk images. We don't want comment threads. We can filter all that + * out later, though, so we just specify "any". + */ + SelectionSet selSet; + int threadMask = GenericEntry::kAnyThread; + + if (fFilesToAction == UseSelectionDialog::kActionSelection) { + selSet.CreateFromSelection(pMain->GetContentList(), threadMask); + } else { + selSet.CreateFromAll(pMain->GetContentList(), threadMask); + } + + if (selSet.GetNumEntries() == 0) { + /* should be impossible */ + MessageBox("No files matched the selection criteria.", + "No match", MB_OK|MB_ICONEXCLAMATION); + return; + } + + XferFileOptions xferOpts; + //xferOpts.fAllowLowerCase = + // pPreferences->GetPrefBool(kPrProDOSAllowLower) != 0; + //xferOpts.fUseSparseBlocks = + // pPreferences->GetPrefBool(kPrProDOSUseSparse) != 0; + + WMSG1("New volume name will be '%s'\n", fVolName); + + /* + * Create a new disk image file. + */ + CString errStr; + char nameBuf[MAX_PATH]; + UINT unique; + unique = GetTempFileName(pMain->GetPreferences()->GetPrefString(kPrTempPath), + "CPdisk", 0, nameBuf); + if (unique == 0) { + DWORD dwerr = ::GetLastError(); + errStr.Format("GetTempFileName failed on '%s' (err=0x%08lx)\n", + pMain->GetPreferences()->GetPrefString(kPrTempPath), dwerr); + ShowFailureMsg(this, errStr, IDS_FAILED); + return; + } + WMSG1(" Will xfer to file '%s'\n", nameBuf); + // annoying -- DiskArchive insists on creating it + (void) unlink(nameBuf); + + DiskArchive::NewOptions options; + memset(&options, 0, sizeof(options)); + options.base.format = DiskImg::kFormatProDOS; + options.base.sectorOrder = DiskImg::kSectorOrderProDOS; + options.prodos.volName = fVolName; + options.prodos.numBlocks = 65535; + + xferOpts.fTarget = new DiskArchive; + + { + CWaitCursor waitc; + errStr = xferOpts.fTarget->New(nameBuf, &options); + } + if (!errStr.IsEmpty()) { + ShowFailureMsg(this, errStr, IDS_FAILED); + } else { + /* + * Set up the progress window as a modal dialog. + */ + GenericArchive::XferStatus result; + + ActionProgressDialog* pActionProgress = new ActionProgressDialog; + pMain->SetActionProgressDialog(pActionProgress); + pActionProgress->Create(ActionProgressDialog::kActionConvFile, this); + pMain->PeekAndPump(); + result = pMain->GetOpenArchive()->XferSelection(pActionProgress, &selSet, + pActionProgress, &xferOpts); + pActionProgress->Cleanup(this); + pMain->SetActionProgressDialog(nil); + + if (result == GenericArchive::kXferOK) { + DiskFS* pDiskFS; + long totalBlocks, freeBlocks; + int unitSize; + DIError dierr; + + WMSG0("SUCCESS\n"); + + pDiskFS = ((DiskArchive*) xferOpts.fTarget)->GetDiskFS(); + ASSERT(pDiskFS != nil); + + dierr = pDiskFS->GetFreeSpaceCount(&totalBlocks, &freeBlocks, + &unitSize); + if (dierr != kDIErrNone) { + errStr.Format("Unable to get free space count: %s.\n", + DiskImgLib::DIStrError(dierr)); + ShowFailureMsg(this, errStr, IDS_FAILED); + } else { + ASSERT(totalBlocks >= freeBlocks); + ASSERT(unitSize == DiskImgLib::kBlockSize); + LimitSizeControls(totalBlocks, totalBlocks - freeBlocks); + } + } else if (result == GenericArchive::kXferCancelled) { + WMSG0("CANCEL - cancel button hit\n"); + ResetSizeControls(); + } else { + WMSG1("FAILURE (result=%d)\n", result); + ResetSizeControls(); + } + } + + // debug + ((DiskArchive*) (xferOpts.fTarget))->GetDiskFS()->DumpFileList(); + + /* clean up */ + delete xferOpts.fTarget; + (void) unlink(nameBuf); +} diff --git a/app/ConvDiskOptionsDialog.h b/app/ConvDiskOptionsDialog.h new file mode 100644 index 0000000..8a257c5 --- /dev/null +++ b/app/ConvDiskOptionsDialog.h @@ -0,0 +1,53 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Options for converting a disk image to a file archive. + */ +#ifndef __CONVDISK_OPTIONS_DIALOG__ +#define __CONVDISK_OPTIONS_DIALOG__ + +#include "UseSelectionDialog.h" +#include "resource.h" + +/* + * Get some options. + */ +class ConvDiskOptionsDialog : public UseSelectionDialog { +public: + ConvDiskOptionsDialog(int selCount, CWnd* pParentWnd = NULL) : + UseSelectionDialog(selCount, pParentWnd, IDD_CONVDISK_OPTS) + { + fDiskSizeIdx = 0; + //fAllowLower = fSparseAlloc = FALSE; + fVolName = "NEW.DISK"; + fNumBlocks = -1; + } + virtual ~ConvDiskOptionsDialog(void) {} + + int fDiskSizeIdx; + //BOOL fAllowLower; + //BOOL fSparseAlloc; + CString fVolName; + + long fNumBlocks; // computed when DoModal finishes + +private: + virtual BOOL OnInitDialog(void); + virtual void DoDataExchange(CDataExchange* pDX); + +// BOOL OnHelpInfo(HELPINFO* lpHelpInfo); + afx_msg void ResetSizeControls(void); + afx_msg void OnCompute(void); + + afx_msg void OnRadioChangeRange(UINT nID); + + void LimitSizeControls(long totalBlocks, long blocksUsed); + bool IsValidVolumeName_ProDOS(const char* name); + + DECLARE_MESSAGE_MAP() +}; + +#endif /*__CONVDISK_OPTIONS_DIALOG__*/ \ No newline at end of file diff --git a/app/ConvFileOptionsDialog.cpp b/app/ConvFileOptionsDialog.cpp new file mode 100644 index 0000000..2cf5a25 --- /dev/null +++ b/app/ConvFileOptionsDialog.cpp @@ -0,0 +1,35 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Support for ConvFileOptionsDialog. + */ +#include "stdafx.h" +#include "ConvFileOptionsDialog.h" +//#include "NufxArchive.h" + +#if 0 +/* + * Set up our modified version of the "use selection" dialog. + */ +BOOL +ConvFileOptionsDialog::OnInitDialog(void) +{ + return UseSelectionDialog::OnInitDialog(); +} +#endif + +/* + * Convert values. + */ +void +ConvFileOptionsDialog::DoDataExchange(CDataExchange* pDX) +{ + //DDX_Check(pDX, IDC_CONVFILE_CONVDOS, fConvDOSText); + //DDX_Check(pDX, IDC_CONVFILE_CONVPASCAL, fConvPascalText); + DDX_Check(pDX, IDC_CONVFILE_PRESERVEDIR, fPreserveEmptyFolders); + + UseSelectionDialog::DoDataExchange(pDX); +} diff --git a/app/ConvFileOptionsDialog.h b/app/ConvFileOptionsDialog.h new file mode 100644 index 0000000..4e560c1 --- /dev/null +++ b/app/ConvFileOptionsDialog.h @@ -0,0 +1,38 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Options for converting a disk image to a file archive. + */ +#ifndef __CONVFILE_OPTIONS_DIALOG__ +#define __CONVFILE_OPTIONS_DIALOG__ + +#include "UseSelectionDialog.h" +#include "resource.h" + +/* + * Get some options. + */ +class ConvFileOptionsDialog : public UseSelectionDialog { +public: + ConvFileOptionsDialog(int selCount, CWnd* pParentWnd = NULL) : + UseSelectionDialog(selCount, pParentWnd, IDD_CONVFILE_OPTS) + { + fPreserveEmptyFolders = FALSE; + } + virtual ~ConvFileOptionsDialog(void) {} + + //BOOL fConvDOSText; + //BOOL fConvPascalText; + BOOL fPreserveEmptyFolders; + +private: + //virtual BOOL OnInitDialog(void); + virtual void DoDataExchange(CDataExchange* pDX); + + //DECLARE_MESSAGE_MAP() +}; + +#endif /*__CONVFILE_OPTIONS_DIALOG__*/ \ No newline at end of file diff --git a/app/CreateImageDialog.cpp b/app/CreateImageDialog.cpp new file mode 100644 index 0000000..6e53c6d --- /dev/null +++ b/app/CreateImageDialog.cpp @@ -0,0 +1,347 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Support for ConvDiskOptionsDialog. + */ +#include "stdafx.h" +#include "CreateImageDialog.h" +#include "NewDiskSize.h" +#include "HelpTopics.h" +#include "../diskimg/DiskImgDetail.h" // need ProDOS filename validator + +BEGIN_MESSAGE_MAP(CreateImageDialog, CDialog) + ON_WM_HELPINFO() + ON_COMMAND(IDHELP, OnHelp) + ON_CONTROL_RANGE(BN_CLICKED, IDC_CREATEFS_DOS32, IDC_CREATEFS_BLANK, + OnFormatChangeRange) + ON_CONTROL_RANGE(BN_CLICKED, IDC_CONVDISK_140K, IDC_CONVDISK_SPECIFY, + OnSizeChangeRange) +END_MESSAGE_MAP() + + + +const int kProDOSVolNameMax = 15; // longest possible ProDOS volume name +const int kPascalVolNameMax = 7; // longest possible Pascal volume name +const int kHFSVolNameMax = 27; // longest possible HFS volume name +const long kMaxBlankBlocks = 16777216; // 8GB in 512-byte blocks + +/* + * Set up our modified version of the "use selection" dialog. + */ +BOOL +CreateImageDialog::OnInitDialog(void) +{ + // high bit set in signed short means key is down + if (::GetKeyState(VK_SHIFT) < 0) { + WMSG0("Shift key is down, enabling extended options\n"); + fExtendedOpts = true; + } + + CEdit* pEdit = (CEdit*) GetDlgItem(IDC_CREATEFSPRODOS_VOLNAME); + ASSERT(pEdit != nil); + pEdit->SetLimitText(kProDOSVolNameMax); + + pEdit = (CEdit*) GetDlgItem(IDC_CREATEFSPASCAL_VOLNAME); + ASSERT(pEdit != nil); + pEdit->SetLimitText(kPascalVolNameMax); + + pEdit = (CEdit*) GetDlgItem(IDC_CREATEFSHFS_VOLNAME); + ASSERT(pEdit != nil); + pEdit->SetLimitText(kHFSVolNameMax); + + pEdit = (CEdit*) GetDlgItem(IDC_CREATEFSDOS_VOLNUM); + ASSERT(pEdit != nil); + pEdit->SetLimitText(3); // 3 digit volume number + + pEdit = (CEdit*) GetDlgItem(IDC_CONVDISK_SPECIFY_EDIT); + ASSERT(pEdit != nil); + pEdit->EnableWindow(FALSE); + + return CDialog::OnInitDialog(); +} + +/* + * Convert values. + */ +void +CreateImageDialog::DoDataExchange(CDataExchange* pDX) +{ + UINT specifyBlocks = 280; + CString errMsg; + + DDX_Radio(pDX, IDC_CONVDISK_140K, fDiskSizeIdx); + DDX_Radio(pDX, IDC_CREATEFS_DOS32, fDiskFormatIdx); + DDX_Check(pDX, IDC_CREATEFSDOS_ALLOCDOS, fAllocTracks_DOS); + DDX_Text(pDX, IDC_CREATEFSDOS_VOLNUM, fDOSVolumeNum); + DDX_Text(pDX, IDC_CREATEFSPRODOS_VOLNAME, fVolName_ProDOS); + DDX_Text(pDX, IDC_CREATEFSPASCAL_VOLNAME, fVolName_Pascal); + DDX_Text(pDX, IDC_CREATEFSHFS_VOLNAME, fVolName_HFS); + DDX_Text(pDX, IDC_CONVDISK_SPECIFY_EDIT, specifyBlocks); + + ASSERT(fDiskSizeIdx >= 0 && fDiskSizeIdx < (int)NewDiskSize::GetNumSizeEntries()); + + if (pDX->m_bSaveAndValidate) { + fNumBlocks = NewDiskSize::GetDiskSizeByIndex(fDiskSizeIdx); + if (fNumBlocks == NewDiskSize::kSpecified) + fNumBlocks = specifyBlocks; + + if (fDiskFormatIdx == kFmtDOS32) { + CString tmpStr; + tmpStr.Format("%d", fDOSVolumeNum); + if (!IsValidVolumeName_DOS(tmpStr)) + errMsg.LoadString(IDS_VALID_VOLNAME_DOS); + } else if (fDiskFormatIdx == kFmtDOS33) { + CString tmpStr; + tmpStr.Format("%d", fDOSVolumeNum); + if (!IsValidVolumeName_DOS(tmpStr)) + errMsg.LoadString(IDS_VALID_VOLNAME_DOS); + + // only needed in "extended" mode -- this stuff is too painful to + // inflict on the average user + if (fNumBlocks < 18*8 || fNumBlocks > 800 || + (fNumBlocks <= 400 && (fNumBlocks % 8) != 0) || + (fNumBlocks > 400 && (fNumBlocks % 16) != 0)) + { + errMsg = "Specify a size between 144 blocks (18 tracks) and" + " 800 blocks (50 tracks/32 sectors). The block count" + " must be a multiple of 8 for 16-sector disks, or a" + " multiple of 16 for 32-sector disks. 32 sector" + " formatting starts at 400 blocks. Disks larger than" + " 400 blocks but less than 800 aren't recognized by" + " CiderPress."; + } + } else if (fDiskFormatIdx == kFmtProDOS) { + // Max is really 65535, but we allow 65536 for creation of volumes + // that can be copied to CFFA cards. + if (fNumBlocks < 16 || fNumBlocks > 65536) { + errMsg = "Specify a size of at least 16 blocks and no more" + " than 65536 blocks."; + } else if (fVolName_ProDOS.IsEmpty() || + fVolName_ProDOS.GetLength() > kProDOSVolNameMax) + { + errMsg = "You must specify a volume name 1-15 characters long."; + } else { + if (!IsValidVolumeName_ProDOS(fVolName_ProDOS)) + errMsg.LoadString(IDS_VALID_VOLNAME_PRODOS); + } + } else if (fDiskFormatIdx == kFmtPascal) { + if (fVolName_Pascal.IsEmpty() || + fVolName_Pascal.GetLength() > kPascalVolNameMax) + { + errMsg = "You must specify a volume name 1-7 characters long."; + } else { + if (!IsValidVolumeName_Pascal(fVolName_Pascal)) + errMsg.LoadString(IDS_VALID_VOLNAME_PASCAL); + } + } else if (fDiskFormatIdx == kFmtHFS) { + if (fNumBlocks < 1600 || fNumBlocks > 4194303) { + errMsg = "Specify a size of at least 1600 blocks and no more" + " than 4194303 blocks."; + } else if (fVolName_HFS.IsEmpty() || + fVolName_HFS.GetLength() > kHFSVolNameMax) + { + errMsg = "You must specify a volume name 1-27 characters long."; + } else { + if (!IsValidVolumeName_HFS(fVolName_HFS)) + errMsg.LoadString(IDS_VALID_VOLNAME_HFS); + } + } else if (fDiskFormatIdx == kFmtBlank) { + if (fNumBlocks < 1 || fNumBlocks > kMaxBlankBlocks) + errMsg = "Specify a size of at least 1 block and no more" + " than 16777216 blocks."; + } else { + ASSERT(false); + } + } else { + OnFormatChangeRange(IDC_CREATEFS_DOS32 + fDiskFormatIdx); + } + + if (!errMsg.IsEmpty()) { + CString appName; + appName.LoadString(IDS_MB_APP_NAME); + MessageBox(errMsg, appName, MB_OK); + pDX->Fail(); + } + + CDialog::DoDataExchange(pDX); +} + +/* + * When the user chooses a format, enable and disable controls as + * appropriate. + */ +void +CreateImageDialog::OnFormatChangeRange(UINT nID) +{ + static const struct { + UINT buttonID; + UINT ctrlID; + } kFormatTab[] = { + { IDC_CREATEFS_DOS32, IDC_CREATEFSDOS_ALLOCDOS }, + { IDC_CREATEFS_DOS32, IDC_CREATEFSDOS_VOLNUM }, + { IDC_CREATEFS_DOS32, IDC_CONVDISK_140K }, + { IDC_CREATEFS_DOS33, IDC_CREATEFSDOS_ALLOCDOS }, + { IDC_CREATEFS_DOS33, IDC_CREATEFSDOS_VOLNUM }, + { IDC_CREATEFS_DOS33, IDC_CONVDISK_140K }, + { IDC_CREATEFS_PRODOS, IDC_CREATEFSPRODOS_VOLNAME }, + { IDC_CREATEFS_PRODOS, IDC_CONVDISK_140K }, + { IDC_CREATEFS_PRODOS, IDC_CONVDISK_800K }, + { IDC_CREATEFS_PRODOS, IDC_CONVDISK_1440K }, + { IDC_CREATEFS_PRODOS, IDC_CONVDISK_5MB }, + { IDC_CREATEFS_PRODOS, IDC_CONVDISK_16MB }, + { IDC_CREATEFS_PRODOS, IDC_CONVDISK_20MB }, + { IDC_CREATEFS_PRODOS, IDC_CONVDISK_32MB }, + { IDC_CREATEFS_PRODOS, IDC_CONVDISK_SPECIFY }, + { IDC_CREATEFS_PASCAL, IDC_CREATEFSPASCAL_VOLNAME }, + { IDC_CREATEFS_PASCAL, IDC_CONVDISK_140K }, + { IDC_CREATEFS_PASCAL, IDC_CONVDISK_800K }, + { IDC_CREATEFS_HFS, IDC_CREATEFSHFS_VOLNAME }, + { IDC_CREATEFS_HFS, IDC_CONVDISK_800K }, + { IDC_CREATEFS_HFS, IDC_CONVDISK_1440K }, + { IDC_CREATEFS_HFS, IDC_CONVDISK_5MB }, + { IDC_CREATEFS_HFS, IDC_CONVDISK_16MB }, + { IDC_CREATEFS_HFS, IDC_CONVDISK_20MB }, + { IDC_CREATEFS_HFS, IDC_CONVDISK_32MB }, + { IDC_CREATEFS_HFS, IDC_CONVDISK_SPECIFY }, + { IDC_CREATEFS_BLANK, IDC_CONVDISK_140K }, + { IDC_CREATEFS_BLANK, IDC_CONVDISK_800K }, + { IDC_CREATEFS_BLANK, IDC_CONVDISK_1440K }, + { IDC_CREATEFS_BLANK, IDC_CONVDISK_5MB }, + { IDC_CREATEFS_BLANK, IDC_CONVDISK_16MB }, + { IDC_CREATEFS_BLANK, IDC_CONVDISK_20MB }, + { IDC_CREATEFS_BLANK, IDC_CONVDISK_32MB }, + { IDC_CREATEFS_BLANK, IDC_CONVDISK_SPECIFY }, + }; + static const UINT kDetailControls[] = { + IDC_CREATEFSDOS_ALLOCDOS, + IDC_CREATEFSDOS_VOLNUM, + IDC_CREATEFSPRODOS_VOLNAME, + IDC_CREATEFSPASCAL_VOLNAME, + IDC_CREATEFSHFS_VOLNAME + }; + int i; + + WMSG1("OnFormatChangeRange id=%d\n", nID); + + /* reset so 140K is highlighted */ + NewDiskSize::EnableButtons_ProDOS(this, 32, 16); + + /* disable all buttons */ + NewDiskSize::EnableButtons(this, FALSE); + + for (i = 0; i < NELEM(kDetailControls); i++) { + CWnd* pWnd = GetDlgItem(kDetailControls[i]); + if (pWnd != nil) + pWnd->EnableWindow(FALSE); + } + + /* re-enable just the ones we like */ + for (i = 0; i < NELEM(kFormatTab); i++) { + if (kFormatTab[i].buttonID == nID) { + CWnd* pWnd = GetDlgItem(kFormatTab[i].ctrlID); + ASSERT(pWnd != nil); + if (pWnd != nil) + pWnd->EnableWindow(TRUE); + } + } + if (fExtendedOpts && nID != IDC_CREATEFS_DOS32) { + CWnd* pWnd = GetDlgItem(IDC_CONVDISK_SPECIFY); + pWnd->EnableWindow(TRUE); + } + + /* make sure 140K is viable; doesn't work for HFS */ + CButton* pButton; + pButton = (CButton*) GetDlgItem(IDC_CONVDISK_140K); + if (!pButton->IsWindowEnabled()) { + pButton->SetCheck(BST_UNCHECKED); + pButton = (CButton*) GetDlgItem(IDC_CONVDISK_800K); + pButton->SetCheck(BST_CHECKED); + } +} + +/* + * When one of the radio buttons is clicked on, update the active status + * and contents of the "specify size" edit box. + */ +void +CreateImageDialog::OnSizeChangeRange(UINT nID) +{ + WMSG1("OnSizeChangeRange id=%d\n", nID); + + CButton* pButton = (CButton*) GetDlgItem(IDC_CONVDISK_SPECIFY); + CEdit* pEdit = (CEdit*) GetDlgItem(IDC_CONVDISK_SPECIFY_EDIT); + pEdit->EnableWindow(pButton->GetCheck() == BST_CHECKED); + + CButton* pBlank; + CButton* pHFS; + pBlank = (CButton*) GetDlgItem(IDC_CREATEFS_BLANK); + pHFS = (CButton*) GetDlgItem(IDC_CREATEFS_HFS); + if (pHFS->GetCheck() == BST_CHECKED) + pEdit->SetLimitText(10); // enough for "2147483647" + else if (pBlank->GetCheck() == BST_CHECKED) + pEdit->SetLimitText(8); // enough for "16777216" + else + pEdit->SetLimitText(5); // enough for "65535" + + NewDiskSize::UpdateSpecifyEdit(this); +} + + +/* + * Test a DOS filename for validity. + */ +bool +CreateImageDialog::IsValidVolumeName_DOS(const char* name) +{ + return DiskImgLib::DiskFSDOS33::IsValidVolumeName(name); +} + +/* + * Test a ProDOS filename for validity. + */ +bool +CreateImageDialog::IsValidVolumeName_ProDOS(const char* name) +{ + return DiskImgLib::DiskFSProDOS::IsValidVolumeName(name); +} + +/* + * Test a Pascal filename for validity. + */ +bool +CreateImageDialog::IsValidVolumeName_Pascal(const char* name) +{ + return DiskImgLib::DiskFSPascal::IsValidVolumeName(name); +} + +/* + * Test an HFS filename for validity. + */ +bool +CreateImageDialog::IsValidVolumeName_HFS(const char* name) +{ + return DiskImgLib::DiskFSHFS::IsValidVolumeName(name); +} + + +/* + * Context help request (question mark button). + */ +BOOL +CreateImageDialog::OnHelpInfo(HELPINFO* lpHelpInfo) +{ + WinHelp((DWORD) lpHelpInfo->iCtrlId, HELP_CONTEXTPOPUP); + return TRUE; // yes, we handled it +} + +/* + * User pressed the "Help" button. + */ +void +CreateImageDialog::OnHelp(void) +{ + WinHelp(HELP_TOPIC_IMAGE_CREATOR, HELP_CONTEXT); +} diff --git a/app/CreateImageDialog.h b/app/CreateImageDialog.h new file mode 100644 index 0000000..5390823 --- /dev/null +++ b/app/CreateImageDialog.h @@ -0,0 +1,75 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Options for creating a blank disk image. + */ +#ifndef __CREATE_IMAGE_DIALOG__ +#define __CREATE_IMAGE_DIALOG__ + +#include "resource.h" + +/* + * Get some options. + */ +class CreateImageDialog : public CDialog { +public: + /* this must match up with control IDs in dialog */ + enum { + kFmtDOS32 = 0, + kFmtDOS33, + kFmtProDOS, + kFmtPascal, + kFmtHFS, + kFmtBlank + }; + + CreateImageDialog(CWnd* pParentWnd = NULL) : + CDialog(IDD_CREATEIMAGE, pParentWnd) + { + fDiskSizeIdx = 0; + fDiskFormatIdx = kFmtProDOS; + fAllocTracks_DOS = TRUE; + fDOSVolumeNum = 254; + fVolName_ProDOS = "NEW.DISK"; + fVolName_Pascal = "BLANK"; + fVolName_HFS = "New Disk"; + fNumBlocks = -2; // -1 has special meaning + fExtendedOpts = false; + } + virtual ~CreateImageDialog(void) {} + + int fDiskSizeIdx; + int fDiskFormatIdx; + BOOL fAllocTracks_DOS; + int fDOSVolumeNum; + CString fVolName_ProDOS; + CString fVolName_Pascal; + CString fVolName_HFS; + + long fNumBlocks; // computed when DoModal finishes + +private: + virtual BOOL OnInitDialog(void); + virtual void DoDataExchange(CDataExchange* pDX); + +// BOOL OnHelpInfo(HELPINFO* lpHelpInfo); + + afx_msg void OnFormatChangeRange(UINT nID); + afx_msg void OnSizeChangeRange(UINT nID); + afx_msg BOOL OnHelpInfo(HELPINFO* lpHelpInfo); + afx_msg void OnHelp(void); + + bool IsValidVolumeName_DOS(const char* name); + bool IsValidVolumeName_ProDOS(const char* name); + bool IsValidVolumeName_Pascal(const char* name); + bool IsValidVolumeName_HFS(const char* name); + + bool fExtendedOpts; + + DECLARE_MESSAGE_MAP() +}; + +#endif /*__CREATE_IMAGE_DIALOG__*/ \ No newline at end of file diff --git a/app/CreateSubdirDialog.cpp b/app/CreateSubdirDialog.cpp new file mode 100644 index 0000000..1d5327a --- /dev/null +++ b/app/CreateSubdirDialog.cpp @@ -0,0 +1,82 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Implementation of CreateSubdirDialog. + * + * Gets the name from the user, validates it against the supplied + * GenericArchive, and returns. + */ +#include "stdafx.h" +#include "CreateSubdirDialog.h" + +BEGIN_MESSAGE_MAP(CreateSubdirDialog, CDialog) + ON_WM_HELPINFO() +END_MESSAGE_MAP() + +/* + * Set up the control. + */ +BOOL +CreateSubdirDialog::OnInitDialog(void) +{ + /* do the DoDataExchange stuff */ + CDialog::OnInitDialog(); + + /* select the default text and set the focus */ + CEdit* pEdit = (CEdit*) GetDlgItem(IDC_CREATESUBDIR_NEW); + ASSERT(pEdit != nil); + pEdit->SetSel(0, -1); + pEdit->SetFocus(); + + return FALSE; // we set the focus +} + +/* + * Convert values. + */ +void +CreateSubdirDialog::DoDataExchange(CDataExchange* pDX) +{ + CString msg, failed; + + msg = ""; + failed.LoadString(IDS_MB_APP_NAME); + + /* put fNewName last so it gets the focus after failure */ + DDX_Text(pDX, IDC_CREATESUBDIR_BASE, fBasePath); + DDX_Text(pDX, IDC_CREATESUBDIR_NEW, fNewName); + + /* validate the path field */ + if (pDX->m_bSaveAndValidate) { + if (fNewName.IsEmpty()) { + msg = "You must specify a new name."; + goto fail; + } + + msg = fpArchive->TestPathName(fpParentEntry, fBasePath, fNewName, + '\0'); + if (!msg.IsEmpty()) + goto fail; + } + + return; + +fail: + ASSERT(!msg.IsEmpty()); + MessageBox(msg, failed, MB_OK); + pDX->Fail(); + return; +} + +/* + * Context help request (question mark button). + */ +BOOL +CreateSubdirDialog::OnHelpInfo(HELPINFO* lpHelpInfo) +{ + WinHelp((DWORD) lpHelpInfo->iCtrlId, HELP_CONTEXTPOPUP); + return TRUE; // yes, we handled it +} diff --git a/app/CreateSubdirDialog.h b/app/CreateSubdirDialog.h new file mode 100644 index 0000000..c8ee049 --- /dev/null +++ b/app/CreateSubdirDialog.h @@ -0,0 +1,45 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Create a subdirectory (e.g. on a ProDOS disk image). + */ +#ifndef __CREATESUBDIRDIALOG__ +#define __CREATESUBDIRDIALOG__ + +#include "GenericArchive.h" +#include "resource.h" + +/* + * Get the name of the subdirectory to create. + */ +class CreateSubdirDialog : public CDialog { +public: + CreateSubdirDialog(CWnd* pParentWnd = NULL) : + CDialog(IDD_CREATE_SUBDIR, pParentWnd) + { + fpArchive = nil; + fpParentEntry = nil; + } + virtual ~CreateSubdirDialog(void) {} + + CString fBasePath; // where subdir will be created + CString fNewName; + const GenericArchive* fpArchive; + const GenericEntry* fpParentEntry; + +protected: + // overrides + virtual BOOL OnInitDialog(void); + virtual void DoDataExchange(CDataExchange* pDX); + + afx_msg BOOL OnHelpInfo(HELPINFO* lpHelpInfo); + +private: + + DECLARE_MESSAGE_MAP() +}; + +#endif /*__CREATESUBDIRDIALOG__*/ diff --git a/app/DEFileDialog.cpp b/app/DEFileDialog.cpp new file mode 100644 index 0000000..20853f0 --- /dev/null +++ b/app/DEFileDialog.cpp @@ -0,0 +1,69 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Implementation of Disk Editor "open file" dialog. + */ +#include "stdafx.h" +#include "DEFileDialog.h" + + +BEGIN_MESSAGE_MAP(DEFileDialog, CDialog) + ON_EN_CHANGE(IDC_DEFILE_FILENAME, OnChange) + ON_WM_HELPINFO() +END_MESSAGE_MAP() + +/* + * Turn off the "OK" button, which is only active when some text + * has been typed in the window. + */ +BOOL +DEFileDialog::OnInitDialog(void) +{ + CWnd* pWnd = GetDlgItem(IDOK); + ASSERT(pWnd != nil); + pWnd->EnableWindow(FALSE); + + return CDialog::OnInitDialog(); +} + +/* + * Get the filename and the "open resource fork" check box. + */ +void +DEFileDialog::DoDataExchange(CDataExchange* pDX) +{ + DDX_Text(pDX, IDC_DEFILE_FILENAME, fName); + DDX_Check(pDX, IDC_DEFILE_RSRC, fOpenRsrcFork); +} + +/* + * The text has changed. If there's nothing in the box, dim the + * "OK" button. + */ +void +DEFileDialog::OnChange(void) +{ + CEdit* pEdit = (CEdit*) GetDlgItem(IDC_DEFILE_FILENAME); + ASSERT(pEdit != nil); + + CString str; + pEdit->GetWindowText(str); + //WMSG2("STR is '%s' (%d)\n", str, str.GetLength()); + + CWnd* pWnd = GetDlgItem(IDOK); + ASSERT(pWnd != nil); + pWnd->EnableWindow(!str.IsEmpty()); +} + +/* + * Context help request (question mark button). + */ +BOOL +DEFileDialog::OnHelpInfo(HELPINFO* lpHelpInfo) +{ + WinHelp((DWORD) lpHelpInfo->iCtrlId, HELP_CONTEXTPOPUP); + return TRUE; // yes, we handled it +} diff --git a/app/DEFileDialog.h b/app/DEFileDialog.h new file mode 100644 index 0000000..b958978 --- /dev/null +++ b/app/DEFileDialog.h @@ -0,0 +1,60 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Disk Edit "open file" dialog. + * + * If the dialog returns with IDOK, "fName" will be a string at least 1 + * character long. + * + * Currently this is a trivial edit box that asks for a name. In the future + * this might present a list or tree view to choose from. + * + * NOTE: we probably want to have a read-only flag here, defaulted to the + * state of the sector editor. The read-only state of the underlying FS + * doesn't matter, since we're writing sectors, not really editing files. + */ +#ifndef __DEFILEDIALOG__ +#define __DEFILEDIALOG__ + +#include "resource.h" +#include "../diskimg/DiskImg.h" +using namespace DiskImgLib; + + +/* + * Class declaration. Nothing special. + */ +class DEFileDialog : public CDialog { +public: + DEFileDialog(CWnd* pParentWnd = NULL) : CDialog(IDD_DEFILE, pParentWnd) + { + fOpenRsrcFork = false; + fName = ""; + } + virtual ~DEFileDialog(void) {} + + void Setup(DiskFS* pDiskFS) { + fpDiskFS = pDiskFS; + } + + CString fName; + int fOpenRsrcFork; + +protected: + // overrides + virtual BOOL OnInitDialog(void); + virtual void DoDataExchange(CDataExchange* pDX); + + afx_msg virtual void OnChange(void); + afx_msg virtual BOOL OnHelpInfo(HELPINFO* lpHelpInfo); + +private: + DiskFS* fpDiskFS; + + DECLARE_MESSAGE_MAP() +}; + +#endif /*__DEFILEDIALOG__*/ \ No newline at end of file diff --git a/app/DiskArchive.cpp b/app/DiskArchive.cpp new file mode 100644 index 0000000..c3cbf93 --- /dev/null +++ b/app/DiskArchive.cpp @@ -0,0 +1,3155 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Bridge between DiskImg and GenericArchive. + */ +#include "stdafx.h" +#include "DiskArchive.h" +#include "NufxArchive.h" +#include "Preferences.h" +#include "Main.h" +#include "ImageFormatDialog.h" +#include "RenameEntryDialog.h" +#include "ConfirmOverwriteDialog.h" +#include "../diskimg/DiskImgDetail.h" + +static const char* kEmptyFolderMarker = ".$$EmptyFolder"; + + +/* + * =========================================================================== + * DiskEntry + * =========================================================================== + */ + +/* + * Extract data from a disk image. + * + * If "*ppText" is non-nil, the data will be read into the pointed-to buffer + * so long as it's shorter than *pLength bytes. The value in "*pLength" + * will be set to the actual length used. + * + * If "*ppText" is nil, the uncompressed data will be placed into a buffer + * allocated with "new[]". + * + * Returns IDOK on success, IDCANCEL if the operation was cancelled by the + * user, and -1 value on failure. On failure, "*pErrMsg" holds an error + * message. + * + * "which" is an anonymous GenericArchive enum (e.g. "kDataThread"). + */ +int +DiskEntry::ExtractThreadToBuffer(int which, char** ppText, long* pLength, + CString* pErrMsg) const +{ + DIError dierr; + A2FileDescr* pOpenFile = nil; + char* dataBuf = nil; + bool rsrcFork; + bool needAlloc = true; + int result = -1; + + ASSERT(fpFile != nil); + ASSERT(pErrMsg != nil); + *pErrMsg = ""; + + if (*ppText != nil) + needAlloc = false; + + if (GetDamaged()) { + *pErrMsg = "File is damaged"; + goto bail; + } + + if (which == kDataThread) + rsrcFork = false; + else if (which == kRsrcThread) + rsrcFork = true; + else { + *pErrMsg = "No such fork"; + goto bail; + } + + LONGLONG len; + if (rsrcFork) + len = fpFile->GetRsrcLength(); + else + len = fpFile->GetDataLength(); + + if (len == 0) { + if (needAlloc) { + *ppText = new char[1]; + **ppText = '\0'; + } + *pLength = 0; + result = IDOK; + goto bail; + } else if (len < 0) { + assert(rsrcFork); // forked files always have a data fork + *pErrMsg = "That fork doesn't exist"; + goto bail; + } + + dierr = fpFile->Open(&pOpenFile, true, rsrcFork); + if (dierr != kDIErrNone) { + *pErrMsg = "File open failed"; + goto bail; + } + + SET_PROGRESS_BEGIN(); + pOpenFile->SetProgressUpdater(DiskArchive::ProgressCallback, len, nil); + + if (needAlloc) { + dataBuf = new char[(int) len]; + if (dataBuf == nil) { + pErrMsg->Format("ERROR: allocation of %ld bytes failed", len); + goto bail; + } + } else { + if (*pLength < (long) len) { + pErrMsg->Format("ERROR: buf size %ld too short (%ld)", + *pLength, (long) len); + goto bail; + } + dataBuf = *ppText; + } + + dierr = pOpenFile->Read(dataBuf, (size_t) len); + if (dierr != kDIErrNone) { + if (dierr == kDIErrCancelled) { + result = IDCANCEL; + } else { + pErrMsg->Format("File read failed: %s", + DiskImgLib::DIStrError(dierr)); + } + goto bail; + } + + if (needAlloc) + *ppText = dataBuf; + *pLength = (long) len; + result = IDOK; + +bail: + if (pOpenFile != nil) + pOpenFile->Close(); + if (result == IDOK) { + SET_PROGRESS_END(); + ASSERT(pErrMsg->IsEmpty()); + } else { + ASSERT(result == IDCANCEL || !pErrMsg->IsEmpty()); + if (needAlloc) { + delete[] dataBuf; + ASSERT(*ppText == nil); + } + } + return result; +} + +/* + * Extract data from a thread to a file. Since we're not copying to memory, + * we can't assume that we're able to hold the entire file all at once. + * + * Returns IDOK on success, IDCANCEL if the operation was cancelled by the + * user, and -1 value on failure. On failure, "*pMsg" holds an + * error message. + */ +int +DiskEntry::ExtractThreadToFile(int which, FILE* outfp, ConvertEOL conv, + ConvertHighASCII convHA, CString* pErrMsg) const +{ + A2FileDescr* pOpenFile = nil; + bool rsrcFork; + int result = -1; + + ASSERT(IDOK != -1 && IDCANCEL != -1); + ASSERT(fpFile != nil); + + if (which == kDataThread) + rsrcFork = false; + else if (which == kRsrcThread) + rsrcFork = true; + else { + /* if we handle disk images, make sure we disable "conv" */ + *pErrMsg = "No such fork"; + goto bail; + } + + LONGLONG len; + if (rsrcFork) + len = fpFile->GetRsrcLength(); + else + len = fpFile->GetDataLength(); + + if (len == 0) { + WMSG0("Empty fork\n"); + result = IDOK; + goto bail; + } else if (len < 0) { + assert(rsrcFork); // forked files always have a data fork + *pErrMsg = "That fork doesn't exist"; + goto bail; + } + + DIError dierr; + dierr = fpFile->Open(&pOpenFile, true, rsrcFork); + if (dierr != kDIErrNone) { + *pErrMsg = "Unable to open file on disk image"; + goto bail; + } + + dierr = CopyData(pOpenFile, outfp, conv, convHA, pErrMsg); + if (dierr != kDIErrNone) { + if (pErrMsg->IsEmpty()) { + pErrMsg->Format("Failed while copying data: %s\n", + DiskImgLib::DIStrError(dierr)); + } + goto bail; + } + + result = IDOK; + +bail: + if (pOpenFile != nil) + pOpenFile->Close(); + return result; +} + +/* + * Copy data from the open A2File to outfp, possibly converting EOL along + * the way. + */ +DIError +DiskEntry::CopyData(A2FileDescr* pOpenFile, FILE* outfp, ConvertEOL conv, + ConvertHighASCII convHA, CString* pMsg) const +{ + DIError dierr = kDIErrNone; + const int kChunkSize = 16384; + char buf[kChunkSize]; + //bool firstChunk = true; + //EOLType sourceType; + bool lastCR = false; + LONGLONG srcLen, dataRem; + + /* get the length of the open file */ + dierr = pOpenFile->Seek(0, DiskImgLib::kSeekEnd); + if (dierr != kDIErrNone) + goto bail; + srcLen = pOpenFile->Tell(); + dierr = pOpenFile->Rewind(); + if (dierr != kDIErrNone) + goto bail; + ASSERT(srcLen > 0); // empty files should've been caught earlier + + SET_PROGRESS_BEGIN(); + pOpenFile->SetProgressUpdater(DiskArchive::ProgressCallback, srcLen, nil); + + /* + * Loop until all data copied. + */ + dataRem = srcLen; + while (dataRem) { + int chunkLen; + + if (dataRem > kChunkSize) + chunkLen = kChunkSize; + else + chunkLen = (int) dataRem; + + /* read a chunk from the source file */ + dierr = pOpenFile->Read(buf, chunkLen); + if (dierr != kDIErrNone) { + pMsg->Format("File read failed: %s", + DiskImgLib::DIStrError(dierr)); + goto bail; + } + + /* write chunk to destination file */ + int err = GenericEntry::WriteConvert(outfp, buf, chunkLen, &conv, + &convHA, &lastCR); + if (err != 0) { + pMsg->Format("File write failed: %s", strerror(err)); + dierr = kDIErrGeneric; + goto bail; + } + + dataRem -= chunkLen; + //SET_PROGRESS_UPDATE(ComputePercent(srcLen - dataRem, srcLen)); + } + +bail: + pOpenFile->ClearProgressUpdater(); + SET_PROGRESS_END(); + return dierr; +} + + +/* + * Figure out whether or not we're allowed to change a file's type and + * aux type. + */ +bool +DiskEntry::GetFeatureFlag(Feature feature) const +{ + DiskImg::FSFormat format; + + format = fpFile->GetDiskFS()->GetDiskImg()->GetFSFormat(); + + switch (feature) { + case kFeatureCanChangeType: + { + //if (GetRecordKind() == kRecordKindVolumeDir) + // return false; + + switch (format) { + case DiskImg::kFormatProDOS: + case DiskImg::kFormatPascal: + case DiskImg::kFormatMacHFS: + case DiskImg::kFormatDOS32: + case DiskImg::kFormatDOS33: + return true; + default: + return false; + } + } + case kFeaturePascalTypes: + { + switch (format) { + case DiskImg::kFormatPascal: + return true; + default: + return false; + } + } + case kFeatureDOSTypes: + { + switch (format) { + case DiskImg::kFormatDOS32: + case DiskImg::kFormatDOS33: + return true; + default: + return false; + } + } + case kFeatureHFSTypes: + { + switch (format) { + case DiskImg::kFormatMacHFS: + return true; + default: + return false; + } + } + case kFeatureHasFullAccess: + { + switch (format) { + case DiskImg::kFormatProDOS: + return true; + default: + return false; + } + } + case kFeatureHasSimpleAccess: + { + switch (format) { + case DiskImg::kFormatDOS33: + case DiskImg::kFormatDOS32: + case DiskImg::kFormatCPM: + case DiskImg::kFormatMacHFS: + return true; + default: + return false; + } + } + case kFeatureHasInvisibleFlag: + { + switch(format) { + case DiskImg::kFormatProDOS: + case DiskImg::kFormatMacHFS: + return true; + default: + return false; + } + } + default: + WMSG1("Unexpected feature flag %d\n", feature); + assert(false); + return false; + } + + assert(false); + return false; +} + + +/* + * =========================================================================== + * DiskArchive + * =========================================================================== + */ + +/* + * Perform one-time initialization of the DiskLib library. + */ +/*static*/ CString +DiskArchive::AppInit(void) +{ + CString result(""); + DIError dierr; + long major, minor, bug; + + WMSG0("Initializing DiskImg library\n"); + + // set this before initializing, so we can see init debug msgs + DiskImgLib::Global::SetDebugMsgHandler(DebugMsgHandler); + + dierr = DiskImgLib::Global::AppInit(); + if (dierr != kDIErrNone) { + result.Format("DiskImg DLL failed to initialize: %s\n", + DiskImgLib::DIStrError(dierr)); + goto bail; + } + + DiskImgLib::Global::GetVersion(&major, &minor, &bug); + if (major != kDiskImgVersionMajor || minor < kDiskImgVersionMinor) { + result.Format("Older or incompatible version of DiskImg DLL found.\r\r" + "Wanted v%d.%d.x, found %ld.%ld.%ld.", + kDiskImgVersionMajor, kDiskImgVersionMinor, + major, minor, bug); + goto bail; + } + +bail: + return result; +} + +/* + * Perform one-time cleanup of DiskImgLib at shutdown time. + */ +/*static*/ void +DiskArchive::AppCleanup(void) +{ + DiskImgLib::Global::AppCleanup(); +} + + +/* + * Handle a debug message from the DiskImg library. + */ +/*static*/ void +DiskArchive::DebugMsgHandler(const char* file, int line, const char* msg) +{ + ASSERT(file != nil); + ASSERT(msg != nil); + +#if defined(_DEBUG_LOG) + //fprintf(gLog, "%s(%d) : %s", file, line, msg); + fprintf(gLog, "%05u %s", gPid, msg); +#elif defined(_DEBUG) + _CrtDbgReport(_CRT_WARN, file, line, NULL, "%s", msg); +#else + /* do nothing */ +#endif +} + +/* + * Progress update callback, called from DiskImgLib during read/write + * operations. + * + * Returns "true" if we should continue; + */ +/*static*/ bool +DiskArchive::ProgressCallback(DiskImgLib::A2FileDescr* pFile, + DiskImgLib::di_off_t max, DiskImgLib::di_off_t current, void* state) +{ + int status; + + //::Sleep(10); + status = SET_PROGRESS_UPDATE(ComputePercent(current, max)); + if (status == IDCANCEL) { + WMSG0("IDCANCEL returned from Main progress updater\n"); + return false; + } + + return true; // tell DiskImgLib to continue what it's doing +} + +/* + * Progress update callback, called from DiskImgLib while scanning a volume + * during Open(). + * + * Returns "true" if we should continue; + */ +/*static*/ bool +DiskArchive::ScanProgressCallback(void* cookie, const char* str, int count) +{ + CString fmt; + bool cont; + + if (count == 0) + fmt = str; + else + fmt.Format("%s (%%d)", str); + cont = SET_PROGRESS_COUNTER_2(fmt, count); + + if (!cont) { + WMSG0("cancelled\n"); + } + + return cont; +} + + +/* + * Finish instantiating a DiskArchive object by opening an existing file. + */ +GenericArchive::OpenResult +DiskArchive::Open(const char* filename, bool readOnly, CString* pErrMsg) +{ + DIError dierr; + CString errMsg; + OpenResult result = kResultUnknown; + const Preferences* pPreferences = GET_PREFERENCES(); + + ASSERT(fpPrimaryDiskFS == nil); + ASSERT(filename != nil); + //ASSERT(ext != nil); + + ASSERT(pPreferences != nil); + + fIsReadOnly = readOnly; + + // special case for volume open + bool isVolume = false; + if (filename[0] >= 'A' && filename[0] <= 'Z' && + filename[1] == ':' && filename[2] == '\\' && + filename[3] == '\0') + { + isVolume = true; + } + + /* + * Open the image. This can be very slow for compressed images, + * especially 3.5" FDI images. + */ + { + CWaitCursor waitc; + + dierr = fDiskImg.OpenImage(filename, PathProposal::kLocalFssep, readOnly); + if (dierr == kDIErrAccessDenied && !readOnly && !isVolume) { + // retry file open with read-only set + // don't do that for volumes -- assume they know what they want + WMSG0(" Retrying open with read-only set\n"); + fIsReadOnly = readOnly = true; + dierr = fDiskImg.OpenImage(filename, PathProposal::kLocalFssep, readOnly); + } + if (dierr != kDIErrNone) { + if (dierr == kDIErrFileArchive) + result = kResultFileArchive; + else { + result = kResultFailure; + errMsg.Format("Unable to open '%s': %s.", filename, + DiskImgLib::DIStrError(dierr)); + } + goto bail; + } + } + + dierr = fDiskImg.AnalyzeImage(); + if (dierr != kDIErrNone) { + result = kResultFailure; + errMsg.Format("Analysis of '%s' failed: %s", filename, + DiskImgLib::DIStrError(dierr)); + goto bail; + } + + /* allow them to override sector order and filesystem, if requested */ + if (pPreferences->GetPrefBool(kPrQueryImageFormat)) { + ImageFormatDialog imf; + + imf.InitializeValues(&fDiskImg); + imf.fFileSource = filename; + imf.SetQueryDisplayFormat(false); + imf.SetAllowGenericFormats(false); + + if (imf.DoModal() != IDOK) { + WMSG0("User bailed on IMF dialog\n"); + result = kResultCancel; + goto bail; + } + + if (imf.fSectorOrder != fDiskImg.GetSectorOrder() || + imf.fFSFormat != fDiskImg.GetFSFormat()) + { + WMSG0("Initial values overridden, forcing img format\n"); + dierr = fDiskImg.OverrideFormat(fDiskImg.GetPhysicalFormat(), + imf.fFSFormat, imf.fSectorOrder); + if (dierr != kDIErrNone) { + result = kResultFailure; + errMsg.Format("Unable to access disk image using selected" + " parameters. Error: %s.", + DiskImgLib::DIStrError(dierr)); + goto bail; + } + } + } + + if (fDiskImg.GetFSFormat() == DiskImg::kFormatUnknown || + fDiskImg.GetSectorOrder() == DiskImg::kSectorOrderUnknown) + { + result = kResultFailure; + errMsg.Format("Unable to identify filesystem on '%s'", filename); + goto bail; + } + + /* create an appropriate DiskFS object */ + fpPrimaryDiskFS = fDiskImg.OpenAppropriateDiskFS(); + if (fpPrimaryDiskFS == nil) { + /* unknown FS should've been caught above! */ + ASSERT(false); + result = kResultFailure; + errMsg.Format("Format of '%s' not recognized.", filename); + goto bail; + } + + fpPrimaryDiskFS->SetScanForSubVolumes(DiskFS::kScanSubEnabled); + + /* + * Scan all files and on the disk image, and recursively descend into + * sub-volumes. Can be slow on physical volumes. + * + * This is really only useful for ProDOS and HFS disks. Nothing else + * can be large enough to really get slow, and nothing else is likely + * to show up in a large multi-partition image. + * + * THOUGHT: only show the dialog if the volume is over a certain size. + */ + { + MainWindow* pMain = GET_MAIN_WINDOW(); + ProgressCounterDialog* pProgress; + + pProgress = new ProgressCounterDialog; + pProgress->Create(_T("Examining contents, please wait..."), pMain); + pProgress->SetCounterFormat("Scanning..."); + pProgress->CenterWindow(); + //pMain->PeekAndPump(); // redraw + CWaitCursor waitc; + + /* set up progress dialog and scan all files */ + pMain->SetProgressCounterDialog(pProgress); + fDiskImg.SetScanProgressCallback(ScanProgressCallback, this); + + dierr = fpPrimaryDiskFS->Initialize(&fDiskImg, DiskFS::kInitFull); + + fDiskImg.SetScanProgressCallback(nil, nil); + pMain->SetProgressCounterDialog(nil); + pProgress->DestroyWindow(); + + if (dierr != kDIErrNone) { + if (dierr == kDIErrCancelled) { + result = kResultCancel; + } else { + result = kResultFailure; + errMsg.Format("Error reading list of files from disk: %s", + DiskImgLib::DIStrError(dierr)); + } + goto bail; + } + } + + if (LoadContents() != 0) { + result = kResultFailure; + errMsg.Format("Failed while loading contents of disk image."); + goto bail; + } + + /* + * Force read-only flag if underlying FS doesn't allow RW. We need to + * consider embedded filesystems, so we only set RO if none of the + * filesystems are writable. + * + * BUG: this only checks the first level. Should be fully recursive. + */ + if (!fpPrimaryDiskFS->GetReadWriteSupported()) { + const DiskFS::SubVolume* pSubVol; + + fIsReadOnly = true; + pSubVol = fpPrimaryDiskFS->GetNextSubVolume(nil); + while (pSubVol != nil) { + if (pSubVol->GetDiskFS()->GetReadWriteSupported()) { + fIsReadOnly = false; + break; + } + + pSubVol = fpPrimaryDiskFS->GetNextSubVolume(pSubVol); + } + } + + /* force read-only if the primary is damaged */ + if (fpPrimaryDiskFS->GetFSDamaged()) + fIsReadOnly = true; + /* force read-only if the DiskImg thinks a wrapper is damaged */ + if (fpPrimaryDiskFS->GetDiskImg()->GetReadOnly()) + fIsReadOnly = true; + +// /* force read-only on .gz/.zip unless pref allows */ +// if (fDiskImg.GetOuterFormat() != DiskImg::kOuterFormatNone) { +// if (pPreferences->GetPrefBool(kPrWriteZips) == 0) +// fIsReadOnly = true; +// } + + SetPathName(filename); + result = kResultSuccess; + + /* set any preferences-based settings */ + PreferencesChanged(); + +bail: + *pErrMsg = errMsg; + if (!errMsg.IsEmpty()) { + assert(result == kResultFailure); + delete fpPrimaryDiskFS; + fpPrimaryDiskFS = nil; + } else { + assert(result != kResultFailure); + } + return result; +} + + +/* + * Finish instantiating a DiskArchive object by creating a new archive. + * + * Returns an error string on failure, or "" on success. + */ +CString +DiskArchive::New(const char* fileName, const void* vOptions) +{ + const Preferences* pPreferences = GET_PREFERENCES(); + NewOptions* pOptions = (NewOptions*) vOptions; + CString volName; + long numBlocks = -1; + long numTracks = -1; + int numSectors; + CString retmsg; + DIError dierr; + bool allowLowerCase; + + ASSERT(fileName != nil); + ASSERT(pOptions != nil); + + allowLowerCase = pPreferences->GetPrefBool(kPrProDOSAllowLower) != 0; + + switch (pOptions->base.format) { + case DiskImg::kFormatUnknown: + numBlocks = pOptions->blank.numBlocks; + break; + case DiskImg::kFormatProDOS: + volName = pOptions->prodos.volName; + numBlocks = pOptions->prodos.numBlocks; + break; + case DiskImg::kFormatPascal: + volName = pOptions->pascalfs.volName; + numBlocks = pOptions->pascalfs.numBlocks; + break; + case DiskImg::kFormatMacHFS: + volName = pOptions->hfs.volName; + numBlocks = pOptions->hfs.numBlocks; + break; + case DiskImg::kFormatDOS32: + numTracks = pOptions->dos.numTracks; + numSectors = pOptions->dos.numSectors; + + if (numTracks < DiskFSDOS33::kMinTracks || + numTracks > DiskFSDOS33::kMaxTracks) + { + retmsg.Format("Invalid DOS32 track count"); + goto bail; + } + if (numSectors != 13) { + retmsg.Format("Invalid DOS32 sector count"); + goto bail; + } + if (pOptions->dos.allocDOSTracks) + volName = "DOS"; + break; + case DiskImg::kFormatDOS33: + numTracks = pOptions->dos.numTracks; + numSectors = pOptions->dos.numSectors; + + if (numTracks < DiskFSDOS33::kMinTracks || + numTracks > DiskFSDOS33::kMaxTracks) + { + retmsg.Format("Invalid DOS33 track count"); + goto bail; + } + if (numSectors != 16 && numSectors != 32) { // no 13-sector (yet) + retmsg.Format("Invalid DOS33 sector count"); + goto bail; + } + if (pOptions->dos.allocDOSTracks) + volName = "DOS"; + break; + default: + retmsg.Format("Unsupported disk format"); + goto bail; + } + + WMSG4("DiskArchive: new '%s' %ld %s in '%s'\n", + (const char*)volName, numBlocks, + DiskImg::ToString(pOptions->base.format), fileName); + + bool canSkipFormat; + if (IsWin9x()) + canSkipFormat = false; + else + canSkipFormat = true; + + /* + * Create an image with the appropriate characteristics. We set + * "skipFormat" because we know this will be a brand-new file, and + * we're not currently creating nibble images. + * + * GLITCH: under Win98/ME, brand-new files contain the previous contents + * of the hard drive. We need to explicitly zero them out. We don't + * want to do it under Win2K/XP because it can be slow for larger + * volumes. + */ + if (numBlocks > 0) { + dierr = fDiskImg.CreateImage(fileName, nil, + DiskImg::kOuterFormatNone, + DiskImg::kFileFormatUnadorned, + DiskImg::kPhysicalFormatSectors, + nil, + pOptions->base.sectorOrder, + DiskImg::kFormatGenericProDOSOrd, // arg must be generic + numBlocks, + canSkipFormat); + } else { + ASSERT(numTracks > 0); + dierr = fDiskImg.CreateImage(fileName, nil, + DiskImg::kOuterFormatNone, + DiskImg::kFileFormatUnadorned, + DiskImg::kPhysicalFormatSectors, + nil, + pOptions->base.sectorOrder, + DiskImg::kFormatGenericProDOSOrd, // arg must be generic + numTracks, numSectors, + canSkipFormat); + } + if (dierr != kDIErrNone) { + retmsg.Format("Unable to create disk image: %s.", + DiskImgLib::DIStrError(dierr)); + goto bail; + } + + if (pOptions->base.format == DiskImg::kFormatUnknown) + goto skip_format; + + if (pOptions->base.format == DiskImg::kFormatDOS33 || + pOptions->base.format == DiskImg::kFormatDOS32) + fDiskImg.SetDOSVolumeNum(pOptions->dos.volumeNum); + + /* + * If we don't allow lower case in ProDOS filenames, don't allow them + * in volume names either. This works because we don't allow ' ' in + * volume names; otherwise we'd need to invoke a ProDOS-specific call + * to convert the ' ' to '.'. (Or we could just do it ourselves.) + * + * We can't ask the ProDOS DiskFS to force upper case for us because + * the ProDOS DiskFS object doesn't yet exist. + */ + if (pOptions->base.format == DiskImg::kFormatProDOS && !allowLowerCase) + volName.MakeUpper(); + + /* format it */ + dierr = fDiskImg.FormatImage(pOptions->base.format, volName); + if (dierr != kDIErrNone) { + retmsg.Format("Unable to format disk image: %s.", + DiskImgLib::DIStrError(dierr)); + goto bail; + } + fpPrimaryDiskFS = fDiskImg.OpenAppropriateDiskFS(false); + if (fpPrimaryDiskFS == nil) { + retmsg.Format("Unable to create DiskFS."); + goto bail; + } + + /* prep it */ + dierr = fpPrimaryDiskFS->Initialize(&fDiskImg, DiskFS::kInitFull); + if (dierr != kDIErrNone) { + retmsg.Format("Error reading list of files from disk: %s", + DiskImgLib::DIStrError(dierr)); + goto bail; + } + + /* this is pretty meaningless, but do it to ensure we're initialized */ + if (LoadContents() != 0) { + retmsg.Format("Failed while loading contents of disk image."); + goto bail; + } + +skip_format: + SetPathName(fileName); + + /* set any preferences-based settings */ + PreferencesChanged(); + +bail: + return retmsg; +} + +/* + * Close the DiskArchive ojbect. + */ +CString +DiskArchive::Close(void) +{ + if (fpPrimaryDiskFS != nil) { + WMSG0("DiskArchive shutdown closing disk image\n"); + delete fpPrimaryDiskFS; + fpPrimaryDiskFS = nil; + } + + DIError dierr; + dierr = fDiskImg.CloseImage(); + if (dierr != kDIErrNone) { + MainWindow* pMainWin = (MainWindow*)::AfxGetMainWnd(); + CString msg, failed; + + msg.Format("Failed while closing disk image: %s.", + DiskImgLib::DIStrError(dierr)); + failed.LoadString(IDS_FAILED); + WMSG1("During close: %s\n", (const char*) msg); + + pMainWin->MessageBox(msg, failed, MB_OK); + } + + return ""; +} + +/* + * Flush the DiskArchive object. + * + * Most of the stuff we do with disk images goes straight through, but in + * the case of compressed disks we don't normally re-compress them until + * it's time to close them. This forces us to update the copy on disk. + * + * Returns an empty string on success, or an error message on failure. + */ +CString +DiskArchive::Flush(void) +{ + DIError dierr; + CWaitCursor waitc; + + assert(fpPrimaryDiskFS != nil); + + dierr = fpPrimaryDiskFS->Flush(DiskImg::kFlushAll); + if (dierr != kDIErrNone) { + CString errMsg; + + errMsg.Format("Attempt to flush the current archive failed: %s.", + DiskImgLib::DIStrError(dierr)); + return errMsg; + } + + return ""; +} + +/* + * Returns "true" if the archive has un-flushed modifications pending. + */ +bool +DiskArchive::IsModified(void) const +{ + assert(fpPrimaryDiskFS != nil); + + return fpPrimaryDiskFS->GetDiskImg()->GetDirtyFlag(); +} + +/* + * Return an description of the disk archive, suitable for display in the + * main title bar. + */ +void +DiskArchive::GetDescription(CString* pStr) const +{ + if (fpPrimaryDiskFS == nil) + return; + + if (fpPrimaryDiskFS->GetVolumeID() != nil) + pStr->Format("Disk Image - %s", fpPrimaryDiskFS->GetVolumeID()); +} + + +/* + * Load the contents of a "disk archive". + * + * Returns 0 on success. + */ +int +DiskArchive::LoadContents(void) +{ + int result; + + WMSG0("DiskArchive LoadContents\n"); + ASSERT(fpPrimaryDiskFS != nil); + + { + MainWindow* pMain = GET_MAIN_WINDOW(); + ExclusiveModelessDialog* pWaitDlg = new ExclusiveModelessDialog; + pWaitDlg->Create(IDD_LOADING, pMain); + pWaitDlg->CenterWindow(); + pMain->PeekAndPump(); // redraw + CWaitCursor waitc; + + result = LoadDiskFSContents(fpPrimaryDiskFS, ""); + + SET_PROGRESS_COUNTER(-1); + + pWaitDlg->DestroyWindow(); + //pMain->PeekAndPump(); // redraw + } + + return result; +} + +/* + * Reload the stuff from the underlying DiskFS. + * + * This also does a "lite" flush of the disk data. For files that are + * essentially being written as we go, this does little more than clear + * the "dirty" flag. Files that need to be recompressed or have some + * other slow operation remain dirty. + * + * We don't need to do the flush as part of the reload -- we can load the + * contents with everything in a perfectly dirty state. We don't need to + * do it at all. We do it to keep the "dirty" flag clear when nothing is + * really dirty, and we do it here because almost all of our functions call + * "reload" after making changes, which makes it convenient to call from here. + */ +CString +DiskArchive::Reload() +{ + fReloadFlag = true; // tell everybody that cached data is invalid + + (void) fpPrimaryDiskFS->Flush(DiskImg::kFlushFastOnly); + + DeleteEntries(); // a GenericArchive operation + + if (LoadContents() != 0) + return "Disk image reload failed."; + + return ""; +} + +/* + * Reload the contents of the archive, showing an error message if the + * reload fails. + * + * Returns 0 on success, -1 on failure. + */ +int +DiskArchive::InternalReload(CWnd* pMsgWnd) +{ + CString errMsg; + + errMsg = Reload(); + + if (!errMsg.IsEmpty()) { + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + return -1; + } + + return 0; +} + +/* + * Load the contents of a DiskFS. + * + * Recursively handle sub-volumes. "volName" holds the name of the + * sub-volume as it should appear in the list. + */ +int +DiskArchive::LoadDiskFSContents(DiskFS* pDiskFS, const char* volName) +{ + static const char* kBlankFileName = ""; + A2File* pFile; + DiskEntry* pNewEntry; + DiskFS::SubVolume* pSubVol; + const Preferences* pPreferences = GET_PREFERENCES(); + bool wantCoerceDOSFilenames = false; + CString ourSubVolName; + + wantCoerceDOSFilenames = pPreferences->GetPrefBool(kPrCoerceDOSFilenames); + + WMSG2("Notes for disk image '%s':\n%s", + volName, pDiskFS->GetDiskImg()->GetNotes()); + + ASSERT(pDiskFS != nil); + pFile = pDiskFS->GetNextFile(nil); + while (pFile != nil) { + pNewEntry = new DiskEntry(pFile); + if (pNewEntry == nil) + return -1; + + CString path(pFile->GetPathName()); + if (path.IsEmpty()) + path = kBlankFileName; + if (DiskImg::UsesDOSFileStructure(pFile->GetFSFormat()) && + wantCoerceDOSFilenames) + { + InjectLowercase(&path); + } + pNewEntry->SetPathName(path); + if (volName[0] != '\0') + pNewEntry->SetSubVolName(volName); + pNewEntry->SetFssep(pFile->GetFssep()); + pNewEntry->SetFileType(pFile->GetFileType()); + pNewEntry->SetAuxType(pFile->GetAuxType()); + pNewEntry->SetAccess(pFile->GetAccess()); + if (pFile->GetCreateWhen() == 0) + pNewEntry->SetCreateWhen(kDateNone); + else + pNewEntry->SetCreateWhen(pFile->GetCreateWhen()); + if (pFile->GetModWhen() == 0) + pNewEntry->SetModWhen(kDateNone); + else + pNewEntry->SetModWhen(pFile->GetModWhen()); + pNewEntry->SetSourceFS(pFile->GetFSFormat()); + pNewEntry->SetHasDataFork(true); + if (pFile->IsVolumeDirectory()) { + /* volume directory entry; only on ProDOS/HFS */ + ASSERT(pFile->GetRsrcLength() < 0); + pNewEntry->SetRecordKind(GenericEntry::kRecordKindVolumeDir); + //pNewEntry->SetUncompressedLen(pFile->GetDataLength()); + pNewEntry->SetDataForkLen(pFile->GetDataLength()); + pNewEntry->SetCompressedLen(pFile->GetDataLength()); + } else if (pFile->IsDirectory()) { + /* directory entry */ + ASSERT(pFile->GetRsrcLength() < 0); + pNewEntry->SetRecordKind(GenericEntry::kRecordKindDirectory); + //pNewEntry->SetUncompressedLen(pFile->GetDataLength()); + pNewEntry->SetDataForkLen(pFile->GetDataLength()); + pNewEntry->SetCompressedLen(pFile->GetDataLength()); + } else if (pFile->GetRsrcLength() >= 0) { + /* has resource fork */ + pNewEntry->SetRecordKind(GenericEntry::kRecordKindForkedFile); + pNewEntry->SetDataForkLen(pFile->GetDataLength()); + pNewEntry->SetRsrcForkLen(pFile->GetRsrcLength()); + //pNewEntry->SetUncompressedLen( + // pFile->GetDataLength() + pFile->GetRsrcLength() ); + pNewEntry->SetCompressedLen( + pFile->GetDataSparseLength() + pFile->GetRsrcSparseLength() ); + pNewEntry->SetHasRsrcFork(true); + } else { + /* just data fork */ + pNewEntry->SetRecordKind(GenericEntry::kRecordKindFile); + //pNewEntry->SetUncompressedLen(pFile->GetDataLength()); + pNewEntry->SetDataForkLen(pFile->GetDataLength()); + pNewEntry->SetCompressedLen(pFile->GetDataSparseLength()); + } + + switch (pNewEntry->GetSourceFS()) { + case DiskImg::kFormatDOS33: + case DiskImg::kFormatDOS32: + case DiskImg::kFormatUNIDOS: + pNewEntry->SetFormatStr("DOS"); + break; + case DiskImg::kFormatProDOS: + pNewEntry->SetFormatStr("ProDOS"); + break; + case DiskImg::kFormatPascal: + pNewEntry->SetFormatStr("Pascal"); + break; + case DiskImg::kFormatCPM: + pNewEntry->SetFormatStr("CP/M"); + break; + case DiskImg::kFormatMSDOS: + pNewEntry->SetFormatStr("MS-DOS"); + break; + case DiskImg::kFormatRDOS33: + case DiskImg::kFormatRDOS32: + case DiskImg::kFormatRDOS3: + pNewEntry->SetFormatStr("RDOS"); + break; + case DiskImg::kFormatMacHFS: + pNewEntry->SetFormatStr("HFS"); + break; + default: + pNewEntry->SetFormatStr("???"); + break; + } + + pNewEntry->SetDamaged(pFile->GetQuality() == A2File::kQualityDamaged); + pNewEntry->SetSuspicious(pFile->GetQuality() == A2File::kQualitySuspicious); + + AddEntry(pNewEntry); + + /* this is not very useful -- all the heavy lifting was done earlier */ + if ((GetNumEntries() % 100) == 0) + SET_PROGRESS_COUNTER(GetNumEntries()); + + pFile = pDiskFS->GetNextFile(pFile); + } + + /* + * Load all sub-volumes. + * + * We define the sub-volume name to use for the next layer down. We + * prepend an underscore to the unmodified name. So long as the volume + * name is a valid Windows path -- which should hold true for most disks, + * though possibly not for Pascal -- it can be extracted directly with + * its full path with no risk of conflict. (The extraction code relies + * on this, so don't put a ':' in the subvol name or Windows will choke.) + */ + pSubVol = pDiskFS->GetNextSubVolume(nil); + while (pSubVol != nil) { + CString concatSubVolName; + const char* subVolName; + int ret; + + subVolName = pSubVol->GetDiskFS()->GetVolumeName(); + if (subVolName == nil) + subVolName = "+++"; // call it *something* + + if (volName[0] == '\0') + concatSubVolName.Format("_%s", subVolName); + else + concatSubVolName.Format("%s_%s", volName, subVolName); + ret = LoadDiskFSContents(pSubVol->GetDiskFS(), concatSubVolName); + if (ret != 0) + return ret; + pSubVol = pDiskFS->GetNextSubVolume(pSubVol); + } + + return 0; +} + + +/* + * User has updated their preferences. Take note. + * + * Setting preferences in a DiskFS causes those prefs to be pushed down + * to all sub-volumes. + */ +void +DiskArchive::PreferencesChanged(void) +{ + const Preferences* pPreferences = GET_PREFERENCES(); + + if (fpPrimaryDiskFS != nil) { + fpPrimaryDiskFS->SetParameter(DiskFS::kParmProDOS_AllowLowerCase, + pPreferences->GetPrefBool(kPrProDOSAllowLower) != 0); + fpPrimaryDiskFS->SetParameter(DiskFS::kParmProDOS_AllocSparse, + pPreferences->GetPrefBool(kPrProDOSUseSparse) != 0); + } +} + + +/* + * Report on what this disk image is capable of. + */ +long +DiskArchive::GetCapability(Capability cap) +{ + switch (cap) { + case kCapCanTest: + return false; + break; + case kCapCanRenameFullPath: + return false; + break; + case kCapCanRecompress: + return false; + break; + case kCapCanEditComment: + return false; + break; + case kCapCanAddDisk: + return false; + break; + case kCapCanConvEOLOnAdd: + return true; + break; + case kCapCanCreateSubdir: + return true; + break; + case kCapCanRenameVolume: + return true; + break; + default: + ASSERT(false); + return -1; + break; + } +} + + +/* + * =========================================================================== + * DiskArchive -- add files + * =========================================================================== + */ + +/* + * Process a bulk "add" request. + * + * Returns "true" on success, "false" on failure. + */ +bool +DiskArchive::BulkAdd(ActionProgressDialog* pActionProgress, + const AddFilesDialog* pAddOpts) +{ + NuError nerr; + CString errMsg; + char curDir[MAX_PATH] = ""; + bool retVal = false; + + WMSG2("Opts: '%s' typePres=%d\n", + pAddOpts->fStoragePrefix, pAddOpts->fTypePreservation); + WMSG3(" sub=%d strip=%d ovwr=%d\n", + pAddOpts->fIncludeSubfolders, pAddOpts->fStripFolderNames, + pAddOpts->fOverwriteExisting); + + ASSERT(fpAddDataHead == nil); + + /* these reset on every new add */ + fOverwriteExisting = false; + fOverwriteNoAsk = false; + + /* we screen for clashes with existing files later; this just ensures + "batch uniqueness" */ + fpPrimaryDiskFS->SetParameter(DiskFS::kParm_CreateUnique, true); + + /* + * Save the current directory and change to the one from the file dialog. + */ + const char* buf = pAddOpts->GetFileNames(); + WMSG2("Selected path = '%s' (offset=%d)\n", buf, + pAddOpts->GetFileNameOffset()); + + if (GetCurrentDirectory(sizeof(curDir), curDir) == 0) { + errMsg = "Unable to get current directory.\n"; + ShowFailureMsg(pActionProgress, errMsg, IDS_FAILED); + goto bail; + } + if (SetCurrentDirectory(buf) == false) { + errMsg.Format("Unable to set current directory to '%s'.\n", buf); + ShowFailureMsg(pActionProgress, errMsg, IDS_FAILED); + goto bail; + } + + buf += pAddOpts->GetFileNameOffset(); + while (*buf != '\0') { + WMSG1(" file '%s'\n", buf); + + /* add the file, calling DoAddFile via the generic AddFile */ + nerr = AddFile(pAddOpts, buf, &errMsg); + if (nerr != kNuErrNone) { + if (errMsg.IsEmpty()) + errMsg.Format("Failed while adding file '%s': %s.", + (LPCTSTR) buf, NuStrError(nerr)); + if (nerr != kNuErrAborted) { + ShowFailureMsg(pActionProgress, errMsg, IDS_FAILED); + } + goto bail; + } + + buf += strlen(buf)+1; + } + + if (fpAddDataHead == nil) { + CString title; + title.LoadString(IDS_MB_APP_NAME); + errMsg = "No files added.\n"; + pActionProgress->MessageBox(errMsg, title, MB_OK | MB_ICONWARNING); + } else { + /* add all pending files */ + retVal = true; + errMsg = ProcessFileAddData(pAddOpts->fpTargetDiskFS, + pAddOpts->fConvEOL); + if (!errMsg.IsEmpty()) { + CString title; + ShowFailureMsg(pActionProgress, errMsg, IDS_FAILED); + retVal = false; + } + + /* success or failure, reload the contents */ + errMsg = Reload(); + if (!errMsg.IsEmpty()) + retVal = false; + } + +bail: + FreeAddDataList(); + if (SetCurrentDirectory(curDir) == false) { + errMsg.Format("Unable to reset current directory to '%s'.\n", buf); + ShowFailureMsg(pActionProgress, errMsg, IDS_FAILED); + // bummer, but don't signal failure + } + return retVal; +} + +/* + * Add a file to a disk image. + * + * Unfortunately we can't just add the files here. We need to figure out + * which pairs of files should be combined into a single "extended" file. + * (Yes, the cursed forked files strike again.) + * + * The way you tell if two files should be one is by comparing their + * filenames and type info. If they match, and one is a data fork and + * one is a resource fork, we have a single split file. + * + * We have to be careful here because we don't know which will be seen + * first and whether they'll be adjacent. We have to dig through the + * list of previously-added files for a match (O(n^2) behavior currently). + * + * We also have to compare the right filename. Comparing the Windows + * filename is a bad idea, because by definition one of them has a resource + * fork tag on it. We need to compare the normalized filename before + * the ProDOS normalizer/uniqifier gets a chance to mangle it. As luck + * would have it, that's exactly what we have in "storageName". + * + * For a NuFX archive, NufxLib does all this nonsense for us, but we have + * to manage it ourselves here. The good news is that, since we have to + * wade through all the filenames, we have an opportunity to make the names + * unique. So long as we ensure that the names we have don't clash with + * anything currently on the disk, we know that anything we add that does + * clash is running into something we just added, which means we can turn + * on CreateFile's "make unique" feature and let the filesystem-specific + * code handle uniqueness. + * + * Any fields we want to keep from the NuFileDetails struct need to be + * copied out. It's a "hairy" struct, so we need to duplicate the strings. + */ +NuError +DiskArchive::DoAddFile(const AddFilesDialog* pAddOpts, + FileDetails* pDetails) +{ + NuError nuerr = kNuErrNone; + DiskFS* pDiskFS = pAddOpts->fpTargetDiskFS; + + DIError dierr; + int neededLen = 64; // reasonable guess + char* fsNormalBuf = nil; + + WMSG2(" +++ ADD file: orig='%s' stor='%s'\n", + pDetails->origName, pDetails->storageName); + +retry: + /* + * Convert "storageName" to a filesystem-normalized path. + */ + delete[] fsNormalBuf; + fsNormalBuf = new char[neededLen]; + dierr = pDiskFS->NormalizePath(pDetails->storageName, + PathProposal::kDefaultStoredFssep, fsNormalBuf, &neededLen); + if (dierr == kDIErrDataOverrun) { + /* not long enough, try again *once* */ + delete[] fsNormalBuf; + fsNormalBuf = new char[neededLen]; + dierr = pDiskFS->NormalizePath(pDetails->storageName, + PathProposal::kDefaultStoredFssep, fsNormalBuf, &neededLen); + } + if (dierr != kDIErrNone) { + nuerr = kNuErrInternal; + goto bail; + } + + /* + * Test to see if the file already exists. If it does, give the user + * the opportunity to rename it, overwrite the original, or skip + * adding it. + * + * The FS-normalized path may not reflect the actual storage name, + * because some features (like ProDOS "allow lower case") aren't + * factored in until later. However, it should be close enough -- it + * has to be, or we'd be in trouble for saying it's going to overwrite + * the file in the archive. + */ + A2File* pExisting; + pExisting = pDiskFS->GetFileByName(fsNormalBuf); + if (pExisting != nil) { + NuResult result; + + result = HandleReplaceExisting(pExisting, pDetails); + if (result == kNuAbort) { + nuerr = kNuErrAborted; + goto bail; + } else if (result == kNuSkip) { + nuerr = kNuErrSkipped; + goto bail; + } else if (result == kNuRename) { + goto retry; + } else if (result == kNuOverwrite) { + /* delete the existing file immediately */ + WMSG1(" Deleting existing file '%s'\n", fsNormalBuf); + dierr = pDiskFS->DeleteFile(pExisting); + if (dierr != kDIErrNone) { + // Would be nice to show a dialog and explain *why*, but + // I'm not sure we have a window here. + WMSG1(" Deletion failed (err=%d)\n", dierr); + goto bail; + } + } else { + WMSG1("GLITCH: bad return %d from HandleReplaceExisting\n",result); + assert(false); + nuerr = kNuErrInternal; + goto bail; + } + } + + /* + * Put all the goodies into a new FileAddData object, and add it to + * the end of the list. + */ + FileAddData* pAddData; + pAddData = new FileAddData(pDetails, fsNormalBuf); + if (pAddData == nil) { + nuerr = kNuErrMalloc; + goto bail; + } + + WMSG1("FSNormalized is '%s'\n", pAddData->GetFSNormalPath()); + + AddToAddDataList(pAddData); + +bail: + delete[] fsNormalBuf; + return nuerr; +} + +/* + * A file we're adding clashes with an existing file. Decide what to do + * about it. + * + * Returns one of the following: + * kNuOverwrite - overwrite the existing file + * kNuSkip - skip adding the existing file + * kNuRename - user wants to rename the file + * kNuAbort - cancel out of the entire add process + * + * Side effects: + * Sets fOverwriteExisting and fOverwriteNoAsk if a "to all" button is hit + * Replaces pDetails->storageName if the user elects to rename + */ +NuResult +DiskArchive::HandleReplaceExisting(const A2File* pExisting, + FileDetails* pDetails) +{ + NuResult result; + + if (fOverwriteNoAsk) { + if (fOverwriteExisting) + return kNuOverwrite; + else + return kNuSkip; + } + + ConfirmOverwriteDialog confOvwr; + + confOvwr.fExistingFile = pExisting->GetPathName(); + confOvwr.fExistingFileModWhen = pExisting->GetModWhen(); + + PathName srcPath(pDetails->origName); + confOvwr.fNewFileSource = pDetails->origName; // or storageName? + confOvwr.fNewFileModWhen = srcPath.GetModWhen(); + + if (confOvwr.DoModal() == IDCANCEL) { + WMSG0("User cancelled out of add-to-diskimg replace-existing\n"); + return kNuAbort; + } + + if (confOvwr.fResultRename) { + /* + * Replace the name in FileDetails. They were asked to modify + * the already-normalized version of the filename. We will run + * it back through the FS-specific normalizer, which will handle + * any oddities they type in. + * + * We don't want to run it through PathProposal.LocalToArchive + * because that'll strip out ':' in the pathnames. + * + * Ideally the rename dialog would have a way to validate the + * full path and reject "OK" if it's not valid. Instead, we just + * allow the FS normalizer to force the filename to be valid. + */ + pDetails->storageName = confOvwr.fExistingFile; + WMSG1("Trying rename to '%s'\n", pDetails->storageName); + return kNuRename; + } + + if (confOvwr.fResultApplyToAll) { + fOverwriteNoAsk = true; + if (confOvwr.fResultOverwrite) + fOverwriteExisting = true; + else + fOverwriteExisting = false; + } + if (confOvwr.fResultOverwrite) + result = kNuOverwrite; + else + result = kNuSkip; + + return result; +} + + +/* + * Process the list of pending file adds. + * + * This is where the rubber (finally!) meets the road. + */ +CString +DiskArchive::ProcessFileAddData(DiskFS* pDiskFS, int addOptsConvEOL) +{ + CString errMsg; + FileAddData* pData; + unsigned char* dataBuf = nil; + unsigned char* rsrcBuf = nil; + long dataLen, rsrcLen; + MainWindow* pMainWin = (MainWindow*)::AfxGetMainWnd(); + + WMSG0("--- ProcessFileAddData\n"); + + /* map the EOL conversion to something we can use */ + GenericEntry::ConvertEOL convEOL; + + switch (addOptsConvEOL) { + case AddFilesDialog::kConvEOLNone: + convEOL = GenericEntry::kConvertEOLOff; + break; + case AddFilesDialog::kConvEOLType: + // will be adjusted each time through the loop + convEOL = GenericEntry::kConvertEOLOff; + break; + case AddFilesDialog::kConvEOLAuto: + convEOL = GenericEntry::kConvertEOLAuto; + break; + case AddFilesDialog::kConvEOLAll: + convEOL = GenericEntry::kConvertEOLOn; + break; + default: + assert(false); + convEOL = GenericEntry::kConvertEOLOff; + break; + } + + + pData = fpAddDataHead; + while (pData != nil) { + const FileDetails* pDataDetails = nil; + const FileDetails* pRsrcDetails = nil; + const FileDetails* pDetails = pData->GetDetails(); + const char* typeStr = "????"; + + switch (pDetails->entryKind) { + case FileDetails::kFileKindDataFork: + pDataDetails = pDetails; + typeStr = "data"; + break; + case FileDetails::kFileKindRsrcFork: + pRsrcDetails = pDetails; + typeStr = "rsrc"; + break; + case FileDetails::kFileKindDiskImage: + pDataDetails = pDetails; + typeStr = "disk"; + break; + case FileDetails::kFileKindBothForks: + case FileDetails::kFileKindDirectory: + default: + assert(false); + return "internal error"; + } + + if (pData->GetOtherFork() != nil) { + pDetails = pData->GetOtherFork()->GetDetails(); + typeStr = "both"; + + switch (pDetails->entryKind) { + case FileDetails::kFileKindDataFork: + assert(pDataDetails == nil); + pDataDetails = pDetails; + break; + case FileDetails::kFileKindRsrcFork: + assert(pRsrcDetails == nil); + pRsrcDetails = pDetails; + break; + case FileDetails::kFileKindDiskImage: + assert(false); + return "(internal) add other disk error"; + case FileDetails::kFileKindBothForks: + case FileDetails::kFileKindDirectory: + default: + assert(false); + return "internal error"; + } + } + + WMSG2("Adding file '%s' (%s)\n", + pDetails->storageName, typeStr); + ASSERT(pDataDetails != nil || pRsrcDetails != nil); + + /* + * The current implementation of DiskImg/DiskFS requires writing each + * fork in one shot. This means loading the entire thing into + * memory. Not so bad for ProDOS, with its 16MB maximum file size, + * but it could be awkward for HFS (not to mention HFS Plus!). + */ + DiskFS::CreateParms parms; + ConvertFDToCP(pData->GetDetails(), &parms); + if (pRsrcDetails != nil) + parms.storageType = kNuStorageExtended; + else + parms.storageType = kNuStorageSeedling; + /* use the FS-normalized path here */ + parms.pathName = pData->GetFSNormalPath(); + + dataLen = rsrcLen = -1; + if (pDataDetails != nil) { + /* figure out text conversion, including high ASCII for DOS */ + /* (HA conversion only happens if text conversion happens) */ + GenericEntry::ConvertHighASCII convHA; + if (addOptsConvEOL == AddFilesDialog::kConvEOLType) { + if (pDataDetails->fileType == kFileTypeTXT || + pDataDetails->fileType == kFileTypeSRC) + { + WMSG0("Enabling text conversion by type\n"); + convEOL = GenericEntry::kConvertEOLOn; + } else { + convEOL = GenericEntry::kConvertEOLOff; + } + } + if (DiskImg::UsesDOSFileStructure(pDiskFS->GetDiskImg()->GetFSFormat())) + convHA = GenericEntry::kConvertHAOn; + else + convHA = GenericEntry::kConvertHAOff; + + errMsg = LoadFile(pDataDetails->origName, &dataBuf, &dataLen, + convEOL, convHA); + if (!errMsg.IsEmpty()) + goto bail; + } + if (pRsrcDetails != nil) { + /* no text conversion on resource forks */ + errMsg = LoadFile(pRsrcDetails->origName, &rsrcBuf, &rsrcLen, + GenericEntry::kConvertEOLOff, GenericEntry::kConvertHAOff); + if (!errMsg.IsEmpty()) + goto bail; + } + + /* really ought to do this separately for each thread */ + SET_PROGRESS_BEGIN(); + SET_PROGRESS_UPDATE2(0, pDetails->origName, + parms.pathName); + + DIError dierr; + dierr = AddForksToDisk(pDiskFS, &parms, dataBuf, dataLen, + rsrcBuf, rsrcLen); + SET_PROGRESS_END(); + if (dierr != kDIErrNone) { + errMsg.Format("Unable to add '%s' to image: %s.", + parms.pathName, DiskImgLib::DIStrError(dierr)); + goto bail; + } + delete[] dataBuf; + delete[] rsrcBuf; + dataBuf = rsrcBuf = nil; + + pData = pData->GetNext(); + } + +bail: + delete[] dataBuf; + delete[] rsrcBuf; + return errMsg; +} + +#define kCharLF '\n' +#define kCharCR '\r' + +/* + * Load a file into a buffer, possibly converting EOL markers and setting + * "high ASCII" along the way. + * + * Returns a pointer to a newly-allocated buffer (new[]) and the data length. + * If the file is empty, no buffer will be allocated. + * + * Returns an empty string on success, or an error message on failure. + * + * HEY: really ought to update the progress counter, especially when reading + * really large files. + */ +CString +DiskArchive::LoadFile(const char* pathName, unsigned char** pBuf, long* pLen, + GenericEntry::ConvertEOL conv, GenericEntry::ConvertHighASCII convHA) const +{ + CString errMsg; + FILE* fp; + long fileLen; + + ASSERT(convHA == GenericEntry::kConvertHAOn || + convHA == GenericEntry::kConvertHAOff); + ASSERT(conv == GenericEntry::kConvertEOLOn || + conv == GenericEntry::kConvertEOLOff || + conv == GenericEntry::kConvertEOLAuto); + ASSERT(pathName != nil); + ASSERT(pBuf != nil); + ASSERT(pLen != nil); + + fp = fopen(pathName, "rb"); + if (fp == nil) { + errMsg.Format("Unable to open '%s': %s.", pathName, + strerror(errno)); + goto bail; + } + + if (fseek(fp, 0, SEEK_END) != 0) { + errMsg.Format("Unable to seek to end of '%s': %s", pathName, + strerror(errno)); + goto bail; + } + fileLen = ftell(fp); + if (fileLen < 0) { + errMsg.Format("Unable to determine length of '%s': %s", pathName, + strerror(errno)); + goto bail; + } + rewind(fp); + + if (fileLen == 0) { // handle zero-length files + *pBuf = nil; + *pLen = 0; + goto bail; + } else if (fileLen > 0x00ffffff) { + errMsg = "Cannot add files larger than 16MB to a disk image."; + goto bail; + } + + *pBuf = new unsigned char[fileLen]; + if (*pBuf == nil) { + errMsg.Format("Unable to allocate %ld bytes for '%s'.", + fileLen, pathName); + goto bail; + } + + /* + * We're ready to load the file. We need to sort out EOL conversion. + * Since we always convert to CR, we know the file will stay the same + * size or get smaller, which means the buffer we've allocated is + * guaranteed to hold the file even if we convert it. + * + * If the text mode is "auto", we need to load a piece of the file and + * analyze it. + */ + if (conv == GenericEntry::kConvertEOLAuto) { + GenericEntry::EOLType eolType; + GenericEntry::ConvertHighASCII dummy; + + int chunkLen = 16384; // nice big chunk + if (chunkLen > fileLen) + chunkLen = fileLen; + + if (fread(*pBuf, chunkLen, 1, fp) != 1) { + errMsg.Format("Unable to read initial chunk of '%s': %s.", + pathName, strerror(errno)); + delete[] *pBuf; + *pBuf = nil; + goto bail; + } + rewind(fp); + + conv = GenericEntry::DetermineConversion(*pBuf, chunkLen, + &eolType, &dummy); + WMSG2("LoadFile DetermineConv returned conv=%d eolType=%d\n", + conv, eolType); + if (conv == GenericEntry::kConvertEOLOn && + eolType == GenericEntry::kEOLCR) + { + WMSG0(" (skipping conversion due to matching eolType)\n"); + conv = GenericEntry::kConvertEOLOff; + } + } + ASSERT(conv != GenericEntry::kConvertEOLAuto); + + /* + * The "high ASCII" conversion is either on or off. In this context, + * "on" means "convert all text files", and "off" means "don't convert + * text files". We never convert non-text files. Conversion should + * always be "on" for DOS 3.2/3.3, and "off" for everything else (except + * RDOS, should we choose to make that writeable). + */ + if (conv == GenericEntry::kConvertEOLOff) { + /* fast path */ + WMSG1(" +++ NOT converting text '%s'\n", pathName); + if (fread(*pBuf, fileLen, 1, fp) != 1) { + errMsg.Format("Unable to read '%s': %s.", pathName, strerror(errno)); + delete[] *pBuf; + *pBuf = nil; + goto bail; + } + } else { + /* + * Convert as we go. + * + * Observation: if we copy a binary file to a DOS disk, and force + * the text conversion, we will convert 0x0a to 0x0d, and thence + * to 0x8d. However, we may still have some 0x8a bytes lying around, + * because we don't convert 0x8a in the original file to anything. + * This means that a CR->CRLF or LF->CRLF conversion can't be + * "undone" on a DOS disk. (Not that anyone cares.) + */ + long count = fileLen; + int mask, ic; + bool lastCR = false; + unsigned char* buf = *pBuf; + + if (convHA == GenericEntry::kConvertHAOn) + mask = 0x80; + else + mask = 0x00; + + WMSG2(" +++ Converting text '%s', mask=0x%02x\n", pathName, mask); + + while (count--) { + ic = getc(fp); + + if (ic == kCharCR) { + *buf++ = (unsigned char) (kCharCR | mask); + lastCR = true; + } else if (ic == kCharLF) { + if (!lastCR) + *buf++ = (unsigned char) (kCharCR | mask); + lastCR = false; + } else { + if (ic == '\0') + *buf++ = (unsigned char) ic; // don't conv 0x00 + else + *buf++ = (unsigned char) (ic | mask); + lastCR = false; + } + } + fileLen = buf - *pBuf; + } + + (void) fclose(fp); + + *pLen = fileLen; + +bail: + return errMsg; +} + +/* + * Add a file with the supplied data to the disk image. + * + * Forks that exist but are empty have a length of zero. Forks that don't + * exist have a length of -1. + * + * Called by XferFile and ProcessFileAddData. + */ +DIError +DiskArchive::AddForksToDisk(DiskFS* pDiskFS, const DiskFS::CreateParms* pParms, + const unsigned char* dataBuf, long dataLen, + const unsigned char* rsrcBuf, long rsrcLen) const +{ + DIError dierr = kDIErrNone; + CString replacementFileName; + const int kFileTypeBIN = 0x06; + const int kFileTypeINT = 0xfa; + const int kFileTypeBAS = 0xfc; + A2File* pNewFile = nil; + A2FileDescr* pOpenFile = nil; + DiskFS::CreateParms parmCopy; + + /* + * Make a temporary copy, pointers and all, so we can rewrite some of + * the fields. This is sort of bad, because we're making copies of a + * const char* filename pointer whose underlying storage we're not + * really familiar with. However, so long as we don't try to retain + * it after this function returns we should be fine. + * + * Might be better to make CreateParms a class instead of a struct, + * make the pathName field new[] storage, and write a copy constructor + * for the operation below. This will be fine for now. + */ + memcpy(&parmCopy, pParms, sizeof(parmCopy)); + + if (rsrcLen >= 0) { + ASSERT(parmCopy.storageType == kNuStorageExtended); + } + + /* + * Look for "empty directory holders" that we put into NuFX archives + * when doing disk-to-archive conversions. These make no sense if + * there's no fssep (because it's coming from DOS), or if there's no + * base path, so we can ignore those cases. We can also ignore it if + * the file is forked or is already a directory. + */ + if (parmCopy.fssep != '\0' && parmCopy.storageType == kNuStorageSeedling) { + const char* cp; + cp = strrchr(parmCopy.pathName, parmCopy.fssep); + if (cp != nil) { + if (strcmp(cp+1, kEmptyFolderMarker) == 0 && dataLen == 0) { + /* drop the junk on the end */ + parmCopy.storageType = kNuStorageDirectory; + replacementFileName = parmCopy.pathName; + replacementFileName = + replacementFileName.Left(cp - parmCopy.pathName -1); + parmCopy.pathName = replacementFileName; + parmCopy.fileType = 0x0f; // DIR + parmCopy.access &= ~(A2FileProDOS::kAccessInvisible); + dataLen = -1; + } + } + } + + /* + * If this is a subdir create request (from the clipboard or an "empty + * directory placeholder" in a NuFX archive), handle it here. If we're + * on a filesystem that doesn't have subdirectories, just skip it. + */ + if (parmCopy.storageType == kNuStorageDirectory) { + A2File* pDummyFile; + ASSERT(dataLen < 0 && rsrcLen < 0); + + if (DiskImg::IsHierarchical(pDiskFS->GetDiskImg()->GetFSFormat())) { + dierr = pDiskFS->CreateFile(&parmCopy, &pDummyFile); + if (dierr == kDIErrDirectoryExists) + dierr = kDIErrNone; // dirs are not made unique + goto bail; + } else { + WMSG0(" Ignoring subdir create req on non-hierarchic FS\n"); + goto bail; + } + } + + /* don't try to put resource forks onto a DOS disk */ + if (!DiskImg::HasResourceForks(pDiskFS->GetDiskImg()->GetFSFormat())) { + if (rsrcLen >= 0) { + rsrcLen = -1; + parmCopy.storageType = kNuStorageSeedling; + + if (dataLen < 0) { + /* this was a resource-fork-only file */ + WMSG1("--- nothing left to write for '%s'\n", + parmCopy.pathName); + goto bail; + } + } else { + ASSERT(parmCopy.storageType == kNuStorageSeedling); + } + } + + /* quick kluge to get the right file type on large DOS files */ + if (DiskImg::UsesDOSFileStructure(pDiskFS->GetDiskImg()->GetFSFormat()) && + dataLen >= 65536) + { + if (parmCopy.fileType == kFileTypeBIN || + parmCopy.fileType == kFileTypeINT || + parmCopy.fileType == kFileTypeBAS) + { + WMSG0("+++ switching DOS file type to $f2\n"); + parmCopy.fileType = 0xf2; // DOS 'S' file + } + } + + /* + * Create the file on the disk. The storage type determines whether + * it has data+rsrc forks or just data (there's no such thing in + * ProDOS as "just a resource fork"). There's no need to open the + * fork if we're not going to write to it. + * + * This holds for resource forks as well, because the storage type + * determines whether or not the file is forked, and we've asserted + * that a file with a non-(-1) rsrcLen is forked. + */ + dierr = pDiskFS->CreateFile(&parmCopy, &pNewFile); + if (dierr != kDIErrNone) { + WMSG1(" CreateFile failed: %s\n", DiskImgLib::DIStrError(dierr)); + goto bail; + } + + /* + * Note: if this was an empty directory holder, pNewFile will be set + * to nil. We used to avoid handling this by just not opening the file + * if it had a length of zero. However, DOS 3.3 needs to write some + * kinds of zero-length files, because e.g. a zero-length 'B' file + * actually has 4 bytes of data in it. + * + * Of course, if dataLen is zero then dataBuf is nil, so we need to + * supply a dummy write buffer. None of this is an issue for resource + * forks, because DOS 3.3 doesn't have those. + */ + + if (dataLen > 0 || + (dataLen == 0 && pNewFile != nil)) + { + ASSERT(pNewFile != nil); + unsigned char dummyBuf[1] = { '\0' }; + + dierr = pNewFile->Open(&pOpenFile, false, false); + if (dierr != kDIErrNone) + goto bail; + + pOpenFile->SetProgressUpdater(DiskArchive::ProgressCallback, + dataLen, nil); + + dierr = pOpenFile->Write(dataBuf != nil ? dataBuf : dummyBuf, dataLen); + if (dierr != kDIErrNone) + goto bail; + + dierr = pOpenFile->Close(); + if (dierr != kDIErrNone) + goto bail; + pOpenFile = nil; + } + + if (rsrcLen > 0) { + ASSERT(pNewFile != nil); + + dierr = pNewFile->Open(&pOpenFile, false, true); + if (dierr != kDIErrNone) + goto bail; + + pOpenFile->SetProgressUpdater(DiskArchive::ProgressCallback, + rsrcLen, nil); + + dierr = pOpenFile->Write(rsrcBuf, rsrcLen); + if (dierr != kDIErrNone) + goto bail; + + dierr = pOpenFile->Close(); + if (dierr != kDIErrNone) + goto bail; + pOpenFile = nil; + } + +bail: + if (pOpenFile != nil) + pOpenFile->Close(); + if (dierr != kDIErrNone && pNewFile != nil) { + /* + * Clean up the partially-written file. This does not, of course, + * erase any subdirectories that were created to contain this file. + * Not worth worrying about. + */ + WMSG1(" Deleting newly-created file '%s'\n", parmCopy.pathName); + (void) pDiskFS->DeleteFile(pNewFile); + } + return dierr; +} + +/* + * Fill out a CreateParms structure from a FileDetails structure. + * + * The NuStorageType values correspond exactly to ProDOS storage types, so + * there's no need to convert them. + */ +void +DiskArchive::ConvertFDToCP(const FileDetails* pDetails, + DiskFS::CreateParms* pCreateParms) +{ + pCreateParms->pathName = pDetails->storageName; + pCreateParms->fssep = (char) pDetails->fileSysInfo; + pCreateParms->storageType = pDetails->storageType; + pCreateParms->fileType = pDetails->fileType; + pCreateParms->auxType = pDetails->extraType; + pCreateParms->access = pDetails->access; + pCreateParms->createWhen = NufxArchive::DateTimeToSeconds(&pDetails->createWhen); + pCreateParms->modWhen = NufxArchive::DateTimeToSeconds(&pDetails->modWhen); +} + + +/* + * Add an entry to the end of the FileAddData list. + * + * If "storeName" (the Windows filename with type goodies stripped, but + * without filesystem normalization) matches an entry already in the list, + * we check to see if these are forks of the same file. If they are + * different forks and we don't already have both forks, we put the + * pointer into the "fork pointer" of the existing file rather than adding + * it to the end of the list. + */ +void +DiskArchive::AddToAddDataList(FileAddData* pData) +{ + ASSERT(pData != nil); + ASSERT(pData->GetNext() == nil); + + /* + * Run through the entire existing list, looking for a match. This is + * O(n^2) behavior, but I'm expecting N to be relatively small (under + * 1000 in almost all cases). + */ + //if (strcasecmp(pData->GetDetails()->storageName, "system\\finder") == 0) + // WMSG0("whee\n"); + FileAddData* pSearch = fpAddDataHead; + FileDetails::FileKind dataKind, listKind; + + dataKind = pData->GetDetails()->entryKind; + while (pSearch != nil) { + if (pSearch->GetOtherFork() == nil && + strcmp(pSearch->GetDetails()->storageName, + pData->GetDetails()->storageName) == 0) + { + //NuThreadID dataID = pData->GetDetails()->threadID; + //NuThreadID listID = pSearch->GetDetails()->threadID; + + listKind = pSearch->GetDetails()->entryKind; + + /* got a name match */ + if (dataKind != listKind && + (dataKind == FileDetails::kFileKindDataFork || dataKind == FileDetails::kFileKindRsrcFork) && + (listKind == FileDetails::kFileKindDataFork || listKind == FileDetails::kFileKindRsrcFork)) + { + /* looks good, hook it in here instead of the list */ + WMSG2("--- connecting forks of '%s' and '%s'\n", + pData->GetDetails()->origName, + pSearch->GetDetails()->origName); + pSearch->SetOtherFork(pData); + return; + } + } + + pSearch = pSearch->GetNext(); + } + + if (fpAddDataHead == nil) { + assert(fpAddDataTail == nil); + fpAddDataHead = fpAddDataTail = pData; + } else { + fpAddDataTail->SetNext(pData); + fpAddDataTail = pData; + } +} + +/* + * Free all entries in the FileAddData list. + */ +void +DiskArchive::FreeAddDataList(void) +{ + FileAddData* pData; + FileAddData* pNext; + + pData = fpAddDataHead; + while (pData != nil) { + pNext = pData->GetNext(); + delete pData->GetOtherFork(); + delete pData; + pData = pNext; + } + + fpAddDataHead = fpAddDataTail = nil; +} + + +/* + * =========================================================================== + * DiskArchive -- create subdir + * =========================================================================== + */ + +/* + * Create a subdirectory named "newName" in "pParentEntry". + */ +bool +DiskArchive::CreateSubdir(CWnd* pMsgWnd, GenericEntry* pParentEntry, + const char* newName) +{ + ASSERT(newName != nil && strlen(newName) > 0); + DiskEntry* pEntry = (DiskEntry*) pParentEntry; + ASSERT(pEntry != nil); + A2File* pFile = pEntry->GetA2File(); + ASSERT(pFile != nil); + DiskFS* pDiskFS = pFile->GetDiskFS(); + ASSERT(pDiskFS != nil); + + if (!pFile->IsDirectory()) { + ASSERT(false); + return false; + } + + DIError dierr; + A2File* pNewFile = nil; + DiskFS::CreateParms parms; + CString pathName; + time_t now = time(nil); + + /* + * Create the full path. + */ + if (pFile->IsVolumeDirectory()) { + pathName = newName; + } else { + pathName = pParentEntry->GetPathName(); + pathName += pParentEntry->GetFssep(); + pathName += newName; + } + ASSERT(strchr(newName, pParentEntry->GetFssep()) == nil); + + /* using NufxLib constants; they match with ProDOS */ + memset(&parms, 0, sizeof(parms)); + parms.pathName = pathName; + parms.fssep = pParentEntry->GetFssep(); + parms.storageType = kNuStorageDirectory; + parms.fileType = 0x0f; // ProDOS DIR + parms.auxType = 0; + parms.access = kNuAccessUnlocked; + parms.createWhen = now; + parms.modWhen = now; + + dierr = pDiskFS->CreateFile(&parms, &pNewFile); + if (dierr != kDIErrNone) { + CString errMsg; + errMsg.Format("Unable to create subdirectory: %s.\n", + DiskImgLib::DIStrError(dierr)); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + return false; + } + + if (InternalReload(pMsgWnd) != 0) + return false; + + return true; +} + + +/* + * =========================================================================== + * DiskArchive -- delete selection + * =========================================================================== + */ + +/* + * Compare DiskEntry display names in descending order (Z-A). + */ +/*static*/ int +DiskArchive::CompareDisplayNamesDesc(const void* ventry1, const void* ventry2) +{ + const DiskEntry* pEntry1 = *((const DiskEntry**) ventry1); + const DiskEntry* pEntry2 = *((const DiskEntry**) ventry2); + + return strcasecmp(pEntry2->GetDisplayName(), pEntry1->GetDisplayName()); +} + +/* + * Delete the records listed in the selection set. + * + * The DiskFS DeleteFile() function will not delete a subdirectory unless + * it is empty. This complicates matters somewhat for us, because the + * selection set isn't in any particular order. We need to sort on the + * pathname and then delete bottom-up. + * + * CiderPress does work to ensure that, if a subdir is selected, everything + * in that subdir is also selected. So if we just delete everything in the + * right order, we should be okay. + */ +bool +DiskArchive::DeleteSelection(CWnd* pMsgWnd, SelectionSet* pSelSet) +{ + CString errMsg; + SelectionEntry* pSelEntry; + DiskEntry* pEntry; + DIError dierr; + bool retVal = false; + + SET_PROGRESS_BEGIN(); + + /* + * Start by copying the DiskEntry pointers out of the selection set and + * into an array. The selection set was created such that there is one + * entry in the set for each file. (The file viewer likes to have one + * entry for each thread.) + */ + int numEntries = pSelSet->GetNumEntries(); + ASSERT(numEntries > 0); + DiskEntry** entryArray = new DiskEntry*[numEntries]; + int idx = 0; + + pSelEntry = pSelSet->IterNext(); + while (pSelEntry != nil) { + pEntry = (DiskEntry*) pSelEntry->GetEntry(); + ASSERT(pEntry != nil); + + entryArray[idx++] = pEntry; + WMSG2("Added 0x%08lx '%s'\n", (long) entryArray[idx-1], + entryArray[idx-1]->GetDisplayName()); + + pSelEntry = pSelSet->IterNext(); + } + ASSERT(idx == numEntries); + + /* + * Sort the file array by descending filename. + */ + ::qsort(entryArray, numEntries, sizeof(DiskEntry*), CompareDisplayNamesDesc); + + /* + * Run through the sorted list, deleting each entry. + */ + for (idx = 0; idx < numEntries; idx++) { + A2File* pFile; + + pEntry = entryArray[idx]; + pFile = pEntry->GetA2File(); + + /* + * We shouldn't be here at all if the main volume were opened + * read-only. However, it's possible that the main is read-write + * and our sub-volumes are read-only (probably because we don't + * support write access to the filesystem). + */ + if (!pFile->GetDiskFS()->GetReadWriteSupported()) { + errMsg.Format("Unable to delete '%s' on '%s': operation not supported.", + pEntry->GetDisplayName(), pFile->GetDiskFS()->GetVolumeName()); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + goto bail; + } + + WMSG2(" Deleting '%s' from '%s'\n", pEntry->GetPathName(), + pFile->GetDiskFS()->GetVolumeName()); + SET_PROGRESS_UPDATE2(0, pEntry->GetPathName(), nil); + + /* + * Ask the DiskFS to delete the file. As soon as this completes, + * "pFile" is invalid and must not be dereferenced. + */ + dierr = pFile->GetDiskFS()->DeleteFile(pFile); + if (dierr != kDIErrNone) { + errMsg.Format("Unable to delete '%s' on '%s': %s.", + pEntry->GetDisplayName(), pFile->GetDiskFS()->GetVolumeName(), + DiskImgLib::DIStrError(dierr)); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + goto bail; + } + SET_PROGRESS_UPDATE(100); + + /* + * Be paranoid and zap the pointer, on the off chance somebody + * tries to redraw the content list from the deleted data. + * + * In practice we don't work this way -- the stuff that gets drawn + * on the screen comes out of GenericEntry, not A2File. If this + * changes we'll need to raise the "reload" flag here, before the + * reload, to prevent the ContentList from chasing a bad pointer. + */ + pEntry->SetA2File(nil); + } + + retVal = true; + +bail: + SET_PROGRESS_END(); + delete[] entryArray; + if (InternalReload(pMsgWnd) != 0) + retVal = false; + + return retVal; +} + +/* + * =========================================================================== + * DiskArchive -- rename files + * =========================================================================== + */ + + /* + * Rename a set of files, one at a time. + * + * If we rename a subdirectory, it could affect the next thing we try to + * rename (because we show the full path). We have to reload our file + * list from the DiskFS after each renamed subdir. The trouble is that + * this invalidates the data displayed in the ContentList, and we won't + * redraw the screen correctly. We can work around the problem by getting + * the pathname directly from the DiskFS instead of from DiskEntry, though + * it's not immediately obvious which is less confusing. + */ +bool +DiskArchive::RenameSelection(CWnd* pMsgWnd, SelectionSet* pSelSet) +{ + CString errMsg; + bool retVal = false; + + WMSG1("Renaming %d entries\n", pSelSet->GetNumEntries()); + + /* + * For each item in the selection set, bring up the "rename" dialog, + * and ask the GenericEntry to process it. + * + * If they hit "cancel" or there's an error, we still flush the + * previous changes. This is so that we don't have to create the + * same sort of deferred-write feature when renaming things in other + * sorts of archives (e.g. disk archives). + */ + SelectionEntry* pSelEntry = pSelSet->IterNext(); + while (pSelEntry != nil) { + RenameEntryDialog renameDlg(pMsgWnd); + DiskEntry* pEntry = (DiskEntry*) pSelEntry->GetEntry(); + + WMSG1(" Renaming '%s'\n", pEntry->GetPathName()); + if (!SetRenameFields(pMsgWnd, pEntry, &renameDlg)) + break; + + int result; + if (pEntry->GetA2File()->IsVolumeDirectory()) + result = IDIGNORE; // don't allow rename of volume dir + else + result = renameDlg.DoModal(); + if (result == IDOK) { + DIError dierr; + DiskFS* pDiskFS; + A2File* pFile; + + pFile = pEntry->GetA2File(); + pDiskFS = pFile->GetDiskFS(); + dierr = pDiskFS->RenameFile(pFile, renameDlg.fNewName); + if (dierr != kDIErrNone) { + errMsg.Format("Unable to rename '%s' to '%s': %s.", + pEntry->GetPathName(), renameDlg.fNewName, + DiskImgLib::DIStrError(dierr)); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + goto bail; + } + WMSG2("Rename of '%s' to '%s' succeeded\n", + pEntry->GetDisplayName(), renameDlg.fNewName); + } else if (result == IDCANCEL) { + WMSG0("Canceling out of remaining renames\n"); + break; + } else { + /* 3rd possibility is IDIGNORE, i.e. skip this entry */ + WMSG1("Skipping rename of '%s'\n", pEntry->GetDisplayName()); + } + + pSelEntry = pSelSet->IterNext(); + } + + /* reload GenericArchive from disk image */ + if (InternalReload(pMsgWnd) == kNuErrNone) + retVal = true; + +bail: + return retVal; +} + +/* + * Set up a RenameEntryDialog for the entry in "*pEntry". + * + * Returns "true" on success, "false" on failure. + */ +bool +DiskArchive::SetRenameFields(CWnd* pMsgWnd, DiskEntry* pEntry, + RenameEntryDialog* pDialog) +{ + DiskFS* pDiskFS; + + ASSERT(pEntry != nil); + + /* + * Figure out if we're allowed to change the entire path. (This is + * doing it the hard way, but what the hell.) + */ + long cap = GetCapability(GenericArchive::kCapCanRenameFullPath); + bool renameFullPath = (cap != 0); + + // a bit round-about, but it works + pDiskFS = pEntry->GetA2File()->GetDiskFS(); + + /* + * Make sure rename is allowed. It's nice to do these *before* putting + * up the rename dialog, so that the user doesn't do a bunch of typing + * before being told that it's pointless. + */ + if (!pDiskFS->GetReadWriteSupported()) { + CString errMsg; + errMsg.Format("Unable to rename '%s': operation not supported.", + pEntry->GetPathName()); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + return false; + } + if (pDiskFS->GetFSDamaged()) { + CString errMsg; + errMsg.Format("Unable to rename '%s': the disk it's on appears to be damaged.", + pEntry->GetPathName()); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + return false; + } + + pDialog->SetCanRenameFullPath(renameFullPath); + pDialog->fOldName = pEntry->GetPathName(); + pDialog->fFssep = pEntry->GetFssep(); + pDialog->fpArchive = this; + pDialog->fpEntry = pEntry; + + return true; +} + +/* + * Verify that the a name is suitable. Called by RenameEntryDialog and + * CreateSubdirDialog. + * + * Tests for context-specific syntax and checks for duplicates. + * + * Returns an empty string on success, or an error message on failure. + */ +CString +DiskArchive::TestPathName(const GenericEntry* pGenericEntry, + const CString& basePath, const CString& newName, char newFssep) const +{ + const DiskEntry* pEntry = (DiskEntry*) pGenericEntry; + DiskImg::FSFormat format; + CString pathName, errMsg; + DiskFS* pDiskFS; + + if (basePath.IsEmpty()) { + pathName = newName; + } else { + pathName = basePath; + pathName += newFssep; + pathName += newName; + } + + pDiskFS = pEntry->GetA2File()->GetDiskFS(); + format = pDiskFS->GetDiskImg()->GetFSFormat(); + + /* look for an existing file, but don't compare against self */ + A2File* existingFile; + existingFile = pDiskFS->GetFileByName(pathName); + if (existingFile != nil && existingFile != pEntry->GetA2File()) { + errMsg = "A file with that name already exists."; + goto bail; + } + + switch (format) { + case DiskImg::kFormatProDOS: + if (!DiskFSProDOS::IsValidFileName(newName)) + errMsg.LoadString(IDS_VALID_FILENAME_PRODOS); + break; + case DiskImg::kFormatDOS33: + case DiskImg::kFormatDOS32: + if (!DiskFSDOS33::IsValidFileName(newName)) + errMsg.LoadString(IDS_VALID_FILENAME_DOS); + break; + case DiskImg::kFormatPascal: + if (!DiskFSPascal::IsValidFileName(newName)) + errMsg.LoadString(IDS_VALID_FILENAME_PASCAL); + break; + case DiskImg::kFormatMacHFS: + if (!DiskFSHFS::IsValidFileName(newName)) + errMsg.LoadString(IDS_VALID_FILENAME_HFS); + break; + default: + errMsg = "Not supported by TestPathName!"; + } + +bail: + return errMsg; +} + + +/* + * =========================================================================== + * DiskArchive -- rename a volume + * =========================================================================== + */ + +/* + * Ask a DiskFS to change its volume name. + * + * Returns "true" on success, "false" on failure. + */ +bool +DiskArchive::RenameVolume(CWnd* pMsgWnd, DiskFS* pDiskFS, + const char* newName) +{ + DIError dierr; + CString errMsg; + bool retVal = true; + + dierr = pDiskFS->RenameVolume(newName); + if (dierr != kDIErrNone) { + errMsg.Format("Unable to rename volume: %s.\n", + DiskImgLib::DIStrError(dierr)); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + retVal = false; + /* fall through to reload anyway */ + } + + /* reload GenericArchive from disk image */ + if (InternalReload(pMsgWnd) != 0) + retVal = false; + + return retVal; +} + +/* + * Test a volume name for validity. + */ +CString +DiskArchive::TestVolumeName(const DiskFS* pDiskFS, + const char* newName) const +{ + DiskImg::FSFormat format; + CString errMsg; + + ASSERT(pDiskFS != nil); + ASSERT(newName != nil); + + format = pDiskFS->GetDiskImg()->GetFSFormat(); + + switch (format) { + case DiskImg::kFormatProDOS: + if (!DiskFSProDOS::IsValidVolumeName(newName)) + errMsg.LoadString(IDS_VALID_VOLNAME_PRODOS); + break; + case DiskImg::kFormatDOS33: + case DiskImg::kFormatDOS32: + if (!DiskFSDOS33::IsValidVolumeName(newName)) + errMsg.LoadString(IDS_VALID_VOLNAME_DOS); + break; + case DiskImg::kFormatPascal: + if (!DiskFSPascal::IsValidVolumeName(newName)) + errMsg.LoadString(IDS_VALID_VOLNAME_PASCAL); + break; + case DiskImg::kFormatMacHFS: + if (!DiskFSHFS::IsValidVolumeName(newName)) + errMsg.LoadString(IDS_VALID_VOLNAME_HFS); + break; + default: + errMsg = "Not supported by TestVolumeName!"; + } + + return errMsg; +} + + +/* + * =========================================================================== + * DiskArchive -- set file properties + * =========================================================================== + */ + +/* + * Set the properties of "pEntry" to what's in "pProps". + * + * [currently only supports file type, aux type, and access flags] + * + * Technically we should reload the GenericArchive from the NufxArchive, + * but the set of changes is pretty small, so we just make them here. + */ +bool +DiskArchive::SetProps(CWnd* pMsgWnd, GenericEntry* pGenericEntry, + const FileProps* pProps) +{ + DIError dierr; + DiskEntry* pEntry = (DiskEntry*) pGenericEntry; + A2File* pFile = pEntry->GetA2File(); + + dierr = pFile->GetDiskFS()->SetFileInfo(pFile, pProps->fileType, + pProps->auxType, pProps->access); + if (dierr != kDIErrNone) { + CString errMsg; + errMsg.Format("Unable to set file info: %s.\n", + DiskImgLib::DIStrError(dierr)); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + return false; + } + + /* do this in lieu of reloading GenericArchive */ + pEntry->SetFileType(pFile->GetFileType()); + pEntry->SetAuxType(pFile->GetAuxType()); + pEntry->SetAccess(pFile->GetAccess()); + + /* DOS 3.2/3.3 may change these as well */ + DiskImg::FSFormat fsFormat; + fsFormat = pFile->GetDiskFS()->GetDiskImg()->GetFSFormat(); + if (fsFormat == DiskImg::kFormatDOS32 || fsFormat == DiskImg::kFormatDOS33) { + WMSG0(" (reloading additional fields after DOS SFI)\n"); + pEntry->SetDataForkLen(pFile->GetDataLength()); + pEntry->SetCompressedLen(pFile->GetDataSparseLength()); + pEntry->SetSuspicious(pFile->GetQuality() == A2File::kQualitySuspicious); + } + + /* clear the dirty flag in trivial cases */ + (void) fpPrimaryDiskFS->Flush(DiskImg::kFlushFastOnly); + + return true; +} + + +/* + * =========================================================================== + * DiskArchive -- transfer files to another archive + * =========================================================================== + */ + +/* + * Transfer the selected files out of this archive and into another. + * + * In this case, it's files on a disk (with unspecified filesystem) to a NuFX + * archive. We get the open archive pointer and some options from "pXferOpts". + * + * The selection set was created with the "any" selection criteria, which + * means there's only one entry for each file regardless of whether it's + * forked or not. + */ +GenericArchive::XferStatus +DiskArchive::XferSelection(CWnd* pMsgWnd, SelectionSet* pSelSet, + ActionProgressDialog* pActionProgress, const XferFileOptions* pXferOpts) +{ + WMSG0("DiskArchive XferSelection!\n"); + unsigned char* dataBuf = nil; + unsigned char* rsrcBuf = nil; + FileDetails fileDetails; + CString errMsg, extractErrMsg, cmpStr; + CString fixedPathName; + XferStatus retval = kXferFailed; + + pXferOpts->fTarget->XferPrepare(pXferOpts); + + SelectionEntry* pSelEntry = pSelSet->IterNext(); + for ( ; pSelEntry != nil; pSelEntry = pSelSet->IterNext()) { + long dataLen=-1, rsrcLen=-1; + DiskEntry* pEntry = (DiskEntry*) pSelEntry->GetEntry(); + int typeOverride = -1; + int result; + + ASSERT(dataBuf == nil); + ASSERT(rsrcBuf == nil); + + if (pEntry->GetDamaged()) { + WMSG1(" XFER skipping damaged entry '%s'\n", + pEntry->GetDisplayName()); + continue; + } + + /* + * Do a quick de-colonizing pass for non-ProDOS volumes, then prepend + * the subvolume name (if any). + */ + fixedPathName = pEntry->GetPathName(); + if (fixedPathName.IsEmpty()) + fixedPathName = _T("(no filename)"); + if (pEntry->GetFSFormat() != DiskImg::kFormatProDOS) + fixedPathName.Replace(PathProposal::kDefaultStoredFssep, '.'); + if (pEntry->GetSubVolName() != nil) { + CString tmpStr; + tmpStr = pEntry->GetSubVolName(); + tmpStr += (char)PathProposal::kDefaultStoredFssep; + tmpStr += fixedPathName; + fixedPathName = tmpStr; + } + + if (pEntry->GetRecordKind() == GenericEntry::kRecordKindVolumeDir) { + /* this is the volume dir */ + WMSG1(" XFER not transferring volume dir '%s'\n", + fixedPathName); + continue; + } else if (pEntry->GetRecordKind() == GenericEntry::kRecordKindDirectory) { + if (pXferOpts->fPreserveEmptyFolders) { + /* if this is an empty directory, create a fake entry */ + cmpStr = fixedPathName; + cmpStr += (char)PathProposal::kDefaultStoredFssep; + + if (pSelSet->CountMatchingPrefix(cmpStr) == 0) { + WMSG1("FOUND empty dir '%s'\n", fixedPathName); + cmpStr += kEmptyFolderMarker; + dataBuf = new unsigned char[1]; + dataLen = 0; + fileDetails.entryKind = FileDetails::kFileKindDataFork; + fileDetails.storageName = cmpStr; + fileDetails.fileType = 0; // NON + fileDetails.access = + pEntry->GetAccess() | GenericEntry::kAccessInvisible; + goto have_stuff2; + } else { + WMSG1("NOT empty dir '%s'\n", fixedPathName); + } + } + + WMSG1(" XFER not transferring directory '%s'\n", + fixedPathName); + continue; + } + + WMSG3(" Xfer '%s' (data=%d rsrc=%d)\n", + fixedPathName, pEntry->GetHasDataFork(), + pEntry->GetHasRsrcFork()); + + dataBuf = nil; + dataLen = 0; + result = pEntry->ExtractThreadToBuffer(GenericEntry::kDataThread, + (char**) &dataBuf, &dataLen, &extractErrMsg); + if (result == IDCANCEL) { + WMSG0("Cancelled during data extract!\n"); + goto bail; /* abort anything that was pending */ + } else if (result != IDOK) { + errMsg.Format("Failed while extracting '%s': %s.", + fixedPathName, extractErrMsg); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + goto bail; + } + ASSERT(dataBuf != nil); + ASSERT(dataLen >= 0); + +#if 0 + if (pXferOpts->fConvDOSText && + DiskImg::UsesDOSFileStructure(pEntry->GetFSFormat()) && + pEntry->GetFileType() == kFileTypeTXT) + { + /* don't need to convert EOL, so just strip in place */ + long len; + unsigned char* ucp; + + WMSG1(" Converting DOS text in '%s'\n", fixedPathName); + for (ucp = dataBuf, len = dataLen; len > 0; len--, ucp++) + *ucp = *ucp & 0x7f; + } +#endif + +#if 0 // annoying to invoke PTX reformatter from here... ReformatHolder, etc. + if (pXferOpts->fConvPascalText && + pEntry->GetFSFormat() == DiskImg::kFormatPascal && + pEntry->GetFileType() == kFileTypePTX) + { + WMSG1("WOULD CONVERT ptx '%s'\n", fixedPathName); + } +#endif + + if (pEntry->GetHasRsrcFork()) { + rsrcBuf = nil; + rsrcLen = 0; + result = pEntry->ExtractThreadToBuffer(GenericEntry::kRsrcThread, + (char**) &rsrcBuf, &rsrcLen, &extractErrMsg); + if (result == IDCANCEL) { + WMSG0("Cancelled during rsrc extract!\n"); + goto bail; /* abort anything that was pending */ + } else if (result != IDOK) { + errMsg.Format("Failed while extracting '%s': %s.", + fixedPathName, extractErrMsg); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + goto bail; + } + } else { + ASSERT(rsrcBuf == nil); + } + + if (pEntry->GetHasDataFork() && pEntry->GetHasRsrcFork()) + fileDetails.entryKind = FileDetails::kFileKindBothForks; + else if (pEntry->GetHasDataFork()) + fileDetails.entryKind = FileDetails::kFileKindDataFork; + else if (pEntry->GetHasRsrcFork()) + fileDetails.entryKind = FileDetails::kFileKindRsrcFork; + else { + ASSERT(false); + fileDetails.entryKind = FileDetails::kFileKindUnknown; + } + + /* + * Set up the FileDetails. + */ + fileDetails.storageName = fixedPathName; + fileDetails.fileType = pEntry->GetFileType(); + fileDetails.access = pEntry->GetAccess(); +have_stuff2: + fileDetails.fileSysFmt = pEntry->GetSourceFS(); + fileDetails.fileSysInfo = PathProposal::kDefaultStoredFssep; + fileDetails.extraType = pEntry->GetAuxType(); + fileDetails.storageType = kNuStorageUnknown; /* let NufxLib deal */ + + time_t when; + when = time(nil); + UNIXTimeToDateTime(&when, &fileDetails.archiveWhen); + when = pEntry->GetModWhen(); + UNIXTimeToDateTime(&when, &fileDetails.modWhen); + when = pEntry->GetCreateWhen(); + UNIXTimeToDateTime(&when, &fileDetails.createWhen); + + pActionProgress->SetArcName(fileDetails.storageName); + if (pActionProgress->SetProgress(0) == IDCANCEL) { + retval = kXferCancelled; + goto bail; + } + + errMsg = pXferOpts->fTarget->XferFile(&fileDetails, &dataBuf, dataLen, + &rsrcBuf, rsrcLen); + if (!errMsg.IsEmpty()) { + WMSG0("XferFile failed!\n"); + errMsg.Format("Failed while transferring '%s': %s.", + pEntry->GetDisplayName(), (const char*) errMsg); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + goto bail; + } + ASSERT(dataBuf == nil); + ASSERT(rsrcBuf == nil); + + if (pActionProgress->SetProgress(100) == IDCANCEL) { + retval = kXferCancelled; + goto bail; + } + } + + //MainWindow* pMainWin; + //pMainWin = (MainWindow*)::AfxGetMainWnd(); + //pMainWin->EventPause(1000); + + retval = kXferOK; + +bail: + if (retval != kXferOK) + pXferOpts->fTarget->XferAbort(pMsgWnd); + else + pXferOpts->fTarget->XferFinish(pMsgWnd); + delete[] dataBuf; + delete[] rsrcBuf; + return retval; +} + +/* + * Prepare for file transfers. + */ +void +DiskArchive::XferPrepare(const XferFileOptions* pXferOpts) +{ + WMSG0("DiskArchive::XferPrepare\n"); + + //fpPrimaryDiskFS->SetParameter(DiskFS::kParmProDOS_AllowLowerCase, + // pXferOpts->fAllowLowerCase); + //fpPrimaryDiskFS->SetParameter(DiskFS::kParmProDOS_AllocSparse, + // pXferOpts->fUseSparseBlocks); + fpPrimaryDiskFS->SetParameter(DiskFS::kParm_CreateUnique, true); + + //fXferStoragePrefix = pXferOpts->fStoragePrefix; + fpXferTargetFS = pXferOpts->fpTargetFS; +} + +/* + * Transfer a file to the disk image. Called from NufxArchive's XferSelection + * and clipboard "paste". + * + * "dataLen" and "rsrcLen" will be -1 if the corresponding fork doesn't + * exist. + * + * Returns 0 on success, nonzero on failure. + * + * On success, *pDataBuf and *pRsrcBuf are freed and set to nil. (It's + * necessary for the interface to work this way because the NufxArchive + * version just tucks the pointers into NufxLib structures.) + */ +CString +DiskArchive::XferFile(FileDetails* pDetails, unsigned char** pDataBuf, + long dataLen, unsigned char** pRsrcBuf, long rsrcLen) +{ + //const int kFileTypeTXT = 0x04; + DiskFS::CreateParms createParms; + DiskFS* pDiskFS; + CString errMsg; + DIError dierr = kDIErrNone; + + WMSG3(" XFER: transfer '%s' (dataLen=%ld rsrcLen=%ld)\n", + pDetails->storageName, dataLen, rsrcLen); + + ASSERT(pDataBuf != nil); + ASSERT(pRsrcBuf != nil); + + /* fill out CreateParms from FileDetails */ + ConvertFDToCP(pDetails, &createParms); + + if (fpXferTargetFS == nil) + pDiskFS = fpPrimaryDiskFS; + else + pDiskFS = fpXferTargetFS; + + /* + * Strip the high ASCII from DOS and RDOS text files, unless we're adding + * them to a DOS disk. Likewise, if we're adding non-DOS text files to + * a DOS disk, we need to add the high bit. + * + * DOS converts both TXT and SRC to 'T', so we have to handle both here. + * Ideally we'd just ask DOS, "do you think this is a text file?", but it's + * not worth adding a new interface just for that. + */ + bool srcIsDOS, dstIsDOS; + srcIsDOS = DiskImg::UsesDOSFileStructure(pDetails->fileSysFmt); + dstIsDOS = DiskImg::UsesDOSFileStructure(pDiskFS->GetDiskImg()->GetFSFormat()); + if (dataLen > 0 && + (pDetails->fileType == kFileTypeTXT || pDetails->fileType == kFileTypeSRC)) + { + unsigned char* ucp = *pDataBuf; + long len = dataLen; + + if (srcIsDOS && !dstIsDOS) { + WMSG1(" Stripping high ASCII from '%s'\n", pDetails->storageName); + + while (len--) + *ucp++ &= 0x7f; + } else if (!srcIsDOS && dstIsDOS) { + WMSG1(" Adding high ASCII to '%s'\n", pDetails->storageName); + + while (len--) { + if (*ucp != '\0') + *ucp |= 0x80; + ucp++; + } + } else if (srcIsDOS && dstIsDOS) { + WMSG1(" --- not altering DOS-to-DOS text '%s'\n", + pDetails->storageName); + } else { + WMSG1(" --- non-DOS transfer '%s'\n", pDetails->storageName); + } + } + + /* add a file with one or two forks */ + if (createParms.storageType == kNuStorageDirectory) { + ASSERT(dataLen < 0 && rsrcLen < 0); + } else { + ASSERT(dataLen >= 0 || rsrcLen >= 0); // at least one fork + } + + /* if we still have something to write, write it */ + dierr = AddForksToDisk(pDiskFS, &createParms, *pDataBuf, dataLen, + *pRsrcBuf, rsrcLen); + if (dierr != kDIErrNone) { + errMsg.Format("%s", DiskImgLib::DIStrError(dierr)); + goto bail; + } + + /* clean up */ + delete[] *pDataBuf; + *pDataBuf = nil; + delete[] *pRsrcBuf; + *pRsrcBuf = nil; + +bail: + return errMsg; +} + + +/* + * Abort our progress. Not really possible, except by throwing the disk + * image away. + */ +void +DiskArchive::XferAbort(CWnd* pMsgWnd) +{ + WMSG0("DiskArchive::XferAbort\n"); + InternalReload(pMsgWnd); +} + +/* + * Transfer is finished. + */ +void +DiskArchive::XferFinish(CWnd* pMsgWnd) +{ + WMSG0("DiskArchive::XferFinish\n"); + InternalReload(pMsgWnd); +} diff --git a/app/DiskArchive.h b/app/DiskArchive.h new file mode 100644 index 0000000..f7fc866 --- /dev/null +++ b/app/DiskArchive.h @@ -0,0 +1,244 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Disk image "archive" support. + */ +#ifndef __DISK_ARCHIVE__ +#define __DISK_ARCHIVE__ + +#include "GenericArchive.h" +#include "../diskimg/DiskImg.h" + +class RenameEntryDialog; + + +/* + * One file in a disk image. + */ +class DiskEntry : public GenericEntry { +public: + DiskEntry(A2File* pFile) : fpFile(pFile) + {} + virtual ~DiskEntry(void) {} + + // retrieve thread data + virtual int ExtractThreadToBuffer(int which, char** ppText, long* pLength, + CString* pErrMsg) const; + virtual int ExtractThreadToFile(int which, FILE* outfp, ConvertEOL conv, + ConvertHighASCII convHA, CString* pErrMsg) const; + virtual long GetSelectionSerial(void) const { return -1; } // idea: T/S block number + + virtual bool GetFeatureFlag(Feature feature) const; + + // return the underlying FS format for this file + virtual DiskImg::FSFormat GetFSFormat(void) const { + ASSERT(fpFile != nil); + return fpFile->GetFSFormat(); + } + + A2File* GetA2File(void) const { return fpFile; } + void SetA2File(A2File* pFile) { fpFile = pFile; } + +private: + DIError CopyData(A2FileDescr* pOpenFile, FILE* outfp, ConvertEOL conv, + ConvertHighASCII convHA, CString* pMsg) const; + + A2File* fpFile; +}; + + +/* + * Disk image add-ons to GenericArchive. + */ +class DiskArchive : public GenericArchive { +public: + DiskArchive(void) : fpPrimaryDiskFS(nil), fIsReadOnly(false), + fpAddDataHead(nil), fpAddDataTail(nil) + {} + virtual ~DiskArchive(void) { (void) Close(); } + + /* pass this as the "options" value to the New() function */ + typedef struct { + DiskImgLib::DiskImg::FSFormat format; + DiskImgLib::DiskImg::SectorOrder sectorOrder; + } NewOptionsBase; + typedef union { + NewOptionsBase base; + + struct { + NewOptionsBase base; + long numBlocks; + } blank; + struct { + NewOptionsBase base; + const char* volName; + long numBlocks; + } prodos; + struct { + NewOptionsBase base; + const char* volName; + long numBlocks; + } pascalfs; // "pascal" is reserved token in MSVC++ + struct { + NewOptionsBase base; + const char* volName; + long numBlocks; + } hfs; + struct { + NewOptionsBase base; + int volumeNum; + long numTracks; + int numSectors; + bool allocDOSTracks; + } dos; + } NewOptions; + + // One-time initialization; returns an error string. + static CString AppInit(void); + // one-time cleanup at app shutdown time + static void AppCleanup(void); + + virtual OpenResult Open(const char* filename, bool readOnly, CString* pErrMsg); + virtual CString New(const char* filename, const void* options); + virtual CString Flush(void); + virtual CString Reload(void); + virtual bool IsReadOnly(void) const { return fIsReadOnly; }; + virtual bool IsModified(void) const; + virtual void GetDescription(CString* pStr) const; + virtual bool BulkAdd(ActionProgressDialog* pActionProgress, + const AddFilesDialog* pAddOpts); + virtual bool AddDisk(ActionProgressDialog* pActionProgress, + const AddFilesDialog* pAddOpts) + { ASSERT(false); return false; } + virtual bool CreateSubdir(CWnd* pMsgWnd, GenericEntry* pParentEntry, + const char* newName); + virtual bool TestSelection(CWnd* pMsgWnd, SelectionSet* pSelSet) + { ASSERT(false); return false; } + virtual bool DeleteSelection(CWnd* pMsgWnd, SelectionSet* pSelSet); + virtual bool RenameSelection(CWnd* pMsgWnd, SelectionSet* pSelSet); + virtual CString TestPathName(const GenericEntry* pGenericEntry, + const CString& basePath, const CString& newName, char newFssep) const; + virtual bool RenameVolume(CWnd* pMsgWnd, DiskFS* pDiskFS, + const char* newName); + virtual CString TestVolumeName(const DiskFS* pDiskFS, + const char* newName) const; + virtual bool RecompressSelection(CWnd* pMsgWnd, SelectionSet* pSelSet, + const RecompressOptionsDialog* pRecompOpts) + { ASSERT(false); return false; } + virtual bool GetComment(CWnd* pMsgWnd, const GenericEntry* pEntry, + CString* pStr) + { ASSERT(false); return false; } + virtual bool SetComment(CWnd* pMsgWnd, GenericEntry* pEntry, + const CString& str) + { ASSERT(false); return false; } + virtual bool DeleteComment(CWnd* pMsgWnd, GenericEntry* pEntry) + { ASSERT(false); return false; } + virtual bool SetProps(CWnd* pMsgWnd, GenericEntry* pEntry, + const FileProps* pProps); + virtual void PreferencesChanged(void); + virtual long GetCapability(Capability cap); + virtual XferStatus XferSelection(CWnd* pMsgWnd, SelectionSet* pSelSet, + ActionProgressDialog* pActionProgress, const XferFileOptions* pXferOpts); + + const DiskImg* GetDiskImg(void) const { return &fDiskImg; } + DiskFS* GetDiskFS(void) const { return fpPrimaryDiskFS; } + + /* internal function, used by DiskArchive and DiskEntry */ + static bool ProgressCallback(DiskImgLib::A2FileDescr* pFile, + DiskImgLib::di_off_t max, DiskImgLib::di_off_t current, void* state); + +private: + virtual CString Close(void); + virtual void XferPrepare(const XferFileOptions* pXferOpts); + virtual CString XferFile(FileDetails* pDetails, unsigned char** pDataBuf, + long dataLen, unsigned char** pRsrcBuf, long rsrcLen); + virtual void XferAbort(CWnd* pMsgWnd); + virtual void XferFinish(CWnd* pMsgWnd); + + /* internal function, used during initial scan of volume */ + static bool ScanProgressCallback(void* cookie, const char* str, + int count); + + + /* + * Internal class used to keep track of files we're adding. + */ + class FileAddData { + public: + FileAddData(const FileDetails* pDetails, const char* fsNormalPath) { + fDetails = *pDetails; + + fFSNormalPath = fsNormalPath; + fpOtherFork = nil; + fpNext = nil; + } + virtual ~FileAddData(void) {} + + FileAddData* GetNext(void) const { return fpNext; } + void SetNext(FileAddData* pNext) { fpNext = pNext; } + FileAddData* GetOtherFork(void) const { return fpOtherFork; } + void SetOtherFork(FileAddData* pData) { fpOtherFork = pData; } + + const FileDetails* GetDetails(void) const { return &fDetails; } + const char* GetFSNormalPath(void) const { return fFSNormalPath; } + + private: + // Three filenames stored here: + // fDetails.origName -- the name of the Windows file + // fDetails.storageName -- the normalized Windows name + // fFSNormalPath -- the FS-normalized version of "storageName" + FileDetails fDetails; + CString fFSNormalPath; + + FileAddData* fpOtherFork; + FileAddData* fpNext; + }; + + virtual ArchiveKind GetArchiveKind(void) { return kArchiveDiskImage; } + virtual NuError DoAddFile(const AddFilesDialog* pAddOpts, + FileDetails* pDetails); + int InternalReload(CWnd* pMsgWnd); + + static int CompareDisplayNamesDesc(const void* ventry1, const void* ventry2); + + int LoadContents(void); + int LoadDiskFSContents(DiskFS* pDiskFS, const char* volName); + void DowncaseSubstring(CString* pStr, int startPos, int endPos, + bool prevWasSpace); + static void DebugMsgHandler(const char* file, int line, const char* msg); + + NuResult HandleReplaceExisting(const A2File* pExisting, + FileDetails* pDetails); + CString ProcessFileAddData(DiskFS* pDiskFS, int addOptsConvEOL); + CString LoadFile(const char* pathName, unsigned char** pBuf, long* pLen, + GenericEntry::ConvertEOL conv, GenericEntry::ConvertHighASCII convHA) const; + DIError AddForksToDisk(DiskFS* pDiskFS, const DiskFS::CreateParms* pParms, + const unsigned char* dataBuf, long dataLen, + const unsigned char* rsrcBuf, long rsrcLen) const; + void AddToAddDataList(FileAddData* pData); + void FreeAddDataList(void); + void ConvertFDToCP(const FileDetails* pDetails, + DiskFS::CreateParms* pCreateParms); + + bool SetRenameFields(CWnd* pMsgWnd, DiskEntry* pEntry, + RenameEntryDialog* pDialog); + + DiskImg fDiskImg; // DiskImg object for entire disk + DiskFS* fpPrimaryDiskFS; // outermost DiskFS + bool fIsReadOnly; + + /* active state while adding files */ + FileAddData* fpAddDataHead; + FileAddData* fpAddDataTail; + bool fOverwriteExisting; + bool fOverwriteNoAsk; + + /* state during xfer */ + //CString fXferStoragePrefix; + DiskFS* fpXferTargetFS; +}; + +#endif /*__DISK_ARCHIVE__*/ \ No newline at end of file diff --git a/app/DiskConvertDialog.cpp b/app/DiskConvertDialog.cpp new file mode 100644 index 0000000..9609880 --- /dev/null +++ b/app/DiskConvertDialog.cpp @@ -0,0 +1,307 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Support for disk image conversion dialog + */ +#include "StdAfx.h" +#include "DiskConvertDialog.h" +#include "HelpTopics.h" + +using namespace DiskImgLib; + +BEGIN_MESSAGE_MAP(DiskConvertDialog, CDialog) + ON_CONTROL_RANGE(BN_CLICKED, IDC_DISKCONV_DOS, IDC_DISKCONV_DDD, OnChangeRadio) + ON_WM_HELPINFO() + ON_COMMAND(IDHELP, OnHelp) +END_MESSAGE_MAP() + + +/* + * Initialize the set of available options based on the source image. + */ +void +DiskConvertDialog::Init(const DiskImg* pDiskImg) +{ + ASSERT(pDiskImg != nil); + const int kMagicNibbles = -1234; + bool hasBlocks = pDiskImg->GetHasBlocks(); + bool hasSectors = pDiskImg->GetHasSectors(); + bool hasNibbles = pDiskImg->GetHasNibbles(); + long diskSizeInSectors; + + ASSERT(fSizeInBlocks == -1); + if (hasBlocks) { + diskSizeInSectors = pDiskImg->GetNumBlocks() * 2; + fSizeInBlocks = diskSizeInSectors / 2; + } else if (hasSectors) { + diskSizeInSectors = + pDiskImg->GetNumTracks() * pDiskImg->GetNumSectPerTrack(); + if (pDiskImg->GetNumSectPerTrack() != 13) + fSizeInBlocks = diskSizeInSectors / 2; + } else { + ASSERT(hasNibbles); + diskSizeInSectors = kMagicNibbles; + } + + if (diskSizeInSectors >= 8388608) { + /* no conversions for files 2GB and larger except .PO */ + fDiskDescription.Format(IDS_CDESC_BLOCKS, diskSizeInSectors/4); + fAllowUnadornedProDOS = true; + fConvertIdx = kConvProDOSRaw; + } else if (diskSizeInSectors == 35*16) { + /* 140K disk image */ + fDiskDescription.LoadString(IDS_CDESC_140K); + fAllowUnadornedDOS = true; + fAllowUnadornedProDOS = true; + fAllowProDOS2MG = true; + fAllowUnadornedNibble = true; + fAllowNuFX = true; + fAllowTrackStar = true; + fAllowSim2eHDV = true; + fAllowDDD = true; + if (hasNibbles) + fConvertIdx = kConvNibbleRaw; + else + fConvertIdx = kConvDOSRaw; + } else if (diskSizeInSectors == 40*16 && + (pDiskImg->GetFileFormat() == DiskImg::kFileFormatTrackStar || + pDiskImg->GetFileFormat() == DiskImg::kFileFormatFDI)) + { + /* 40-track TrackStar or FDI image; allow conversion to 35-track formats */ + fDiskDescription.LoadString(IDS_CDESC_40TRACK); + ASSERT(pDiskImg->GetHasBlocks()); + fAllowUnadornedDOS = true; + fAllowUnadornedProDOS = true; + fAllowProDOS2MG = true; + fAllowUnadornedNibble = true; + fAllowNuFX = true; + fAllowSim2eHDV = true; + fAllowDDD = true; + fAllowTrackStar = true; + fConvertIdx = kConvDOSRaw; + } else if (diskSizeInSectors == 35*13) { + /* 13-sector 5.25" floppy */ + fDiskDescription.LoadString(IDS_CDEC_140K_13); + fAllowUnadornedNibble = true; + fAllowD13 = true; + fConvertIdx = kConvNibbleRaw; + } else if (diskSizeInSectors == kMagicNibbles) { + /* blob of nibbles with no recognizable format; allow self-convert */ + fDiskDescription.LoadString(IDS_CDEC_RAWNIB); + if (pDiskImg->GetPhysicalFormat() == DiskImg::kPhysicalFormatNib525_6656) + { + fAllowUnadornedNibble = true; + fConvertIdx = kConvNibbleRaw; + } else if (pDiskImg->GetPhysicalFormat() == DiskImg::kPhysicalFormatNib525_Var) + { + fAllowTrackStar = true; + fConvertIdx = kConvTrackStar; + } else if (pDiskImg->GetPhysicalFormat() == DiskImg::kPhysicalFormatNib525_6384) + { + /* don't currently allow .nb2 output */ + WMSG0(" GLITCH: we don't allow self-convert of .nb2 files\n"); + ASSERT(false); + } else { + /* this should be impossible */ + ASSERT(false); + } + } else if (diskSizeInSectors == 3200) { + /* 800K disk image */ + fDiskDescription.LoadString(IDS_CDESC_800K); + fAllowUnadornedDOS = true; + fAllowUnadornedProDOS = true; + fAllowProDOS2MG = true; + fAllowDiskCopy42 = true; + fAllowNuFX = true; + fAllowSim2eHDV = true; + fConvertIdx = kConvProDOS2MG; + } else { + /* odd-sized disk image; could allow DOS if hasSectors */ + fDiskDescription.Format(IDS_CDESC_BLOCKS, diskSizeInSectors/4); + fAllowUnadornedProDOS = true; + fAllowProDOS2MG = true; + fAllowNuFX = true; + fAllowSim2eHDV = true; + fConvertIdx = kConvProDOS2MG; + } +} + +/* + * Initialize options for a bulk transfer. + */ +void +DiskConvertDialog::Init(int fileCount) +{ + /* allow everything */ + fAllowUnadornedDOS = fAllowUnadornedProDOS = fAllowProDOS2MG = + fAllowUnadornedNibble = fAllowD13 = fAllowDiskCopy42 = + fAllowNuFX = fAllowTrackStar = fAllowSim2eHDV = fAllowDDD = true; + fConvertIdx = kConvDOSRaw; // default choice == first in list + fBulkFileCount = fileCount; + fDiskDescription.Format("%d images selected", fBulkFileCount); +} + + +/* + * Disable unavailable options. + */ +BOOL +DiskConvertDialog::OnInitDialog(void) +{ + CWnd* pWnd; + + ASSERT(fConvertIdx != -1); // must call Init before DoModal + + if (!fAllowUnadornedDOS) { + pWnd = GetDlgItem(IDC_DISKCONV_DOS); + pWnd->EnableWindow(FALSE); + pWnd = GetDlgItem(IDC_DISKCONV_DOS2MG); + pWnd->EnableWindow(FALSE); + } + if (!fAllowUnadornedProDOS) { + pWnd = GetDlgItem(IDC_DISKCONV_PRODOS); + pWnd->EnableWindow(FALSE); + } + if (!fAllowProDOS2MG) { + pWnd = GetDlgItem(IDC_DISKCONV_PRODOS2MG); + pWnd->EnableWindow(FALSE); + } + if (!fAllowUnadornedNibble) { + pWnd = GetDlgItem(IDC_DISKCONV_NIB); + pWnd->EnableWindow(FALSE); + pWnd = GetDlgItem(IDC_DISKCONV_NIB2MG); + pWnd->EnableWindow(FALSE); + } + if (!fAllowD13) { + pWnd = GetDlgItem(IDC_DISKCONV_D13); + pWnd->EnableWindow(FALSE); + } + if (!fAllowDiskCopy42) { + pWnd = GetDlgItem(IDC_DISKCONV_DC42); + pWnd->EnableWindow(FALSE); + } + if (!fAllowNuFX) { + pWnd = GetDlgItem(IDC_DISKCONV_SDK); + pWnd->EnableWindow(FALSE); + } + if (!fAllowTrackStar) { + pWnd = GetDlgItem(IDC_DISKCONV_TRACKSTAR); + pWnd->EnableWindow(FALSE); + } + if (!fAllowSim2eHDV) { + pWnd = GetDlgItem(IDC_DISKCONV_HDV); + pWnd->EnableWindow(FALSE); + } + if (!fAllowDDD) { + pWnd = GetDlgItem(IDC_DISKCONV_DDD); + pWnd->EnableWindow(FALSE); + } + + if (fBulkFileCount < 0) { + pWnd = GetDlgItem(IDC_IMAGE_TYPE); + pWnd->SetWindowText(fDiskDescription); + } else { + CRect rect; + int right; + pWnd = GetDlgItem(IDC_IMAGE_TYPE); + pWnd->GetWindowRect(&rect); + ScreenToClient(&rect); + right = rect.right; + pWnd->DestroyWindow(); + + pWnd = GetDlgItem(IDC_IMAGE_SIZE_TEXT); + pWnd->GetWindowRect(&rect); + ScreenToClient(&rect); + rect.right = right; + pWnd->MoveWindow(&rect); + pWnd->SetWindowText(fDiskDescription); + } + + OnChangeRadio(0); // set the gzip box + + CDialog::OnInitDialog(); + + return TRUE; +} + +/* + * Convert options in and out. + */ +void +DiskConvertDialog::DoDataExchange(CDataExchange* pDX) +{ + DDX_Check(pDX, IDC_DISKCONV_GZIP, fAddGzip); + DDX_Radio(pDX, IDC_DISKCONV_DOS, fConvertIdx); + + if (pDX->m_bSaveAndValidate) { + switch (fConvertIdx) { + case kConvDOSRaw: fExtension = "do"; break; + case kConvDOS2MG: fExtension = "2mg"; break; + case kConvProDOSRaw: fExtension = "po"; break; + case kConvProDOS2MG: fExtension = "2mg"; break; + case kConvNibbleRaw: fExtension = "nib"; break; + case kConvNibble2MG: fExtension = "2mg"; break; + case kConvD13: fExtension = "d13"; break; + case kConvDiskCopy42: fExtension = "dsk"; break; + case kConvNuFX: fExtension = "sdk"; break; + case kConvTrackStar: fExtension = "app"; break; + case kConvSim2eHDV: fExtension = "hdv"; break; + case kConvDDD: fExtension = "ddd"; break; + default: + fExtension = "???"; + ASSERT(false); + break; + } + + if (fAddGzip && fConvertIdx != kConvNuFX) + fExtension += ".gz"; + + WMSG1(" DCD recommending extension '%s'\n", (LPCTSTR) fExtension); + } +} + +/* + * If the radio button selection changes, we may need to disable the gzip + * checkbox to show that NuFX can't be combined with gzip. + * + * If the underlying disk is over 32MB, disable gzip, because we won't be + * able to open the disk we create. + */ +void +DiskConvertDialog::OnChangeRadio(UINT nID) +{ + CWnd* pGzip = GetDlgItem(IDC_DISKCONV_GZIP); + ASSERT(pGzip != nil); + CButton* pNuFX = (CButton*)GetDlgItem(IDC_DISKCONV_SDK); + ASSERT(pNuFX != nil); + + if (fSizeInBlocks > DiskImgLib::kGzipMax / 512) + pGzip->EnableWindow(FALSE); + else + pGzip->EnableWindow(pNuFX->GetCheck() == BST_UNCHECKED); +} + +/* + * Context help request (question mark button). + */ +BOOL +DiskConvertDialog::OnHelpInfo(HELPINFO* lpHelpInfo) +{ + WinHelp((DWORD) lpHelpInfo->iCtrlId, HELP_CONTEXTPOPUP); + return TRUE; // yes, we handled it +} + +/* + * User pressed the "Help" button. + */ +void +DiskConvertDialog::OnHelp(void) +{ + if (fBulkFileCount < 0) + WinHelp(HELP_TOPIC_DISK_CONV, HELP_CONTEXT); + else + WinHelp(HELP_TOPIC_BULK_DISK_CONV, HELP_CONTEXT); +} diff --git a/app/DiskConvertDialog.h b/app/DiskConvertDialog.h new file mode 100644 index 0000000..34a4f59 --- /dev/null +++ b/app/DiskConvertDialog.h @@ -0,0 +1,85 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Let the user choose how they want to convert a disk image. + */ +#ifndef __DISKCONVERTDIALOG__ +#define __DISKCONVERTDIALOG__ + +#include "resource.h" +#include "../diskimg/DiskImg.h" + +/* + * The set of conversions available depends on the format of the source image. + */ +class DiskConvertDialog : public CDialog { +public: + DiskConvertDialog(CWnd* pParentWnd) : CDialog(IDD_DISKCONV, pParentWnd) + { + fAllowUnadornedDOS = fAllowUnadornedProDOS = fAllowProDOS2MG = + fAllowUnadornedNibble = fAllowD13 = fAllowDiskCopy42 = + fAllowNuFX = fAllowTrackStar = fAllowSim2eHDV = fAllowDDD = false; + fAddGzip = FALSE; + fConvertIdx = -1; + fBulkFileCount = -1; + fSizeInBlocks = -1; + } + virtual ~DiskConvertDialog(void) {} + + void Init(const DiskImgLib::DiskImg* pDiskImg); // single file init + void Init(int fileCount); // bulk init + + /* must match up with dialog */ + enum { + kConvDOSRaw = 0, + kConvDOS2MG = 1, + kConvProDOSRaw = 2, + kConvProDOS2MG = 3, + kConvNibbleRaw = 4, + kConvNibble2MG = 5, + kConvD13 = 6, + kConvDiskCopy42 = 7, + kConvNuFX = 8, + kConvTrackStar = 9, + kConvSim2eHDV = 10, + kConvDDD = 11, + }; + int fConvertIdx; + + BOOL fAddGzip; + + // this is set to proper extension for the type chosen (e.g. "do") + CString fExtension; + +private: + BOOL OnInitDialog(void); + void DoDataExchange(CDataExchange* pDX); + + afx_msg void OnChangeRadio(UINT nID); + afx_msg void OnHelp(void); + + BOOL OnHelpInfo(HELPINFO* lpHelpInfo); + + CString fDiskDescription; + bool fAllowUnadornedDOS; + bool fAllowUnadornedProDOS; + bool fAllowProDOS2MG; + bool fAllowUnadornedNibble; + bool fAllowD13; + bool fAllowDiskCopy42; + bool fAllowNuFX; + bool fAllowTrackStar; + bool fAllowSim2eHDV; + bool fAllowDDD; + + int fBulkFileCount; + + long fSizeInBlocks; + + DECLARE_MESSAGE_MAP() +}; + +#endif /*__DISKCONVERTDIALOG__*/ \ No newline at end of file diff --git a/app/DiskEditDialog.cpp b/app/DiskEditDialog.cpp new file mode 100644 index 0000000..1bbcd60 --- /dev/null +++ b/app/DiskEditDialog.cpp @@ -0,0 +1,1641 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Disk editor implementation. + * + * Note to self: it should be possible to open an image in "nibble" mode, + * switch to one of the various "sector" modes, and maybe even try out a + * "block" mode for a while. Locking the editor into one particular mode + * makes for a cumbersome interface when dealing with certain kinds of disks. + * It should also be possible to configure the "custom" NibbleDescr and then + * re-analyze the disk, possibly transferring the customization over to + * the main file listing. (Perhaps the customization affects a global slot? + * Probably want more than one set of custom nibble values, with the config + * dialog accessible from main menu and from within disk editor.) + * + * A track copier would be neat too. + */ +#include "stdafx.h" +#include "SubVolumeDialog.h" +#include "DEFileDialog.h" +#include "HelpTopics.h" +#include "DiskEditDialog.h" + + +/* + * =========================================================================== + * DiskEditDialog (base class) + * =========================================================================== + */ + +BEGIN_MESSAGE_MAP(DiskEditDialog, CDialog) + ON_BN_CLICKED(IDC_DISKEDIT_DONE, OnDone) + ON_BN_CLICKED(IDC_DISKEDIT_HEX, OnHexMode) + ON_BN_CLICKED(IDC_DISKEDIT_DOREAD, OnDoRead) + ON_BN_CLICKED(IDC_DISKEDIT_DOWRITE, OnDoWrite) + ON_BN_CLICKED(IDC_DISKEDIT_PREV, OnReadPrev) + ON_BN_CLICKED(IDC_DISKEDIT_NEXT, OnReadNext) + ON_BN_CLICKED(IDC_DISKEDIT_SUBVOLUME, OnSubVolume) + ON_BN_CLICKED(IDC_DISKEDIT_OPENFILE, OnOpenFile) + ON_BN_CLICKED(IDHELP, OnHelp) + ON_CBN_SELCHANGE(IDC_DISKEDIT_NIBBLE_PARMS, OnNibbleParms) + ON_WM_HELPINFO() +END_MESSAGE_MAP() + +/* + * Initialize the controls. + */ +BOOL +DiskEditDialog::OnInitDialog(void) +{ + ASSERT(!fFileName.IsEmpty()); + ASSERT(fpDiskFS != nil); + + /* + * Disable the write button. + */ + if (fReadOnly) { + CButton* pButton = (CButton*) GetDlgItem(IDC_DISKEDIT_DOWRITE); + ASSERT(pButton != nil); + pButton->EnableWindow(FALSE); + } + + /* + * Use modified spin controls so we're not limited to 16 bits. + */ + ReplaceSpinCtrl(&fTrackSpinner, IDC_DISKEDIT_TRACKSPIN, + IDC_DISKEDIT_TRACK); + ReplaceSpinCtrl(&fSectorSpinner, IDC_DISKEDIT_SECTORSPIN, + IDC_DISKEDIT_SECTOR); + + + + /* + * Configure the RichEdit control. + */ + CRichEditCtrl* pEdit = (CRichEditCtrl*) GetDlgItem(IDC_DISKEDIT_EDIT); + ASSERT(pEdit != nil); + + /* set the font to 10-point Courier New */ + CHARFORMAT cf; + cf.cbSize = sizeof(CHARFORMAT); + cf.dwMask = CFM_FACE | CFM_SIZE; + ::lstrcpy(cf.szFaceName, "Courier New"); + cf.yHeight = 10 * 20; // point size in twips + BOOL cc = pEdit->SetDefaultCharFormat(cf); + if (cc == FALSE) { + WMSG0("SetDefaultCharFormat failed?\n"); + ASSERT(FALSE); + } + + /* set to read-only */ + pEdit->SetReadOnly(); + /* retain the selection even if we lose focus [can't do this in OnInit] */ + pEdit->SetOptions(ECOOP_OR, ECO_SAVESEL); + + /* + * Disable the sub-volume and/or file open buttons if the DiskFS doesn't + * have the appropriate stuff inside. + */ + if (fpDiskFS->GetNextFile(nil) == nil) { + CWnd* pWnd = GetDlgItem(IDC_DISKEDIT_OPENFILE); + pWnd->EnableWindow(FALSE); + } + if (fpDiskFS->GetNextSubVolume(nil) == nil) { + CWnd* pWnd = GetDlgItem(IDC_DISKEDIT_SUBVOLUME); + pWnd->EnableWindow(FALSE); + } + + /* + * Configure the nibble parm drop-list in an appropriate fashion. + */ + InitNibbleParmList(); + + /* + * If this is a sub-volume edit window, pop us up slightly offset from the + * parent window so the user can see that there's more than one thing + * open. + */ + if (fPositionShift != 0) { + CRect rect; + GetWindowRect(&rect); + rect.top += fPositionShift; + rect.left += fPositionShift; + rect.bottom += fPositionShift; + rect.right += fPositionShift; + MoveWindow(&rect); + } + + /* + * Set the window title. + */ + CString title("Disk Viewer - "); + title += fFileName; + if (fpDiskFS->GetVolumeID() != nil) { + title += " ("; + title += fpDiskFS->GetVolumeID(); + title += ")"; + } + SetWindowText(title); + + return TRUE; +} + +/* + * Initialize the nibble parm drop-list. + */ +void +DiskEditDialog::InitNibbleParmList(void) +{ + ASSERT(fpDiskFS != nil); + DiskImg* pDiskImg = fpDiskFS->GetDiskImg(); + CComboBox* pCombo; + + pCombo = (CComboBox*) GetDlgItem(IDC_DISKEDIT_NIBBLE_PARMS); + ASSERT(pCombo != nil); + + if (pDiskImg->GetHasNibbles()) { + const DiskImg::NibbleDescr* pTable; + const DiskImg::NibbleDescr* pCurrent; + int i, count; + + pTable = pDiskImg->GetNibbleDescrTable(&count); + if (pTable == nil || count <= 0) { + WMSG2("WHOOPS: nibble parm got table=0x%08lx, count=%d\n", + (long) pTable, count); + return; + } + pCurrent = pDiskImg->GetNibbleDescr(); + + /* configure the selection to match the disk analysis */ + int dflt = -1; + if (pCurrent != nil) { + for (i = 0; i < count; i++) { + if (memcmp(&pTable[i], pCurrent, sizeof(*pCurrent)) == 0) { + WMSG1(" NibbleParm match on entry %d\n", i); + dflt = i; + break; + } + } + + if (dflt == -1) { + WMSG0(" GLITCH: no match on nibble descr in table?!\n"); + dflt = 0; + } + } + + for (i = 0; i < count; i++) { + if (pTable[i].numSectors > 0) + pCombo->AddString(pTable[i].description); + else { + /* only expecting this on the last, "custom" entry */ + ASSERT(i == count-1); + } + } + pCombo->SetCurSel(dflt); + } else { + pCombo->AddString("Nibble Parms"); + pCombo->SetCurSel(0); + pCombo->EnableWindow(FALSE); + } +} + + +/* + * Replace a spin button with our improved version. + */ +int +DiskEditDialog::ReplaceSpinCtrl(MySpinCtrl* pNewSpin, int idSpin, int idEdit) +{ + CSpinButtonCtrl* pSpin; +// CRect rect; + DWORD style; + + pSpin = (CSpinButtonCtrl*)GetDlgItem(idSpin); + if (pSpin == nil) + return -1; +// pSpin->GetWindowRect(&rect); +// ScreenToClient(&rect); + style = pSpin->GetStyle(); + style &= ~(UDS_SETBUDDYINT); + //style |= UDS_AUTOBUDDY; + ASSERT(!(style & UDS_AUTOBUDDY)); + pSpin->DestroyWindow(); + pNewSpin->Create(style, CRect(0,0,0,0), this, idSpin); + pNewSpin->SetBuddy(GetDlgItem(idEdit)); + + return 0; +} + +/* + * Special keypress handling. + */ +BOOL +DiskEditDialog::PreTranslateMessage(MSG* pMsg) +{ + if (pMsg->message == WM_KEYDOWN && + pMsg->wParam == VK_RETURN) + { + //WMSG0("RETURN!\n"); + LoadData(); + return TRUE; + } + + return CDialog::PreTranslateMessage(pMsg); +} + +/* + * F1 key hit, or '?' button in title bar used to select help for an + * item in the dialog. + */ +BOOL +DiskEditDialog::OnHelpInfo(HELPINFO* lpHelpInfo) +{ + WMSG4("HELP: size=%d contextType=%d ctrlID=0x%x contextID=0x%08lx\n", + lpHelpInfo->cbSize, lpHelpInfo->iContextType, lpHelpInfo->iCtrlId, + lpHelpInfo->dwContextId); + + DWORD context = lpHelpInfo->iCtrlId; + + /* map all of the track/sector selection stuff to one item */ + if (context == IDC_DISKEDIT_TRACK || + context == IDC_DISKEDIT_TRACKSPIN || + context == IDC_STEXT_TRACK || + context == IDC_DISKEDIT_SECTOR || + context == IDC_DISKEDIT_SECTORSPIN || + context == IDC_STEXT_SECTOR) + { + context = IDC_DISKEDIT_TRACK; + } + + WinHelp(context, HELP_CONTEXTPOPUP); + return TRUE; // indicate success?? +} + +/* + * User pressed the "Help" button. + */ +void +DiskEditDialog::OnHelp(void) +{ + WinHelp(HELP_TOPIC_DISKEDIT, HELP_CONTEXT); +} + +/* + * Handle the "Done" button. We don't use IDOK because we don't want + * to bail out of the dialog. + */ +void +DiskEditDialog::OnDone(void) +{ + WMSG0("DiskEditDialog OnDone\n"); + EndDialog(IDOK); +} + +/* + * Toggle the spin button / edit controls. + */ +void +DiskEditDialog::OnHexMode(void) +{ + int base; + + CButton* pButton = (CButton*) GetDlgItem(IDC_DISKEDIT_HEX); + ASSERT(pButton != nil); + + if (pButton->GetCheck() == 0) + base = 10; + else + base = 16; + + SetSpinMode(IDC_DISKEDIT_TRACKSPIN, base); + SetSpinMode(IDC_DISKEDIT_SECTORSPIN, base); +} + +/* + * Create a new instance of the disk edit dialog, for a sub-volume. + */ +void +DiskEditDialog::OnSubVolume(void) +{ + SubVolumeDialog subv(this); + bool showAsBlocks; + + subv.Setup(fpDiskFS); + if (subv.DoModal() == IDOK) { + WMSG1("SELECTED subv %d\n", subv.fListBoxIndex); + DiskFS::SubVolume* pSubVol = fpDiskFS->GetNextSubVolume(nil); + if (pSubVol == nil) + return; + + while (subv.fListBoxIndex-- > 0) { + pSubVol = fpDiskFS->GetNextSubVolume(pSubVol); + } + ASSERT(pSubVol != nil); + + BlockEditDialog blockEdit; + SectorEditDialog sectorEdit; + DiskEditDialog* pEditDialog; + + showAsBlocks = pSubVol->GetDiskImg()->ShowAsBlocks(); + if (showAsBlocks) + pEditDialog = &blockEdit; + else + pEditDialog = §orEdit; + pEditDialog->Setup(pSubVol->GetDiskFS(), fpDiskFS->GetVolumeID()); + pEditDialog->SetPositionShift(8); + (void) pEditDialog->DoModal(); + } +} + +/* + * Change the mode of a spin button. The Windows control doesn't + * immediately update with a hex display, so we do it manually. (Our + * replacement class does this correctly, but I'm leaving the code alone + * for now.) + */ +void +DiskEditDialog::SetSpinMode(int id, int base) +{ + CString valStr; + + ASSERT(base == 10 || base == 16); + + MySpinCtrl* pSpin = (MySpinCtrl*) GetDlgItem(id); + if (pSpin == nil) { + // expected behavior in "block" mode for sector button + WMSG1("Couldn't find spin button %d\n", id); + return; + } + + long val = pSpin->GetPos(); + if (val & 0xff000000) { + WMSG0("NOTE: hex transition while value is invalid\n"); + val = 0; + } + + if (base == 10) + valStr.Format("%d", val); + else + valStr.Format("%X", val); + + pSpin->SetBase(base); + pSpin->GetBuddy()->SetWindowText(valStr); + WMSG2("Set spin button base to %d val=%d\n", base, val); +} + +/* + * Read a value from a spin control. + * + * Returns 0 on success, -1 if the return value from the spin control was + * invalid. In the latter case, an error dialog will be displayed. + */ +int +DiskEditDialog::ReadSpinner(int id, long* pVal) +{ + MySpinCtrl* pSpin = (MySpinCtrl*) GetDlgItem(id); + ASSERT(pSpin != nil); + + long val = pSpin->GetPos(); + if (val & 0xff000000) { + /* error */ + CString msg; + CString err; + err.LoadString(IDS_ERROR); + int lower, upper; + pSpin->GetRange32(lower, upper); + msg.Format("Please enter a value between %d and %d (0x%x and 0x%x).", + lower, upper, lower, upper); + MessageBox(msg, err, MB_OK|MB_ICONEXCLAMATION); + return -1; + } + + *pVal = val; + return 0; +} + +/* + * Set the value of a spin control. + */ +void +DiskEditDialog::SetSpinner(int id, long val) +{ + MySpinCtrl* pSpin = (MySpinCtrl*) GetDlgItem(id); + ASSERT(pSpin != nil); + + /* sanity check */ + int lower, upper; + pSpin->GetRange32(lower, upper); + ASSERT(val >= lower && val <= upper); + + pSpin->SetPos(val); +} + +/* + * Convert a chunk of data into a hex dump, and stuff it into the edit control. + */ +void +DiskEditDialog::DisplayData(const unsigned char* srcBuf, int size) +{ + char textBuf[80 * 16 * 2]; + char* cp; + int i, j; + + ASSERT(srcBuf != nil); + ASSERT(size == kSectorSize || size == kBlockSize); + + CRichEditCtrl* pEdit = (CRichEditCtrl*)GetDlgItem(IDC_DISKEDIT_EDIT); + ASSERT(pEdit != nil); + + /* + * If we have an alert message, show that instead. + */ + if (!fAlertMsg.IsEmpty()) { + const int kWidth = 72; + int indent = (kWidth/2) - (fAlertMsg.GetLength() / 2); + if (indent < 0) + indent = 0; + + CString msg = " " + " "; + ASSERT(msg.GetLength() == kWidth); + msg = msg.Left(indent); + msg += fAlertMsg; + for (i = 0; i < (size / 16)-2; i += 2) { + textBuf[i] = '\r'; + textBuf[i+1] = '\n'; + } + strcpy(&textBuf[i], msg); + pEdit->SetWindowText(textBuf); + + return; + } + + /* + * No alert, do the usual thing. + */ + cp = textBuf; + for (i = 0; i < size/16; i++) { + if (size == kSectorSize) { + /* two-nybble addr */ + sprintf(cp, " %02x: %02x %02x %02x %02x %02x %02x %02x %02x " + "%02x %02x %02x %02x %02x %02x %02x %02x ", + i * 16, + srcBuf[0], srcBuf[1], srcBuf[2], srcBuf[3], + srcBuf[4], srcBuf[5], srcBuf[6], srcBuf[7], + srcBuf[8], srcBuf[9], srcBuf[10], srcBuf[11], + srcBuf[12], srcBuf[13], srcBuf[14], srcBuf[15]); + } else { + /* three-nybble addr */ + sprintf(cp, "%03x: %02x %02x %02x %02x %02x %02x %02x %02x " + "%02x %02x %02x %02x %02x %02x %02x %02x ", + i * 16, + srcBuf[0], srcBuf[1], srcBuf[2], srcBuf[3], + srcBuf[4], srcBuf[5], srcBuf[6], srcBuf[7], + srcBuf[8], srcBuf[9], srcBuf[10], srcBuf[11], + srcBuf[12], srcBuf[13], srcBuf[14], srcBuf[15]); + } + ASSERT(strlen(cp) == 54); + cp += 54; // strlen(cp) + for (j = 0; j < 16; j++) + *cp++ = PrintableChar(srcBuf[j]); + + *cp++ = '\r'; + *cp++ = '\n'; + + srcBuf += 16; + } + /* kill the last EOL, so the cursor doesn't move past that line */ + cp--; + *cp = '\0'; + + pEdit->SetWindowText(textBuf); +} + +/* + * Display a track full of nibble data. + */ +void +DiskEditDialog::DisplayNibbleData(const unsigned char* srcBuf, int size) +{ + ASSERT(srcBuf != nil); + ASSERT(size > 0); + ASSERT(fAlertMsg.IsEmpty()); + + int bufSize = ((size+15) / 16) * 80; + char* textBuf = new char[bufSize]; + char* cp; + int i; + + if (textBuf == nil) + return; + + cp = textBuf; + for (i = 0; size > 0; i++) { + if (size >= 16) { + sprintf(cp, "%04x: %02x %02x %02x %02x %02x %02x %02x %02x " + "%02x %02x %02x %02x %02x %02x %02x %02x", + i * 16, + srcBuf[0], srcBuf[1], srcBuf[2], srcBuf[3], + srcBuf[4], srcBuf[5], srcBuf[6], srcBuf[7], + srcBuf[8], srcBuf[9], srcBuf[10], srcBuf[11], + srcBuf[12], srcBuf[13], srcBuf[14], srcBuf[15]); + ASSERT(strlen(cp) == 53); + cp += 53; // strlen(cp) + } else { + sprintf(cp, "%04x:", i * 16); + cp += 5; + for (int j = 0; j < size; j++) { + sprintf(cp, " %02x", srcBuf[j]); + cp += 3; + } + } + + *cp++ = '\r'; + *cp++ = '\n'; + + srcBuf += 16; + size -= 16; + } + /* kill the last EOL, so the cursor doesn't move past that line */ + cp--; + *cp = '\0'; + + CRichEditCtrl* pEdit = (CRichEditCtrl*)GetDlgItem(IDC_DISKEDIT_EDIT); + ASSERT(pEdit != nil); + pEdit->SetWindowText(textBuf); + + /* + * Handle resize of edit box. We have to do this late or the scroll bar + * won't appear under Win98. (Whatever.) + */ + if (fFirstResize) { + fFirstResize = false; + + const int kStretchHeight = 249; + CRect rect; + + GetWindowRect(&rect); + + CRect inner; + pEdit->GetRect(&inner); + inner.bottom += kStretchHeight; + pEdit->GetWindowRect(&rect); + ScreenToClient(&rect); + rect.bottom += kStretchHeight; + pEdit->MoveWindow(&rect); + pEdit->SetRect(&inner); + } + + delete[] textBuf; +} + + +/* + * Open a file in a disk image. + * + * Returns a pointer to the A2File and A2FileDescr structures on success, nil + * on failure. The pointer placed in "ppOpenFile" must be freed by invoking + * its Close function. + */ +DIError +DiskEditDialog::OpenFile(const char* fileName, bool openRsrc, A2File** ppFile, + A2FileDescr** ppOpenFile) +{ + A2File* pFile; + A2FileDescr* pOpenFile = nil; + + WMSG2(" OpenFile '%s' rsrc=%d\n", fileName, openRsrc); + pFile = fpDiskFS->GetFileByName(fileName); + if (pFile == nil) { + CString msg, failed; + + msg.Format(IDS_DEFILE_FIND_FAILED, fileName); + failed.LoadString(IDS_FAILED); + MessageBox(msg, failed, MB_OK | MB_ICONSTOP); + return kDIErrFileNotFound; + } + + DIError dierr; + dierr = pFile->Open(&pOpenFile, true, openRsrc); + if (dierr != kDIErrNone) { + CString msg, failed; + + msg.Format(IDS_DEFILE_OPEN_FAILED, fileName, + DiskImgLib::DIStrError(dierr)); + failed.LoadString(IDS_FAILED); + MessageBox(msg, failed, MB_OK | MB_ICONSTOP); + return dierr; + } + + *ppFile = pFile; + *ppOpenFile = pOpenFile; + + return kDIErrNone; +} + + +/* + * Change the nibble parms. + * + * Assumes the parm list is linear and unbroken. + */ +void +DiskEditDialog::OnNibbleParms(void) +{ + DiskImg* pDiskImg = fpDiskFS->GetDiskImg(); + CComboBox* pCombo; + int sel; + + ASSERT(pDiskImg != nil); + ASSERT(pDiskImg->GetHasNibbles()); + + pCombo = (CComboBox*) GetDlgItem(IDC_DISKEDIT_NIBBLE_PARMS); + ASSERT(pCombo != nil); + + sel = pCombo->GetCurSel(); + if (sel == CB_ERR) + return; + + WMSG1(" OnNibbleParms: entry %d now selected\n", sel); + const DiskImg::NibbleDescr* pNibbleTable; + int count; + pNibbleTable = pDiskImg->GetNibbleDescrTable(&count); + ASSERT(sel < count); + pDiskImg->SetNibbleDescr(sel); + + LoadData(); +} + + +#if 0 +/* + * Make a "sparse" block in a file obvious by filling it with the word + * "sparse". + */ +void +DiskEditDialog::FillWithPattern(unsigned char* buf, int size, + const char* pattern) +{ + const char* cp; + unsigned char* ucp; + + ucp = buf; + cp = pattern; + while (ucp < buf+size) { + *ucp++ = *cp++; + if (*cp == '\0') + cp = pattern; + } +} +#endif + + +/* + * =========================================================================== + * SectorEditDialog + * =========================================================================== + */ + +/* + * Prep the dialog. + */ +BOOL +SectorEditDialog::OnInitDialog(void) +{ + /* + * Do base-class construction. + */ + DiskEditDialog::OnInitDialog(); + + /* + * Change track/sector text. + */ + CString trackStr; + CWnd* pWnd; + trackStr.Format("Track (%d):", fpDiskFS->GetDiskImg()->GetNumTracks()); + pWnd = GetDlgItem(IDC_STEXT_TRACK); + ASSERT(pWnd != nil); + pWnd->SetWindowText(trackStr); + trackStr.Format("Sector (%d):", fpDiskFS->GetDiskImg()->GetNumSectPerTrack()); + pWnd = GetDlgItem(IDC_STEXT_SECTOR); + ASSERT(pWnd != nil); + pWnd->SetWindowText(trackStr); + + /* + * Configure the spin buttons. + */ + MySpinCtrl* pSpin; + pSpin = (MySpinCtrl*)GetDlgItem(IDC_DISKEDIT_TRACKSPIN); + ASSERT(pSpin != nil); + pSpin->SetRange32(0, fpDiskFS->GetDiskImg()->GetNumTracks()-1); + pSpin->SetPos(0); + pSpin = (MySpinCtrl*)GetDlgItem(IDC_DISKEDIT_SECTORSPIN); + ASSERT(pSpin != nil); + pSpin->SetRange32(0, fpDiskFS->GetDiskImg()->GetNumSectPerTrack()-1); + pSpin->SetPos(0); + + /* give us something to look at */ + LoadData(); + + return TRUE; +} + +/* + * Load the current track/sector data into the edit control. + * + * Returns 0 on success, -1 on error. + */ +int +SectorEditDialog::LoadData(void) +{ + //WMSG0("SED LoadData\n"); + ASSERT(fpDiskFS != nil); + ASSERT(fpDiskFS->GetDiskImg() != nil); + + if (ReadSpinner(IDC_DISKEDIT_TRACKSPIN, &fTrack) != 0) + return -1; + if (ReadSpinner(IDC_DISKEDIT_SECTORSPIN, &fSector) != 0) + return -1; + + WMSG2("LoadData reading t=%d s=%d\n", fTrack, fSector); + + fAlertMsg = ""; + DIError dierr; + dierr = fpDiskFS->GetDiskImg()->ReadTrackSector(fTrack, fSector, fSectorData); + if (dierr != kDIErrNone) { + WMSG1("SED sector read failed: %s\n", DiskImgLib::DIStrError(dierr)); + //CString msg; + //CString err; + //err.LoadString(IDS_ERROR); + //msg.Format(IDS_DISKEDIT_NOREADTS, fTrack, fSector); + //MessageBox(msg, err, MB_OK|MB_ICONSTOP); + fAlertMsg.LoadString(IDS_DISKEDITMSG_BADSECTOR); + //return -1; + } + + DisplayData(); + + return 0; +} + +/* + * Read the currently specified track/sector. + */ +void +SectorEditDialog::OnDoRead(void) +{ + LoadData(); +} + +/* + * Write the currently loaded track/sector. + */ +void +SectorEditDialog::OnDoWrite(void) +{ + MessageBox("Write!"); +} + +/* + * Back up to the previous track/sector. + */ +void +SectorEditDialog::OnReadPrev(void) +{ + if (fTrack == 0 && fSector == 0) + return; + + if (fSector == 0) { + fSector = fpDiskFS->GetDiskImg()->GetNumSectPerTrack() -1; + fTrack--; + } else { + fSector--; + } + + SetSpinner(IDC_DISKEDIT_TRACKSPIN, fTrack); + SetSpinner(IDC_DISKEDIT_SECTORSPIN, fSector); + + LoadData(); +} + +/* + * Same as OnReadPrev, but moving forward. + */ +void +SectorEditDialog::OnReadNext(void) +{ + int numTracks = fpDiskFS->GetDiskImg()->GetNumTracks(); + int numSects = fpDiskFS->GetDiskImg()->GetNumSectPerTrack(); + + if (fTrack == numTracks-1 && fSector == numSects-1) + return; + + if (fSector == numSects-1) { + fSector = 0; + fTrack++; + } else { + fSector++; + } + + SetSpinner(IDC_DISKEDIT_TRACKSPIN, fTrack); + SetSpinner(IDC_DISKEDIT_SECTORSPIN, fSector); + + LoadData(); +} + + +/* + * Open a file on the disk image. If successful, open a new edit dialog + * that's in "file follow" mode. + */ +void +SectorEditDialog::OnOpenFile(void) +{ + DEFileDialog fileDialog(this); + + if (fileDialog.DoModal() == IDOK) { + SectorFileEditDialog fileEdit(this, this); + A2File* pFile; + A2FileDescr* pOpenFile = nil; + DIError dierr; + + dierr = OpenFile(fileDialog.fName, fileDialog.fOpenRsrcFork != 0, + &pFile, &pOpenFile); + if (dierr != kDIErrNone) + return; + + fileEdit.SetupFile(fileDialog.fName, fileDialog.fOpenRsrcFork != 0, + pFile, pOpenFile); + fileEdit.SetPositionShift(8); + (void) fileEdit.DoModal(); + + pOpenFile->Close(); + } +} + + +/* + * =========================================================================== + * SectorFileEditDialog + * =========================================================================== + */ + +/* + * Minor changes for file editing. + */ +BOOL +SectorFileEditDialog::OnInitDialog(void) +{ + BOOL retval; + + /* do base class first */ + retval = SectorEditDialog::OnInitDialog(); + + /* disable direct entry of tracks and sectors */ + CWnd* pWnd; + pWnd = GetDlgItem(IDC_DISKEDIT_TRACKSPIN); + ASSERT(pWnd != nil); + pWnd->EnableWindow(FALSE); + pWnd = GetDlgItem(IDC_DISKEDIT_SECTORSPIN); + ASSERT(pWnd != nil); + pWnd->EnableWindow(FALSE); + + /* disallow opening of sub-volumes and files */ + pWnd = GetDlgItem(IDC_DISKEDIT_OPENFILE); + pWnd->EnableWindow(FALSE); + pWnd = GetDlgItem(IDC_DISKEDIT_SUBVOLUME); + pWnd->EnableWindow(FALSE); + + CEdit* pEdit; + pEdit = (CEdit*) GetDlgItem(IDC_DISKEDIT_TRACK); + ASSERT(pEdit != nil); + pEdit->SetReadOnly(TRUE); + pEdit = (CEdit*) GetDlgItem(IDC_DISKEDIT_SECTOR); + ASSERT(pEdit != nil); + pEdit->SetReadOnly(TRUE); + + /* set the window title */ + CString title; + CString rsrcIndic; + rsrcIndic.LoadString(IDS_INDIC_RSRC); + title.Format("Disk Viewer - %s%s (%ld bytes)", + fpFile->GetPathName(), // use fpFile version to get case + fOpenRsrcFork ? (LPCTSTR)rsrcIndic : "", fLength); + SetWindowText(title); + + return retval; +} + +/* + * Load data from the current offset into the edit control. + * + * Returns 0 on success, -1 on error. + */ +int +SectorFileEditDialog::LoadData(void) +{ + ASSERT(fpDiskFS != nil); + ASSERT(fpDiskFS->GetDiskImg() != nil); + + DIError dierr; + WMSG1("SFED LoadData reading index=%d\n", fSectorIdx); + +#if 0 + WMSG1("LoadData reading offset=%d\n", fOffset); + size_t actual = 0; + dierr = fpFile->Seek(fOffset, EmbeddedFD::kSeekSet); + if (dierr == kDIErrNone) { + dierr = fpFile->Read(fSectorData, 1 /*kSectorSize*/, &actual); + } + if (dierr != kDIErrNone) { + CString msg, failed; + failed.LoadString(IDS_FAILED); + msg.Format(IDS_DISKEDIT_FIRDFAILED, DiskImg::DIStrError(dierr)); + MessageBox(msg, failed, MB_OK); + // TO DO: mark contents as invalid, so editing fails + return -1; + } + + if (actual != kSectorSize) { + WMSG1(" SFED partial read of %d bytes\n", actual); + ASSERT(actual < kSectorSize && actual >= 0); + } + + /* + * We've read the data, but we can't use it. We're a sector editor, + * not a file editor, and we need to get the actual sector data without + * EOF trimming or CP/M 0xe5 removal. + */ + fpFile->GetLastLocationRead(&fTrack, &fSector); + if (fTrack == A2File::kLastWasSparse && fSector == A2File::kLastWasSparse) + + ; +#endif + + fAlertMsg = ""; + + dierr = fpOpenFile->GetStorage(fSectorIdx, &fTrack, &fSector); + if (dierr == kDIErrInvalidIndex && fSectorIdx == 0) { + // no first sector; should only happen on CP/M + //FillWithPattern(fSectorData, sizeof(fSectorData), _T("EMPTY ")); + fAlertMsg.LoadString(IDS_DISKEDITMSG_EMPTY); + } else if (dierr != kDIErrNone) { + CString msg, failed; + failed.LoadString(IDS_FAILED); + msg.Format(IDS_DISKEDIT_FIRDFAILED, DiskImgLib::DIStrError(dierr)); + MessageBox(msg, failed, MB_OK); + fAlertMsg.LoadString(IDS_FAILED); + // TO DO: mark contents as invalid, so editing fails + return -1; + } else { + if (fTrack == 0 && fSector == 0) { + WMSG0("LoadData Sparse sector\n"); + //FillWithPattern(fSectorData, sizeof(fSectorData), _T("SPARSE ")); + fAlertMsg.Format(IDS_DISKEDITMSG_SPARSE, fSectorIdx); + } else { + WMSG2("LoadData reading T=%d S=%d\n", fTrack, fSector); + + dierr = fpDiskFS->GetDiskImg()->ReadTrackSector(fTrack, fSector, + fSectorData); + if (dierr != kDIErrNone) { + //CString msg; + //CString err; + //err.LoadString(IDS_ERROR); + //msg.Format(IDS_DISKEDIT_NOREADTS, fTrack, fSector); + //MessageBox(msg, err, MB_OK|MB_ICONSTOP); + fAlertMsg.LoadString(IDS_DISKEDITMSG_BADSECTOR); + //return -1; + } + } + } + + SetSpinner(IDC_DISKEDIT_TRACKSPIN, fTrack); + SetSpinner(IDC_DISKEDIT_SECTORSPIN, fSector); + + CWnd* pWnd; + pWnd = GetDlgItem(IDC_DISKEDIT_PREV); + ASSERT(pWnd != nil); + pWnd->EnableWindow(fSectorIdx > 0); + if (!pWnd->IsWindowEnabled() && GetFocus() == nil) + GetDlgItem(IDC_DISKEDIT_NEXT)->SetFocus(); + + pWnd = GetDlgItem(IDC_DISKEDIT_NEXT); + ASSERT(pWnd != nil); + pWnd->EnableWindow(fSectorIdx+1 < fpOpenFile->GetSectorCount()); + if (!pWnd->IsWindowEnabled() && GetFocus() == nil) + GetDlgItem(IDC_DISKEDIT_PREV)->SetFocus(); + + DisplayData(); + + return 0; +} + +/* + * Move to the previous sector in the file. + */ +void +SectorFileEditDialog::OnReadPrev(void) +{ + if (fSectorIdx == 0) + return; + + fSectorIdx--; + ASSERT(fSectorIdx >= 0); + LoadData(); +} + +/* + * Move to the next sector in the file. + */ +void +SectorFileEditDialog::OnReadNext(void) +{ + if (fSectorIdx+1 >= fpOpenFile->GetSectorCount()) + return; + + fSectorIdx++; + ASSERT(fSectorIdx < fpOpenFile->GetSectorCount()); + LoadData(); +} + + +/* + * =========================================================================== + * BlockEditDialog + * =========================================================================== + */ + +/* + * Rearrange the DiskEdit dialog (which defaults to SectorEdit mode) to + * accommodate block editing. + */ +BOOL +BlockEditDialog::OnInitDialog(void) +{ + /* + * Get rid of the "sector" input item, and change the "track" input + * item to accept blocks instead. + */ + CWnd* pWnd; + + pWnd = GetDlgItem(IDC_STEXT_SECTOR); + pWnd->DestroyWindow(); + pWnd = GetDlgItem(IDC_DISKEDIT_SECTOR); + pWnd->DestroyWindow(); + pWnd = GetDlgItem(IDC_DISKEDIT_SECTORSPIN); + pWnd->DestroyWindow(); + + CString blockStr; + //blockStr.LoadString(IDS_BLOCK); + blockStr.Format("Block (%d):", fpDiskFS->GetDiskImg()->GetNumBlocks()); + pWnd = GetDlgItem(IDC_STEXT_TRACK); + ASSERT(pWnd != nil); + pWnd->SetWindowText(blockStr); + + /* + * Increase the size of the window to accommodate the larger block size. + */ + const int kStretchHeight = 250; + CRect rect; + + GetWindowRect(&rect); + rect.bottom += kStretchHeight; + MoveWindow(&rect); + + CRichEditCtrl* pEdit = (CRichEditCtrl*)GetDlgItem(IDC_DISKEDIT_EDIT); + ASSERT(pEdit != nil); + CRect inner; + pEdit->GetRect(&inner); + inner.bottom += kStretchHeight; + pEdit->GetWindowRect(&rect); + ScreenToClient(&rect); + rect.bottom += kStretchHeight; + pEdit->MoveWindow(&rect); + pEdit->SetRect(&inner); + + MoveControl(this, IDC_DISKEDIT_DONE, 0, kStretchHeight); + MoveControl(this, IDC_DISKEDIT_OPENFILE, 0, kStretchHeight); + MoveControl(this, IDC_DISKEDIT_SUBVOLUME, 0, kStretchHeight); + MoveControl(this, IDHELP, 0, kStretchHeight); + MoveControl(this, IDC_DISKEDIT_NIBBLE_PARMS, 0, kStretchHeight); + + /* + * Do base-class construction. + */ + DiskEditDialog::OnInitDialog(); + + /* + * Configure the spin button. We use the "track" spin button for blocks. + */ + MySpinCtrl* pSpin; + pSpin = (MySpinCtrl*)GetDlgItem(IDC_DISKEDIT_TRACKSPIN); + ASSERT(pSpin != nil); + pSpin->SetRange32(0, fpDiskFS->GetDiskImg()->GetNumBlocks()-1); + pSpin->SetPos(0); + + /* give us something to look at */ + if (LoadData() != 0) { + WMSG0("WHOOPS: LoadData() failed, but we're in OnInitDialog\n"); + } + + return TRUE; +} + +#if 0 +/* + * Move a control so it maintains its same position relative to the bottom + * and right edges. + */ +void +BlockEditDialog::MoveControl(int id, int deltaX, int deltaY) +{ + CWnd* pWnd; + CRect rect; + + pWnd = GetDlgItem(id); + ASSERT(pWnd != nil); + + pWnd->GetWindowRect(&rect); + ScreenToClient(&rect); + rect.left += deltaX; + rect.right += deltaX; + rect.top += deltaY; + rect.bottom += deltaY; + pWnd->MoveWindow(&rect, TRUE); +} +#endif + + +/* + * Load the current block data into the edit control. + */ +int +BlockEditDialog::LoadData(void) +{ + //WMSG0("BED LoadData\n"); + ASSERT(fpDiskFS != nil); + ASSERT(fpDiskFS->GetDiskImg() != nil); + + if (ReadSpinner(IDC_DISKEDIT_TRACKSPIN, &fBlock) != 0) + return -1; + + WMSG1("LoadData reading block=%d\n", fBlock); + + fAlertMsg = ""; + DIError dierr; + dierr = fpDiskFS->GetDiskImg()->ReadBlock(fBlock, fBlockData); + if (dierr != kDIErrNone) { + WMSG1("BED block read failed: %s\n", DiskImgLib::DIStrError(dierr)); + //CString msg; + //CString err; + //err.LoadString(IDS_ERROR); + //msg.Format(IDS_DISKEDIT_NOREADBLOCK, fBlock); + //MessageBox(msg, err, MB_OK|MB_ICONSTOP); + fAlertMsg.LoadString(IDS_DISKEDITMSG_BADBLOCK); + //return -1; + } + + DisplayData(); + + return 0; +} + +/* + * Read the currently specified track/sector. + */ +void +BlockEditDialog::OnDoRead(void) +{ + LoadData(); +} + +/* + * Write the currently loaded track/sector. + */ +void +BlockEditDialog::OnDoWrite(void) +{ + MessageBox("Write!"); +} + +/* + * Back up to the previous track/sector, or (in follow-file mode) to the + * previous sector in the file. + */ +void +BlockEditDialog::OnReadPrev(void) +{ + if (fBlock == 0) + return; + + fBlock--; + SetSpinner(IDC_DISKEDIT_TRACKSPIN, fBlock); + LoadData(); +} + +/* + * Same as OnReadPrev, but moving forward. + */ +void +BlockEditDialog::OnReadNext(void) +{ + ASSERT(fpDiskFS != nil); + if (fBlock == fpDiskFS->GetDiskImg()->GetNumBlocks() - 1) + return; + + fBlock++; + SetSpinner(IDC_DISKEDIT_TRACKSPIN, fBlock); + LoadData(); +} + +/* + * Open a file on the disk image. If successful, open a new edit dialog + * that's in "file follow" mode. + */ +void +BlockEditDialog::OnOpenFile(void) +{ + DEFileDialog fileDialog(this); + + if (fileDialog.DoModal() == IDOK) { + BlockFileEditDialog fileEdit(this, this); + A2File* pFile; + A2FileDescr* pOpenFile = nil; + DIError dierr; + + dierr = OpenFile(fileDialog.fName, fileDialog.fOpenRsrcFork != 0, + &pFile, &pOpenFile); + if (dierr != kDIErrNone) + return; + + fileEdit.SetupFile(fileDialog.fName, fileDialog.fOpenRsrcFork != 0, + pFile, pOpenFile); + fileEdit.SetPositionShift(8); + (void) fileEdit.DoModal(); + + pOpenFile->Close(); + } +} + + +/* + * =========================================================================== + * BlockFileEditDialog + * =========================================================================== + */ + +/* + * Minor changes for file editing. + */ +BOOL +BlockFileEditDialog::OnInitDialog(void) +{ + BOOL retval; + + /* do base class first */ + retval = BlockEditDialog::OnInitDialog(); + + /* disable direct entry of tracks and Blocks */ + CWnd* pWnd; + pWnd = GetDlgItem(IDC_DISKEDIT_TRACKSPIN); + ASSERT(pWnd != nil); + pWnd->EnableWindow(FALSE); + + /* disallow opening of sub-volumes and files */ + pWnd = GetDlgItem(IDC_DISKEDIT_OPENFILE); + pWnd->EnableWindow(FALSE); + pWnd = GetDlgItem(IDC_DISKEDIT_SUBVOLUME); + pWnd->EnableWindow(FALSE); + + CEdit* pEdit; + pEdit = (CEdit*) GetDlgItem(IDC_DISKEDIT_TRACK); + ASSERT(pEdit != nil); + pEdit->SetReadOnly(TRUE); + + /* set the window title */ + CString title; + CString rsrcIndic; + rsrcIndic.LoadString(IDS_INDIC_RSRC); + title.Format("Disk Viewer - %s%s (%ld bytes)", + fpFile->GetPathName(), // use fpFile version to get case + fOpenRsrcFork ? (LPCTSTR)rsrcIndic : "", fLength); + SetWindowText(title); + + return retval; +} + +/* + * Load data from the current offset into the edit control. + * + * Returns 0 on success, -1 on error. + */ +int +BlockFileEditDialog::LoadData(void) +{ + ASSERT(fpDiskFS != nil); + ASSERT(fpDiskFS->GetDiskImg() != nil); + + DIError dierr; + WMSG1("BFED LoadData reading index=%d\n", fBlockIdx); + +#if 0 + WMSG1("LoadData reading offset=%d\n", fOffset); + size_t actual = 0; + dierr = fpFile->Seek(fOffset, EmbeddedFD::kSeekSet); + if (dierr == kDIErrNone) { + dierr = fpFile->Read(fBlockData, 1 /*kBlockSize*/, &actual); + } + if (dierr != kDIErrNone) { + CString msg, failed; + failed.LoadString(IDS_FAILED); + msg.Format(IDS_DISKEDIT_FIRDFAILED, DiskImg::DIStrError(dierr)); + MessageBox(msg, failed, MB_OK); + // TO DO: mark contents as invalid, so editing fails + return -1; + } + + if (actual != kBlockSize) { + WMSG1(" BFED partial read of %d bytes\n", actual); + ASSERT(actual < kBlockSize && actual >= 0); + } + + /* + * We've read the data, but we can't use it. We're a Block editor, + * not a file editor, and we need to get the actual Block data without + * EOF trimming or CP/M 0xe5 removal. + */ + fpFile->GetLastLocationRead(&fBlock); + if (fBlock == A2File::kLastWasSparse) + ; +#endif + + fAlertMsg = ""; + + dierr = fpOpenFile->GetStorage(fBlockIdx, &fBlock); + if (dierr == kDIErrInvalidIndex && fBlockIdx == 0) { + // no first sector; should only happen on CP/M + //FillWithPattern(fBlockData, sizeof(fBlockData), _T("EMPTY ")); + fAlertMsg.LoadString(IDS_DISKEDITMSG_EMPTY); + } else if (dierr != kDIErrNone) { + CString msg, failed; + failed.LoadString(IDS_FAILED); + msg.Format(IDS_DISKEDIT_FIRDFAILED, DiskImgLib::DIStrError(dierr)); + MessageBox(msg, failed, MB_OK); + fAlertMsg.LoadString(IDS_FAILED); + // TO DO: mark contents as invalid, so editing fails + return -1; + } else { + if (fBlock == 0) { + WMSG0("LoadData Sparse block\n"); + //FillWithPattern(fBlockData, sizeof(fBlockData), _T("SPARSE ")); + fAlertMsg.Format(IDS_DISKEDITMSG_SPARSE, fBlockIdx); + } else { + WMSG1("LoadData reading B=%d\n", fBlock); + + dierr = fpDiskFS->GetDiskImg()->ReadBlock(fBlock, fBlockData); + if (dierr != kDIErrNone) { + //CString msg; + //CString err; + //err.LoadString(IDS_ERROR); + //msg.Format(IDS_DISKEDIT_NOREADBLOCK, fBlock); + //MessageBox(msg, err, MB_OK|MB_ICONSTOP); + fAlertMsg.LoadString(IDS_DISKEDITMSG_BADBLOCK); + //return -1; + } + } + } + + SetSpinner(IDC_DISKEDIT_TRACKSPIN, fBlock); + + CWnd* pWnd; + pWnd = GetDlgItem(IDC_DISKEDIT_PREV); + ASSERT(pWnd != nil); + pWnd->EnableWindow(fBlockIdx > 0); + if (!pWnd->IsWindowEnabled() && GetFocus() == nil) + GetDlgItem(IDC_DISKEDIT_NEXT)->SetFocus(); + + pWnd = GetDlgItem(IDC_DISKEDIT_NEXT); + ASSERT(pWnd != nil); + pWnd->EnableWindow(fBlockIdx+1 < fpOpenFile->GetBlockCount()); + if (!pWnd->IsWindowEnabled() && GetFocus() == nil) + GetDlgItem(IDC_DISKEDIT_PREV)->SetFocus(); + + DisplayData(); + + return 0; +} + +/* + * Move to the previous Block in the file. + */ +void +BlockFileEditDialog::OnReadPrev(void) +{ + if (fBlockIdx == 0) + return; + + fBlockIdx--; + ASSERT(fBlockIdx >= 0); + LoadData(); +} + +/* + * Move to the next Block in the file. + */ +void +BlockFileEditDialog::OnReadNext(void) +{ + if (fBlockIdx+1 >= fpOpenFile->GetBlockCount()) + return; + + fBlockIdx++; + ASSERT(fBlockIdx < fpOpenFile->GetBlockCount()); + LoadData(); +} + + +/* + * =========================================================================== + * NibbleEditDialog + * =========================================================================== + */ + +/* + * Rearrange the DiskEdit dialog (which defaults to SectorEdit mode) to + * accommodate nibble editing. + */ +BOOL +NibbleEditDialog::OnInitDialog(void) +{ + /* + * Get rid of the "sector" input item. + */ + CWnd* pWnd; + + pWnd = GetDlgItem(IDC_STEXT_SECTOR); + pWnd->DestroyWindow(); + pWnd = GetDlgItem(IDC_DISKEDIT_SECTOR); + pWnd->DestroyWindow(); + pWnd = GetDlgItem(IDC_DISKEDIT_SECTORSPIN); + pWnd->DestroyWindow(); + + CString trackStr; + trackStr.Format("Track (%d):", fpDiskFS->GetDiskImg()->GetNumTracks()); + pWnd = GetDlgItem(IDC_STEXT_TRACK); + ASSERT(pWnd != nil); + pWnd->SetWindowText(trackStr); + + /* + * Increase the size of the window so it's the same height as blocks. + * + * NOTE: using a pixel constant is probably bad. We want to use something + * like GetTextMetrics, but I'm not sure how to get that without a + * device context. + */ + CRichEditCtrl* pEdit = (CRichEditCtrl*)GetDlgItem(IDC_DISKEDIT_EDIT); + ASSERT(pEdit != nil); + const int kStretchHeight = 249; + CRect rect; + + GetWindowRect(&rect); + rect.bottom += kStretchHeight; + MoveWindow(&rect); + + /* + * Must postpone resize of edit ctrl until after data has been loaded, or + * scroll bars fail to appear under Win98. Makes no sense whatsoever, but + * that's Windows for you. + */ +#if 0 + CRect inner; + pEdit->GetRect(&inner); + inner.bottom += kStretchHeight; + pEdit->GetWindowRect(&rect); + ScreenToClient(&rect); + rect.bottom += kStretchHeight; + pEdit->MoveWindow(&rect); + pEdit->SetRect(&inner); +#endif + + /* show the scroll bar */ + pEdit->ShowScrollBar(SB_VERT); + + MoveControl(this, IDC_DISKEDIT_DONE, 0, kStretchHeight); + MoveControl(this, IDC_DISKEDIT_OPENFILE, 0, kStretchHeight); + MoveControl(this, IDC_DISKEDIT_SUBVOLUME, 0, kStretchHeight); + MoveControl(this, IDHELP, 0, kStretchHeight); + MoveControl(this, IDC_DISKEDIT_NIBBLE_PARMS, 0, kStretchHeight); + + /* disable opening of files and sub-volumes */ + pWnd = GetDlgItem(IDC_DISKEDIT_OPENFILE); + pWnd->EnableWindow(FALSE); + pWnd = GetDlgItem(IDC_DISKEDIT_SUBVOLUME); + pWnd->EnableWindow(FALSE); + + /* + * Do base-class construction. + */ + DiskEditDialog::OnInitDialog(); + + /* + * This currently has no effect on the nibble editor. Someday we may + * want to highlight and/or decode address fields. + */ + pWnd = GetDlgItem(IDC_DISKEDIT_NIBBLE_PARMS); + pWnd->EnableWindow(FALSE); + + /* + * Configure the track spin button. + */ + MySpinCtrl* pSpin; + pSpin = (MySpinCtrl*)GetDlgItem(IDC_DISKEDIT_TRACKSPIN); + ASSERT(pSpin != nil); + pSpin->SetRange32(0, fpDiskFS->GetDiskImg()->GetNumTracks()-1); + pSpin->SetPos(0); + + /* give us something to look at */ + LoadData(); + + return TRUE; +} + +/* + * Load the current track data into the edit control. + */ +int +NibbleEditDialog::LoadData(void) +{ + //WMSG0("BED LoadData\n"); + ASSERT(fpDiskFS != nil); + ASSERT(fpDiskFS->GetDiskImg() != nil); + + if (ReadSpinner(IDC_DISKEDIT_TRACKSPIN, &fTrack) != 0) + return -1; + + WMSG1("LoadData reading track=%d\n", fTrack); + + fAlertMsg = ""; + DIError dierr; + dierr = fpDiskFS->GetDiskImg()->ReadNibbleTrack(fTrack, fNibbleData, + &fNibbleDataLen); + if (dierr != kDIErrNone) { + WMSG1("NED track read failed: %s\n", DiskImgLib::DIStrError(dierr)); + fAlertMsg.LoadString(IDS_DISKEDITMSG_BADTRACK); + } + + DisplayData(); + + return 0; +} + +/* + * Read the currently specified track/sector. + */ +void +NibbleEditDialog::OnDoRead(void) +{ + LoadData(); +} + +/* + * Write the currently loaded track/sector. + */ +void +NibbleEditDialog::OnDoWrite(void) +{ + MessageBox("Write!"); +} + +/* + * Back up to the previous track/sector, or (in follow-file mode) to the + * previous sector in the file. + */ +void +NibbleEditDialog::OnReadPrev(void) +{ + if (fTrack == 0) + return; + + fTrack--; + SetSpinner(IDC_DISKEDIT_TRACKSPIN, fTrack); + LoadData(); +} + +/* + * Same as OnReadPrev, but moving forward. + */ +void +NibbleEditDialog::OnReadNext(void) +{ + ASSERT(fpDiskFS != nil); + if (fTrack == fpDiskFS->GetDiskImg()->GetNumTracks() - 1) + return; + + fTrack++; + SetSpinner(IDC_DISKEDIT_TRACKSPIN, fTrack); + LoadData(); +} diff --git a/app/DiskEditDialog.h b/app/DiskEditDialog.h new file mode 100644 index 0000000..5bc397d --- /dev/null +++ b/app/DiskEditDialog.h @@ -0,0 +1,315 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Class definition for DiskEdit dialog. + */ +#ifndef __DISK_EDIT_DIALOG__ +#define __DISK_EDIT_DIALOG__ + +#include "../diskimg/DiskImg.h" +#include "../util/UtilLib.h" +#include "resource.h" + +/* + * An abstract base class to support "sector editing" and "block editing" + * dialogs, which differ chiefly in how much data they present at a time. + * + * NOTE: override OnCancel to insert an "are you sure" message when the + * block is dirty. + * + * NOTE to self: if the initial block/sector read fails, we can be left + * with invalid stuff in the buffer. Keep that in mind if editing is + * enabled. + */ +class DiskEditDialog : public CDialog { +public: + DiskEditDialog(UINT nIDTemplate, CWnd* pParentWnd = NULL) : + CDialog(nIDTemplate, pParentWnd) + { + fReadOnly = true; + fpDiskFS = nil; + fFileName = ""; + fPositionShift = 0; + fFirstResize = true; + } + virtual ~DiskEditDialog() {} + + void Setup(DiskFS* pDiskFS, const char* fileName) { + ASSERT(pDiskFS != nil); + ASSERT(fileName != nil); + fpDiskFS = pDiskFS; + fFileName = fileName; + } + + enum { kSectorSize=256, kBlockSize=512 }; + + virtual int LoadData(void) = 0; + + virtual void DisplayData(void) = 0; + virtual void DisplayData(const unsigned char* buf, int size); + virtual void DisplayNibbleData(const unsigned char* srcBuf, int size); + + bool GetReadOnly(void) const { return fReadOnly; } + void SetReadOnly(bool val) { fReadOnly = val; } + int GetPositionShift(void) const { return fPositionShift; } + void SetPositionShift(int val) { fPositionShift = val; } + DiskFS* GetDiskFS(void) const { return fpDiskFS; } + const char* GetFileName(void) const { return fFileName; } + +protected: + // return a low-ASCII character so we can read high-ASCII files + inline char PrintableChar(unsigned char ch) { + if (ch < 0x20) + return '.'; + else if (ch < 0x80) + return ch; + else if (ch < 0xa0 || ch == 0xff) // 0xff becomes 0x7f + return '.'; + else + return ch & 0x7f; + } + + // overrides + virtual BOOL OnInitDialog(void); + + afx_msg virtual BOOL OnHelpInfo(HELPINFO* lpHelpInfo); + afx_msg virtual void OnDone(void); + afx_msg virtual void OnHexMode(void); + afx_msg virtual void OnDoRead(void) = 0; + afx_msg virtual void OnDoWrite(void) = 0; + afx_msg virtual void OnReadPrev(void) = 0; + afx_msg virtual void OnReadNext(void) = 0; + afx_msg virtual void OnSubVolume(void); + afx_msg virtual void OnOpenFile(void) = 0; + afx_msg virtual void OnNibbleParms(void); + afx_msg virtual void OnHelp(void); + + virtual BOOL PreTranslateMessage(MSG* pMsg); + + void SetSpinMode(int id, int base); + int ReadSpinner(int id, long* pVal); + void SetSpinner(int id, long val); + + //void FillWithPattern(unsigned char* buf, int size, const char* pattern); + DIError OpenFile(const char* fileName, bool openRsrc, A2File** ppFile, + A2FileDescr** ppOpenFile); + + DiskFS* fpDiskFS; + CString fFileName; + CString fAlertMsg; + bool fReadOnly; + int fPositionShift; + +private: + void InitNibbleParmList(void); + int ReplaceSpinCtrl(MySpinCtrl* pNewSpin, int idSpin, int idEdit); + MySpinCtrl fTrackSpinner; + MySpinCtrl fSectorSpinner; + bool fFirstResize; + + //afx_msg void OnPaint(); + DECLARE_MESSAGE_MAP() +}; + + +/* + * The "sector edit" dialog, which displays 256 bytes at a time, and + * accesses a disk by track/sector. + */ +class SectorEditDialog : public DiskEditDialog { +public: + SectorEditDialog(CWnd* pParentWnd = NULL) : + DiskEditDialog(IDD_DISKEDIT, pParentWnd) + { + fTrack = 0; + fSector = 0; + } + virtual ~SectorEditDialog() {} + + virtual int LoadData(void); // load the current track/sector + virtual void DisplayData(void) { + DiskEditDialog::DisplayData(fSectorData, kSectorSize); + } + + //void SetTrack(int val) { fTrack = val; } + //void SetSector(int val) { fSector = val; } + + // overrides + virtual BOOL OnInitDialog(void); + +protected: + afx_msg virtual void OnDoRead(void); + afx_msg virtual void OnDoWrite(void); + afx_msg virtual void OnReadPrev(void); + afx_msg virtual void OnReadNext(void); + afx_msg virtual void OnOpenFile(void); + + long fTrack; + long fSector; + unsigned char fSectorData[kSectorSize]; +}; + +/* + * Edit a file sector-by-sector. + */ +class SectorFileEditDialog : public SectorEditDialog { +public: + SectorFileEditDialog(SectorEditDialog* pSectEdit, CWnd* pParentWnd = NULL): + SectorEditDialog(pParentWnd) + { + DiskEditDialog::Setup(pSectEdit->GetDiskFS(), + pSectEdit->GetFileName()); + fSectorIdx = 0; + } + virtual ~SectorFileEditDialog() {} + + /* we do NOT own pOpenFile, and should not delete it */ + void SetupFile(const char* fileName, bool rsrcFork, A2File* pFile, + A2FileDescr* pOpenFile) + { + fOpenFileName = fileName; + fOpenRsrcFork = rsrcFork; + fpFile = pFile; + fpOpenFile = pOpenFile; + fLength = 0; + if (fpOpenFile->Seek(0, DiskImgLib::kSeekEnd) == kDIErrNone) + fLength = fpOpenFile->Tell(); + } + + virtual int LoadData(void); // load data from the current offset + +private: + // overrides + virtual BOOL OnInitDialog(void); + + afx_msg virtual void OnReadPrev(void); + afx_msg virtual void OnReadNext(void); + + CString fOpenFileName; + bool fOpenRsrcFork; + A2File* fpFile; + A2FileDescr* fpOpenFile; + //long fOffset; + long fSectorIdx; + di_off_t fLength; +}; + + +/* + * The "block edit" dialog, which displays 512 bytes at a time, and + * accesses a disk by linear block number. + */ +class BlockEditDialog : public DiskEditDialog { +public: + BlockEditDialog(CWnd* pParentWnd = NULL) : + DiskEditDialog(IDD_DISKEDIT, pParentWnd) + { + fBlock = 0; + } + virtual ~BlockEditDialog() {} + + virtual int LoadData(void); // load the current block + virtual void DisplayData(void) { + DiskEditDialog::DisplayData(fBlockData, kBlockSize); + } + + // overrides + virtual BOOL OnInitDialog(void); + +protected: + //void MoveControl(int id, int deltaX, int deltaY); + + afx_msg virtual void OnDoRead(void); + afx_msg virtual void OnDoWrite(void); + afx_msg virtual void OnReadPrev(void); + afx_msg virtual void OnReadNext(void); + afx_msg virtual void OnOpenFile(void); + + long fBlock; + unsigned char fBlockData[kBlockSize]; +}; + + +/* + * Edit a file block-by-block. + */ +class BlockFileEditDialog : public BlockEditDialog { +public: + BlockFileEditDialog(BlockEditDialog* pBlockEdit, CWnd* pParentWnd = NULL) : + BlockEditDialog(pParentWnd) + { + DiskEditDialog::Setup(pBlockEdit->GetDiskFS(), + pBlockEdit->GetFileName()); + fBlockIdx = 0; + } + virtual ~BlockFileEditDialog() {} + + /* we do NOT own pOpenFile, and should not delete it */ + void SetupFile(const char* fileName, bool rsrcFork, A2File* pFile, + A2FileDescr* pOpenFile) + { + fOpenFileName = fileName; + fOpenRsrcFork = rsrcFork; + fpFile = pFile; + fpOpenFile = pOpenFile; + fLength = 0; + if (fpOpenFile->Seek(0, DiskImgLib::kSeekEnd) == kDIErrNone) + fLength = fpOpenFile->Tell(); + } + + virtual int LoadData(void); // load data from the current offset + +private: + // overrides + virtual BOOL OnInitDialog(void); + + afx_msg virtual void OnReadPrev(void); + afx_msg virtual void OnReadNext(void); + + CString fOpenFileName; + bool fOpenRsrcFork; + A2File* fpFile; + A2FileDescr* fpOpenFile; + //long fOffset; + long fBlockIdx; + di_off_t fLength; +}; + +/* + * The "sector edit" dialog, which displays 256 bytes at a time, and + * accesses a disk by track/sector. + */ +class NibbleEditDialog : public DiskEditDialog { +public: + NibbleEditDialog(CWnd* pParentWnd = NULL) : + DiskEditDialog(IDD_DISKEDIT, pParentWnd) + { + fTrack = 0; + } + virtual ~NibbleEditDialog() {} + + virtual int LoadData(void); // load the current track/sector + virtual void DisplayData(void) { + DiskEditDialog::DisplayNibbleData(fNibbleData, fNibbleDataLen); + } + + // overrides + virtual BOOL OnInitDialog(void); + +protected: + afx_msg virtual void OnDoRead(void); + afx_msg virtual void OnDoWrite(void); + afx_msg virtual void OnReadPrev(void); + afx_msg virtual void OnReadNext(void); + afx_msg virtual void OnOpenFile(void) { ASSERT(false); } + afx_msg virtual void OnNibbleParms(void) { ASSERT(false); } + + long fTrack; + unsigned char fNibbleData[DiskImgLib::kTrackAllocSize]; + long fNibbleDataLen; +}; + +#endif /*__DISK_EDIT_DIALOG__*/ \ No newline at end of file diff --git a/app/DiskEditOpenDialog.cpp b/app/DiskEditOpenDialog.cpp new file mode 100644 index 0000000..c0c7783 --- /dev/null +++ b/app/DiskEditOpenDialog.cpp @@ -0,0 +1,53 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Simple dialog class that returns when any of its buttons are hit. + */ +#include "StdAfx.h" +#include "DiskEditOpenDialog.h" + +BEGIN_MESSAGE_MAP(DiskEditOpenDialog, CDialog) + ON_BN_CLICKED(IDC_DEOW_FILE, OnButtonFile) + ON_BN_CLICKED(IDC_DEOW_VOLUME, OnButtonVolume) + ON_BN_CLICKED(IDC_DEOW_CURRENT, OnButtonCurrent) +END_MESSAGE_MAP() + + +BOOL +DiskEditOpenDialog::OnInitDialog(void) +{ + if (!fArchiveOpen) { + CButton* pButton = (CButton*) GetDlgItem(IDC_DEOW_CURRENT); + ASSERT(pButton != nil); + pButton->EnableWindow(FALSE); + } + + return CDialog::OnInitDialog(); +} + +/* user clicked "open file" button */ +void +DiskEditOpenDialog::OnButtonFile(void) +{ + fOpenWhat = kOpenFile; + OnOK(); +} + +/* user clicked "open volume" button */ +void +DiskEditOpenDialog::OnButtonVolume(void) +{ + fOpenWhat = kOpenVolume; + OnOK(); +} + +/* user clicked "open current" button */ +void +DiskEditOpenDialog::OnButtonCurrent(void) +{ + fOpenWhat = kOpenCurrent; + OnOK(); +} diff --git a/app/DiskEditOpenDialog.h b/app/DiskEditOpenDialog.h new file mode 100644 index 0000000..b30b8f8 --- /dev/null +++ b/app/DiskEditOpenDialog.h @@ -0,0 +1,49 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Decide how to open the disk editor. + */ +#ifndef __DISKEDITOPENDIALOG__ +#define __DISKEDITOPENDIALOG__ + +#include +#include "resource.h" + +/* + * Very simple dialog class with three buttons (plus "cancel"). + * + * The button chosen will be returned in "fOpenWhat". + */ +class DiskEditOpenDialog : public CDialog { +public: + typedef enum { + kOpenUnknown = 0, + kOpenFile, + kOpenVolume, + kOpenCurrent, + } OpenWhat; + + DiskEditOpenDialog(CWnd* pParentWnd = NULL) : + CDialog(IDD_DISKEDIT_OPENWHICH, pParentWnd), + fArchiveOpen(false), fOpenWhat(kOpenUnknown) + {} + + // set this if the main content list has a file open + bool fArchiveOpen; + // return value -- which button was hit + OpenWhat fOpenWhat; + +private: + virtual BOOL OnInitDialog(void); + + afx_msg void OnButtonFile(void); + afx_msg void OnButtonVolume(void); + afx_msg void OnButtonCurrent(void); + + DECLARE_MESSAGE_MAP() +}; + +#endif /*__DISKEDITOPENDIALOG__*/ diff --git a/app/DiskFSTree.cpp b/app/DiskFSTree.cpp new file mode 100644 index 0000000..6cf4ef8 --- /dev/null +++ b/app/DiskFSTree.cpp @@ -0,0 +1,252 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * DiskFSTree implementation. + */ +#include "StdAfx.h" +#include "ChooseAddTargetDialog.h" +#include "HelpTopics.h" + +using namespace DiskImgLib; + +/* + * Build the tree. + */ +bool +DiskFSTree::BuildTree(DiskFS* pDiskFS, CTreeCtrl* pTree) +{ + ASSERT(pDiskFS != nil); + ASSERT(pTree != nil); + + pTree->SetImageList(&fTreeImageList, TVSIL_NORMAL); + return AddDiskFS(pTree, TVI_ROOT, pDiskFS, 1); +} + +/* + * Load the specified DiskFS into the tree, recursively adding any + * sub-volumes. + * + * Pass in an initial depth of 1. + * + * Returns "true" on success, "false" on failure. + */ +bool +DiskFSTree::AddDiskFS(CTreeCtrl* pTree, HTREEITEM parent, + DiskImgLib::DiskFS* pDiskFS, int depth) +{ + const DiskFS::SubVolume* pSubVol; + TargetData* pTarget; + HTREEITEM hLocalRoot; + TVITEM tvi; + TVINSERTSTRUCT tvins; + + /* + * Insert an entry for the current item. + */ + pTarget = AllocTargetData(); + pTarget->kind = kTargetDiskFS; + pTarget->pDiskFS = pDiskFS; + pTarget->pFile = nil; // could also use volume dir for ProDOS + tvi.mask = TVIF_TEXT | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_PARAM; + tvi.pszText = const_cast(pDiskFS->GetVolumeID()); + tvi.cchTextMax = 0; // not needed for insertitem +// tvi.iImage = kTreeImageFolderClosed; +// tvi.iSelectedImage = kTreeImageFolderOpen; + if (pDiskFS->GetReadWriteSupported() && !pDiskFS->GetFSDamaged()) { + tvi.iImage = kTreeImageHardDriveRW; + pTarget->selectable = true; + } else { + tvi.iImage = kTreeImageHardDriveRO; + pTarget->selectable = false; + } + tvi.iSelectedImage = tvi.iImage; + tvi.lParam = (LPARAM) pTarget; + tvins.item = tvi; + tvins.hInsertAfter = parent; + tvins.hParent = parent; + hLocalRoot = pTree->InsertItem(&tvins); + if (hLocalRoot == nil) { + WMSG0("Tree root InsertItem failed\n"); + return false; + } + + /* + * Scan for and handle all sub-volumes. + */ + pSubVol = pDiskFS->GetNextSubVolume(nil); + while (pSubVol != nil) { + if (!AddDiskFS(pTree, hLocalRoot, pSubVol->GetDiskFS(), depth+1)) + return false; + + pSubVol = pDiskFS->GetNextSubVolume(pSubVol); + } + + /* + * If this volume has sub-directories, and is read-write, add the subdirs + * to the tree. + * + * We use "depth" rather than "depth+1" because the first subdir entry + * (the volume dir) doesn't get its own entry. We use the disk entry + * to represent the disk's volume dir. + */ + if (fIncludeSubdirs && pDiskFS->GetReadWriteSupported() && + !pDiskFS->GetFSDamaged()) + { + AddSubdir(pTree, hLocalRoot, pDiskFS, nil, depth); + } + + /* + * If we're above the max expansion depth, expand the node. + */ + if (fExpandDepth == -1 || depth <= fExpandDepth) + pTree->Expand(hLocalRoot, TVE_EXPAND); + + /* + * Finally, if this is the root node, select it. + */ + if (parent == TVI_ROOT) { + pTree->Select(hLocalRoot, TVGN_CARET); + } + + + return true; +} + + +/* + * Add the subdir and all of the subdirectories of the current subdir. + * + * The files are held in a linear list in the DiskFS, so we have to + * reconstruct the hierarchy from the path names. Pass in nil for the + * root volume. + * + * Returns a pointer to the next A2File in the list (i.e. the first one + * that we couldn't digest). This assumes that the contents of a + * subdirectory are grouped together in the linear list, so that we can + * immediately bail when the first misfit is encountered. + */ +DiskImgLib::A2File* +DiskFSTree::AddSubdir(CTreeCtrl* pTree, HTREEITEM parent, + DiskImgLib::DiskFS* pDiskFS, DiskImgLib::A2File* pParentFile, + int depth) +{ + A2File* pFile; + TargetData* pTarget; + HTREEITEM hLocalRoot; + TVITEM tvi; + TVINSERTSTRUCT tvins; + + pFile = pDiskFS->GetNextFile(pParentFile); + if (pFile == nil && pParentFile == nil) { + /* this can happen on an empty DOS 3.3 disk; under ProDOS, we always + have the volume entry */ + /* note pFile will be nil if this happens to be a subdirectory + positioned as the very last file on the disk */ + return nil; + } + + if (pParentFile == nil) { + /* + * This is the root of the disk. We already have a DiskFS entry for + * it, so don't add a new tree item here. + * + * Check to see if this disk has a volume directory entry. + */ + if (pFile->IsVolumeDirectory()) { + pParentFile = pFile; + pFile = pDiskFS->GetNextFile(pFile); + } + hLocalRoot = parent; + } else { + /* + * Add an entry for this subdir (the "parent" entry). + */ + pTarget = AllocTargetData(); + pTarget->kind = kTargetSubdir; + pTarget->selectable = true; + pTarget->pDiskFS = pDiskFS; + pTarget->pFile = pParentFile; + tvi.mask = TVIF_TEXT | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_PARAM; + tvi.pszText = const_cast(pParentFile->GetFileName()); + tvi.cchTextMax = 0; // not needed for insertitem + tvi.iImage = kTreeImageFolderClosed; + tvi.iSelectedImage = kTreeImageFolderOpen; + tvi.lParam = (LPARAM) pTarget; + tvins.item = tvi; + tvins.hInsertAfter = parent; + tvins.hParent = parent; + hLocalRoot = pTree->InsertItem(&tvins); + if (hLocalRoot == nil) { + WMSG1("Tree insert '%s' failed\n", tvi.pszText); + return nil; + } + } + + while (pFile != nil) { + if (pFile->IsDirectory()) { + ASSERT(!pFile->IsVolumeDirectory()); + + if (pFile->GetParent() == pParentFile) { + /* this is a subdir of us */ + pFile = AddSubdir(pTree, hLocalRoot, pDiskFS, pFile, depth+1); + if (pFile == nil) + break; // out of while -- disk is done + } else { + /* not one of our subdirs; pop up a level */ + break; // out of while -- subdir is done + } + } else { + pFile = pDiskFS->GetNextFile(pFile); + } + } + + /* expand as appropriate */ + if (fExpandDepth == -1 || depth <= fExpandDepth) + pTree->Expand(hLocalRoot, TVE_EXPAND); + + return pFile; +} + + +/* + * Allocate a new TargetData struct, and add it to our list. + */ +DiskFSTree::TargetData* +DiskFSTree::AllocTargetData(void) +{ + TargetData* pNew = new TargetData; + + if (pNew == nil) + return nil; + memset(pNew, 0, sizeof(*pNew)); + + /* insert it at the head of the list, and update the head pointer */ + pNew->pNext = fpTargetData; + fpTargetData = pNew; + + return pNew; +} + +/* + * Free up the TargetData structures we created. + * + * Rather than + */ +void +DiskFSTree::FreeAllTargetData(void) +{ + TargetData* pTarget; + TargetData* pNext; + + pTarget = fpTargetData; + while (pTarget != nil) { + pNext = pTarget->pNext; + delete pTarget; + pTarget = pNext; + } + + fpTargetData = nil; +} diff --git a/app/DiskFSTree.h b/app/DiskFSTree.h new file mode 100644 index 0000000..b4b7750 --- /dev/null +++ b/app/DiskFSTree.h @@ -0,0 +1,81 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Fill out a CTreeCtrl with the results of a tree search through a DiskFS and + * its sub-volumes. + */ +#ifndef __DISKFSTREE__ +#define __DISKFSTREE__ + +#include "resource.h" +#include "../diskimg/DiskImg.h" + +/* + * This class could probably be part of DiskArchive, but things are pretty + * cluttered up there already. + */ +class DiskFSTree { +public: + DiskFSTree(void) { + fIncludeSubdirs = false; + fExpandDepth = 0; + + fpDiskFS = nil; + fpTargetData = nil; + LoadTreeImages(); + } + virtual ~DiskFSTree(void) { FreeAllTargetData(); } + + /* + * Create the contents of the tree control. + */ + bool BuildTree(DiskImgLib::DiskFS* pDiskFS, CTreeCtrl* pTree); + + /* if set, includes folders as well as disks */ + bool fIncludeSubdirs; + /* start with the tree expanded to this depth (0=none, -1=all) */ + int fExpandDepth; + + typedef enum { + kTargetUnknown = 0, kTargetDiskFS, kTargetSubdir + } TargetKind; + typedef struct TargetData { + TargetKind kind; + bool selectable; + DiskImgLib::DiskFS* pDiskFS; + DiskImgLib::A2File* pFile; + + // easier to keep a list than to chase through the tree + struct TargetData* pNext; + } TargetData; + +private: + bool AddDiskFS(CTreeCtrl* pTree, HTREEITEM root, + DiskImgLib::DiskFS* pDiskFS, int depth); + DiskImgLib::A2File* AddSubdir(CTreeCtrl* pTree, HTREEITEM parent, + DiskImgLib::DiskFS* pDiskFS, DiskImgLib::A2File* pFile, + int depth); + TargetData* AllocTargetData(void); + void FreeAllTargetData(void); + + void LoadTreeImages(void) { + if (!fTreeImageList.Create(IDB_TREE_PICS, 16, 1, CLR_DEFAULT)) + WMSG0("GLITCH: list image create failed\n"); + fTreeImageList.SetBkColor(::GetSysColor(COLOR_WINDOW)); + } + enum { // defs for IDB_TREE_PICS + kTreeImageFolderClosed = 0, + kTreeImageFolderOpen = 1, + kTreeImageHardDriveRW = 2, + kTreeImageHardDriveRO = 3, + }; + CImageList fTreeImageList; + + DiskImgLib::DiskFS* fpDiskFS; + TargetData* fpTargetData; +}; + +#endif /*__DISKFSTREE__*/ diff --git a/app/DoneOpenDialog.h b/app/DoneOpenDialog.h new file mode 100644 index 0000000..5d38b0a --- /dev/null +++ b/app/DoneOpenDialog.h @@ -0,0 +1,16 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Simple dialog to offer the opportunity to open the file we just created. + */ +#include "resource.h" + +class DoneOpenDialog : public CDialog { +public: + DoneOpenDialog(CWnd* pParentWnd = NULL) : CDialog(IDD_DONEOPEN, pParentWnd) + {} + virtual ~DoneOpenDialog(void) {} +}; diff --git a/app/EOLScanDialog.cpp b/app/EOLScanDialog.cpp new file mode 100644 index 0000000..ffc55d0 --- /dev/null +++ b/app/EOLScanDialog.cpp @@ -0,0 +1,59 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Trivial implementation of EOLScanDialog. + * + * I'd stuff the whole thing in the header, but I need the "help" button to + * work, and it's easiest to do that through a message map. + */ +#include "StdAfx.h" +#include "EOLScanDialog.h" +#include "HelpTopics.h" + +BEGIN_MESSAGE_MAP(EOLScanDialog, CDialog) + ON_COMMAND(IDHELP, OnHelp) +END_MESSAGE_MAP() + +/* + * Fill in the blanks. + */ +BOOL +EOLScanDialog::OnInitDialog(void) +{ + CWnd* pWnd; + CString fmt; + + fmt.Format("%ld", fCountChars); + pWnd = GetDlgItem(IDC_EOLSCAN_CHARS); + pWnd->SetWindowText(fmt); + + fmt.Format("%ld", fCountCR); + pWnd = GetDlgItem(IDC_EOLSCAN_CR); + pWnd->SetWindowText(fmt); + + fmt.Format("%ld", fCountLF); + pWnd = GetDlgItem(IDC_EOLSCAN_LF); + pWnd->SetWindowText(fmt); + + fmt.Format("%ld", fCountCRLF); + pWnd = GetDlgItem(IDC_EOLSCAN_CRLF); + pWnd->SetWindowText(fmt); + + fmt.Format("%ld", fCountHighASCII); + pWnd = GetDlgItem(IDC_EOLSCAN_HIGHASCII); + pWnd->SetWindowText(fmt); + + return CDialog::OnInitDialog(); +} + +/* + * User pressed the "Help" button. + */ +void +EOLScanDialog::OnHelp(void) +{ + WinHelp(HELP_TOPIC_EOL_SCAN, HELP_CONTEXT); +} diff --git a/app/EOLScanDialog.h b/app/EOLScanDialog.h new file mode 100644 index 0000000..68bb642 --- /dev/null +++ b/app/EOLScanDialog.h @@ -0,0 +1,37 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * A simple dialog to display the results of an EOL scan. + */ +#ifndef __EOLSCANDIALOG__ +#define __EOLSCANDIALOG__ + +#include "resource.h" + +/* + * Entire class is here. + */ +class EOLScanDialog : public CDialog { +public: + EOLScanDialog(CWnd* pParentWnd = NULL) : + CDialog(IDD_EOLSCAN, pParentWnd) + {} + virtual ~EOLScanDialog(void) {} + + long fCountChars; + long fCountCR; + long fCountLF; + long fCountCRLF; + long fCountHighASCII; + +private: + BOOL OnInitDialog(void); + afx_msg void OnHelp(void); + + DECLARE_MESSAGE_MAP() +}; + +#endif /*__EOLSCANDIALOG__*/ diff --git a/app/EditAssocDialog.cpp b/app/EditAssocDialog.cpp new file mode 100644 index 0000000..c11a7c4 --- /dev/null +++ b/app/EditAssocDialog.cpp @@ -0,0 +1,159 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Support for EditAssocDialog. + */ +#include "stdafx.h" +#include "EditAssocDialog.h" +#include "MyApp.h" +#include "Registry.h" +#include "HelpTopics.h" + +BEGIN_MESSAGE_MAP(EditAssocDialog, CDialog) + ON_WM_HELPINFO() + ON_COMMAND(IDHELP, OnHelp) +END_MESSAGE_MAP() + +/* this comes from VC++6.0 MSDN help */ +#ifndef ListView_SetCheckState + #define ListView_SetCheckState(hwndLV, i, fCheck) \ + ListView_SetItemState(hwndLV, i, \ + INDEXTOSTATEIMAGEMASK((fCheck)+1), LVIS_STATEIMAGEMASK) +#endif + +/* + * Tweak the controls. + */ +BOOL +EditAssocDialog::OnInitDialog(void) +{ + CListCtrl* pListView = (CListCtrl*) GetDlgItem(IDC_ASSOCIATION_LIST); + + ASSERT(pListView != nil); + //pListView->ModifyStyleEx(0, LVS_EX_CHECKBOXES); + ListView_SetExtendedListViewStyleEx(pListView->m_hWnd, + LVS_EX_CHECKBOXES, LVS_EX_CHECKBOXES); + + /* move it over slightly so we see some overlap */ + CRect rect; + GetWindowRect(&rect); + rect.left += 10; + rect.right += 10; + MoveWindow(&rect); + + + /* + * Initialize this before DDX stuff happens. If the caller didn't + * provide a set, load our own. + */ + if (fOurAssociations == nil) { + fOurAssociations = new bool[gMyApp.fRegistry.GetNumFileAssocs()]; + Setup(true); + } else { + Setup(false); + } + + return CDialog::OnInitDialog(); +} + +/* + * Load the list view control. + * + * This list isn't sorted, so we don't need to stuff anything into lParam to + * keep the list and source data tied. + * + * If "loadAssoc" is true, we also populate the fOurAssocations table. + */ +void +EditAssocDialog::Setup(bool loadAssoc) +{ + WMSG0("Setup!\n"); + + CListCtrl* pListView = (CListCtrl*) GetDlgItem(IDC_ASSOCIATION_LIST); + ASSERT(pListView != nil); + + ASSERT(fOurAssociations != nil); + + /* two columns */ + CRect rect; + pListView->GetClientRect(&rect); + int width; + + width = pListView->GetStringWidth("XXExtensionXX"); + pListView->InsertColumn(0, "Extension", LVCFMT_LEFT, width); + pListView->InsertColumn(1, "Association", LVCFMT_LEFT, + rect.Width() - width); + + int num = gMyApp.fRegistry.GetNumFileAssocs(); + int idx = 0; + while (num--) { + CString ext, handler; + CString dispStr; + bool ours; + + gMyApp.fRegistry.GetFileAssoc(idx, &ext, &handler, &ours); + + pListView->InsertItem(idx, ext); + pListView->SetItemText(idx, 1, handler); + + if (loadAssoc) + fOurAssociations[idx] = ours; + idx++; + } + + //DeleteAllItems(); // for Reload case +} + +/* + * Copy state in and out of dialog. + */ +void +EditAssocDialog::DoDataExchange(CDataExchange* pDX) +{ + CListCtrl* pListView = (CListCtrl*) GetDlgItem(IDC_ASSOCIATION_LIST); + + ASSERT(fOurAssociations != nil); + if (fOurAssociations == nil) + return; + + int num = gMyApp.fRegistry.GetNumFileAssocs(); + + if (!pDX->m_bSaveAndValidate) { + /* load fixed set of file associations */ + int idx = 0; + while (num--) { + ListView_SetCheckState(pListView->m_hWnd, idx, + fOurAssociations[idx]); + idx++; + } + } else { + /* copy the checkboxes out */ + int idx = 0; + while (num--) { + fOurAssociations[idx] = + (ListView_GetCheckState(pListView->m_hWnd, idx) != 0); + idx++; + } + } +} + +/* + * Context help request (question mark button). + */ +BOOL +EditAssocDialog::OnHelpInfo(HELPINFO* lpHelpInfo) +{ + return ShowContextHelp(this, lpHelpInfo); +} + +/* + * User pressed the "Help" button. + */ +void +EditAssocDialog::OnHelp(void) +{ + WinHelp(HELP_TOPIC_EDIT_ASSOC, HELP_CONTEXT); +} diff --git a/app/EditAssocDialog.h b/app/EditAssocDialog.h new file mode 100644 index 0000000..0a78548 --- /dev/null +++ b/app/EditAssocDialog.h @@ -0,0 +1,45 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * File associations edit dialog. + */ +#ifndef __EDITASSOCDIALOG__ +#define __EDITASSOCDIALOG__ + +#include "resource.h" + +/* + * Edit whatever associations our registry class cares about. + */ +class EditAssocDialog : public CDialog { +public: + EditAssocDialog(CWnd* pParentWnd = nil) : + CDialog(IDD_ASSOCIATIONS, pParentWnd), + fOurAssociations(nil) + {} + virtual ~EditAssocDialog() { + delete[] fOurAssociations; + } + + // Which associations are ours. This should be left uninitialized; + // Setup() takes care of that. The caller may "steal" the array + // afterward, freeing it with delete[]. + bool* fOurAssociations; + +protected: + // overrides + virtual BOOL OnInitDialog(void); + BOOL OnHelpInfo(HELPINFO* lpHelpInfo); + void DoDataExchange(CDataExchange* pDX); + + afx_msg void OnHelp(void); + + void Setup(bool loadAssoc); + + DECLARE_MESSAGE_MAP() +}; + +#endif /*__EDITASSOCDIALOG__*/ \ No newline at end of file diff --git a/app/EditCommentDialog.cpp b/app/EditCommentDialog.cpp new file mode 100644 index 0000000..eaec3da --- /dev/null +++ b/app/EditCommentDialog.cpp @@ -0,0 +1,79 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Support for EditCommentDialog. + */ +#include "stdafx.h" +#include "EditCommentDialog.h" +#include "HelpTopics.h" + +BEGIN_MESSAGE_MAP(EditCommentDialog, CDialog) + ON_BN_CLICKED(IDC_COMMENT_DELETE, OnDelete) + ON_WM_HELPINFO() + ON_COMMAND(IDHELP, OnHelp) +END_MESSAGE_MAP() + + +/* + * Set up the control. If this is a new comment, don't show the delete + * button. + */ +BOOL +EditCommentDialog::OnInitDialog(void) +{ + if (fNewComment) { + CWnd* pWnd = GetDlgItem(IDC_COMMENT_DELETE); + pWnd->EnableWindow(FALSE); + } + + return CDialog::OnInitDialog(); +} + +/* + * Convert values. + */ +void +EditCommentDialog::DoDataExchange(CDataExchange* pDX) +{ + DDX_Text(pDX, IDC_COMMENT_EDIT, fComment); +} + +/* + * User wants to delete the comment. Verify first. + */ +void +EditCommentDialog::OnDelete(void) +{ + CString question, title; + int result; + + title.LoadString(IDS_EDIT_COMMENT); + question.LoadString(IDS_DEL_COMMENT_OK); + result = MessageBox(question, title, MB_OKCANCEL | MB_ICONQUESTION); + if (result == IDCANCEL) + return; + + EndDialog(kDeleteCommentID); +} + +/* + * Context help request (question mark button). + */ +BOOL +EditCommentDialog::OnHelpInfo(HELPINFO* lpHelpInfo) +{ + WinHelp((DWORD) lpHelpInfo->iCtrlId, HELP_CONTEXTPOPUP); + return TRUE; // yes, we handled it +} + +/* + * User pressed the "Help" button. + */ +void +EditCommentDialog::OnHelp(void) +{ + WinHelp(HELP_TOPIC_EDIT_COMMENT, HELP_CONTEXT); +} diff --git a/app/EditCommentDialog.h b/app/EditCommentDialog.h new file mode 100644 index 0000000..2591da5 --- /dev/null +++ b/app/EditCommentDialog.h @@ -0,0 +1,47 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Edit a comment. + */ +#ifndef __EDITCOMMENTDIALOG__ +#define __EDITCOMMENTDIALOG__ + +#include "GenericArchive.h" +#include "resource.h" + +/* + * Edit a comment. We don't currently put a length limit on the comment + * field. + */ +class EditCommentDialog : public CDialog { +public: + EditCommentDialog(CWnd* pParentWnd = NULL) : + CDialog(IDD_COMMENT_EDIT, pParentWnd) + { + //fComment = ""; + fNewComment = false; + } + virtual ~EditCommentDialog(void) {} + + enum { kDeleteCommentID = IDC_COMMENT_DELETE }; + + CString fComment; + bool fNewComment; // entry doesn't already have one + +protected: + // overrides + virtual BOOL OnInitDialog(void); + virtual void DoDataExchange(CDataExchange* pDX); + + afx_msg BOOL OnHelpInfo(HELPINFO* lpHelpInfo); + afx_msg void OnHelp(void); + afx_msg void OnDelete(void); + +private: + DECLARE_MESSAGE_MAP() +}; + +#endif /*__EDITCOMMENTDIALOG__*/ \ No newline at end of file diff --git a/app/EditPropsDialog.cpp b/app/EditPropsDialog.cpp new file mode 100644 index 0000000..50efa4b --- /dev/null +++ b/app/EditPropsDialog.cpp @@ -0,0 +1,523 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Support for file properties edit dialog. + */ +#include "StdAfx.h" +#include "EditPropsDialog.h" +#include "FileNameConv.h" +#include "HelpTopics.h" + +using namespace DiskImgLib; + +BEGIN_MESSAGE_MAP(EditPropsDialog, CDialog) + ON_BN_CLICKED(IDC_PROPS_ACCESS_W, UpdateSimpleAccess) + ON_BN_CLICKED(IDC_PROPS_HFS_MODE, UpdateHFSMode) + ON_CBN_SELCHANGE(IDC_PROPS_FILETYPE, OnTypeChange) + ON_EN_CHANGE(IDC_PROPS_AUXTYPE, OnTypeChange) + ON_EN_CHANGE(IDC_PROPS_HFS_FILETYPE, OnHFSTypeChange) + ON_EN_CHANGE(IDC_PROPS_HFS_AUXTYPE, OnHFSTypeChange) + ON_WM_HELPINFO() + ON_COMMAND(IDHELP, OnHelp) +END_MESSAGE_MAP() + + +/* + * Initialize fProps from the stuff in pEntry. + */ +void +EditPropsDialog::InitProps(GenericEntry* pEntry) +{ + fPathName = pEntry->GetPathName(); + fProps.fileType = pEntry->GetFileType(); + fProps.auxType = pEntry->GetAuxType(); + fProps.access = pEntry->GetAccess(); + fProps.createWhen = pEntry->GetCreateWhen(); + fProps.modWhen = pEntry->GetModWhen(); + + if (!pEntry->GetFeatureFlag(GenericEntry::kFeatureCanChangeType)) + fAllowedTypes = kAllowedNone; + else if (pEntry->GetFeatureFlag(GenericEntry::kFeaturePascalTypes)) + fAllowedTypes = kAllowedPascal; + else if (pEntry->GetFeatureFlag(GenericEntry::kFeatureDOSTypes)) + fAllowedTypes = kAllowedDOS; + else if (pEntry->GetFeatureFlag(GenericEntry::kFeatureHFSTypes)) + fAllowedTypes = kAllowedHFS; // for HFS disks and ShrinkIt archives + else + fAllowedTypes = kAllowedProDOS; + if (!pEntry->GetFeatureFlag(GenericEntry::kFeatureHasFullAccess)) { + if (pEntry->GetFeatureFlag(GenericEntry::kFeatureHasSimpleAccess)) + fSimpleAccess = true; + else + fNoChangeAccess = true; + } + if (pEntry->GetFeatureFlag(GenericEntry::kFeatureHasInvisibleFlag)) + fAllowInvis = true; +} + + +/* + * Set up the control. We need to load the drop list with the file type + * info, and configure any controls that aren't set by DoDataExchange. + * + * If this is a disk archive, we might want to make the aux type read-only, + * though this would provide a way for users to fix badly-formed archives. + */ +BOOL +EditPropsDialog::OnInitDialog(void) +{ + static const int kPascalTypes[] = { + 0x00 /*NON*/, 0x01 /*BAD*/, 0x02 /*PCD*/, 0x03 /*PTX*/, + 0xf3 /*$F3*/, 0x05 /*PDA*/, 0xf4 /*$F4*/, 0x08 /*FOT*/, + 0xf5 /*$f5*/ + }; + static const int kDOSTypes[] = { + 0x04 /*TXT*/, 0x06 /*BIN*/, 0xf2 /*$F2*/, 0xf3 /*$F3*/, + 0xf4 /*$F4*/, 0xfa /*INT*/, 0xfc /*BAS*/, 0xfe /*REL*/ + }; + CComboBox* pCombo; + CWnd* pWnd; + int comboIdx; + + pCombo = (CComboBox*) GetDlgItem(IDC_PROPS_FILETYPE); + ASSERT(pCombo != nil); + + pCombo->InitStorage(256, 256 * 8); + + for (int type = 0; type < 256; type++) { + const char* str; + char buf[10]; + + if (fAllowedTypes == kAllowedPascal) { + /* not the most efficient way, but it'll do */ + for (int j = 0; j < NELEM(kPascalTypes); j++) { + if (kPascalTypes[j] == type) + break; + } + if (j == NELEM(kPascalTypes)) + continue; + } else if (fAllowedTypes == kAllowedDOS) { + for (int j = 0; j < NELEM(kDOSTypes); j++) { + if (kDOSTypes[j] == type) + break; + } + if (j == NELEM(kDOSTypes)) + continue; + } + + str = PathProposal::FileTypeString(type); + if (str[0] == '$') + sprintf(buf, "??? $%02X", type); + else + sprintf(buf, "%s $%02X", str, type); + comboIdx = pCombo->AddString(buf); + pCombo->SetItemData(comboIdx, type); + + if ((int) fProps.fileType == type) + pCombo->SetCurSel(comboIdx); + } + if (fProps.fileType >= 256) { + if (fAllowedTypes == kAllowedHFS) { + pCombo->SetCurSel(0); + } else { + // unexpected -- bogus data out of DiskFS? + comboIdx = pCombo->AddString("???"); + pCombo->SetCurSel(comboIdx); + pCombo->SetItemData(comboIdx, 256); + } + } + + CString dateStr; + pWnd = GetDlgItem(IDC_PROPS_CREATEWHEN); + ASSERT(pWnd != nil); + FormatDate(fProps.createWhen, &dateStr); + pWnd->SetWindowText(dateStr); + + pWnd = GetDlgItem(IDC_PROPS_MODWHEN); + ASSERT(pWnd != nil); + FormatDate(fProps.modWhen, &dateStr); + pWnd->SetWindowText(dateStr); + //WMSG2("USING DATE '%s' from 0x%08lx\n", dateStr, fProps.modWhen); + + CEdit* pEdit = (CEdit*) GetDlgItem(IDC_PROPS_AUXTYPE); + ASSERT(pEdit != nil); + pEdit->SetLimitText(4); // max len of aux type str + pEdit = (CEdit*) GetDlgItem(IDC_PROPS_HFS_FILETYPE); + pEdit->SetLimitText(4); + pEdit = (CEdit*) GetDlgItem(IDC_PROPS_HFS_AUXTYPE); + pEdit->SetLimitText(4); + + if (fReadOnly || fAllowedTypes == kAllowedNone) { + pWnd = GetDlgItem(IDC_PROPS_FILETYPE); + pWnd->EnableWindow(FALSE); + pWnd = GetDlgItem(IDC_PROPS_AUXTYPE); + pWnd->EnableWindow(FALSE); + } else if (fAllowedTypes == kAllowedPascal) { + pWnd = GetDlgItem(IDC_PROPS_AUXTYPE); + pWnd->EnableWindow(FALSE); + } + if (fReadOnly || fSimpleAccess || fNoChangeAccess) { + pWnd = GetDlgItem(IDC_PROPS_ACCESS_R); + pWnd->EnableWindow(FALSE); + pWnd = GetDlgItem(IDC_PROPS_ACCESS_B); + pWnd->EnableWindow(FALSE); + pWnd = GetDlgItem(IDC_PROPS_ACCESS_N); + pWnd->EnableWindow(FALSE); + pWnd = GetDlgItem(IDC_PROPS_ACCESS_D); + pWnd->EnableWindow(FALSE); + } + if (fReadOnly || !fAllowInvis) { + pWnd = GetDlgItem(IDC_PROPS_ACCESS_I); + pWnd->EnableWindow(FALSE); + } + if (fReadOnly || fNoChangeAccess) { + pWnd = GetDlgItem(IDC_PROPS_ACCESS_W); + pWnd->EnableWindow(FALSE); + } + if (fReadOnly) { + pWnd = GetDlgItem(IDOK); + pWnd->EnableWindow(FALSE); + + CString title; + GetWindowText(/*ref*/ title); + title = title + " (read only)"; + SetWindowText(title); + } + + if (fAllowedTypes != kAllowedHFS) { + CButton* pButton = (CButton*) GetDlgItem(IDC_PROPS_HFS_MODE); + pButton->EnableWindow(FALSE); + } + + return CDialog::OnInitDialog(); +} + +/* + * Convert values. + */ +void +EditPropsDialog::DoDataExchange(CDataExchange* pDX) +{ + int fileTypeIdx; + BOOL accessR, accessW, accessI, accessB, accessN, accessD; + CComboBox* pCombo = (CComboBox*) GetDlgItem(IDC_PROPS_FILETYPE); + + if (pDX->m_bSaveAndValidate) { + CString appName; + CButton *pButton; + bool typeChanged = false; + + appName.LoadString(IDS_MB_APP_NAME); + + pButton = (CButton*) GetDlgItem(IDC_PROPS_HFS_MODE); + if (pButton->GetCheck() == BST_CHECKED) { + /* HFS mode */ + CString type, creator; + DDX_Text(pDX, IDC_PROPS_HFS_FILETYPE, type); + DDX_Text(pDX, IDC_PROPS_HFS_AUXTYPE, creator); + if (type.GetLength() != 4 || creator.GetLength() != 4) { + MessageBox("The file and creator types must be exactly" + " 4 characters each.", + appName, MB_OK); + pDX->Fail(); + return; + } + fProps.fileType = ((unsigned char) type[0]) << 24 | + ((unsigned char) type[1]) << 16 | + ((unsigned char) type[2]) << 8 | + ((unsigned char) type[3]); + fProps.auxType = ((unsigned char) creator[0]) << 24 | + ((unsigned char) creator[1]) << 16 | + ((unsigned char) creator[2]) << 8 | + ((unsigned char) creator[3]); + } else { + /* ProDOS mode */ + if (GetAuxType() < 0) { + MessageBox("The AuxType field must be a valid 4-digit" + " hexadecimal number.", + appName, MB_OK); + pDX->Fail(); + return; + } + fProps.auxType = GetAuxType(); + + /* pull the file type out, but don't disturb >= 256 */ + DDX_CBIndex(pDX, IDC_PROPS_FILETYPE, fileTypeIdx); + if (fileTypeIdx != 256) { + unsigned long oldType = fProps.fileType; + fProps.fileType = pCombo->GetItemData(fileTypeIdx); + if (fProps.fileType != oldType) + typeChanged = true; + } + } + + DDX_Check(pDX, IDC_PROPS_ACCESS_R, accessR); + DDX_Check(pDX, IDC_PROPS_ACCESS_W, accessW); + DDX_Check(pDX, IDC_PROPS_ACCESS_I, accessI); + DDX_Check(pDX, IDC_PROPS_ACCESS_B, accessB); + DDX_Check(pDX, IDC_PROPS_ACCESS_N, accessN); + DDX_Check(pDX, IDC_PROPS_ACCESS_D, accessD); + fProps.access = (accessR ? GenericEntry::kAccessRead : 0) | + (accessW ? GenericEntry::kAccessWrite : 0) | + (accessI ? GenericEntry::kAccessInvisible : 0) | + (accessB ? GenericEntry::kAccessBackup : 0) | + (accessN ? GenericEntry::kAccessRename : 0) | + (accessD ? GenericEntry::kAccessDelete : 0); + + if (fAllowedTypes == kAllowedDOS && typeChanged && + (fProps.fileType == kFileTypeBIN || + fProps.fileType == kFileTypeINT || + fProps.fileType == kFileTypeBAS)) + { + CString msg; + int result; + + msg.LoadString(IDS_PROPS_DOS_TYPE_CHANGE); + result = MessageBox(msg, appName, MB_ICONQUESTION|MB_OKCANCEL); + if (result != IDOK) { + pDX->Fail(); + return; + } + } + } else { + accessR = (fProps.access & GenericEntry::kAccessRead) != 0; + accessW = (fProps.access & GenericEntry::kAccessWrite) != 0; + accessI = (fProps.access & GenericEntry::kAccessInvisible) != 0; + accessB = (fProps.access & GenericEntry::kAccessBackup) != 0; + accessN = (fProps.access & GenericEntry::kAccessRename) != 0; + accessD = (fProps.access & GenericEntry::kAccessDelete) != 0; + DDX_Check(pDX, IDC_PROPS_ACCESS_R, accessR); + DDX_Check(pDX, IDC_PROPS_ACCESS_W, accessW); + DDX_Check(pDX, IDC_PROPS_ACCESS_I, accessI); + DDX_Check(pDX, IDC_PROPS_ACCESS_B, accessB); + DDX_Check(pDX, IDC_PROPS_ACCESS_N, accessN); + DDX_Check(pDX, IDC_PROPS_ACCESS_D, accessD); + + if (fAllowedTypes == kAllowedHFS && + (fProps.fileType > 0xff || fProps.auxType > 0xffff)) + { + char type[5], creator[5]; + + type[0] = (unsigned char) (fProps.fileType >> 24); + type[1] = (unsigned char) (fProps.fileType >> 16); + type[2] = (unsigned char) (fProps.fileType >> 8); + type[3] = (unsigned char) fProps.fileType; + type[4] = '\0'; + creator[0] = (unsigned char) (fProps.auxType >> 24); + creator[1] = (unsigned char) (fProps.auxType >> 16); + creator[2] = (unsigned char) (fProps.auxType >> 8); + creator[3] = (unsigned char) fProps.auxType; + creator[4] = '\0'; + + CString tmpStr; + tmpStr = type; + DDX_Text(pDX, IDC_PROPS_HFS_FILETYPE, tmpStr); + tmpStr = creator; + DDX_Text(pDX, IDC_PROPS_HFS_AUXTYPE, tmpStr); + tmpStr = "0000"; + DDX_Text(pDX, IDC_PROPS_AUXTYPE, tmpStr); + + CButton* pButton = (CButton*) GetDlgItem(IDC_PROPS_HFS_MODE); + pButton->SetCheck(BST_CHECKED); + } else { + //fileTypeIdx = fProps.fileType; + //if (fileTypeIdx > 256) + // fileTypeIdx = 256; + //DDX_CBIndex(pDX, IDC_PROPS_FILETYPE, fileTypeIdx); + + /* write the aux type as a hex string */ + fAuxType.Format("%04X", fProps.auxType); + DDX_Text(pDX, IDC_PROPS_AUXTYPE, fAuxType); + } + OnTypeChange(); // set the description field + UpdateHFSMode(); // set up fields + UpdateSimpleAccess(); // coordinate N/D with W + } + + DDX_Text(pDX, IDC_PROPS_PATHNAME, fPathName); +} + +/* + * This is called when the file type selection changes or something is + * typed in the aux type box. + * + * We use this notification to configure the type description field. + * + * Typing in the ProDOS aux type box causes us to nuke the HFS values. + * If we were in "HFS mode" we reset the file type to zero. + */ +void +EditPropsDialog::OnTypeChange(void) +{ + static const char* kUnknownFileType = "Unknown file type"; + CComboBox* pCombo; + CWnd* pWnd; + int fileType, fileTypeIdx; + long auxType; + const char* descr = nil; + + pCombo = (CComboBox*) GetDlgItem(IDC_PROPS_FILETYPE); + ASSERT(pCombo != nil); + + fileTypeIdx = pCombo->GetCurSel(); + fileType = pCombo->GetItemData(fileTypeIdx); + if (fileType >= 256) { + descr = kUnknownFileType; + } else { + auxType = GetAuxType(); + if (auxType < 0) + auxType = 0; + descr = PathProposal::FileTypeDescription(fileType, auxType); + if (descr == nil) + descr = kUnknownFileType; + } + + pWnd = GetDlgItem(IDC_PROPS_TYPEDESCR); + ASSERT(pWnd != nil); + pWnd->SetWindowText(descr); + + /* DOS aux type only applies to BIN */ + if (!fReadOnly && fAllowedTypes == kAllowedDOS) { + pWnd = GetDlgItem(IDC_PROPS_AUXTYPE); + pWnd->EnableWindow(fileType == kFileTypeBIN); + } +} + +/* + * Called when something is typed in one of the HFS type boxes. + */ +void +EditPropsDialog::OnHFSTypeChange(void) +{ + assert(fAllowedTypes == kAllowedHFS); +} + +/* + * Called initially and when switching modes. + */ +void +EditPropsDialog::UpdateHFSMode(void) +{ + CButton* pButton = (CButton*) GetDlgItem(IDC_PROPS_HFS_MODE); + CComboBox* pCombo; + CWnd* pWnd; + + if (pButton->GetCheck() == BST_CHECKED) { + /* switch to HFS mode */ + WMSG0("Switching to HFS mode\n"); + //fHFSMode = true; + + pWnd = GetDlgItem(IDC_PROPS_HFS_FILETYPE); + pWnd->EnableWindow(TRUE); + pWnd = GetDlgItem(IDC_PROPS_HFS_AUXTYPE); + pWnd->EnableWindow(TRUE); + pWnd = GetDlgItem(IDC_PROPS_HFS_LABEL); + pWnd->EnableWindow(TRUE); + + /* point the file type at something safe */ + pCombo = (CComboBox*) GetDlgItem(IDC_PROPS_FILETYPE); + pCombo->EnableWindow(FALSE); + + pWnd = GetDlgItem(IDC_PROPS_AUXTYPE); + pWnd->EnableWindow(FALSE); + + pWnd = GetDlgItem(IDC_PROPS_TYPEDESCR); + ASSERT(pWnd != nil); + pWnd->SetWindowText("(HFS type)"); + OnHFSTypeChange(); + } else { + /* switch to ProDOS mode */ + WMSG0("Switching to ProDOS mode\n"); + //fHFSMode = false; + pCombo = (CComboBox*) GetDlgItem(IDC_PROPS_FILETYPE); + pCombo->EnableWindow(TRUE); + pWnd = GetDlgItem(IDC_PROPS_AUXTYPE); + pWnd->EnableWindow(TRUE); + + pWnd = GetDlgItem(IDC_PROPS_HFS_FILETYPE); + pWnd->EnableWindow(FALSE); + pWnd = GetDlgItem(IDC_PROPS_HFS_AUXTYPE); + pWnd->EnableWindow(FALSE); + pWnd = GetDlgItem(IDC_PROPS_HFS_LABEL); + pWnd->EnableWindow(FALSE); + OnTypeChange(); + } +} + +/* + * For "simple" access formats, i.e. DOS 3.2/3.3, the "write" button acts + * as a "locked" flag. We want the other rename/delete flags to track this + * one. + */ +void +EditPropsDialog::UpdateSimpleAccess(void) +{ + if (!fSimpleAccess) + return; + + CButton* pButton; + UINT checked; + + pButton = (CButton*) GetDlgItem(IDC_PROPS_ACCESS_W); + checked = pButton->GetCheck(); + + pButton = (CButton*) GetDlgItem(IDC_PROPS_ACCESS_N); + pButton->SetCheck(checked); + pButton = (CButton*) GetDlgItem(IDC_PROPS_ACCESS_D); + pButton->SetCheck(checked); +} + + +/* + * Get the aux type. + * + * Returns -1 if something was wrong with the string (e.g. empty or has + * invalid chars). + */ +long +EditPropsDialog::GetAuxType(void) +{ + CWnd* pWnd = GetDlgItem(IDC_PROPS_AUXTYPE); + ASSERT(pWnd != nil); + + CString aux; + pWnd->GetWindowText(aux); + + const char* str = aux; + char* end; + long val; + + if (str[0] == '\0') { + WMSG0(" HEY: blank aux type, returning -1\n"); + return -1; + } + val = strtoul(aux, &end, 16); + if (end != str + strlen(str)) { + WMSG1(" HEY: found some garbage in aux type '%s', returning -1\n", + (LPCTSTR) aux); + return -1; + } + return val; +} + +/* + * Context help request (question mark button). + */ +BOOL +EditPropsDialog::OnHelpInfo(HELPINFO* lpHelpInfo) +{ + WinHelp((DWORD) lpHelpInfo->iCtrlId, HELP_CONTEXTPOPUP); + return TRUE; // yes, we handled it +} + +/* + * User pressed the "Help" button. + */ +void +EditPropsDialog::OnHelp(void) +{ + WinHelp(HELP_TOPIC_EDIT_PROPS, HELP_CONTEXT); +} diff --git a/app/EditPropsDialog.h b/app/EditPropsDialog.h new file mode 100644 index 0000000..96c3bf9 --- /dev/null +++ b/app/EditPropsDialog.h @@ -0,0 +1,88 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Edit file properties. + */ +#ifndef __EDITPROPSDIALOG__ +#define __EDITPROPSDIALOG__ + +#include "GenericArchive.h" +#include "resource.h" + +/* + * Edit ProDOS file attributes, such as file type and auxtype. + */ +class EditPropsDialog : public CDialog { +public: + typedef enum AllowedTypes { + kAllowedUnknown = 0, + kAllowedProDOS, // 8-bit type, 16-bit aux + kAllowedHFS, // 32-bit type, 32-bit aux + kAllowedNone, // CP/M + kAllowedPascal, // UCSD Pascal + kAllowedDOS, // DOS 3.2/3.3 + } AllowedTypes; + + EditPropsDialog(CWnd* pParentWnd = NULL) : + CDialog(IDD_PROPS_EDIT, pParentWnd) + { + memset(&fProps, 0, sizeof(fProps)); + fReadOnly = false; + fAllowedTypes = kAllowedProDOS; + fSimpleAccess = false; + fNoChangeAccess = false; + fAllowInvis = false; + //fHFSMode = false; + fHFSComboIdx = -1; + } + ~EditPropsDialog(void) {} + + /* these get handed to GenericArchive */ + FileProps fProps; + + /* initialize fProps and other fields from pEntry */ + void InitProps(GenericEntry* pEntry); + + /* set this to disable editing of all fields */ + bool fReadOnly; + +private: + // overrides + virtual BOOL OnInitDialog(void); + virtual void DoDataExchange(CDataExchange* pDX); + + afx_msg void OnTypeChange(void); + afx_msg void OnHFSTypeChange(void); + afx_msg void OnHelp(void); + afx_msg BOOL OnHelpInfo(HELPINFO* lpHelpInfo); + + void UpdateSimpleAccess(void); + void UpdateHFSMode(void); + long GetAuxType(void); + //void ShowHFSType(void); + + /* what sort of type changes do we allow? */ + AllowedTypes fAllowedTypes; + /* set this to disable access to fields other than 'W' */ + bool fSimpleAccess; + /* set this to disable file access fields */ + bool fNoChangeAccess; + /* this enabled the 'I' flag, independent of other settings */ + bool fAllowInvis; + + /* are we in "ProDOS mode" or "HFS mode"? */ + //bool fHFSMode; + /* fake file type entry that says "(HFS)" */ + int fHFSComboIdx; + + /* these are displayed locally */ + CString fPathName; + CString fAuxType; // DDX doesn't do hex conversion + + DECLARE_MESSAGE_MAP() +}; + +#endif /*__EDITPROPSDIALOG__*/ \ No newline at end of file diff --git a/app/EnterRegDialog.cpp b/app/EnterRegDialog.cpp new file mode 100644 index 0000000..f3e1a6d --- /dev/null +++ b/app/EnterRegDialog.cpp @@ -0,0 +1,211 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +#if 0 +/* + * Support for entering registration data. + */ +#include "stdafx.h" +#include "EnterRegDialog.h" +#include "MyApp.h" +#include "HelpTopics.h" + +BEGIN_MESSAGE_MAP(EnterRegDialog, CDialog) + ON_EN_CHANGE(IDC_REGENTER_USER, OnUserChange) + ON_EN_CHANGE(IDC_REGENTER_COMPANY, OnCompanyChange) + ON_EN_CHANGE(IDC_REGENTER_REG, OnRegChange) + ON_COMMAND(IDHELP, OnHelp) +END_MESSAGE_MAP() + + +/* + * Disable the "OK" button initially. + */ +BOOL +EnterRegDialog::OnInitDialog(void) +{ + //CWnd* pWnd = GetDlgItem(IDOK); + //ASSERT(pWnd != nil); + //pWnd->EnableWindow(false); + + fMyEdit.ReplaceDlgCtrl(this, IDC_REGENTER_REG); + fMyEdit.SetProperties(MyEdit::kCapsOnly | MyEdit::kNoWhiteSpace); + + /* place a reasonable cap on the field lengths, since these go + straight into the registry */ + CEdit* pEdit; + pEdit = (CEdit*) GetDlgItem(IDC_REGENTER_USER); + ASSERT(pEdit != nil); + pEdit->SetLimitText(120); + pEdit = (CEdit*) GetDlgItem(IDC_REGENTER_COMPANY); + ASSERT(pEdit != nil); + pEdit->SetLimitText(120); + pEdit = (CEdit*) GetDlgItem(IDC_REGENTER_REG); + ASSERT(pEdit != nil); + pEdit->SetLimitText(40); + + return CDialog::OnInitDialog(); +} + +/* + * Shuffle data in and out of the edit fields. We do an extra validation + * step on the registration key before accepting it. + */ +void +EnterRegDialog::DoDataExchange(CDataExchange* pDX) +{ + DDX_Text(pDX, IDC_REGENTER_USER, fUserName); + DDX_Text(pDX, IDC_REGENTER_COMPANY, fCompanyName); + DDX_Text(pDX, IDC_REGENTER_REG, fRegKey); + + /* validate the reg field */ + if (pDX->m_bSaveAndValidate) { + ASSERT(!fUserName.IsEmpty()); + ASSERT(!fRegKey.IsEmpty()); + + if (gMyApp.fRegistry.IsValidRegistrationKey(fUserName, fCompanyName, + fRegKey)) + { + WMSG3("Correct key entered: '%s' '%s' '%s'\n", + (LPCTSTR)fUserName, (LPCTSTR)fCompanyName, (LPCTSTR)fRegKey); + } else { + WMSG0("Incorrect key entered, rejecting\n"); + CString appName, msg; + appName.LoadString(IDS_MB_APP_NAME); + msg.LoadString(IDS_REG_BAD_ENTRY); + MessageBox(msg, appName, MB_ICONWARNING|MB_OK); + pDX->Fail(); + } + } else { + OnUserChange(); + OnCompanyChange(); + OnRegChange(); + } +} + +/* + * Call this when the text in an edit field has changed. + * + * If there's nothing in the "user name" or "reg key" fields, dim the OK + * button. + */ +void +EnterRegDialog::HandleEditChange(int editID, int crcID) +{ + CString userStr, regStr; + CEdit* pEdit; + CWnd* pWnd; + + /* + * Update the CRC for the modified control. + */ + pEdit = (CEdit*) GetDlgItem(editID); + ASSERT(pEdit != nil); + pEdit->GetWindowText(userStr); + unsigned short crc; + crc = gMyApp.fRegistry.ComputeStringCRC(userStr); + userStr.Format("%04X", crc); + pWnd = GetDlgItem(crcID); + ASSERT(pWnd != nil); + pWnd->SetWindowText(userStr); + + /* + * Update the OK button. + */ + pEdit = (CEdit*) GetDlgItem(IDC_REGENTER_USER); + ASSERT(pEdit != nil); + pEdit->GetWindowText(userStr); + + pEdit = (CEdit*) GetDlgItem(IDC_REGENTER_REG); + ASSERT(pEdit != nil); + pEdit->GetWindowText(regStr); + + pWnd = GetDlgItem(IDOK); + ASSERT(pWnd != nil); + pWnd->EnableWindow(!userStr.IsEmpty() && !regStr.IsEmpty()); +} + +/* + * Handle changes in the three edit fields. + */ +void +EnterRegDialog::OnUserChange(void) +{ + HandleEditChange(IDC_REGENTER_USER, IDC_REGENTER_USERCRC); +} +void +EnterRegDialog::OnCompanyChange(void) +{ + HandleEditChange(IDC_REGENTER_COMPANY, IDC_REGENTER_COMPCRC); +} +void +EnterRegDialog::OnRegChange(void) +{ + HandleEditChange(IDC_REGENTER_REG, IDC_REGENTER_REGCRC); +} + + +/* + * User pressed the "Help" button. + */ +void +EnterRegDialog::OnHelp(void) +{ + WinHelp(HELP_TOPIC_ENTER_REG_DATA, HELP_CONTEXT); +} + + +/* + * Get registration info from the user. This is a static utility function + * that can be called from elsewhere in the app. + * + * Returns 0 on successful registration, nonzero on failure or if the user + * cancels out of the dialog. + */ +/*static*/ int +EnterRegDialog::GetRegInfo(CWnd* pWnd) +{ + CString user, company, reg, versions, expire; + + /* + * Get current data (if any). This call only fails if the registry itself + * appears to be generally inaccessible. + */ + if (gMyApp.fRegistry.GetRegistration(&user, &company, ®, &versions, + &expire) != 0) + { + CString msg; + msg.LoadString(IDS_REG_FAILURE); + ShowFailureMsg(pWnd, msg, IDS_FAILED); + return -1; + } + + /* + * Post the dialog. + */ + EnterRegDialog dlg(pWnd); + int result = -1; + + if (dlg.DoModal() == IDOK) { + user = dlg.fUserName; + company = dlg.fCompanyName; + reg = dlg.fRegKey; + + /* data was validated by EnterRegDialog, so just save it to registry */ + if (gMyApp.fRegistry.SetRegistration(user, company, reg, versions, + expire) != 0) + { + CString msg; + msg.LoadString(IDS_REG_FAILURE); + ShowFailureMsg(pWnd, msg, IDS_FAILED); + } else { + result = 0; + } + } + + return result; +} + +#endif /*0*/ \ No newline at end of file diff --git a/app/EnterRegDialog.h b/app/EnterRegDialog.h new file mode 100644 index 0000000..b17f3d4 --- /dev/null +++ b/app/EnterRegDialog.h @@ -0,0 +1,50 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Dialog allowing the user to enter registration data. + */ +#ifndef __ENTERREGDIALOG__ +#define __ENTERREGDIALOG__ + +#include "../util/UtilLib.h" +#include "resource.h" + +/* + * Straightforward dialog. We validate the registration key in the DDX + * function, so an IDOK is a guarantee that they have entered valid data. It + * is up to the caller to store the values in the registry. + */ +class EnterRegDialog : public CDialog { +public: + EnterRegDialog(CWnd* pParent = nil) : CDialog(IDD_REGISTRATION, pParent) + { fDepth = 0; } + virtual ~EnterRegDialog(void) {} + + CString fUserName; + CString fCompanyName; + CString fRegKey; + + static int GetRegInfo(CWnd* pWnd); + +private: + // overrides + virtual BOOL OnInitDialog(void); + virtual void DoDataExchange(CDataExchange* pDX); + + afx_msg void OnUserChange(void); + afx_msg void OnCompanyChange(void); + afx_msg void OnRegChange(void); + afx_msg void OnHelp(void); + + void HandleEditChange(int editID, int crcID); + + MyEdit fMyEdit; + int fDepth; + + DECLARE_MESSAGE_MAP() +}; + +#endif /*__ENTERREGDIALOG__*/ \ No newline at end of file diff --git a/app/ExtractOptionsDialog.cpp b/app/ExtractOptionsDialog.cpp new file mode 100644 index 0000000..f4b80be --- /dev/null +++ b/app/ExtractOptionsDialog.cpp @@ -0,0 +1,208 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Support for ExtractOptionsDialog. + */ +#include "stdafx.h" +#include "ExtractOptionsDialog.h" +#include "HelpTopics.h" +#include "ChooseDirDialog.h" + +BEGIN_MESSAGE_MAP(ExtractOptionsDialog, CDialog) + ON_BN_CLICKED(IDC_EXT_CHOOSE_FOLDER, OnChooseFolder) + ON_BN_CLICKED(IDC_EXT_CONVEOLNONE, OnChangeTextConv) + ON_BN_CLICKED(IDC_EXT_CONVEOLTYPE, OnChangeTextConv) + ON_BN_CLICKED(IDC_EXT_CONVEOLTEXT, OnChangeTextConv) + ON_BN_CLICKED(IDC_EXT_CONVEOLALL, OnChangeTextConv) + ON_BN_CLICKED(IDC_EXT_CONFIG_PRESERVE, OnConfigPreserve) + ON_BN_CLICKED(IDC_EXT_CONFIG_CONVERT, OnConfigConvert) + ON_WM_HELPINFO() + ON_COMMAND(IDHELP, OnHelp) +END_MESSAGE_MAP() + + +/* + * Set up the dialog that lets the user choose file extraction options. + * + * All we really need to do is update the string that indicates how many + * files have been selected. + */ +BOOL +ExtractOptionsDialog::OnInitDialog(void) +{ + CString countFmt; + CString selStr; + CWnd* pWnd; + + /* grab the radio button with the selection count */ + pWnd = GetDlgItem(IDC_EXT_SELECTED); + ASSERT(pWnd != nil); + + /* set the count string using a string table entry */ + if (fSelectedCount == 1) { + countFmt.LoadString(IDS_EXT_SELECTED_COUNT); + pWnd->SetWindowText(countFmt); + } else { + countFmt.LoadString(IDS_EXT_SELECTED_COUNTS_FMT); + selStr.Format((LPCTSTR) countFmt, fSelectedCount); + pWnd->SetWindowText(selStr); + + if (fSelectedCount == 0) + pWnd->EnableWindow(FALSE); + } + + /* if "no convert" is selected, disable high ASCII button */ + if (fConvEOL == kConvEOLNone) { + pWnd = GetDlgItem(IDC_EXT_CONVHIGHASCII); + pWnd->EnableWindow(false); + } + + /* replace the existing button with one of our bitmap buttons */ + fChooseFolderButton.ReplaceDlgCtrl(this, IDC_EXT_CHOOSE_FOLDER); + fChooseFolderButton.SetBitmapID(IDB_CHOOSE_FOLDER); + + return CDialog::OnInitDialog(); + //return TRUE; // let Windows set the focus +} + +/* + * Convert values. + * + * Should probably verify that fFilesToExtract is not set to kExtractSelection + * when fSelectedCount is zero. + */ +void +ExtractOptionsDialog::DoDataExchange(CDataExchange* pDX) +{ + DDX_Text(pDX, IDC_EXT_PATH, fExtractPath); + + DDX_Radio(pDX, IDC_EXT_SELECTED, fFilesToExtract); + + DDX_Check(pDX, IDC_EXT_DATAFORK, fIncludeDataForks); + DDX_Check(pDX, IDC_EXT_RSRCFORK, fIncludeRsrcForks); + DDX_Check(pDX, IDC_EXT_DISKIMAGE, fIncludeDiskImages); + + DDX_Check(pDX, IDC_EXT_REFORMAT, fEnableReformat); + DDX_Check(pDX, IDC_EXT_DISK_2MG, fDiskTo2MG); + + DDX_Check(pDX, IDC_EXT_ADD_PRESERVE, fAddTypePreservation); + DDX_Check(pDX, IDC_EXT_ADD_EXTEN, fAddExtension); + DDX_Check(pDX, IDC_EXT_STRIP_FOLDER, fStripFolderNames); + + DDX_Radio(pDX, IDC_EXT_CONVEOLNONE, fConvEOL); + DDX_Check(pDX, IDC_EXT_CONVHIGHASCII, fConvHighASCII); + + DDX_Check(pDX, IDC_EXT_OVERWRITE_EXIST, fOverwriteExisting); +} + +/* + * Reconfigure controls for best preservation of Apple II formats. + */ +void +ExtractOptionsDialog::OnConfigPreserve(void) +{ + // IDC_EXT_PATH, IDC_EXT_SELECTED + SetDlgButtonCheck(this, IDC_EXT_DATAFORK, BST_CHECKED); + SetDlgButtonCheck(this, IDC_EXT_RSRCFORK, BST_CHECKED); + SetDlgButtonCheck(this, IDC_EXT_DISKIMAGE, BST_CHECKED); + SetDlgButtonCheck(this, IDC_EXT_REFORMAT, BST_UNCHECKED); + SetDlgButtonCheck(this, IDC_EXT_DISK_2MG, BST_UNCHECKED); + SetDlgButtonCheck(this, IDC_EXT_ADD_PRESERVE, BST_CHECKED); + SetDlgButtonCheck(this, IDC_EXT_ADD_EXTEN, BST_UNCHECKED); + //SetDlgButtonCheck(this, IDC_EXT_STRIP_FOLDER, BST_CHECKED); + SetDlgButtonCheck(this, IDC_EXT_CONVEOLNONE, BST_CHECKED); + SetDlgButtonCheck(this, IDC_EXT_CONVEOLTYPE, BST_UNCHECKED); + SetDlgButtonCheck(this, IDC_EXT_CONVEOLTEXT, BST_UNCHECKED); + SetDlgButtonCheck(this, IDC_EXT_CONVEOLALL, BST_UNCHECKED); + SetDlgButtonCheck(this, IDC_EXT_CONVHIGHASCII, BST_UNCHECKED); + //SetDlgButtonCheck(this, IDC_EXT_OVERWRITE_EXIST, BST_CHECKED); + + OnChangeTextConv(); +} + +/* + * Reconfigure controls for easiest viewing under Windows. + */ +void +ExtractOptionsDialog::OnConfigConvert(void) +{ + // IDC_EXT_PATH, IDC_EXT_SELECTED + SetDlgButtonCheck(this, IDC_EXT_DATAFORK, BST_CHECKED); + SetDlgButtonCheck(this, IDC_EXT_RSRCFORK, BST_UNCHECKED); + SetDlgButtonCheck(this, IDC_EXT_DISKIMAGE, BST_CHECKED); + SetDlgButtonCheck(this, IDC_EXT_REFORMAT, BST_CHECKED); + SetDlgButtonCheck(this, IDC_EXT_DISK_2MG, BST_CHECKED); + SetDlgButtonCheck(this, IDC_EXT_ADD_PRESERVE, BST_UNCHECKED); + SetDlgButtonCheck(this, IDC_EXT_ADD_EXTEN, BST_CHECKED); + //SetDlgButtonCheck(this, IDC_EXT_STRIP_FOLDER, BST_CHECKED); + SetDlgButtonCheck(this, IDC_EXT_CONVEOLNONE, BST_UNCHECKED); + SetDlgButtonCheck(this, IDC_EXT_CONVEOLTYPE, BST_UNCHECKED); + SetDlgButtonCheck(this, IDC_EXT_CONVEOLTEXT, BST_CHECKED); + SetDlgButtonCheck(this, IDC_EXT_CONVEOLALL, BST_UNCHECKED); + SetDlgButtonCheck(this, IDC_EXT_CONVHIGHASCII, BST_CHECKED); + //SetDlgButtonCheck(this, IDC_EXT_OVERWRITE_EXIST, BST_CHECKED); + + OnChangeTextConv(); +} + +/* + * Enable or disable the "Convert high ASCII" button based on the current + * setting of the radio button above it. + */ +void +ExtractOptionsDialog::OnChangeTextConv(void) +{ + CButton* pButton = (CButton*) GetDlgItem(IDC_EXT_CONVEOLNONE); + ASSERT(pButton != nil); + bool convDisabled = (pButton->GetCheck() == BST_CHECKED); + + CWnd* pWnd = GetDlgItem(IDC_EXT_CONVHIGHASCII); + ASSERT(pWnd != nil); + pWnd->EnableWindow(!convDisabled); +} + +/* + * They want to choose the folder from a tree. + */ +void +ExtractOptionsDialog::OnChooseFolder(void) +{ + ChooseDirDialog chooseDir(this); + CWnd* pEditWnd; + CString editPath; + + /* get the currently-showing text from the edit field */ + pEditWnd = GetDlgItem(IDC_EXT_PATH); + ASSERT(pEditWnd != nil); + pEditWnd->GetWindowText(editPath); + + chooseDir.SetPathName(editPath); + if (chooseDir.DoModal() == IDOK) { + const char* ccp = chooseDir.GetPathName(); + WMSG1("New extract path chosen = '%s'\n", ccp); + + pEditWnd->SetWindowText(ccp); + } +} + +/* + * Context help request (question mark button). + */ +BOOL +ExtractOptionsDialog::OnHelpInfo(HELPINFO* lpHelpInfo) +{ + WinHelp((DWORD) lpHelpInfo->iCtrlId, HELP_CONTEXTPOPUP); + return TRUE; // yes, we handled it +} + +/* + * User pressed the "Help" button. + */ +void +ExtractOptionsDialog::OnHelp(void) +{ + WinHelp(HELP_TOPIC_EXT_OPTIONS, HELP_CONTEXT); +} diff --git a/app/ExtractOptionsDialog.h b/app/ExtractOptionsDialog.h new file mode 100644 index 0000000..80fbcfc --- /dev/null +++ b/app/ExtractOptionsDialog.h @@ -0,0 +1,87 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Choose options related to file extraction. + */ +#ifndef __EXTRACT_OPTIONS_DIALOG__ +#define __EXTRACT_OPTIONS_DIALOG__ + +#include "../util/UtilLib.h" +#include "resource.h" + +/* + * Our somewhat complicated extraction options dialog. + */ +class ExtractOptionsDialog : public CDialog { +public: + ExtractOptionsDialog(int selCount, CWnd* pParentWnd = NULL) : + CDialog(IDD_EXTRACT_FILES, pParentWnd), fSelectedCount(selCount) + { + // init values; these should be overridden before DoModal + fExtractPath = ""; + fFilesToExtract = 0; + fConvEOL = 0; + fConvHighASCII = FALSE; + fIncludeDataForks = fIncludeRsrcForks = fIncludeDiskImages = FALSE; + fEnableReformat = fDiskTo2MG = FALSE; + fAddTypePreservation = fAddExtension = fStripFolderNames = FALSE; + fOverwriteExisting = FALSE; + } + virtual ~ExtractOptionsDialog(void) { + //WMSG0("~ExtractOptionsDialog()\n"); + } + + CString fExtractPath; + + enum { kExtractSelection = 0, kExtractAll = 1 }; + int fFilesToExtract; + +// enum { kPreserveNone = 0, kPreserveTypes, kPreserveAndExtend }; +// int fTypePreservation; + + // this must match tab order of radio buttons in dialog + enum { kConvEOLNone = 0, kConvEOLType, kConvEOLAuto, kConvEOLAll }; + int fConvEOL; + BOOL fConvHighASCII; + +// enum { kDiskImageNoExtract = 0, kDiskImageAsPO = 1, kDiskImageAs2MG }; +// int fDiskImageExtract; + + BOOL fIncludeDataForks; + BOOL fIncludeRsrcForks; + BOOL fIncludeDiskImages; + + BOOL fEnableReformat; + BOOL fDiskTo2MG; + + BOOL fAddTypePreservation; + BOOL fAddExtension; + BOOL fStripFolderNames; + + BOOL fOverwriteExisting; + + bool ShouldTryReformat(void) const { + return fEnableReformat != 0; + } + +private: + virtual BOOL OnInitDialog(void); + virtual void DoDataExchange(CDataExchange* pDX); + + afx_msg void OnConfigPreserve(void); + afx_msg void OnConfigConvert(void); + afx_msg void OnChangeTextConv(void); + afx_msg void OnChooseFolder(void); + afx_msg BOOL OnHelpInfo(HELPINFO* lpHelpInfo); + afx_msg void OnHelp(void); + + MyBitmapButton fChooseFolderButton; + int fSelectedCount; + + DECLARE_MESSAGE_MAP() +}; + +#endif /*__EXTRACT_OPTIONS_DIALOG__*/ \ No newline at end of file diff --git a/app/FileNameConv.cpp b/app/FileNameConv.cpp new file mode 100644 index 0000000..435b60a --- /dev/null +++ b/app/FileNameConv.cpp @@ -0,0 +1,1421 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Filename manipulation, including file type preservation. This is + * substantially ripped from NuLib2, which would be a GPL violation if + * it weren't my code to begin with. + */ +#include "stdafx.h" +#include "FileNameConv.h" +#include "GenericArchive.h" +#include "AddFilesDialog.h" +#include + + +#define WINDOWS_LIKE +/* replace unsupported chars with '%xx' */ +#define kForeignIndic '%' + +/* convert single hex digit char to number */ +#define HexDigit(x) ( !isxdigit((int)(x)) ? -1 : \ + (x) <= '9' ? (x) - '0' : toupper(x) +10 - 'A' ) + +/* convert number from 0-15 to hex digit */ +#define HexConv(x) ( ((unsigned int)(x)) <= 15 ? \ + ( (x) <= 9 ? (x) + '0' : (x) -10 + 'A') : -1 ) + + +/* + * =========================================================================== + * Common definitions + * =========================================================================== + */ + +#define kPreserveIndic '#' /* use # rather than $ for hex indication */ +#define kFilenameExtDelim '.' /* separates extension from filename */ +#define kResourceFlag 'r' +#define kDiskImageFlag 'i' +#define kMaxExtLen 5 /* ".1234" */ +#define kResourceStr _T("_rsrc_") + +/* must be longer then strlen(kResourceStr)... no problem there */ +#define kMaxPathGrowth (sizeof("#XXXXXXXXYYYYYYYYZ")-1 + kMaxExtLen+1) + + +/* ProDOS file type names; must be entirely in upper case */ +static const char gFileTypeNames[256][4] = { + "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", + "$80", "$81", "$82", "$83", "$84", "$85", "$86", "$87", + "$88", "$89", "$8A", "$8B", "$8C", "$8D", "$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" +}; + +/* + * Some file extensions we recognize. When adding files with "extended" + * preservation mode, we try to assign types to files that weren't + * explicitly preserved, but nevertheless have a recognizeable type. + * + * geoff at gwlink.net pointed out that this really ought to be in an external + * file rather than a hard-coded table. Ought to fix that someday. + */ +static const struct { + const char* label; + unsigned short fileType; + unsigned long auxType; + unsigned char flags; +} gRecognizedExtensions[] = { + { "ASM", 0xb0, 0x0003, 0 }, /* APW assembly source */ + { "C", 0xb0, 0x000a, 0 }, /* APW C source */ + { "H", 0xb0, 0x000a, 0 }, /* APW C header */ + { "CPP", 0xb0, 0x0000, 0 }, /* generic source file */ + { "BNY", 0xe0, 0x8000, 0 }, /* Binary II lib */ + { "BQY", 0xe0, 0x8000, 0 }, /* Binary II lib, w/ compress */ + { "BXY", 0xe0, 0x8000, 0 }, /* Binary II wrap around SHK */ + { "BSE", 0xe0, 0x8000, 0 }, /* Binary II wrap around SEA */ + { "SEA", 0xb3, 0xdb07, 0 }, /* GSHK SEA */ + { "GIF", 0xc0, 0x8006, 0 }, /* GIF image */ + { "JPG", 0x06, 0x0000, 0 }, /* JPEG (nicer than 'NON') */ + { "JPEG", 0x06, 0x0000, 0 }, /* JPEG (nicer than 'NON') */ + //{ "ACU", 0xe0, 0x8001, 0 }, /* ACU archive */ + { "SHK", 0xe0, 0x8002, 0 }, /* ShrinkIt archive */ +}; + + +/* + * Return a pointer to the three-letter representation of the file type name. + * + * Note to self: code down below tests first char for '?'. + */ +/*static*/ const char* +PathProposal::FileTypeString(unsigned long fileType) +{ + if (fileType < NELEM(gFileTypeNames)) + return gFileTypeNames[fileType]; + else + return kUnknownTypeStr; +} + +/* + * Description table. + * + * The first item that matches will be used, but the table is searched + * bottom-up, so it's important to have the most general entry first. + * + * In retrospect, it might have made sense to use the same format as the + * "FTD" file type description file that the IIgs Finder used. Might have + * made sense to just ship that and load it on startup (although copyright + * issues would have to be investigated). + */ +static const struct { + unsigned short fileType; + unsigned short minAuxType; // start of range for which this applies + unsigned short maxAuxType; // end of range + const char* descr; +} gTypeDescriptions[] = { + /*NON*/ { 0x00, 0x0000, 0xffff, "Untyped file" }, + /*BAD*/ { 0x01, 0x0000, 0xffff, "Bad blocks" }, + /*PCD*/ { 0x02, 0x0000, 0xffff, "Pascal code" }, + /*PTX*/ { 0x03, 0x0000, 0xffff, "Pascal text" }, + /*TXT*/ { 0x04, 0x0000, 0xffff, "ASCII text" }, + /*PDA*/ { 0x05, 0x0000, 0xffff, "Pascal data" }, + /*BIN*/ { 0x06, 0x0000, 0xffff, "Binary" }, + /*FNT*/ { 0x07, 0x0000, 0xffff, "Apple /// font" }, + /*FOT*/ { 0x08, 0x0000, 0xffff, "Apple II or /// graphics" }, + /* */ { 0x08, 0x0000, 0x3fff, "Apple II graphics" }, + /* */ { 0x08, 0x4000, 0x4000, "Packed hi-res image" }, + /* */ { 0x08, 0x4001, 0x4001, "Packed double hi-res image" }, + /* */ { 0x08, 0x8001, 0x8001, "Printographer packed HGR file" }, + /* */ { 0x08, 0x8002, 0x8002, "Printographer packed DHGR file" }, + /* */ { 0x08, 0x8003, 0x8003, "Softdisk hi-res image" }, + /* */ { 0x08, 0x8004, 0x8004, "Softdisk double hi-res image" }, + /*BA3*/ { 0x09, 0x0000, 0xffff, "Apple /// BASIC program" }, + /*DA3*/ { 0x0a, 0x0000, 0xffff, "Apple /// BASIC data" }, + /*WPF*/ { 0x0b, 0x0000, 0xffff, "Apple II or /// word processor" }, + /* */ { 0x0b, 0x8001, 0x8001, "Write This Way document" }, + /* */ { 0x0b, 0x8002, 0x8002, "Writing & Publishing document" }, + /*SOS*/ { 0x0c, 0x0000, 0xffff, "Apple /// SOS system" }, + /*DIR*/ { 0x0f, 0x0000, 0xffff, "Folder" }, + /*RPD*/ { 0x10, 0x0000, 0xffff, "Apple /// RPS data" }, + /*RPI*/ { 0x11, 0x0000, 0xffff, "Apple /// RPS index" }, + /*AFD*/ { 0x12, 0x0000, 0xffff, "Apple /// AppleFile discard" }, + /*AFM*/ { 0x13, 0x0000, 0xffff, "Apple /// AppleFile model" }, + /*AFR*/ { 0x14, 0x0000, 0xffff, "Apple /// AppleFile report format" }, + /*SCL*/ { 0x15, 0x0000, 0xffff, "Apple /// screen library" }, + /*PFS*/ { 0x16, 0x0000, 0xffff, "PFS document" }, + /* */ { 0x16, 0x0001, 0x0001, "PFS:File document" }, + /* */ { 0x16, 0x0002, 0x0002, "PFS:Write document" }, + /* */ { 0x16, 0x0003, 0x0003, "PFS:Graph document" }, + /* */ { 0x16, 0x0004, 0x0004, "PFS:Plan document" }, + /* */ { 0x16, 0x0016, 0x0016, "PFS internal data" }, + /*ADB*/ { 0x19, 0x0000, 0xffff, "AppleWorks data base" }, + /*AWP*/ { 0x1a, 0x0000, 0xffff, "AppleWorks word processor" }, + /*ASP*/ { 0x1b, 0x0000, 0xffff, "AppleWorks spreadsheet" }, + /*TDM*/ { 0x20, 0x0000, 0xffff, "Desktop Manager document" }, + /*???*/ { 0x21, 0x0000, 0xffff, "Instant Pascal source" }, + /*???*/ { 0x22, 0x0000, 0xffff, "UCSD Pascal volume" }, + /*???*/ { 0x29, 0x0000, 0xffff, "Apple /// SOS dictionary" }, + /*8SC*/ { 0x2a, 0x0000, 0xffff, "Apple II source code" }, + /* */ { 0x2a, 0x8001, 0x8001, "EBBS command script" }, + /*8OB*/ { 0x2b, 0x0000, 0xffff, "Apple II object code" }, + /* */ { 0x2b, 0x8001, 0x8001, "GBBS Pro object Code" }, + /*8IC*/ { 0x2c, 0x0000, 0xffff, "Apple II interpreted code" }, + /* */ { 0x2c, 0x8003, 0x8003, "APEX Program File" }, + /* */ { 0x2c, 0x8005, 0x8005, "EBBS tokenized command script" }, + /*8LD*/ { 0x2d, 0x0000, 0xffff, "Apple II language data" }, + /* */ { 0x2d, 0x8006, 0x8005, "EBBS message bundle" }, + /* */ { 0x2d, 0x8007, 0x8007, "EBBS compressed message bundle" }, + /*P8C*/ { 0x2e, 0x0000, 0xffff, "ProDOS 8 code module" }, + /* */ { 0x2e, 0x8001, 0x8001, "Davex 8 Command" }, + /*PTP*/ { 0x2e, 0x8002, 0x8002, "Point-to-Point drivers" }, + /*PTP*/ { 0x2e, 0x8003, 0x8003, "Point-to-Point code" }, + /* */ { 0x2e, 0x8004, 0x8004, "Softdisk printer driver" }, + /*DIC*/ { 0x40, 0x0000, 0xffff, "Dictionary file" }, + /*???*/ { 0x41, 0x0000, 0xffff, "OCR data" }, + /* */ { 0x41, 0x8001, 0x8001, "InWords OCR font table" }, + /*FTD*/ { 0x42, 0x0000, 0xffff, "File type names" }, + /*???*/ { 0x43, 0x0000, 0xffff, "Peripheral data" }, + /* */ { 0x43, 0x8001, 0x8001, "Express document" }, + /*???*/ { 0x44, 0x0000, 0xffff, "Personal information" }, + /* */ { 0x44, 0x8001, 0x8001, "ResuMaker personal information" }, + /* */ { 0x44, 0x8002, 0x8002, "ResuMaker resume" }, + /* */ { 0x44, 0x8003, 0x8003, "II Notes document" }, + /* */ { 0x44, 0x8004, 0x8004, "Softdisk scrapbook document" }, + /* */ { 0x44, 0x8005, 0x8005, "Don't Forget document" }, + /* */ { 0x44, 0x80ff, 0x80ff, "What To Do data" }, + /* */ { 0x44, 0xbeef, 0xbeef, "Table Scraps scrapbook" }, + /*???*/ { 0x45, 0x0000, 0xffff, "Mathematical document" }, + /* */ { 0x45, 0x8001, 0x8001, "GSymbolix 3D graph document" }, + /* */ { 0x45, 0x8002, 0x8002, "GSymbolix formula document" }, + /*???*/ { 0x46, 0x0000, 0xffff, "AutoSave profiles" }, + /* */ { 0x46, 0x8001, 0x8001, "AutoSave profiles" }, + /*GWP*/ { 0x50, 0x0000, 0xffff, "Apple IIgs Word Processor" }, + /* */ { 0x50, 0x8001, 0x8001, "DeluxeWrite document" }, + /* */ { 0x50, 0x8003, 0x8003, "Personal Journal document" }, + /* */ { 0x50, 0x8010, 0x8010, "AppleWorks GS word processor" }, + /* */ { 0x50, 0x8011, 0x8011, "Softdisk issue text" }, + /* */ { 0x50, 0x5445, 0x5445, "Teach document" }, + /*GSS*/ { 0x51, 0x0000, 0xffff, "Apple IIgs spreadsheet" }, + /* */ { 0x51, 0x8010, 0x8010, "AppleWorks GS spreadsheet" }, + /* */ { 0x51, 0x2358, 0x2358, "QC Calc spreadsheet " }, + /*GDB*/ { 0x52, 0x0000, 0xffff, "Apple IIgs data base" }, + /* */ { 0x52, 0x8001, 0x8001, "GTv database" }, + /* */ { 0x52, 0x8010, 0x8010, "AppleWorks GS data base" }, + /* */ { 0x52, 0x8011, 0x8011, "AppleWorks GS DB template" }, + /* */ { 0x52, 0x8013, 0x8013, "GSAS database" }, + /* */ { 0x52, 0x8014, 0x8014, "GSAS accounting journals" }, + /* */ { 0x52, 0x8015, 0x8015, "Address Manager document" }, + /* */ { 0x52, 0x8016, 0x8016, "Address Manager defaults" }, + /* */ { 0x52, 0x8017, 0x8017, "Address Manager index" }, + /*DRW*/ { 0x53, 0x0000, 0xffff, "Drawing" }, + /* */ { 0x53, 0x8002, 0x8002, "Graphic Disk Labeler document" }, + /* */ { 0x53, 0x8010, 0x8010, "AppleWorks GS graphics" }, + /*GDP*/ { 0x54, 0x0000, 0xffff, "Desktop publishing" }, + /* */ { 0x54, 0x8002, 0x8002, "GraphicWriter document" }, + /* */ { 0x54, 0x8003, 0x8003, "Label It document" }, + /* */ { 0x54, 0x8010, 0x8010, "AppleWorks GS Page Layout" }, + /* */ { 0x54, 0xdd3e, 0xdd3e, "Medley document" }, + /*HMD*/ { 0x55, 0x0000, 0xffff, "Hypermedia" }, + /* */ { 0x55, 0x0001, 0x0001, "HyperCard IIgs stack" }, + /* */ { 0x55, 0x8001, 0x8001, "Tutor-Tech document" }, + /* */ { 0x55, 0x8002, 0x8002, "HyperStudio document" }, + /* */ { 0x55, 0x8003, 0x8003, "Nexus document" }, + /* */ { 0x55, 0x8004, 0x8004, "HyperSoft stack" }, + /* */ { 0x55, 0x8005, 0x8005, "HyperSoft card" }, + /* */ { 0x55, 0x8006, 0x8006, "HyperSoft external command" }, + /*EDU*/ { 0x56, 0x0000, 0xffff, "Educational Data" }, + /* */ { 0x56, 0x8001, 0x8001, "Tutor-Tech scores" }, + /* */ { 0x56, 0x8007, 0x8007, "GradeBook data" }, + /*STN*/ { 0x57, 0x0000, 0xffff, "Stationery" }, + /* */ { 0x57, 0x8003, 0x8003, "Music Writer format" }, + /*HLP*/ { 0x58, 0x0000, 0xffff, "Help file" }, + /* */ { 0x58, 0x8002, 0x8002, "Davex 8 help file" }, + /* */ { 0x58, 0x8005, 0x8005, "Micol Advanced Basic help file" }, + /* */ { 0x58, 0x8006, 0x8006, "Locator help document" }, + /* */ { 0x58, 0x8007, 0x8007, "Personal Journal help" }, + /* */ { 0x58, 0x8008, 0x8008, "Home Refinancer help" }, + /* */ { 0x58, 0x8009, 0x8009, "The Optimizer help" }, + /* */ { 0x58, 0x800a, 0x800a, "Text Wizard help" }, + /* */ { 0x58, 0x800b, 0x800b, "WordWorks Pro help system" }, + /* */ { 0x58, 0x800c, 0x800c, "Sound Wizard help" }, + /* */ { 0x58, 0x800d, 0x800d, "SeeHear help system" }, + /* */ { 0x58, 0x800e, 0x800e, "QuickForms help system" }, + /* */ { 0x58, 0x800f, 0x800f, "Don't Forget help system" }, + /*COM*/ { 0x59, 0x0000, 0xffff, "Communications file" }, + /* */ { 0x59, 0x8002, 0x8002, "AppleWorks GS communications" }, + /*CFG*/ { 0x5a, 0x0000, 0xffff, "Configuration file" }, + /* */ { 0x5a, 0x0000, 0x0000, "Sound settings files" }, + /* */ { 0x5a, 0x0002, 0x0002, "Battery RAM configuration" }, + /* */ { 0x5a, 0x0003, 0x0003, "AutoLaunch preferences" }, + /* */ { 0x5a, 0x0004, 0x0004, "SetStart preferences" }, + /* */ { 0x5a, 0x0005, 0x0005, "GSBug configuration" }, + /* */ { 0x5a, 0x0006, 0x0006, "Archiver preferences" }, + /* */ { 0x5a, 0x0007, 0x0007, "Archiver table of contents" }, + /* */ { 0x5a, 0x0008, 0x0008, "Font Manager data" }, + /* */ { 0x5a, 0x0009, 0x0009, "Print Manager data" }, + /* */ { 0x5a, 0x000a, 0x000a, "IR preferences" }, + /* */ { 0x5a, 0x8001, 0x8001, "Master Tracks Jr. preferences" }, + /* */ { 0x5a, 0x8002, 0x8002, "GraphicWriter preferences" }, + /* */ { 0x5a, 0x8003, 0x8003, "Z-Link configuration" }, + /* */ { 0x5a, 0x8004, 0x8004, "JumpStart configuration" }, + /* */ { 0x5a, 0x8005, 0x8005, "Davex 8 configuration" }, + /* */ { 0x5a, 0x8006, 0x8006, "Nifty List configuration" }, + /* */ { 0x5a, 0x8007, 0x8007, "GTv videodisc configuration" }, + /* */ { 0x5a, 0x8008, 0x8008, "GTv Workshop configuration" }, + /*PTP*/ { 0x5a, 0x8009, 0x8009, "Point-to-Point preferences" }, + /* */ { 0x5a, 0x800a, 0x800a, "ORCA/Disassembler preferences" }, + /* */ { 0x5a, 0x800b, 0x800b, "SnowTerm preferences" }, + /* */ { 0x5a, 0x800c, 0x800c, "My Word! preferences" }, + /* */ { 0x5a, 0x800d, 0x800d, "Chipmunk configuration" }, + /* */ { 0x5a, 0x8010, 0x8010, "AppleWorks GS configuration" }, + /* */ { 0x5a, 0x8011, 0x8011, "SDE Shell preferences" }, + /* */ { 0x5a, 0x8012, 0x8012, "SDE Editor preferences" }, + /* */ { 0x5a, 0x8013, 0x8013, "SDE system tab ruler" }, + /* */ { 0x5a, 0x8014, 0x8014, "Nexus configuration" }, + /* */ { 0x5a, 0x8015, 0x8015, "DesignMaster preferences" }, + /* */ { 0x5a, 0x801a, 0x801a, "MAX/Edit keyboard template" }, + /* */ { 0x5a, 0x801b, 0x801b, "MAX/Edit tab ruler set" }, + /* */ { 0x5a, 0x801c, 0x801c, "Platinum Paint preferences" }, + /* */ { 0x5a, 0x801d, 0x801d, "Sea Scan 1000" }, + /* */ { 0x5a, 0x801e, 0x801e, "Allison preferences" }, + /* */ { 0x5a, 0x801f, 0x801f, "Gold of the Americas options" }, + /* */ { 0x5a, 0x8021, 0x8021, "GSAS accounting setup" }, + /* */ { 0x5a, 0x8022, 0x8022, "GSAS accounting document" }, + /* */ { 0x5a, 0x8023, 0x8023, "UtilityLaunch preferences" }, + /* */ { 0x5a, 0x8024, 0x8024, "Softdisk configuration" }, + /* */ { 0x5a, 0x8025, 0x8025, "Quit-To configuration" }, + /* */ { 0x5a, 0x8026, 0x8026, "Big Edit Thing" }, + /* */ { 0x5a, 0x8027, 0x8027, "ZMaker preferences" }, + /* */ { 0x5a, 0x8028, 0x8028, "Minstrel configuration" }, + /* */ { 0x5a, 0x8029, 0x8029, "WordWorks Pro preferences" }, + /* */ { 0x5a, 0x802b, 0x802b, "Pointless preferences" }, + /* */ { 0x5a, 0x802c, 0x802c, "Micol Advanced Basic config" }, + /* */ { 0x5a, 0x802e, 0x802e, "Label It configuration" }, + /* */ { 0x5a, 0x802f, 0x802f, "Cool Cursor document" }, + /* */ { 0x5a, 0x8030, 0x8030, "Locator preferences" }, + /* */ { 0x5a, 0x8031, 0x8031, "Replicator preferences" }, + /* */ { 0x5a, 0x8032, 0x8032, "Kangaroo configuration" }, + /* */ { 0x5a, 0x8033, 0x8033, "Kangaroo data" }, + /* */ { 0x5a, 0x8034, 0x8034, "TransProg III configuration" }, + /* */ { 0x5a, 0x8035, 0x8035, "Home Refinancer preferences" }, + /* */ { 0x5a, 0x8036, 0x8036, "Easy Eyes settings" }, + /* */ { 0x5a, 0x8037, 0x8037, "The Optimizer settings" }, + /* */ { 0x5a, 0x8038, 0x8038, "Text Wizard settings" }, + /* */ { 0x5a, 0x803b, 0x803b, "Disk Access II preferences" }, + /* */ { 0x5a, 0x803d, 0x803d, "Quick DA configuration" }, + /* */ { 0x5a, 0x803e, 0x803e, "Crazy 8s preferences" }, + /* */ { 0x5a, 0x803f, 0x803f, "Sound Wizard settings" }, + /* */ { 0x5a, 0x8041, 0x8041, "Quick Window configuration" }, + /* */ { 0x5a, 0x8044, 0x8044, "Universe Master disk map" }, + /* */ { 0x5a, 0x8046, 0x8046, "Autopilot configuration" }, + /* */ { 0x5a, 0x8047, 0x8047, "EGOed preferences" }, + /* */ { 0x5a, 0x8049, 0x8049, "Quick DA preferences" }, + /* */ { 0x5a, 0x804b, 0x804b, "HardPressed volume preferences" }, + /* */ { 0x5a, 0x804c, 0x804c, "HardPressed global preferences" }, + /* */ { 0x5a, 0x804d, 0x804d, "HardPressed profile" }, + /* */ { 0x5a, 0x8050, 0x8050, "Don't Forget settings" }, + /* */ { 0x5a, 0x8052, 0x8052, "ProBOOT preferences" }, + /* */ { 0x5a, 0x8054, 0x8054, "Battery Brain preferences" }, + /* */ { 0x5a, 0x8055, 0x8055, "Rainbow configuration" }, + /* */ { 0x5a, 0x8061, 0x8061, "TypeSet preferences" }, + /* */ { 0x5a, 0x8063, 0x8063, "Cool Cursor preferences" }, + /* */ { 0x5a, 0x806e, 0x806e, "Balloon preferences" }, + /* */ { 0x5a, 0x80fe, 0x80fe, "Special Edition configuration" }, + /* */ { 0x5a, 0x80ff, 0x80ff, "Sun Dial preferences" }, + /*ANM*/ { 0x5b, 0x0000, 0xffff, "Animation file" }, + /* */ { 0x5b, 0x8001, 0x8001, "Cartooners movie" }, + /* */ { 0x5b, 0x8002, 0x8002, "Cartooners actors" }, + /* */ { 0x5b, 0x8005, 0x8005, "Arcade King Super document" }, + /* */ { 0x5b, 0x8006, 0x8006, "Arcade King DHRG document" }, + /* */ { 0x5b, 0x8007, 0x8007, "DreamVision movie" }, + /*MUM*/ { 0x5c, 0x0000, 0xffff, "Multimedia document" }, + /* */ { 0x5c, 0x8001, 0x8001, "GTv multimedia playlist" }, + /*ENT*/ { 0x5d, 0x0000, 0xffff, "Game/Entertainment document" }, + /* */ { 0x5d, 0x8001, 0x8001, "Solitaire Royale document" }, + /* */ { 0x5d, 0x8002, 0x8002, "BattleFront scenario" }, + /* */ { 0x5d, 0x8003, 0x8003, "BattleFront saved game" }, + /* */ { 0x5d, 0x8004, 0x8004, "Gold of the Americas game" }, + /* */ { 0x5d, 0x8006, 0x8006, "Blackjack Tutor document" }, + /* */ { 0x5d, 0x8008, 0x8008, "Canasta document" }, + /* */ { 0x5d, 0x800b, 0x800b, "Word Search document" }, + /* */ { 0x5d, 0x800c, 0x800c, "Tarot deal" }, + /* */ { 0x5d, 0x800d, 0x800d, "Tarot tournament" }, + /* */ { 0x5d, 0x800e, 0x800e, "Full Metal Planet game" }, + /* */ { 0x5d, 0x800f, 0x800f, "Full Metal Planet player" }, + /* */ { 0x5d, 0x8010, 0x8010, "Quizzical high scores" }, + /* */ { 0x5d, 0x8011, 0x8011, "Meltdown high scores" }, + /* */ { 0x5d, 0x8012, 0x8012, "BlockWords high scores" }, + /* */ { 0x5d, 0x8013, 0x8013, "Lift-A-Gon scores" }, + /* */ { 0x5d, 0x8014, 0x8014, "Softdisk Adventure" }, + /* */ { 0x5d, 0x8015, 0x8015, "Blankety Blank document" }, + /* */ { 0x5d, 0x8016, 0x8016, "Son of Star Axe champion" }, + /* */ { 0x5d, 0x8017, 0x8017, "Digit Fidget high scores" }, + /* */ { 0x5d, 0x8018, 0x8018, "Eddie map" }, + /* */ { 0x5d, 0x8019, 0x8019, "Eddie tile set" }, + /* */ { 0x5d, 0x8122, 0x8122, "Wolfenstein 3D scenario" }, + /* */ { 0x5d, 0x8123, 0x8123, "Wolfenstein 3D saved game" }, + /*DVU*/ { 0x5e, 0x0000, 0xffff, "Development utility document" }, + /* */ { 0x5e, 0x0001, 0x0001, "Resource file" }, + /* */ { 0x5e, 0x8001, 0x8001, "ORCA/Disassembler template" }, + /* */ { 0x5e, 0x8003, 0x8003, "DesignMaster document" }, + /* */ { 0x5e, 0x8008, 0x8008, "ORCA/C symbol file" }, + /*FIN*/ { 0x5f, 0x0000, 0xffff, "Financial document" }, + /* */ { 0x5f, 0x8001, 0x8001, "Your Money Matters document" }, + /* */ { 0x5f, 0x8002, 0x8002, "Home Refinancer document" }, + /*BIO*/ { 0x6b, 0x0000, 0xffff, "PC Transporter BIOS" }, + /*TDR*/ { 0x6d, 0x0000, 0xffff, "PC Transporter driver" }, + /*PRE*/ { 0x6e, 0x0000, 0xffff, "PC Transporter pre-boot" }, + /*HDV*/ { 0x6f, 0x0000, 0xffff, "PC Transporter volume" }, + /*WP */ { 0xa0, 0x0000, 0xffff, "WordPerfect document" }, + /*GSB*/ { 0xab, 0x0000, 0xffff, "Apple IIgs BASIC program" }, + /*TDF*/ { 0xac, 0x0000, 0xffff, "Apple IIgs BASIC TDF" }, + /*BDF*/ { 0xad, 0x0000, 0xffff, "Apple IIgs BASIC data" }, + /*SRC*/ { 0xb0, 0x0000, 0xffff, "Apple IIgs source code" }, + /* */ { 0xb0, 0x0001, 0x0001, "APW Text file" }, + /* */ { 0xb0, 0x0003, 0x0003, "APW 65816 Assembly source code" }, + /* */ { 0xb0, 0x0005, 0x0005, "ORCA/Pascal source code" }, + /* */ { 0xb0, 0x0006, 0x0006, "APW command file" }, + /* */ { 0xb0, 0x0008, 0x0008, "ORCA/C source code" }, + /* */ { 0xb0, 0x0009, 0x0009, "APW Linker command file" }, + /* */ { 0xb0, 0x000a, 0x000a, "APW C source code" }, + /* */ { 0xb0, 0x000c, 0x000c, "ORCA/Desktop command file" }, + /* */ { 0xb0, 0x0015, 0x0015, "APW Rez source file" }, + /* */ { 0xb0, 0x0017, 0x0017, "Installer script" }, + /* */ { 0xb0, 0x001e, 0x001e, "TML Pascal source code" }, + /* */ { 0xb0, 0x0116, 0x0116, "ORCA/Disassembler script" }, + /* */ { 0xb0, 0x0503, 0x0503, "SDE Assembler source code" }, + /* */ { 0xb0, 0x0506, 0x0506, "SDE command script" }, + /* */ { 0xb0, 0x0601, 0x0601, "Nifty List data" }, + /* */ { 0xb0, 0x0719, 0x0719, "PostScript file" }, + /*OBJ*/ { 0xb1, 0x0000, 0xffff, "Apple IIgs object code" }, + /*LIB*/ { 0xb2, 0x0000, 0xffff, "Apple IIgs Library file" }, + /*S16*/ { 0xb3, 0x0000, 0xffff, "GS/OS application" }, + /*RTL*/ { 0xb4, 0x0000, 0xffff, "GS/OS run-time library" }, + /*EXE*/ { 0xb5, 0x0000, 0xffff, "GS/OS shell application" }, + /*PIF*/ { 0xb6, 0x0000, 0xffff, "Permanent initialization file" }, + /*TIF*/ { 0xb7, 0x0000, 0xffff, "Temporary initialization file" }, + /*NDA*/ { 0xb8, 0x0000, 0xffff, "New desk accessory" }, + /*CDA*/ { 0xb9, 0x0000, 0xffff, "Classic desk accessory" }, + /*TOL*/ { 0xba, 0x0000, 0xffff, "Tool" }, + /*DVR*/ { 0xbb, 0x0000, 0xffff, "Apple IIgs device driver file" }, + /* */ { 0xbb, 0x7e01, 0x7e01, "GNO/ME terminal device driver" }, + /* */ { 0xbb, 0x7f01, 0x7f01, "GTv videodisc serial driver" }, + /* */ { 0xbb, 0x7f02, 0x7f02, "GTv videodisc game port driver" }, + /*LDF*/ { 0xbc, 0x0000, 0xffff, "Load file (generic)" }, + /* */ { 0xbc, 0x4001, 0x4001, "Nifty List module" }, + /* */ { 0xbc, 0xc001, 0xc001, "Nifty List module" }, + /* */ { 0xbc, 0x4002, 0x4002, "Super Info module" }, + /* */ { 0xbc, 0xc002, 0xc002, "Super Info module" }, + /* */ { 0xbc, 0x4004, 0x4004, "Twilight document" }, + /* */ { 0xbc, 0xc004, 0xc004, "Twilight document" }, + /* */ { 0xbc, 0x4006, 0x4006, "Foundation resource editor" }, + /* */ { 0xbc, 0xc006, 0xc006, "Foundation resource editor" }, + /* */ { 0xbc, 0x4007, 0x4007, "HyperStudio new button action" }, + /* */ { 0xbc, 0xc007, 0xc007, "HyperStudio new button action" }, + /* */ { 0xbc, 0x4008, 0x4008, "HyperStudio screen transition" }, + /* */ { 0xbc, 0xc008, 0xc008, "HyperStudio screen transition" }, + /* */ { 0xbc, 0x4009, 0x4009, "DreamGrafix module" }, + /* */ { 0xbc, 0xc009, 0xc009, "DreamGrafix module" }, + /* */ { 0xbc, 0x400a, 0x400a, "HyperStudio Extra utility" }, + /* */ { 0xbc, 0xc00a, 0xc00a, "HyperStudio Extra utility" }, + /* */ { 0xbc, 0x400f, 0x400f, "HardPressed module" }, + /* */ { 0xbc, 0xc00f, 0xc00f, "HardPressed module" }, + /* */ { 0xbc, 0x4010, 0x4010, "Graphic Exchange translator" }, + /* */ { 0xbc, 0xc010, 0xc010, "Graphic Exchange translator" }, + /* */ { 0xbc, 0x4011, 0x4011, "Desktop Enhancer blanker" }, + /* */ { 0xbc, 0xc011, 0xc011, "Desktop Enhancer blanker" }, + /* */ { 0xbc, 0x4083, 0x4083, "Marinetti link layer module" }, + /* */ { 0xbc, 0xc083, 0xc083, "Marinetti link layer module" }, + /*FST*/ { 0xbd, 0x0000, 0xffff, "GS/OS File System Translator" }, + /*DOC*/ { 0xbf, 0x0000, 0xffff, "GS/OS document" }, + /*PNT*/ { 0xc0, 0x0000, 0xffff, "Packed super hi-res picture" }, + /* */ { 0xc0, 0x0000, 0x0000, "Paintworks packed picture" }, + /* */ { 0xc0, 0x0001, 0x0001, "Packed super hi-res image" }, + /* */ { 0xc0, 0x0002, 0x0002, "Apple Preferred Format picture" }, + /* */ { 0xc0, 0x0003, 0x0003, "Packed QuickDraw II PICT file" }, + /* */ { 0xc0, 0x0080, 0x0080, "TIFF document" }, + /* */ { 0xc0, 0x0081, 0x0081, "JFIF (JPEG) document" }, + /* */ { 0xc0, 0x8001, 0x8001, "GTv background image" }, + /* */ { 0xc0, 0x8005, 0x8005, "DreamGrafix document" }, + /* */ { 0xc0, 0x8006, 0x8006, "GIF document" }, + /*PIC*/ { 0xc1, 0x0000, 0xffff, "Super hi-res picture" }, + /* */ { 0xc1, 0x0000, 0x0000, "Super hi-res screen image" }, + /* */ { 0xc1, 0x0001, 0x0001, "QuickDraw PICT file" }, + /* */ { 0xc1, 0x0002, 0x0002, "Super hi-res 3200-color screen image" }, + /* */ { 0xc1, 0x8001, 0x8001, "Allison raw image doc" }, + /* */ { 0xc1, 0x8002, 0x8002, "ThunderScan image doc" }, + /* */ { 0xc1, 0x8003, 0x8003, "DreamGrafix document" }, + /*ANI*/ { 0xc2, 0x0000, 0xffff, "Paintworks animation" }, + /*PAL*/ { 0xc3, 0x0000, 0xffff, "Paintworks palette" }, + /*OOG*/ { 0xc5, 0x0000, 0xffff, "Object-oriented graphics" }, + /* */ { 0xc5, 0x8000, 0x8000, "Draw Plus document" }, + /* */ { 0xc5, 0xc000, 0xc000, "DYOH architecture doc" }, + /* */ { 0xc5, 0xc001, 0xc001, "DYOH predrawn objects" }, + /* */ { 0xc5, 0xc002, 0xc002, "DYOH custom objects" }, + /* */ { 0xc5, 0xc003, 0xc003, "DYOH clipboard" }, + /* */ { 0xc5, 0xc004, 0xc004, "DYOH interiors document" }, + /* */ { 0xc5, 0xc005, 0xc005, "DYOH patterns" }, + /* */ { 0xc5, 0xc006, 0xc006, "DYOH landscape document" }, + /* */ { 0xc5, 0xc007, 0xc007, "PyWare Document" }, + /*SCR*/ { 0xc6, 0x0000, 0xffff, "Script" }, + /* */ { 0xc6, 0x8001, 0x8001, "Davex 8 script" }, + /* */ { 0xc6, 0x8002, 0x8002, "Universe Master backup script" }, + /* */ { 0xc6, 0x8003, 0x8003, "Universe Master Chain script" }, + /*CDV*/ { 0xc7, 0x0000, 0xffff, "Control Panel document" }, + /*FON*/ { 0xc8, 0x0000, 0xffff, "Font" }, + /* */ { 0xc8, 0x0000, 0x0000, "Font (Standard Apple IIgs QuickDraw II Font)" }, + /* */ { 0xc8, 0x0001, 0x0001, "TrueType font resource" }, + /* */ { 0xc8, 0x0008, 0x0008, "Postscript font resource" }, + /* */ { 0xc8, 0x0081, 0x0081, "TrueType font file" }, + /* */ { 0xc8, 0x0088, 0x0088, "Postscript font file" }, + /*FND*/ { 0xc9, 0x0000, 0xffff, "Finder data" }, + /*ICN*/ { 0xca, 0x0000, 0xffff, "Icons" }, + /*MUS*/ { 0xd5, 0x0000, 0xffff, "Music sequence" }, + /* */ { 0xd5, 0x0000, 0x0000, "Music Construction Set song" }, + /* */ { 0xd5, 0x0001, 0x0001, "MIDI Synth sequence" }, + /* */ { 0xd5, 0x0007, 0x0007, "SoundSmith document" }, + /* */ { 0xd5, 0x8002, 0x8002, "Diversi-Tune sequence" }, + /* */ { 0xd5, 0x8003, 0x8003, "Master Tracks Jr. sequence" }, + /* */ { 0xd5, 0x8004, 0x8004, "Music Writer document" }, + /* */ { 0xd5, 0x8005, 0x8005, "Arcade King Super music" }, + /* */ { 0xd5, 0x8006, 0x8006, "Music Composer file" }, + /*INS*/ { 0xd6, 0x0000, 0xffff, "Instrument" }, + /* */ { 0xd6, 0x0000, 0x0000, "Music Construction Set instrument" }, + /* */ { 0xd6, 0x0001, 0x0001, "MIDI Synth instrument" }, + /* */ { 0xd6, 0x8002, 0x8002, "Diversi-Tune instrument" }, + /*MDI*/ { 0xd7, 0x0000, 0xffff, "MIDI data" }, + /* */ { 0xd7, 0x0000, 0x0000, "MIDI standard data" }, + /* */ { 0xd7, 0x0080, 0x0080, "MIDI System Exclusive data" }, + /* */ { 0xd7, 0x8001, 0x8001, "MasterTracks Pro Sysex file" }, + /*SND*/ { 0xd8, 0x0000, 0xffff, "Sampled sound" }, + /* */ { 0xd8, 0x0000, 0x0000, "Audio IFF document" }, + /* */ { 0xd8, 0x0001, 0x0001, "AIFF-C document" }, + /* */ { 0xd8, 0x0002, 0x0002, "ASIF instrument" }, + /* */ { 0xd8, 0x0003, 0x0003, "Sound resource file" }, + /* */ { 0xd8, 0x0004, 0x0004, "MIDI Synth wave data" }, + /* */ { 0xd8, 0x8001, 0x8001, "HyperStudio sound" }, + /* */ { 0xd8, 0x8002, 0x8002, "Arcade King Super sound" }, + /* */ { 0xd8, 0x8003, 0x8003, "SoundOff! sound bank" }, + /*DBM*/ { 0xdb, 0x0000, 0xffff, "DB Master document" }, + /* */ { 0xdb, 0x0001, 0x0001, "DB Master document" }, + /*???*/ { 0xdd, 0x0000, 0xffff, "DDD Deluxe archive" }, // unofficial + /*LBR*/ { 0xe0, 0x0000, 0xffff, "Archival library" }, + /* */ { 0xe0, 0x0000, 0x0000, "ALU library" }, + /* */ { 0xe0, 0x0001, 0x0001, "AppleSingle file" }, + /* */ { 0xe0, 0x0002, 0x0002, "AppleDouble header file" }, + /* */ { 0xe0, 0x0003, 0x0003, "AppleDouble data file" }, + /* */ { 0xe0, 0x0004, 0x0004, "Archiver archive" }, + /* */ { 0xe0, 0x0005, 0x0005, "DiskCopy 4.2 disk image" }, + /* */ { 0xe0, 0x0100, 0x0100, "Apple 5.25 disk image" }, + /* */ { 0xe0, 0x0101, 0x0101, "Profile 5MB disk image" }, + /* */ { 0xe0, 0x0102, 0x0102, "Profile 10MB disk image" }, + /* */ { 0xe0, 0x0103, 0x0103, "Apple 3.5 disk image" }, + /* */ { 0xe0, 0x0104, 0x0104, "SCSI device image" }, + /* */ { 0xe0, 0x0105, 0x0105, "SCSI hard disk image" }, + /* */ { 0xe0, 0x0106, 0x0106, "SCSI tape image" }, + /* */ { 0xe0, 0x0107, 0x0107, "SCSI CD-ROM image" }, + /* */ { 0xe0, 0x010e, 0x010e, "RAM disk image" }, + /* */ { 0xe0, 0x010f, 0x010f, "ROM disk image" }, + /* */ { 0xe0, 0x0110, 0x0110, "File server image" }, + /* */ { 0xe0, 0x0113, 0x0113, "Hard disk image" }, + /* */ { 0xe0, 0x0114, 0x0114, "Floppy disk image" }, + /* */ { 0xe0, 0x0115, 0x0115, "Tape image" }, + /* */ { 0xe0, 0x011e, 0x011e, "AppleTalk file server image" }, + /* */ { 0xe0, 0x0120, 0x0120, "DiskCopy 6 disk image" }, + /* */ { 0xe0, 0x0130, 0x0130, "Universal Disk Image file" }, + /* */ { 0xe0, 0x8000, 0x8000, "Binary II file" }, + /* */ { 0xe0, 0x8001, 0x8001, "AppleLink ACU document" }, + /* */ { 0xe0, 0x8002, 0x8002, "ShrinkIt (NuFX) document" }, + /* */ { 0xe0, 0x8003, 0x8003, "Universal Disk Image file" }, + /* */ { 0xe0, 0x8004, 0x8004, "Davex archived volume" }, + /* */ { 0xe0, 0x8006, 0x8006, "EZ Backup Saveset doc" }, + /* */ { 0xe0, 0x8007, 0x8007, "ELS DOS 3.3 volume" }, + /* */ { 0xe0, 0x8008, 0x8008, "UtilityWorks document" }, + /* */ { 0xe0, 0x800a, 0x800a, "Replicator document" }, + /* */ { 0xe0, 0x800b, 0x800b, "AutoArk compressed document" }, + /* */ { 0xe0, 0x800d, 0x800d, "HardPressed compressed data (data fork)" }, + /* */ { 0xe0, 0x800e, 0x800e, "HardPressed compressed data (rsrc fork)" }, + /* */ { 0xe0, 0x800f, 0x800f, "HardPressed compressed data (both forks)" }, + /* */ { 0xe0, 0x8010, 0x8010, "LHA archive" }, + /*ATK*/ { 0xe2, 0x0000, 0xffff, "AppleTalk data" }, + /* */ { 0xe2, 0xffff, 0xffff, "EasyMount document" }, + /*R16*/ { 0xee, 0x0000, 0xffff, "EDASM 816 relocatable file" }, + /*PAS*/ { 0xef, 0x0000, 0xffff, "Pascal area" }, + /*CMD*/ { 0xf0, 0x0000, 0xffff, "BASIC command" }, + /*???*/ { 0xf1, 0x0000, 0xffff, "User type #1" }, + /*???*/ { 0xf2, 0x0000, 0xffff, "User type #2" }, + /*???*/ { 0xf3, 0x0000, 0xffff, "User type #3" }, + /*???*/ { 0xf4, 0x0000, 0xffff, "User type #4" }, + /*???*/ { 0xf5, 0x0000, 0xffff, "User type #5" }, + /*???*/ { 0xf6, 0x0000, 0xffff, "User type #6" }, + /*???*/ { 0xf7, 0x0000, 0xffff, "User type #7" }, + /*???*/ { 0xf8, 0x0000, 0xffff, "User type #8" }, + /*OS */ { 0xf9, 0x0000, 0xffff, "GS/OS system file" }, + /*OS */ { 0xfa, 0x0000, 0xffff, "Integer BASIC program" }, + /*OS */ { 0xfb, 0x0000, 0xffff, "Integer BASIC variables" }, + /*OS */ { 0xfc, 0x0000, 0xffff, "AppleSoft BASIC program" }, + /*OS */ { 0xfd, 0x0000, 0xffff, "AppleSoft BASIC variables" }, + /*OS */ { 0xfe, 0x0000, 0xffff, "Relocatable code" }, + /*OS */ { 0xff, 0x0000, 0xffff, "ProDOS 8 application" }, +}; + +/* + * Find an entry in the type description table that matches both file type and + * aux type. If no match is found, nil is returned. + */ +/*static*/ const char* +PathProposal::FileTypeDescription(long fileType, long auxType) +{ + int i; + + for (i = NELEM(gTypeDescriptions)-1; i >= 0; i--) { + if (fileType == gTypeDescriptions[i].fileType && + auxType >= gTypeDescriptions[i].minAuxType && + auxType <= gTypeDescriptions[i].maxAuxType) + { + return gTypeDescriptions[i].descr; + } + } + + return nil; +} + + +/* + * =========================================================================== + * Filename/filetype conversion + * =========================================================================== + */ + +/* + * Convert a pathname pulled out of an archive to something suitable for the + * local filesystem. + * + * The new pathname may be shorter (because characters were removed) or + * longer (if we add a "#XXYYYYZ" extension or replace chars with '%' codes). + */ +void +PathProposal::ArchiveToLocal(void) +{ + char* pathBuf; + const char* startp; + const char* endp; + char* dstp; + int newBufLen; + + /* init output fields */ + fLocalFssep = kLocalFssep; + fLocalPathName = ""; + + /* + * Set up temporary buffer space. The maximum possible expansion + * requires converting all chars to '%' codes and adding the longest + * possible preservation string. + */ + newBufLen = fStoredPathName.GetLength()*3 + kMaxPathGrowth +1; + pathBuf = fLocalPathName.GetBuffer(newBufLen); + ASSERT(pathBuf != nil); + + startp = fStoredPathName; + dstp = pathBuf; + while (*startp == fStoredFssep) { + /* ignore leading path sep; always extract to current dir */ + startp++; + } + + /* normalize all directory components and the filename component */ + while (startp != nil) { + endp = nil; + if (fStoredFssep != '\0') + endp = strchr(startp, fStoredFssep); + if (endp != nil && endp == startp) { + /* zero-length subdir component */ + WMSG1("WARNING: zero-length subdir component in '%s'\n", startp); + startp++; + continue; + } + if (endp != nil) { + /* normalize directory component */ + NormalizeDirectoryName(startp, endp - startp, + fStoredFssep, &dstp, newBufLen); + + *dstp++ = fLocalFssep; + + startp = endp +1; + } else { + /* normalize filename component */ + NormalizeFileName(startp, strlen(startp), + fStoredFssep, &dstp, newBufLen); + *dstp++ = '\0'; + + /* add/replace extension if necessary */ + char extBuf[kMaxPathGrowth +1] = ""; + if (fPreservation) { + AddPreservationString(pathBuf, extBuf); + } else if (fThreadKind == GenericEntry::kRsrcThread) { + /* add this in lieu of the preservation extension */ + strcat(pathBuf, kResourceStr); + } + if (fAddExtension) { + AddTypeExtension(pathBuf, extBuf); + } + ASSERT(strlen(extBuf) <= kMaxPathGrowth); + strcat(pathBuf, extBuf); + + startp = nil; /* we're done, break out of loop */ + } + } + + /* check for overflow */ + ASSERT(dstp - pathBuf <= newBufLen); + + /* + * If "junk paths" is set, drop everything but the last component. + */ + if (fJunkPaths) { + char* lastFssep; + lastFssep = strrchr(pathBuf, fLocalFssep); + if (lastFssep != nil) { + ASSERT(*(lastFssep+1) != '\0'); /* should already have been caught*/ + memmove(pathBuf, lastFssep+1, strlen(lastFssep+1)+1); + } + } + + fLocalPathName.ReleaseBuffer(); +} + +#if defined(WINDOWS_LIKE) +/* + * You can't create files or directories with these names on a FAT filesystem, + * because they're MS-DOS "device special files". + * + * The list comes from the Linux kernel's fs/msdos/namei.c. + * + * The trick is that the name can't start with any of these. That could mean + * that the name is just "aux", or it could be "aux.this.txt". + */ +static const char* gFatReservedNames3[] = { + "CON", "PRN", "NUL", "AUX", nil +}; +static const char* gFatReservedNames4[] = { + "LPT1", "LPT2", "LPT3", "LPT4", "COM1", "COM2", "COM3", "COM4", nil +}; + +/* + * Filename normalization for Win32 filesystems. You can't use [ \/:*?"<>| ] + * or control characters, and it's probably unwise to use high-ASCII stuff. + */ +void +PathProposal::Win32NormalizeFileName(const char* srcp, long srcLen, + char fssep, char** pDstp, long dstLen) +{ + char* dstp = *pDstp; + const char* startp = srcp; + static const char* kInvalid = "\\/:*?\"<>|"; + + /* match on "aux" or "aux.blah" */ + if (srcLen >= 3) { + const char** ppcch; + + for (ppcch = gFatReservedNames3; *ppcch != nil; ppcch++) { + if (strncasecmp(srcp, *ppcch, 3) == 0 && + (srcp[3] == '.' || srcLen == 3)) + { + WMSG1("--- fixing '%s'\n", *ppcch); + if (fPreservation) { + *dstp++ = kForeignIndic; + *dstp++ = '0'; + *dstp++ = '0'; + } else + *dstp++ = '_'; + break; + } + } + } + if (srcLen >= 4) { + const char** ppcch; + + for (ppcch = gFatReservedNames4; *ppcch != nil; ppcch++) { + if (strncasecmp(srcp, *ppcch, 4) == 0 && + (srcp[4] == '.' || srcLen == 4)) + { + WMSG1("--- fixing '%s'\n", *ppcch); + if (fPreservation) { + *dstp++ = kForeignIndic; + *dstp++ = '0'; + *dstp++ = '0'; + } else + *dstp++ = '_'; + break; + } + } + } + + + while (srcLen--) { /* don't go until null found! */ + ASSERT(*srcp != '\0'); + + if (*srcp == kForeignIndic) { + /* change '%' to "%%" */ + if (fPreservation) + *dstp++ = *srcp; + *dstp++ = *srcp++; + } else if (strchr(kInvalid, *srcp) != nil || + *srcp < 0x20 || *srcp >= 0x7f) + { + /* change invalid char to "%2f" or '_' */ + if (fPreservation) { + *dstp++ = kForeignIndic; + *dstp++ = HexConv(*srcp >> 4 & 0x0f); + *dstp++ = HexConv(*srcp & 0x0f); + } else { + *dstp++ = '_'; + } + srcp++; + } else { + /* no need to fiddle with it */ + *dstp++ = *srcp++; + } + } + + *dstp = '\0'; /* end the string, but don't advance past the null */ + ASSERT(*pDstp - dstp <= dstLen); /* make sure we didn't overflow */ + *pDstp = dstp; +} +#endif + + +/* + * Normalize a file name to local filesystem conventions. The input + * is quite possibly *NOT* null-terminated, since it may represent a + * substring of a full pathname. Use "srcLen". + * + * The output filename is copied to *pDstp, which is advanced forward. + * + * The output buffer must be able to hold 3x the original string length. + */ +void +PathProposal::NormalizeFileName(const char* srcp, long srcLen, + char fssep, char** pDstp, long dstLen) +{ + ASSERT(srcp != nil); + ASSERT(srcLen > 0); + ASSERT(dstLen > srcLen); + ASSERT(pDstp != nil); + ASSERT(*pDstp != nil); + +#if defined(UNIX_LIKE) + UNIXNormalizeFileName(srcp, srcLen, fssep, pDstp, dstLen); +#elif defined(WINDOWS_LIKE) + Win32NormalizeFileName(srcp, srcLen, fssep, pDstp, dstLen); +#else + #error "port this" +#endif +} + + +/* + * Normalize a directory name to local filesystem conventions. + */ +void +PathProposal::NormalizeDirectoryName(const char* srcp, long srcLen, + char fssep, char** pDstp, long dstLen) +{ + /* in general, directories and filenames are the same */ + ASSERT(fssep > ' ' && fssep < 0x7f); + NormalizeFileName(srcp, srcLen, fssep, pDstp, dstLen); +} + + +/* + * Add a preservation string. + * + * "pathBuf" is assumed to have enough space to hold the current path + * plus kMaxPathGrowth more. It will be modified in place. + */ +void +PathProposal::AddPreservationString(const char* pathBuf, char* extBuf) +{ + char* cp; + + ASSERT(pathBuf != nil); + ASSERT(extBuf != nil); + ASSERT(fPreservation); + + cp = extBuf + strlen(extBuf); + + /* + * Cons up a preservation string. On some platforms "sprintf" doesn't + * return the #of characters written, so we add it up manually. + */ + if (fFileType < 0x100 && fAuxType < 0x10000) { + sprintf(cp, "%c%02lx%04lx", kPreserveIndic, fFileType, fAuxType); + cp += 7; + } else { + sprintf(cp, "%c%08lx%08lx", kPreserveIndic, fFileType, fAuxType); + cp += 17; + } + + if (fThreadKind == GenericEntry::kRsrcThread) + *cp++ = kResourceFlag; + else if (fThreadKind == GenericEntry::kDiskImageThread) + *cp++ = kDiskImageFlag; + + + /* make sure it's terminated */ + *cp = '\0'; +} + +/* + * Add a ".foo" extension to the filename. + * + * We either need to retain the existing extension (possibly obscured by file + * type preservation) or append an extension based on the ProDOS file type. + */ +void +PathProposal::AddTypeExtension(const char* pathBuf, char* extBuf) +{ + const char* pPathExt = nil; + const char* pWantedExt = nil; + const char* pTypeExt = nil; + char* end; + char* cp; + + cp = extBuf + strlen(extBuf); + + /* + * Find extension in the local filename that we've prepared so far. + * Note FindExtension guarantees there's at least one char after '.'. + */ + pPathExt = FindExtension(pathBuf, fLocalFssep); + if (pPathExt == nil) { + /* + * There's no extension on the filename. Use the standard + * ProDOS type, if one exists for this entry. We don't use + * the table if it's NON, "???", or a hex value. + */ + if (fFileType) { + pTypeExt = FileTypeString(fFileType); + if (pTypeExt[0] == '?' || pTypeExt[0] == '$') + pTypeExt = nil; + } + } else { + pPathExt++; // skip leading '.' + } + + /* + * Figure out what extension we want this file to have. Files of type + * text are *always* ".TXT", and our extracted disk images are always + * ".PO". If it's not one of these two, we either retain the file's + * original extension, or generate one for it from the ProDOS file type. + */ + if (fFileType == 0x04) + pWantedExt = "TXT"; + else if (fThreadKind == GenericEntry::kDiskImageThread) + pWantedExt = "PO"; + else { + /* + * We want to use the extension currently on the file, if it has one. + * If not, use the one from the file type. + */ + if (pPathExt != nil) { + pWantedExt = pPathExt; + } else { + pWantedExt = pTypeExt; + } + } + /* pWantedExt != nil unless we failed to find a pTypeExt */ + + + /* + * Now we know which one we want. Figure out if we want to add it. + */ + if (pWantedExt != nil) { + if (extBuf[0] == '\0' && pPathExt != nil && + strcasecmp(pPathExt, pWantedExt) == 0) + { + /* don't add an extension that's already there */ + pWantedExt = nil; + goto know_ext; + } + + if (strlen(pWantedExt) >= kMaxExtLen) { + /* too long, forget it */ + pWantedExt = nil; + goto know_ext; + } + + /* if it's strictly decimal-numeric, don't use it (.1, .2, etc) */ + (void) strtoul(pWantedExt, &end, 10); + if (*end == '\0') { + pWantedExt = nil; + goto know_ext; + } + + /* if '#' appears in it, don't use it -- it'll confuse us */ + //WMSG2("LOOKING FOR '%c' in '%s'\n", kPreserveIndic, ccp); + const char* ccp = pWantedExt; + while (*ccp != '\0') { + if (*ccp == kPreserveIndic) { + pWantedExt = nil; + goto know_ext; + } + ccp++; + } + } +know_ext: + + /* + * If pWantedExt is non-nil, it points to a filename extension without + * the leading '.'. + */ + if (pWantedExt != nil) { + *cp++ = kFilenameExtDelim; + strcpy(cp, pWantedExt); + //cp += strlen(pWantedExt); + } +} + + +/* + * =========================================================================== + * File type restoration + * =========================================================================== + */ + +typedef bool Boolean; + +/* + * Convert a local path into something suitable for storage in an archive. + * Type preservation strings are interpreted and stripped as appropriate. + * + * This does *not* do filesystem-specific normalization here. (It could, but + * it's better to leave that for later so we can do uniqueification.) + * + * In the current implementation, fStoredPathName will always get smaller, + * but it would be unwise to rely on that. + */ +void +PathProposal::LocalToArchive(const AddFilesDialog* pAddOpts) +{ + Boolean wasPreserved; + Boolean doJunk = false; + Boolean adjusted; + char slashDotDotSlash[5] = "_.._"; + + fStoredPathName = fLocalPathName; + char* livePathStr = fStoredPathName.GetBuffer(0); + + fStoredFssep = kDefaultStoredFssep; + + /* convert '/' to '\' */ + ReplaceFssep(livePathStr, + kAltLocalFssep, //NState_GetAltSystemPathSeparator(pState), + kLocalFssep, //NState_GetSystemPathSeparator(pState), + kLocalFssep); //NState_GetSystemPathSeparator(pState)); + + /* + * Check for file type preservation info in the filename. If present, + * set the file type values and truncate the filename. + */ + wasPreserved = false; + if (pAddOpts->fTypePreservation == AddFilesDialog::kPreserveTypes || + pAddOpts->fTypePreservation == AddFilesDialog::kPreserveAndExtend) + { + wasPreserved = ExtractPreservationString(livePathStr); + } + + /* + * Do a "denormalization" pass, where we convert invalid chars (such + * as '/') from percent-codes back to 8-bit characters. The filename + * will always be the same size or smaller, so we can do it in place. + */ + if (wasPreserved) + DenormalizePath(livePathStr); + + /* + * If we're in "extended" mode, and the file wasn't preserved, take a + * guess at what the file type should be based on the file extension. + */ + if (!wasPreserved && + pAddOpts->fTypePreservation == AddFilesDialog::kPreserveAndExtend) + { + InterpretExtension(livePathStr); + } + + if (fStripDiskImageSuffix) + StripDiskImageSuffix(livePathStr); + + /* + * Strip bad chars off the front of the pathname. Every time we + * remove one thing we potentially expose another, so we have to + * loop until it's sanitized. + * + * The outer loop isn't really necessary under Win32, because you'd + * need to do something like ".\\foo", which isn't allowed. UNIX + * silently allows ".//foo", so this is a problem there. (We could + * probably do away with the inner loops, but those were already + * written when I saw the larger problem.) + */ + do { + adjusted = false; + + /* + * Check for other unpleasantness, such as a leading fssep. + */ + ASSERT(kLocalFssep != '\0'); + while (livePathStr[0] == kLocalFssep) { + /* slide it down, len is (strlen +1), -1 (dropping first char)*/ + memmove(livePathStr, livePathStr+1, strlen(livePathStr)); + adjusted = true; + } + + /* + * Remove leading "./". + */ + while (livePathStr[0] == '.' && livePathStr[1] == kLocalFssep) + { + /* slide it down, len is (strlen +1) -2 (dropping two chars) */ + memmove(livePathStr, livePathStr+2, strlen(livePathStr)-1); + adjusted = true; + } + } while (adjusted); + + /* + * If there's a "/../" present anywhere in the name, junk everything + * but the filename. + * + * This won't catch "foo/bar/..", but that should've been caught as + * a directory anyway. + */ + slashDotDotSlash[0] = kLocalFssep; + slashDotDotSlash[3] = kLocalFssep; + if ((livePathStr[0] == '.' && livePathStr[1] == '.') || + (strstr(livePathStr, slashDotDotSlash) != nil)) + { + WMSG1("Found dot dot in '%s', keeping only filename\n", livePathStr); + doJunk = true; + } + + /* + * Scan for and remove "/./" and trailing "/.". They're filesystem + * no-ops that work just fine under Win32 and UNIX but could confuse + * a IIgs. (Of course, the user could just omit them from the pathname.) + */ + /* TO DO 20030208 */ + + /* + * If "junk paths" is set, drop everything before the last fssep char. + */ + if (pAddOpts->fStripFolderNames || doJunk) { + char* lastFssep; + lastFssep = strrchr(livePathStr, kLocalFssep); + if (lastFssep != nil) { + ASSERT(*(lastFssep+1) != '\0'); /* should already have been caught*/ + memmove(livePathStr, lastFssep+1, strlen(lastFssep+1)+1); + } + } + + /* + * Finally, substitute our generally-accepted path separator in place of + * the local one, stomping on anything with a ':' in it as we do. The + * goal is to avoid having "subdir:foo/bar" turn into "subdir/foo/bar", + * so we change it to "subdirXfoo:bar". Were we a general-purpose + * archiver, this might be a mistake, but we're not. NuFX doesn't really + * give us a choice. + */ + ReplaceFssep(livePathStr, kLocalFssep, + PathProposal::kDefaultStoredFssep, 'X'); + + /* let the CString manage itself again */ + fStoredPathName.ReleaseBuffer(); +} + +/* + * Replace "oldc" with "newc". If we find an instance of "newc" already + * in the string, replace it with "newSubst". + */ +void +PathProposal::ReplaceFssep(char* str, char oldc, char newc, char newSubst) +{ + while (*str != '\0') { + if (*str == oldc) + *str = newc; + else if (*str == newc) + *str = newSubst; + str++; + } +} + + +/* + * Try to figure out what file type is associated with a filename extension. + * + * This checks the standard list of ProDOS types (which should catch things + * like "TXT" and "BIN") and the separate list of recognized extensions. + */ +void +PathProposal::LookupExtension(const char* ext) +{ + char uext3[4]; + int i, extLen; + + extLen = strlen(ext); + ASSERT(extLen > 0); + + /* + * First step is to try to find it in the recognized types list. + */ + for (i = 0; i < NELEM(gRecognizedExtensions); i++) { + if (strcasecmp(ext, gRecognizedExtensions[i].label) == 0) { + fFileType = gRecognizedExtensions[i].fileType; + fAuxType = gRecognizedExtensions[i].auxType; + goto bail; + } + } + + /* + * Second step is to try to find it in the ProDOS types list. + * + * The extension is converted to upper case and padded with spaces. + * + * [do we want to obstruct matching on things like '$f7' here?] + */ + if (extLen <= 3) { + for (i = 2; i >= extLen; i--) + uext3[i] = ' '; + for ( ; i >= 0; i--) + uext3[i] = toupper(ext[i]); + uext3[3] = '\0'; + + /*printf("### converted '%s' to '%s'\n", ext, uext3);*/ + + for (i = 0; i < NELEM(gFileTypeNames); i++) { + if (strcmp(uext3, gFileTypeNames[i]) == 0) { + fFileType = i; + goto bail; + } + } + } + +bail: + return; +} + +/* + * Try to associate some meaning with the file extension. + */ +void +PathProposal::InterpretExtension(const char* pathName) +{ + const char* pExt; + + ASSERT(pathName != nil); + + pExt = FindExtension(pathName, fLocalFssep); + if (pExt != nil) + LookupExtension(pExt+1); +} + + +/* + * Check to see if there's a preservation string on the filename. If so, + * set the filetype and auxtype information, and trim the preservation + * string off. + * + * We have to be careful not to trip on false-positive occurrences of '#' + * in the filename. + */ +Boolean +PathProposal::ExtractPreservationString(char* pathname) +{ + char numBuf[9]; + unsigned long fileType, auxType; + int threadMask; + char* pPreserve; + char* cp; + int digitCount; + + ASSERT(pathname != nil); + + pPreserve = strrchr(pathname, kPreserveIndic); + if (pPreserve == nil) + return false; + + /* count up the #of hex digits */ + digitCount = 0; + for (cp = pPreserve+1; *cp != '\0' && isxdigit((int)*cp); cp++) + digitCount++; + + /* extract the file and aux type */ + switch (digitCount) { + case 6: + /* ProDOS 1-byte type and 2-byte aux */ + memcpy(numBuf, pPreserve+1, 2); + numBuf[2] = 0; + fileType = strtoul(numBuf, &cp, 16); + ASSERT(cp == numBuf + 2); + + auxType = strtoul(pPreserve+3, &cp, 16); + ASSERT(cp == pPreserve + 7); + break; + case 16: + /* HFS 4-byte type and 4-byte creator */ + memcpy(numBuf, pPreserve+1, 8); + numBuf[8] = 0; + fileType = strtoul(numBuf, &cp, 16); + ASSERT(cp == numBuf + 8); + + auxType = strtoul(pPreserve+9, &cp, 16); + ASSERT(cp == pPreserve + 17); + break; + default: + /* not valid */ + return false; + } + + /* check for a threadID specifier */ + //threadID = kNuThreadIDDataFork; + threadMask = GenericEntry::kDataThread; + switch (*cp) { + case kResourceFlag: + //threadID = kNuThreadIDRsrcFork; + threadMask = GenericEntry::kRsrcThread; + cp++; + break; + case kDiskImageFlag: + //threadID = kNuThreadIDDiskImage; + threadMask = GenericEntry::kDiskImageThread; + cp++; + break; + default: + /* do nothing... yet */ + break; + } + + /* make sure we were the very last component */ + switch (*cp) { + case kFilenameExtDelim: /* redundant "-ee" extension */ + case '\0': /* end of string! */ + break; + default: + return false; + } + + /* truncate the original string, and return what we got */ + *pPreserve = '\0'; + fFileType = fileType; + fAuxType = auxType; + fThreadKind = threadMask; + //*pThreadID = threadID; + + return true; +} + + +/* + * Remove NuLib2's normalization magic (e.g. "%2f" for '/'). + * + * This always results in the filename staying the same length or getting + * smaller, so we can do it in place in the buffer. + */ +void +PathProposal::DenormalizePath(char* pathBuf) +{ + const char* srcp; + char* dstp; + char ch; + + srcp = pathBuf; + dstp = pathBuf; + + while (*srcp != '\0') { + if (*srcp == kForeignIndic) { + srcp++; + if (*srcp == kForeignIndic) { + *dstp++ = kForeignIndic; + srcp++; + } else if (isxdigit((int)*srcp)) { + ch = HexDigit(*srcp) << 4; + srcp++; + if (isxdigit((int)*srcp)) { + /* valid, output char */ + ch += HexDigit(*srcp); + *dstp++ = ch; + srcp++; + } else { + /* bogus '%' with trailing hex digit found! */ + *dstp++ = kForeignIndic; + *dstp++ = *(srcp-1); + } + } else { + /* bogus lone '%s' found! */ + *dstp++ = kForeignIndic; + } + + } else { + *dstp++ = *srcp++; + } + } + + *dstp = '\0'; + ASSERT(dstp <= srcp); +} + +/* + * Remove a disk image suffix. + * + * Useful when adding disk images directly from a .SDK or .2MG file. We + * don't want them to retain their original suffix. + */ +void +PathProposal::StripDiskImageSuffix(char* pathName) +{ + static const char diskExt[][4] = { + "SHK", "SDK", "IMG", "PO", "DO", "2MG", "DSK" + }; + char* pExt; + int i; + + pExt = (char*)FindExtension(pathName, fLocalFssep); + if (pExt == nil || pExt == pathName) + return; + + for (i = 0; i < NELEM(diskExt); i++) { + if (strcasecmp(pExt+1, diskExt[i]) == 0) { + WMSG2("Dropping '%s' from '%s'\n", pExt, pathName); + *pExt = '\0'; + return; + } + } +} diff --git a/app/FileNameConv.h b/app/FileNameConv.h new file mode 100644 index 0000000..6279582 --- /dev/null +++ b/app/FileNameConv.h @@ -0,0 +1,139 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * File name conversion. + */ +#ifndef __FILENAMECONV__ +#define __FILENAMECONV__ + +#include "GenericArchive.h" + +#define kUnknownTypeStr "???" + +/* + * Proposal for an output pathname, based on the contents of a GenericEntry. + */ +class PathProposal { +public: + typedef GenericEntry::RecordKind RecordKind; + enum { + kDefaultStoredFssep = ':', + kLocalFssep = '\\', // PATH_SEP + kAltLocalFssep = '/' // PATH_SEP2 + }; + + PathProposal(void) { + fStoredPathName = ":BOGUS:"; + fStoredFssep = '['; + fFileType = 256; + fAuxType = 65536; + fThreadKind = 0; + + fLocalPathName = ":HOSED:"; + fLocalFssep = ']'; + + fPreservation = false; + fAddExtension = false; + fJunkPaths = false; + fStripDiskImageSuffix = false; + } + virtual ~PathProposal(void) {} + + // init the "extract from archive" side from a GenericEntry struct + void Init(GenericEntry* pEntry) { + fStoredPathName = pEntry->GetPathName(); + fStoredFssep = pEntry->GetFssep(); + //if (fStoredFssep == '\0') // e.g. embedded DOS 3.3 volume + // fStoredFssep = kDefaultStoredFssep; + fFileType = pEntry->GetFileType(); + fAuxType = pEntry->GetAuxType(); + //fThreadKind set from SelectionEntry + // reset the "output" fields + fLocalPathName = ":HOSED:"; + fLocalFssep = ']'; + // I expect these to be as-yet unset; check it + ASSERT(!fPreservation); + ASSERT(!fAddExtension); + ASSERT(!fJunkPaths); + } + + // init the "add to archive" side + void Init(const char* localPathName) { + //ASSERT(basePathName[strlen(basePathName)-1] != kLocalFssep); + //fLocalPathName = localPathName + strlen(basePathName)+1; + fLocalPathName = localPathName; + fLocalFssep = kLocalFssep; + // reset the "output" fields + fStoredPathName = ":HOSED:"; + fStoredFssep = '['; + fFileType = 0; + fAuxType = 0; + fThreadKind = GenericEntry::kDataThread; + // I expect these to be as-yet unset; check it + ASSERT(!fPreservation); + ASSERT(!fAddExtension); + ASSERT(!fJunkPaths); + } + + // Convert a partial pathname from the archive to a local partial path. + void ArchiveToLocal(void); + // Same thing, other direction. + void LocalToArchive(const AddFilesDialog* pAddOpts); + + /* + * Fields for the "archive" side. + */ + // pathname from record or full pathname from disk image + CString fStoredPathName; + // filesystem separator char (or '\0' for things like DOS 3.3) + char fStoredFssep; + // file type, aux type, and what piece of the file this is + unsigned long fFileType; + unsigned long fAuxType; + int fThreadKind; // GenericEntry, e.g. kDataThread + + /* + * Fields for the "local" Side. + */ + // relative path of file for local filesystem + CString fLocalPathName; + // filesystem separator char for new path (always '\\' for us) + char fLocalFssep; + + /* + * Flags. + */ + // filename/filetype preservation flags + bool fPreservation; + bool fAddExtension; + bool fJunkPaths; + bool fStripDiskImageSuffix; + + /* + * Misc utility functions. + */ + static const char* FileTypeString(unsigned long fileType); + static const char* FileTypeDescription(long fileType, long auxType); + +private: + void Win32NormalizeFileName(const char* srcp, long srcLen, + char fssep, char** pDstp, long dstLen); + void NormalizeFileName(const char* srcp, long srcLen, + char fssep, char** pDstp, long dstLen); + void NormalizeDirectoryName(const char* srcp, long srcLen, + char fssep, char** pDstp, long dstLen); + void AddPreservationString(const char* pathBuf, char* extBuf); + void AddTypeExtension(const char* pathBuf, char* extBuf); + + void ReplaceFssep(char* str, char oldc, char newc, char newSubst); + void LookupExtension(const char* ext); + bool ExtractPreservationString(char* pathName); + void InterpretExtension(const char* pathName); + void DenormalizePath(char* pathBuf); + void StripDiskImageSuffix(char* pathName); +}; + +#endif /*__FILENAMECONV__*/ \ No newline at end of file diff --git a/app/GenericArchive.cpp b/app/GenericArchive.cpp new file mode 100644 index 0000000..4f9ad76 --- /dev/null +++ b/app/GenericArchive.cpp @@ -0,0 +1,1362 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Implementation of GenericArchive and GenericEntry. + * + * These serve as abstract base classes for archive-specific classes. + */ +#include "stdafx.h" +#include "GenericArchive.h" +#include "FileNameConv.h" +#include "ContentList.h" +#include "Main.h" +#include +#include + +/* + * For systems (e.g. Visual C++ 6.0) that don't have these standard values. + */ +#ifndef S_IRUSR +# define S_IRUSR 0400 +# define S_IWUSR 0200 +# define S_IXUSR 0100 +# define S_IRWXU (S_IRUSR|S_IWUSR|S_IXUSR) +# define S_IRGRP (S_IRUSR >> 3) +# define S_IWGRP (S_IWUSR >> 3) +# define S_IXGRP (S_IXUSR >> 3) +# define S_IRWXG (S_IRWXU >> 3) +# define S_IROTH (S_IRGRP >> 3) +# define S_IWOTH (S_IWGRP >> 3) +# define S_IXOTH (S_IXGRP >> 3) +# define S_IRWXO (S_IRWXG >> 3) +#endif +#ifndef S_ISREG +# define S_ISREG(m) (((m) & S_IFMT) == S_IFREG) +# define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR) +#endif + + +/* + * =========================================================================== + * GenericEntry + * =========================================================================== + */ + +/* + * Initialize all data members. + */ +GenericEntry::GenericEntry(void) +{ + fPathName = nil; + fFileName = nil; + fFssep = '\0'; + fSubVolName = nil; + fDisplayName = nil; + fFileType = 0; + fAuxType = 0; + fAccess = 0; + fModWhen = kDateNone; + fCreateWhen = kDateNone; + fRecordKind = kRecordKindUnknown; + fFormatStr = "Unknown"; + fCompressedLen = 0; + //fUncompressedLen = 0; + fDataForkLen = fRsrcForkLen = 0; + + fSourceFS = DiskImg::kFormatUnknown; + + fHasDataFork = false; + fHasRsrcFork = false; + fHasDiskImage = false; + fHasComment = false; + fHasNonEmptyComment = false; + + fIndex = -1; + fpPrev = nil; + fpNext = nil; + + fDamaged = fSuspicious = false; +} + +/* + * Throw out anything we allocated. + */ +GenericEntry::~GenericEntry(void) +{ + delete[] fPathName; + delete[] fSubVolName; + delete[] fDisplayName; +} + +/* + * Pathname getters and setters. + */ +void +GenericEntry::SetPathName(const char* path) +{ + ASSERT(path != nil && strlen(path) > 0); + if (fPathName != nil) + delete fPathName; + fPathName = new char[strlen(path)+1]; + strcpy(fPathName, path); + // nuke the derived fields + fFileName = nil; + fFileNameExtension = nil; + delete[] fDisplayName; + fDisplayName = nil; + + /* + * Warning: to be 100% pedantically correct here, we should NOT do this + * if the fssep char is '_'. However, that may not have been set by + * the time we got here, so to do this "correctly" we'd need to delay + * the underscorage until the first GetPathName call. + */ + const Preferences* pPreferences = GET_PREFERENCES(); + if (pPreferences->GetPrefBool(kPrSpacesToUnder)) + SpacesToUnderscores(fPathName); +} +const char* +GenericEntry::GetFileName(void) +{ + ASSERT(fPathName != nil); + if (fFileName == nil) + fFileName = FilenameOnly(fPathName, fFssep); + return fFileName; +} +const char* +GenericEntry::GetFileNameExtension(void) +{ + ASSERT(fPathName != nil); + if (fFileNameExtension == nil) + fFileNameExtension = FindExtension(fPathName, fFssep); + return fFileNameExtension; +} +void +GenericEntry::SetSubVolName(const char* name) +{ + delete[] fSubVolName; + fSubVolName = nil; + if (name != nil) { + fSubVolName = new char[strlen(name)+1]; + strcpy(fSubVolName, name); + } +} +const char* +GenericEntry::GetDisplayName(void) const +{ + ASSERT(fPathName != nil); + if (fDisplayName != nil) + return fDisplayName; + + GenericEntry* pThis = const_cast(this); + + int len = strlen(fPathName) +1; + if (fSubVolName != nil) + len += strlen(fSubVolName) +1; + pThis->fDisplayName = new char[len]; + if (fSubVolName != nil) { + char xtra[2] = { DiskFS::kDIFssep, '\0' }; + strcpy(pThis->fDisplayName, fSubVolName); + strcat(pThis->fDisplayName, xtra); + } else + pThis->fDisplayName[0] = '\0'; + strcat(pThis->fDisplayName, fPathName); + return pThis->fDisplayName; +} + +/* + * Get a string for this entry's filetype. + */ +const char* +GenericEntry::GetFileTypeString(void) const +{ + return PathProposal::FileTypeString(fFileType); +} + +/* + * Convert spaces to underscores. + */ +/*static*/ void +GenericEntry::SpacesToUnderscores(char* buf) +{ + while (*buf != '\0') { + if (*buf == ' ') + *buf = '_'; + buf++; + } +} + + +/* + * (Pulled from NufxLib Funnel.c.) + * + * Check to see if this is a high-ASCII file. To qualify, EVERY + * character must have its high bit set, except for spaces (0x20, + * courtesy Glen Bredon's "Merlin") and nulls (0x00, because of random- + * access text files). + * + * The test for 0x00 is actually useless in many circumstances because the + * NULLs will cause the text file auto-detector to flunk the file. It will, + * however, allow the user to select "convert ALL files" and still have the + * stripping enabled. + */ +/*static*/ bool +GenericEntry::CheckHighASCII(const unsigned char* buffer, + unsigned long count) +{ + bool isHighASCII; + + ASSERT(buffer != nil); + ASSERT(count != 0); + + isHighASCII = true; + while (count--) { + if ((*buffer & 0x80) == 0 && *buffer != 0x20 && *buffer != 0x00) { + WMSG1("Flunking CheckHighASCII on 0x%02x\n", *buffer); + isHighASCII = false; + break; + } + + buffer++; + } + + return isHighASCII; +} + +/* + * (Pulled from NufxLib Funnel.c.) + * + * Table determining what's a binary character and what isn't. It would + * possibly be more compact to generate this from a simple description, + * but I'm hoping static/const data will end up in the code segment and + * save space on the heap. + * + * This corresponds to less-316's ISO-latin1 "8bcccbcc18b95.33b.". This + * may be too loose by itself; we may want to require that the lower-ASCII + * values appear in higher proportions than the upper-ASCII values. + * Otherwise we run the risk of converting a binary file with specific + * properties. (Note that "upper-ASCII" refers to umlauts and other + * accented characters, not DOS 3.3 "high ASCII".) + * + * The auto-detect mechanism will never be perfect though, so there's not + * much point in tweaking it to death. + */ +static const char gIsBinary[256] = { + 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, /* ^@-^O */ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* ^P-^_ */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* - / */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0 - ? */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* @ - O */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* P - _ */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* ` - o */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, /* p - DEL */ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 0x80 */ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 0x90 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0xa0 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0xb0 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0xc0 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0xd0 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0xe0 */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0xf0 */ +}; + +#define kNuMaxUpperASCII 1 /* max #of binary chars per 100 bytes */ +#define kMinConvThreshold 40 /* min of 40 chars for auto-detect */ +#define kCharLF '\n' +#define kCharCR '\r' +/* + * Decide, based on the contents of the buffer, whether we should do an + * EOL conversion on the data. + * + * We need to decide if we are looking at text data, and if so, what kind + * of line terminator is in use. + * + * If we don't have enough data to make a determination, don't mess with it. + * (Thought for the day: add a "bias" flag, based on the NuRecord fileType, + * that causes us to handle borderline or sub-min-threshold cases more + * reasonably. If it's of type TXT, it's probably text.) + * + * We try to figure out whether it's CR, LF, or CRLF, so that we can + * skip the CPU-intensive conversion process if it isn't necessary. + * + * We will also investigate enabling a "high-ASCII" stripper if requested. + * This is only enabled when EOL conversions are enabled. Set "*pConvHA" + * to on/off/auto before calling. If it's initially set to "off", no + * attempt to evaluate high ASCII will be made. If "on" or "auto", the + * buffer will be scanned, and if the input appears to be high ASCII then + * it will be stripped *before* the EOL determination is made. + * + * Returns kConvEOLOff or kConvEOLOn. + */ +/*static*/ GenericEntry::ConvertEOL +GenericEntry::DetermineConversion(const unsigned char* buffer, long count, + EOLType* pSourceType, ConvertHighASCII* pConvHA) +{ + ConvertHighASCII wantConvHA = *pConvHA; + long bufCount, numBinary, numLF, numCR; + bool isHighASCII; + unsigned char val; + + *pSourceType = kEOLUnknown; + *pConvHA = kConvertHAOff; + + if (count < kMinConvThreshold) + return kConvertEOLOff; + + /* + * Check to see if the buffer is all high-ASCII characters. If it is, + * we want to strip characters before we test them below. + * + * If high ASCII conversion is disabled, assume that any high-ASCII + * characters are not meant to be line terminators, i.e. 0x8d != 0x0d. + */ + if (wantConvHA == kConvertHAOn || wantConvHA == kConvertHAAuto) { + isHighASCII = CheckHighASCII(buffer, count); + WMSG1(" +++ Determined isHighASCII=%d\n", isHighASCII); + } else { + isHighASCII = false; + WMSG0(" +++ Not even checking isHighASCII\n"); + } + + bufCount = count; + numBinary = numLF = numCR = 0; + while (bufCount--) { + val = *buffer++; + if (isHighASCII) + val &= 0x7f; + if (gIsBinary[val]) + numBinary++; + if (val == kCharLF) + numLF++; + if (val == kCharCR) + numCR++; + } + + /* if #found is > #allowed, it's a binary file */ + if (count < 100) { + /* use simplified check on files between kNuMinConvThreshold and 100 */ + if (numBinary > kNuMaxUpperASCII) + return kConvertEOLOff; + } else if (numBinary > (count / 100) * kNuMaxUpperASCII) + return kConvertEOLOff; + + /* + * If our "convert to" setting is the same as what we're converting + * from, we can turn off the converter and speed things up. + * + * These are simplistic, but this is intended as an optimization. We + * will blow it if the input has lots of CRs and LFs scattered about, + * and they just happen to be in equal amounts, but it's not clear + * to me that an automatic EOL conversion makes sense on that sort + * of file anyway. + * + * None of this applies if we also need to do a high-ASCII conversion, + * because we can't bypass the processing. + */ + if (isHighASCII) { + *pConvHA = kConvertHAOn; + } else { + if (numLF && !numCR) + *pSourceType = kEOLLF; + else if (!numLF && numCR) + *pSourceType = kEOLCR; + else if (numLF && numLF == numCR) + *pSourceType = kEOLCRLF; + else + *pSourceType = kEOLUnknown; + } + + return kConvertEOLOn; +} + +/* + * Output CRLF. + */ +static inline void +PutEOL(FILE* fp) +{ + putc(kCharCR, fp); + putc(kCharLF, fp); +} + +/* + * Write data to a file, possibly converting EOL markers to Windows CRLF + * and stripping high ASCII. + * + * If "*pConv" is kConvertEOLAuto, this will try to auto-detect whether + * the input is a text file or not by scanning the input buffer. + * + * Ditto for "*pConvHA". + * + * "fp" is the output file, "buf" is the input, "len" is the buffer length. + * "*pLastCR" should initially be "false", and carried across invocations. + * + * Returns 0 on success, or an errno value on error. + */ +/*static*/ int +GenericEntry::WriteConvert(FILE* fp, const char* buf, size_t len, + ConvertEOL* pConv, ConvertHighASCII* pConvHA, bool* pLastCR) +{ + int err = 0; + + WMSG2("+++ WriteConvert conv=%d convHA=%d\n", *pConv, *pConvHA); + + if (len == 0) { + WMSG0("WriteConvert asked to write 0 bytes; returning\n"); + return err; + } + + /* if we're in "auto" mode, scan the input for EOL and high ASCII */ + if (*pConv == kConvertEOLAuto) { + EOLType sourceType; + *pConv = DetermineConversion((unsigned char*)buf, len, &sourceType, + pConvHA); + if (*pConv == kConvertEOLOn && sourceType == kEOLCRLF) { + WMSG0(" Auto-detected text conversion from CRLF; disabling\n"); + *pConv = kConvertEOLOff; + } + WMSG2(" Auto-detected EOL conv=%d ha=%d\n", *pConv, *pConvHA); + } else if (*pConvHA == kConvertHAAuto) { + if (*pConv == kConvertEOLOn) { + /* definitely converting EOL, test for high ASCII */ + if (CheckHighASCII((unsigned char*)buf, len)) + *pConvHA = kConvertHAOn; + else + *pConvHA = kConvertHAOff; + } else { + /* not converting EOL, don't convert high ASCII */ + *pConvHA = kConvertHAOff; + } + } + WMSG2("+++ After auto, conv=%d convHA=%d\n", *pConv, *pConvHA); + ASSERT(*pConv == kConvertEOLOn || *pConv == kConvertEOLOff); + ASSERT(*pConvHA == kConvertHAOn || *pConvHA == kConvertHAOff); + + /* write the output */ + if (*pConv == kConvertEOLOff) { + if (fwrite(buf, len, 1, fp) != 1) { + err = errno; + WMSG1("WriteConvert failed, err=%d\n", errno); + } + } else { + ASSERT(*pConv == kConvertEOLOn); + bool lastCR = *pLastCR; + unsigned char uch; + int mask; + + if (*pConvHA == kConvertHAOn) + mask = 0x7f; + else + mask = 0xff; + + while (len--) { + uch = (*buf) & mask; + + if (uch == kCharCR) { + PutEOL(fp); + lastCR = true; + } else if (uch == kCharLF) { + if (!lastCR) + PutEOL(fp); + lastCR = false; + } else { + putc(uch, fp); + lastCR = false; + } + buf++; + } + *pLastCR = lastCR; + } + + return err; +} + + +/* + * =========================================================================== + * GenericArchive + * =========================================================================== + */ + +/* + * Add a new entry to the end of the list. + */ +void +GenericArchive::AddEntry(GenericEntry* pEntry) +{ + if (fEntryHead == nil) { + ASSERT(fEntryTail == nil); + fEntryHead = pEntry; + fEntryTail = pEntry; + ASSERT(pEntry->GetPrev() == nil); + ASSERT(pEntry->GetNext() == nil); + } else { + ASSERT(fEntryTail != nil); + ASSERT(pEntry->GetPrev() == nil); + pEntry->SetPrev(fEntryTail); + ASSERT(fEntryTail->GetNext() == nil); + fEntryTail->SetNext(pEntry); + fEntryTail = pEntry; + } + + fNumEntries++; + + //if (fEntryIndex != nil) { + // WMSG0("Resetting fEntryIndex\n"); + // delete [] fEntryIndex; + // fEntryIndex = nil; + //} +} + +/* + * Delete the "entries" list. + */ +void +GenericArchive::DeleteEntries(void) +{ + GenericEntry* pEntry; + GenericEntry* pNext; + + WMSG1("Deleting %d archive entries\n", fNumEntries); + + pEntry = GetEntries(); + while (pEntry != nil) { + pNext = pEntry->GetNext(); + delete pEntry; + pEntry = pNext; + } + + //delete [] fEntryIndex; + fNumEntries = 0; + fEntryHead = fEntryTail = nil; +} + +#if 0 +/* + * Create an index for fast access. + */ +void +GenericArchive::CreateIndex(void) +{ + GenericEntry* pEntry; + int num; + + WMSG1("Creating entry index (%d entries)\n", fNumEntries); + + ASSERT(fNumEntries != 0); + + fEntryIndex = new GenericEntry*[fNumEntries]; + if (fEntryIndex == nil) + return; + + pEntry = GetEntries(); + num = 0; + while (pEntry != nil) { + fEntryIndex[num] = pEntry; + pEntry = pEntry->GetNext(); + num++; + } +} +#endif + +/* + * Generate a temp name from a file name. + * + * The key is to come up with the name of a temp file in the same directory + * (or at least on the same disk volume) so that the temp file can be + * renamed on top of the original. + * + * Windows _mktemp does appear to test for the existence of the file, which + * is good. It doesn't actually open the file, which creates a small window + * in which bad things could happen, but it should be okay. + */ +/*static*/ CString +GenericArchive::GenDerivedTempName(const char* filename) +{ + static const char* kTmpTemplate = "CPtmp_XXXXXX"; + CString mangle(filename); + int idx, len; + + ASSERT(filename != nil); + + len = mangle.GetLength(); + ASSERT(len > 0); + idx = mangle.ReverseFind('\\'); + if (idx < 0) { + /* generally shouldn't happen -- we're using full paths */ + return kTmpTemplate; + } else { + mangle.Delete(idx+1, len-(idx+1)); /* delete out to the end */ + mangle += kTmpTemplate; + } + WMSG2("GenDerived: passed '%s' returned '%s'\n", filename, mangle); + + return mangle; +} + + +/* + * Do a strcasecmp-like comparison, taking equivalent fssep chars into + * account. + * + * The tricky part is with files like "foo:bar" ':' -- "foo:bar" '/'. The + * names appear to match, but the fssep chars are different, so they don't. + * If we just return (char1 - char2), though, we'll be returning 0 because + * the ASCII values match even if the character *meanings* don't. + * + * This assumes that the fssep char is not affected by tolower(). + * + * [This may not sort correctly...haven't verified that I'm returning the + * right thing for ascending ASCII sort.] + */ +/*static*/ int +GenericArchive::ComparePaths(const CString& name1, char fssep1, + const CString& name2, char fssep2) +{ + const char* cp1 = name1; + const char* cp2 = name2; + + while (*cp1 != '\0' && *cp2 != '\0') { + if (*cp1 == fssep1) { + if (*cp2 != fssep2) { + /* one fssep, one not, no match */ + if (*cp1 == *cp2) + return 1; + else + return *cp1 - *cp2; + } else { + /* both are fssep, it's a match even if ASCII is different */ + } + } else if (*cp2 == fssep2) { + /* one fssep, one not */ + if (*cp1 == *cp2) + return -1; + else + return *cp1 - *cp2; + } else if (tolower(*cp1) != tolower(*cp2)) { + /* mismatch */ + return tolower(*cp1) - tolower(*cp2); + } + + cp1++; + cp2++; + } + + return *cp1 - *cp2; +} + + +/* + * =========================================================================== + * GenericArchive -- "add files" stuff + * =========================================================================== + */ + +/* + * This comes straight out of NuLib2, and uses NufxLib data structures. While + * it may seem strange to use these structures for non-NuFX archives, they are + * convenient and hold at least as much information as any other format needs. + */ + +typedef bool Boolean; + +/* + * Convert from time in seconds to Apple IIgs DateTime format. + */ +/*static*/ void +GenericArchive::UNIXTimeToDateTime(const time_t* pWhen, NuDateTime* pDateTime) +{ + struct tm* ptm; + + ASSERT(pWhen != nil); + ASSERT(pDateTime != nil); + + ptm = localtime(pWhen); + if (ptm == nil) { + ASSERT(*pWhen == kDateNone || *pWhen == kDateInvalid); + memset(pDateTime, 0, sizeof(*pDateTime)); + return; + } + pDateTime->second = ptm->tm_sec; + pDateTime->minute = ptm->tm_min; + pDateTime->hour = ptm->tm_hour; + pDateTime->day = ptm->tm_mday -1; + pDateTime->month = ptm->tm_mon; + pDateTime->year = ptm->tm_year; + pDateTime->extra = 0; + pDateTime->weekDay = ptm->tm_wday +1; +} + + +/* + * Set the contents of a NuFileDetails structure, based on the pathname + * and characteristics of the file. + * + * For efficiency and simplicity, the pathname fields are set to CStrings in + * the GenericArchive object instead of newly-allocated storage. + */ +NuError +GenericArchive::GetFileDetails(const AddFilesDialog* pAddOpts, + const char* pathname, struct stat* psb, FileDetails* pDetails) +{ + //char* livePathStr; + time_t now; + + ASSERT(pAddOpts != nil); + ASSERT(pathname != nil); + ASSERT(pDetails != nil); + + /* init to defaults */ + //pDetails->threadID = kNuThreadIDDataFork; + pDetails->entryKind = FileDetails::kFileKindDataFork; + //pDetails->fileSysID = kNuFileSysUnknown; + pDetails->fileSysFmt = DiskImg::kFormatUnknown; + pDetails->fileSysInfo = PathProposal::kDefaultStoredFssep; + pDetails->fileType = 0; + pDetails->extraType = 0; + pDetails->storageType = kNuStorageUnknown; /* let NufxLib worry about it */ + if (psb->st_mode & S_IWUSR) + pDetails->access = kNuAccessUnlocked; + else + pDetails->access = kNuAccessLocked; + +#if 0 + /* if this is a disk image, fill in disk-specific fields */ + if (NState_GetModAddAsDisk(pState)) { + if ((psb->st_size & 0x1ff) != 0) { + /* reject anything whose size isn't a multiple of 512 bytes */ + printf("NOT storing odd-sized (%ld) file as disk image: %s\n", + (long)psb->st_size, livePathStr); + } else { + /* set fields; note the "preserve" stuff can override this */ + pDetails->threadID = kNuThreadIDDiskImage; + pDetails->storageType = 512; + pDetails->extraType = psb->st_size / 512; + } + } +#endif + + now = time(nil); + UNIXTimeToDateTime(&now, &pDetails->archiveWhen); + UNIXTimeToDateTime(&psb->st_mtime, &pDetails->modWhen); + UNIXTimeToDateTime(&psb->st_ctime, &pDetails->createWhen); + + /* get adjusted filename, along with any preserved type info */ + PathProposal pathProp; + pathProp.Init(pathname); + pathProp.LocalToArchive(pAddOpts); + + /* set up the local and archived pathnames */ + pDetails->storageName = ""; + if (!pAddOpts->fStoragePrefix.IsEmpty()) { + pDetails->storageName += pAddOpts->fStoragePrefix; + pDetails->storageName += pathProp.fStoredFssep; + } + pDetails->storageName += pathProp.fStoredPathName; + + /* + * Fill in the NuFileDetails struct. + * + * We use GetBuffer to get the string to ensure that the CString object + * doesn't do anything while we're not looking. The string won't be + * modified, and it won't be around for very long, so it's not strictly + * necessary to do this. It is, however, the correct approach. + */ + pDetails->origName = pathname; + pDetails->fileSysInfo = pathProp.fStoredFssep; + pDetails->fileType = pathProp.fFileType; + pDetails->extraType = pathProp.fAuxType; + switch (pathProp.fThreadKind) { + case GenericEntry::kDataThread: + //pDetails->threadID = kNuThreadIDDataFork; + pDetails->entryKind = FileDetails::kFileKindDataFork; + break; + case GenericEntry::kRsrcThread: + //pDetails->threadID = kNuThreadIDRsrcFork; + pDetails->entryKind = FileDetails::kFileKindRsrcFork; + break; + case GenericEntry::kDiskImageThread: + //pDetails->threadID = kNuThreadIDDiskImage; + pDetails->entryKind = FileDetails::kFileKindDiskImage; + break; + default: + ASSERT(false); + // was initialized to default earlier + break; + } + +/*bail:*/ + return kNuErrNone; +} + +/* + * Directory structure and functions, based on zDIR in Info-Zip sources. + */ +typedef struct Win32dirent { + char d_attr; + char d_name[MAX_PATH]; + int d_first; + HANDLE d_hFindFile; +} Win32dirent; + +static const char* kWildMatchAll = "*.*"; + +/* + * Prepare a directory for reading. + * + * Allocates a Win32dirent struct that must be freed by the caller. + */ +Win32dirent* +GenericArchive::OpenDir(const char* name) +{ + Win32dirent* dir = nil; + char* tmpStr = nil; + char* cp; + WIN32_FIND_DATA fnd; + + dir = (Win32dirent*) malloc(sizeof(*dir)); + tmpStr = (char*) malloc(strlen(name) + (2 + sizeof(kWildMatchAll))); + if (dir == nil || tmpStr == nil) + goto failed; + + strcpy(tmpStr, name); + cp = tmpStr + strlen(tmpStr); + + /* don't end in a colon (e.g. "C:") */ + if ((cp - tmpStr) > 0 && strrchr(tmpStr, ':') == (cp - 1)) + *cp++ = '.'; + /* must end in a slash */ + if ((cp - tmpStr) > 0 && + strrchr(tmpStr, PathProposal::kLocalFssep) != (cp - 1)) + *cp++ = PathProposal::kLocalFssep; + + strcpy(cp, kWildMatchAll); + + dir->d_hFindFile = FindFirstFile(tmpStr, &fnd); + if (dir->d_hFindFile == INVALID_HANDLE_VALUE) + goto failed; + + strcpy(dir->d_name, fnd.cFileName); + dir->d_attr = (unsigned char) fnd.dwFileAttributes; + dir->d_first = 1; + +bail: + free(tmpStr); + return dir; + +failed: + free(dir); + dir = nil; + goto bail; +} + +/* + * Get an entry from an open directory. + * + * Returns a nil pointer after the last entry has been read. + */ +Win32dirent* +GenericArchive::ReadDir(Win32dirent* dir) +{ + if (dir->d_first) + dir->d_first = 0; + else { + WIN32_FIND_DATA fnd; + + if (!FindNextFile(dir->d_hFindFile, &fnd)) + return nil; + strcpy(dir->d_name, fnd.cFileName); + dir->d_attr = (unsigned char) fnd.dwFileAttributes; + } + + return dir; +} + +/* + * Close a directory. + */ +void +GenericArchive::CloseDir(Win32dirent* dir) +{ + if (dir == nil) + return; + + FindClose(dir->d_hFindFile); + free(dir); +} + + +/* might as well blend in with the UNIX version */ +#define DIR_NAME_LEN(dirent) ((int)strlen((dirent)->d_name)) + +//static NuError Win32AddFile(NulibState* pState, NuArchive* pArchive, +// const char* pathname); + + +/* + * Win32 recursive directory descent. Scan the contents of a directory. + * If a subdirectory is found, follow it; otherwise, call Win32AddFile to + * add the file. + */ +NuError +GenericArchive::Win32AddDirectory(const AddFilesDialog* pAddOpts, + const char* dirName, CString* pErrMsg) +{ + NuError err = kNuErrNone; + Win32dirent* dirp = nil; + Win32dirent* entry; + char nbuf[MAX_PATH]; /* malloc might be better; this soaks stack */ + char fssep; + int len; + + ASSERT(pAddOpts != nil); + ASSERT(dirName != nil); + + WMSG1("+++ DESCEND: '%s'\n", dirName); + + dirp = OpenDir(dirName); + if (dirp == nil) { + if (errno == ENOTDIR) + err = kNuErrNotDir; + else + err = errno ? (NuError)errno : kNuErrOpenDir; + + pErrMsg->Format("Failed on '%s': %s.", dirName, NuStrError(err)); + goto bail; + } + + fssep = PathProposal::kLocalFssep; + + /* could use readdir_r, but we don't care about reentrancy here */ + while ((entry = ReadDir(dirp)) != nil) { + /* skip the dotsies */ + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) + continue; + + len = strlen(dirName); + if (len + DIR_NAME_LEN(entry) +2 > MAX_PATH) { + err = kNuErrInternal; + WMSG4("ERROR: Filename exceeds %d bytes: %s%c%s", + MAX_PATH, dirName, fssep, entry->d_name); + goto bail; + } + + /* form the new name, inserting an fssep if needed */ + strcpy(nbuf, dirName); + if (dirName[len-1] != fssep) + nbuf[len++] = fssep; + strcpy(nbuf+len, entry->d_name); + + err = Win32AddFile(pAddOpts, nbuf, pErrMsg); + if (err != kNuErrNone) + goto bail; + } + +bail: + if (dirp != nil) + (void)CloseDir(dirp); + return err; +} + +/* + * Add a file to the list we're adding to the archive. If it's a directory, + * and the recursive descent feature is enabled, call Win32AddDirectory to + * add the contents of the dir. + * + * Returns with an error if the file doesn't exist or isn't readable. + */ +NuError +GenericArchive::Win32AddFile(const AddFilesDialog* pAddOpts, + const char* pathname, CString* pErrMsg) +{ + NuError err = kNuErrNone; + Boolean exists, isDir, isReadable; + FileDetails details; + struct stat sb; + + ASSERT(pAddOpts != nil); + ASSERT(pathname != nil); + + PathName checkPath(pathname); + int ierr = checkPath.CheckFileStatus(&sb, &exists, &isReadable, &isDir); + if (ierr != 0) { + err = kNuErrGeneric; + pErrMsg->Format("Unexpected error while examining '%s': %s.", pathname, + NuStrError((NuError) ierr)); + goto bail; + } + + if (!exists) { + err = kNuErrFileNotFound; + pErrMsg->Format("Couldn't find '%s'", pathname); + goto bail; + } + if (!isReadable) { + err = kNuErrFileNotReadable; + pErrMsg->Format("File '%s' isn't readable.", pathname); + goto bail; + } + if (isDir) { + if (pAddOpts->fIncludeSubfolders) + err = Win32AddDirectory(pAddOpts, pathname, pErrMsg); + goto bail; + } + + /* + * We've found a file that we want to add. We need to decide what + * filetype and auxtype it has, and whether or not it's actually the + * resource fork of another file. + */ + WMSG1("+++ ADD '%s'\n", pathname); + + /* + * Fill out the "details" structure. The class has an automatic + * conversion to NuFileDetails, but it relies on the CString storage + * in the FileDetails, so be careful how you use it. + */ + err = GetFileDetails(pAddOpts, pathname, &sb, &details); + if (err != kNuErrNone) + goto bail; + + assert(strcmp(pathname, details.origName) == 0); + err = DoAddFile(pAddOpts, &details); + if (err == kNuErrSkipped) // ignore "skipped" result + err = kNuErrNone; + if (err != kNuErrNone) + goto bail; + +bail: + if (err != kNuErrNone && pErrMsg->IsEmpty()) { + pErrMsg->Format("Unable to add file '%s': %s.", + pathname, NuStrError(err)); + } + return err; +} + +/* + * External entry point; just calls the system-specific version. + * + * [ I figure the GS/OS version will want to pass a copy of the file + * info from the GSOSAddDirectory function back into GSOSAddFile, so we'd + * want to call it from here with a nil pointer indicating that we + * don't yet have the file info. That way we can get the file info + * from the directory read call and won't have to check it again in + * GSOSAddFile. ] + */ +NuError +GenericArchive::AddFile(const AddFilesDialog* pAddOpts, const char* pathname, + CString* pErrMsg) +{ + *pErrMsg = ""; + return Win32AddFile(pAddOpts, pathname, pErrMsg); +} + +/* + * =========================================================================== + * GenericArchive::FileDetails + * =========================================================================== + */ + +/* + * Constructor. + */ +GenericArchive::FileDetails::FileDetails(void) +{ + //threadID = 0; + entryKind = kFileKindUnknown; + fileSysFmt = DiskImg::kFormatUnknown; + fileSysInfo = storageType = 0; + access = fileType = extraType = 0; + memset(&createWhen, 0, sizeof(createWhen)); + memset(&modWhen, 0, sizeof(modWhen)); + memset(&archiveWhen, 0, sizeof(archiveWhen)); +} + +/* + * Automatic cast conversion to NuFileDetails. + * + * Note the NuFileDetails will have a string pointing into our storage. + * This is not a good thing, but it's tough to work around. + */ +GenericArchive::FileDetails::operator const NuFileDetails() const +{ + NuFileDetails details; + + //details.threadID = threadID; + switch (entryKind) { + case kFileKindDataFork: + details.threadID = kNuThreadIDDataFork; + break; + case kFileKindBothForks: // not exactly supported, doesn't really matter + case kFileKindRsrcFork: + details.threadID = kNuThreadIDRsrcFork; + break; + case kFileKindDiskImage: + details.threadID = kNuThreadIDDiskImage; + break; + case kFileKindDirectory: + default: + WMSG1("Invalid entryKind (%d) for NuFileDetails conversion\n", + entryKind); + ASSERT(false); + details.threadID = 0; // that makes it an old-style comment?! + break; + } + + details.origName = origName; // CString to char* + details.storageName = storageName; // CString to char* + //details.fileSysID = fileSysID; + details.fileSysInfo = fileSysInfo; + details.access = access; + details.fileType = fileType; + details.extraType = extraType; + details.storageType = storageType; + details.createWhen = createWhen; + details.modWhen = modWhen; + details.archiveWhen = archiveWhen; + + switch (fileSysFmt) { + case DiskImg::kFormatProDOS: + case DiskImg::kFormatDOS33: + case DiskImg::kFormatDOS32: + case DiskImg::kFormatPascal: + case DiskImg::kFormatMacHFS: + case DiskImg::kFormatMacMFS: + case DiskImg::kFormatLisa: + case DiskImg::kFormatCPM: + //kFormatCharFST + case DiskImg::kFormatMSDOS: + //kFormatHighSierra + case DiskImg::kFormatISO9660: + /* these map directly */ + details.fileSysID = (enum NuFileSysID) fileSysFmt; + break; + + case DiskImg::kFormatRDOS33: + case DiskImg::kFormatRDOS32: + case DiskImg::kFormatRDOS3: + /* these look like DOS33, e.g. text is high-ASCII */ + details.fileSysID = kNuFileSysDOS33; + break; + + default: + details.fileSysID = kNuFileSysUnknown; + break; + } + + + // Return stack copy, which copies into compiler temporary with our + // copy constructor. + return details; +} + +/* + * Copy the contents of our object to a new object. + * + * Useful for operator= and copy construction. + */ +/*static*/ void +GenericArchive::FileDetails::CopyFields(FileDetails* pDst, + const FileDetails* pSrc) +{ + //pDst->threadID = pSrc->threadID; + pDst->entryKind = pSrc->entryKind; + pDst->origName = pSrc->origName; + pDst->storageName = pSrc->storageName; + pDst->fileSysFmt = pSrc->fileSysFmt; + pDst->fileSysInfo = pSrc->fileSysInfo; + pDst->access = pSrc->access; + pDst->fileType = pSrc->fileType; + pDst->extraType = pSrc->extraType; + pDst->storageType = pSrc->storageType; + pDst->createWhen = pSrc->createWhen; + pDst->modWhen = pSrc->modWhen; + pDst->archiveWhen = pSrc->archiveWhen; +} + + +/* + * =========================================================================== + * SelectionSet + * =========================================================================== + */ + +/* + * Create a selection set from the selected items in a ContentList. + * + * This grabs the items in the order in which they appear in the display + * (at least under Win2K), which is a good thing. It appears that, if you + * just grab indices 0..N, you will get them in order. + */ +void +SelectionSet::CreateFromSelection(ContentList* pContentList, int threadMask) +{ + WMSG1("CreateFromSelection (threadMask=0x%02x)\n", threadMask); + + POSITION posn; + posn = pContentList->GetFirstSelectedItemPosition(); + ASSERT(posn != nil); + if (posn == nil) + return; + while (posn != nil) { + int num = pContentList->GetNextSelectedItem(/*ref*/ posn); + GenericEntry* pEntry = (GenericEntry*) pContentList->GetItemData(num); + + AddToSet(pEntry, threadMask); + } +} + +/* + * Like CreateFromSelection, but includes the entire list. + */ +void +SelectionSet::CreateFromAll(ContentList* pContentList, int threadMask) +{ + WMSG1("CreateFromAll (threadMask=0x%02x)\n", threadMask); + + int count = pContentList->GetItemCount(); + for (int idx = 0; idx < count; idx++) { + GenericEntry* pEntry = (GenericEntry*) pContentList->GetItemData(idx); + + AddToSet(pEntry, threadMask); + } +} + +/* + * Add a GenericEntry to the set, but only if we can find a thread that + * matches the flags in "threadMask". + */ +void +SelectionSet::AddToSet(GenericEntry* pEntry, int threadMask) +{ + SelectionEntry* pSelEntry; + + //WMSG1(" Sel '%s'\n", pEntry->GetPathName()); + + if (!(threadMask & GenericEntry::kAllowVolumeDir) && + pEntry->GetRecordKind() == GenericEntry::kRecordKindVolumeDir) + { + /* only include volume dir if specifically requested */ + //WMSG1(" Excluding volume dir '%s' from set\n", pEntry->GetPathName()); + return; + } + + if (!(threadMask & GenericEntry::kAllowDirectory) && + pEntry->GetRecordKind() == GenericEntry::kRecordKindDirectory) + { + /* only include directories if specifically requested */ + //WMSG1(" Excluding folder '%s' from set\n", pEntry->GetPathName()); + return; + } + + if (!(threadMask & GenericEntry::kAllowDamaged) && pEntry->GetDamaged()) + { + /* only include "damaged" files if specifically requested */ + return; + } + + bool doAdd = false; + + if (threadMask & GenericEntry::kAnyThread) + doAdd = true; + + if ((threadMask & GenericEntry::kCommentThread) && pEntry->GetHasComment()) + doAdd = true; + if ((threadMask & GenericEntry::kDataThread) && pEntry->GetHasDataFork()) + doAdd = true; + if ((threadMask & GenericEntry::kRsrcThread) && pEntry->GetHasRsrcFork()) + doAdd = true; + if ((threadMask & GenericEntry::kDiskImageThread) && pEntry->GetHasDiskImage()) + doAdd = true; + + if (doAdd) { + pSelEntry = new SelectionEntry(pEntry); + AddEntry(pSelEntry); + } +} + +/* + * Add a new entry to the end of the list. + */ +void +SelectionSet::AddEntry(SelectionEntry* pEntry) +{ + if (fEntryHead == nil) { + ASSERT(fEntryTail == nil); + fEntryHead = pEntry; + fEntryTail = pEntry; + ASSERT(pEntry->GetPrev() == nil); + ASSERT(pEntry->GetNext() == nil); + } else { + ASSERT(fEntryTail != nil); + ASSERT(pEntry->GetPrev() == nil); + pEntry->SetPrev(fEntryTail); + ASSERT(fEntryTail->GetNext() == nil); + fEntryTail->SetNext(pEntry); + fEntryTail = pEntry; + } + + fNumEntries++; +} + +/* + * Delete the "entries" list. + */ +void +SelectionSet::DeleteEntries(void) +{ + SelectionEntry* pEntry; + SelectionEntry* pNext; + + WMSG0("Deleting selection entries\n"); + + pEntry = GetEntries(); + while (pEntry != nil) { + pNext = pEntry->GetNext(); + delete pEntry; + pEntry = pNext; + } +} + +/* + * Count the #of entries whose display name matches the prefix string. + */ +int +SelectionSet::CountMatchingPrefix(const char* prefix) +{ + SelectionEntry* pEntry; + int count = 0; + int len = strlen(prefix); + ASSERT(len > 0); + + pEntry = GetEntries(); + while (pEntry != nil) { + GenericEntry* pGeneric = pEntry->GetEntry(); + + if (strncasecmp(prefix, pGeneric->GetDisplayName(), len) == 0) + count++; + pEntry = pEntry->GetNext(); + } + + return count; +} + +/* + * Dump the contents of a selection set. + */ +void +SelectionSet::Dump(void) +{ + const SelectionEntry* pEntry; + + WMSG1("SelectionSet: %d entries\n", fNumEntries); + + pEntry = fEntryHead; + while (pEntry != nil) { + WMSG1(" : name='%s'\n", pEntry->GetEntry()->GetPathName()); + pEntry = pEntry->GetNext(); + } +} diff --git a/app/GenericArchive.h b/app/GenericArchive.h new file mode 100644 index 0000000..43f4237 --- /dev/null +++ b/app/GenericArchive.h @@ -0,0 +1,690 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Generic Apple II archive handling. + * + * These are abstract base classes. + */ +#ifndef __GENERIC_ARCHIVE__ +#define __GENERIC_ARCHIVE__ + +#include "Preferences.h" +#include "../util/UtilLib.h" +#include "../diskimg/DiskImg.h" +#include "../prebuilt/NufxLib.h" +#include "../reformat/Reformat.h" +#include +#include + +// this shouldn't be in a header file, but in here it's probably okay +using namespace DiskImgLib; + +class ActionProgressDialog; +class AddFilesDialog; +class RecompressOptionsDialog; +class SelectionSet; +struct Win32dirent; +class GenericArchive; + +const int kFileTypeTXT = 0x04; +const int kFileTypeBIN = 0x06; +const int kFileTypeSRC = 0xb0; +const int kFileTypeINT = 0xfa; +const int kFileTypeBAS = 0xfc; + +/* + * Set of data allowed in file property "set file info" calls. + */ +typedef struct FileProps { + unsigned long fileType; + unsigned long auxType; + unsigned long access; + time_t createWhen; + time_t modWhen; +} FileProps; + +/* + * Options for converting between file archives and disk archives. + */ +class XferFileOptions { +public: + XferFileOptions(void) : + fTarget(nil), fPreserveEmptyFolders(false), fpTargetFS(nil) + {} + ~XferFileOptions(void) {} + + /* where the stuff is going */ + GenericArchive* fTarget; + + /* these really only have meaning when converting disk to file */ + //bool fConvDOSText; + //bool fConvPascalText; + bool fPreserveEmptyFolders; + + /* only useful when converting files to a disk image */ + //CString fStoragePrefix; + DiskImgLib::DiskFS* fpTargetFS; +}; + + +/* + * Generic description of an Apple II file. + * + * Everything returned by the basic "get" calls for display in the ContentList + * must be held in local storage (i.e. not just pointers into DiskFS data). + * Otherwise, we run the risk of doing some DiskFS updates and having weird + * things happen in the ContentList. + */ +class GenericEntry { +public: + GenericEntry(void); + virtual ~GenericEntry(void); + + /* kinds of files found in archives */ + enum RecordKind { + kRecordKindUnknown = 0, + kRecordKindDisk, + kRecordKindFile, + kRecordKindForkedFile, + kRecordKindDirectory, + kRecordKindVolumeDir, + }; + /* + * Threads we will view or extract (threadMask). This is no longer used + * for viewing files, but still plays a role when extracting. + */ + enum { + // create one entry for each matching thread + kDataThread = 0x01, + kRsrcThread = 0x02, + kDiskImageThread = 0x04, + kCommentThread = 0x08, + + // grab any of the above threads + kAnyThread = 0x10, + + // set this if we allow matches on directory entries + kAllowDirectory = 0x20, + + // and volume directory entries + kAllowVolumeDir = 0x40, + + // set to include "damaged" files + kAllowDamaged = 0x80, + }; + /* EOL conversion mode for threads being extracted */ + typedef enum ConvertEOL { + kConvertUnknown = 0, kConvertEOLOff, kConvertEOLOn, kConvertEOLAuto + } ConvertEOL; + typedef enum EOLType { + kEOLUnknown = 0, kEOLCR, kEOLLF, kEOLCRLF + }; + /* high ASCII conversion mode for threads being extracted */ + typedef enum ConvertHighASCII { + kConvertHAUnknown = 0, kConvertHAOff, kConvertHAOn, kConvertHAAuto + } ConvertHighASCII; + + /* ProDOS access flags, used for all filesystems */ + enum { + kAccessRead = 0x01, + kAccessWrite = 0x02, + kAccessInvisible = 0x04, + kAccessBackup = 0x20, + kAccessRename = 0x40, + kAccessDelete = 0x80 + }; + + /* features supported by underlying archive */ + typedef enum Feature { + kFeatureCanChangeType, + kFeaturePascalTypes, + kFeatureDOSTypes, + kFeatureHFSTypes, + kFeatureHasFullAccess, + kFeatureHasSimpleAccess, // mutually exclusive with FullAccess + kFeatureHasInvisibleFlag, + } Feature; + + // retrieve thread (or filesystem) data + virtual int ExtractThreadToBuffer(int which, char** ppText, long* pLength, + CString* pErrMsg) const = 0; + virtual int ExtractThreadToFile(int which, FILE* outfp, ConvertEOL conv, + ConvertHighASCII convHA, CString* pErrMsg) const = 0; + + // This helps us retain the ContentList selection across a Reload(). Only + // necessary for read-write archives, since those are the only ones that + // ever need to be reloaded. Value must be nonzero to be used. + virtual long GetSelectionSerial(void) const = 0; + + /* are we allowed to change the file/aux type of this entry? */ + /* (may need to generalize this to "changeable attrs" bitmask) */ + virtual bool GetFeatureFlag(Feature feature) const = 0; + + long GetIndex(void) const { return fIndex; } + void SetIndex(long idx) { fIndex = idx; } + + const char* GetPathName(void) const { return fPathName; } + void SetPathName(const char* path); + const char* GetFileName(void); + const char* GetFileNameExtension(void); // returns e.g. ".SHK" + void SetSubVolName(const char* name); + const char* GetSubVolName(void) const { return fSubVolName; } + const char* GetDisplayName(void) const; // not really "const" + + char GetFssep(void) const { return fFssep; } + void SetFssep(char fssep) { fFssep = fssep; } + long GetFileType(void) const { return fFileType; } + void SetFileType(long type) { fFileType = type; } + long GetAuxType(void) const { return fAuxType; } + void SetAuxType(long type) { fAuxType = type; } + long GetAccess(void) const { return fAccess; } + void SetAccess(long access) { fAccess = access; } + time_t GetCreateWhen(void) const { return fCreateWhen; } + void SetCreateWhen(time_t when) { fCreateWhen = when; } + time_t GetModWhen(void) const { return fModWhen; } + void SetModWhen(time_t when) { fModWhen = when; } + RecordKind GetRecordKind(void) const { return fRecordKind; } + void SetRecordKind(RecordKind recordKind) { fRecordKind = recordKind; } + const char* GetFormatStr(void) const { return fFormatStr; } + void SetFormatStr(const char* str) { fFormatStr = str; } // arg not copied, must be static! + LONGLONG GetCompressedLen(void) const { return fCompressedLen; } + void SetCompressedLen(LONGLONG len) { fCompressedLen = len; } + LONGLONG GetUncompressedLen(void) const { + return fDataForkLen + fRsrcForkLen; + } + //void SetUncompressedLen(LONGLONG len) { fUncompressedLen = len; } + LONGLONG GetDataForkLen(void) const { return fDataForkLen; } + void SetDataForkLen(LONGLONG len) { fDataForkLen = len; } + LONGLONG GetRsrcForkLen(void) const { return fRsrcForkLen; } + void SetRsrcForkLen(LONGLONG len) { fRsrcForkLen = len; } + + DiskImg::FSFormat GetSourceFS(void) const { return fSourceFS; } + void SetSourceFS(DiskImg::FSFormat fmt) { fSourceFS = fmt; } + + bool GetHasDataFork(void) const { return fHasDataFork; } + void SetHasDataFork(bool val) { fHasDataFork = val; } + bool GetHasRsrcFork(void) const { return fHasRsrcFork; } + void SetHasRsrcFork(bool val) { fHasRsrcFork = val; } + bool GetHasDiskImage(void) const { return fHasDiskImage; } + void SetHasDiskImage(bool val) { fHasDiskImage = val; } + bool GetHasComment(void) const { return fHasComment; } + void SetHasComment(bool val) { fHasComment = val; } + bool GetHasNonEmptyComment(void) const { return fHasNonEmptyComment; } + void SetHasNonEmptyComment(bool val) { fHasNonEmptyComment = val; } + + bool GetDamaged(void) const { return fDamaged; } + void SetDamaged(bool val) { fDamaged = val; } + bool GetSuspicious(void) const { return fSuspicious; } + void SetSuspicious(bool val) { fSuspicious = val; } + + GenericEntry* GetPrev(void) const { return fpPrev; } + void SetPrev(GenericEntry* pEntry) { fpPrev = pEntry; } + GenericEntry* GetNext(void) const { return fpNext; } + void SetNext(GenericEntry* pEntry) { fpNext = pEntry; } + + // Utility functions. + const char* GetFileTypeString(void) const; + static bool CheckHighASCII(const unsigned char* buffer, + unsigned long count); + + static ConvertEOL DetermineConversion(const unsigned char* buffer, + long count, EOLType* pSourceType, ConvertHighASCII* pConvHA); + static int GenericEntry::WriteConvert(FILE* fp, const char* buf, + size_t len, ConvertEOL* pConv, ConvertHighASCII* pConvHA, + bool* pLastCR); + +protected: + static void SpacesToUnderscores(char* buf); + +private: + char* fPathName; + const char* fFileName; // points within fPathName + const char* fFileNameExtension; // points within fPathName + char fFssep; + char* fSubVolName; // sub-volume prefix, or nil if none + char* fDisplayName; // combination of sub-vol and path + long fFileType; + long fAuxType; + long fAccess; + time_t fCreateWhen; + time_t fModWhen; + RecordKind fRecordKind; // forked file, disk image, ?? + const char* fFormatStr; // static str; compression or fs format + //LONGLONG fUncompressedLen; + LONGLONG fDataForkLen; // also for disk images + LONGLONG fRsrcForkLen; // set to 0 when nonexistent + LONGLONG fCompressedLen; // data/disk + rsrc + + DiskImg::FSFormat fSourceFS; // if DOS3.3, text files have funky format + + bool fHasDataFork; + bool fHasRsrcFork; + bool fHasDiskImage; + bool fHasComment; + bool fHasNonEmptyComment; // set if fHasComment and it isn't empty + + bool fDamaged; // if set, don't try to open file + bool fSuspicious; // if set, file *might* be damaged + + long fIndex; // serial index, for sorting "unsorted" view + GenericEntry* fpPrev; + GenericEntry* fpNext; +}; + +/* + * Generic representation of a collection of Apple II files. + * + * This raises the "reload" flag whenever its data is reloaded. Any code that + * keeps pointers to stuff (e.g. local copies of pointers to GenericEntry + * objects) needs to check the "reload" flag before dereferencing them. + */ +class GenericArchive { +public: + GenericArchive(void) { + fPathName = nil; + fNumEntries = 0; + fEntryHead = fEntryTail = nil; + fReloadFlag = true; + //fEntryIndex = nil; + } + virtual ~GenericArchive(void) { + //WMSG0("Deleting GenericArchive\n"); + DeleteEntries(); + delete fPathName; + } + + virtual GenericEntry* GetEntries(void) const { + return fEntryHead; + } + virtual long GetNumEntries(void) const { + return fNumEntries; + } + //virtual GenericEntry* GetEntry(long num) { + // ASSERT(num >= 0 && num < fNumEntries); + // if (fEntryIndex == nil) + // CreateIndex(); + // return fEntryIndex[num]; + //} + + typedef enum { + kResultUnknown = 0, + kResultSuccess, // open succeeded + kResultFailure, // open failed + kResultCancel, // open was cancelled by user + kResultFileArchive, // found a file archive rather than disk image + } OpenResult; + + // Open an archive and do fun things with the innards. + virtual OpenResult Open(const char* filename, bool readOnly, + CString* pErrMsg) = 0; + // Create a new archive with the specified name. + virtual CString New(const char* filename, const void* options) = 0; + // Flush any unwritten data to disk + virtual CString Flush(void) = 0; + // Force a re-read from the underlying storage. + virtual CString Reload(void) = 0; + // Do we allow modification? + virtual bool IsReadOnly(void) const = 0; + // Does the underlying storage have un-flushed modifications? + virtual bool IsModified(void) const = 0; + + virtual bool GetReloadFlag(void) { return fReloadFlag; } + virtual void ClearReloadFlag(void) { fReloadFlag = false; } + + // One of these for every sub-class. This is used to ensure that, should + // we need to down-cast an object, we did it correctly (without needing + // to include RTTI support). + typedef enum { + kArchiveUnknown = 0, + kArchiveNuFX, + kArchiveBNY, + kArchiveACU, + kArchiveDiskImage, + } ArchiveKind; + virtual ArchiveKind GetArchiveKind(void) = 0; + + // Get a nice description for the title bar. + virtual void GetDescription(CString* pStr) const = 0; + + // Do a bulk add. + virtual bool BulkAdd(ActionProgressDialog* pActionProgress, + const AddFilesDialog* pAddOpts) = 0; + // Do a disk add. + virtual bool AddDisk(ActionProgressDialog* pActionProgress, + const AddFilesDialog* pAddOpts) = 0; + // Create a subdirectory. + virtual bool CreateSubdir(CWnd* pMsgWnd, GenericEntry* pParentEntry, + const char* newName) = 0; + + // Test a set of files. + virtual bool TestSelection(CWnd* pMsgWnd, SelectionSet* pSelSet) = 0; + + // Delete a set of files. + virtual bool DeleteSelection(CWnd* pMsgWnd, SelectionSet* pSelSet) = 0; + + // Rename a set of files. + virtual bool RenameSelection(CWnd* pMsgWnd, SelectionSet* pSelSet) = 0; + virtual CString TestPathName(const GenericEntry* pGenericEntry, + const CString& basePath, const CString& newName, char newFssep) const = 0; + + // Rename a volume (or sub-volume) + virtual bool RenameVolume(CWnd* pMsgWnd, DiskFS* pDiskFS, + const char* newName) = 0; + virtual CString TestVolumeName(const DiskFS* pDiskFS, + const char* newName) const = 0; + + // Recompress a set of files. + virtual bool RecompressSelection(CWnd* pMsgWnd, SelectionSet* pSelSet, + const RecompressOptionsDialog* pRecompOpts) = 0; + + // Transfer files out of this archive and into another. + typedef enum { + kXferOK = 0, kXferFailed = 1, kXferCancelled = 2, kXferOutOfSpace = 3 + } XferStatus; + virtual XferStatus XferSelection(CWnd* pMsgWnd, SelectionSet* pSelSet, + ActionProgressDialog* pActionProgress, + const XferFileOptions* pXferOpts) = 0; + + // Get, set, or delete the comment on an entry. + virtual bool GetComment(CWnd* pMsgWnd, const GenericEntry* pEntry, + CString* pStr) = 0; + virtual bool SetComment(CWnd* pMsgWnd, GenericEntry* pEntry, + const CString& str) = 0; + virtual bool DeleteComment(CWnd* pMsgWnd, GenericEntry* pEntry) = 0; + + // Set ProDOS file properties (e.g. file type, access flags). + virtual bool SetProps(CWnd* pMsgWnd, GenericEntry* pEntry, + const FileProps* pProps) = 0; + + // Preferences have changed, update library state as needed. + virtual void PreferencesChanged(void) = 0; + + // Determine an archive's capabilities. This is specific to the object + // instance, so this must not be made a static function. + typedef enum { + kCapUnknown = 0, + + kCapCanTest, // NuFX, BNY + kCapCanRenameFullPath, // NuFX, BNY + kCapCanRecompress, // NuFX, BNY + kCapCanEditComment, // NuFX + kCapCanAddDisk, // NuFX + kCapCanConvEOLOnAdd, // Disk + kCapCanCreateSubdir, // Disk + kCapCanRenameVolume, // Disk + } Capability; + virtual long GetCapability(Capability cap) = 0; + + // Get the pathname of the file we opened. + const char* GetPathName(void) const { return fPathName; } + + // Generic utility function. + static CString GenDerivedTempName(const char* filename); + static int ComparePaths(const CString& name1, char fssep1, + const CString& name2, char fssep2); + + void AddEntry(GenericEntry* pEntry); + + /* + * This class holds details about a file that we're adding. + * + * It's based on the NuFileDetails class from NufxLib (which used to be + * used everywhere). + */ + class FileDetails { + public: + FileDetails(void); + virtual ~FileDetails(void) {} + + /* + * Automatic cast to NuFileDetails. The NuFileDetails structure will + * have a pointer to at least one of our strings, so structures + * filled out this way need to be short-lived. (Yes, this is + * annoying, but it's how NufxLib works.) + */ + operator const NuFileDetails() const; + + /* + * Provide operator= and copy constructor. This'd be easier without + * the strings. + */ + FileDetails& operator=(const FileDetails& src) { + if (&src != this) + CopyFields(this, &src); + return *this; + } + FileDetails(const FileDetails& src) { + CopyFields(this, &src); + } + + /* + * What kind of file this is. Files being added to NuFX from Windows + * can't be "BothForks" because each the forks are stored in + * separate files. However, files being transferred from a NuFX + * archive, a disk image, or in from the clipboard can be both. + * + * (NOTE: this gets embedded into clipboard data. If you change + * these values, update the version info in Clipboard.cpp.) + */ + typedef enum FileKind { + kFileKindUnknown = 0, + kFileKindDataFork, + kFileKindRsrcFork, + kFileKindDiskImage, + kFileKindBothForks, + kFileKindDirectory, + } FileKind; + + /* + * Data fields. While transitioning from general use of NuFileDetails + * (v1.2.x to v2.0) I'm just going to leave these public. + */ + //NuThreadID threadID; /* data, rsrc, disk img? */ + FileKind entryKind; + CString origName; + + CString storageName; /* normalized (NOT FS-normalized) */ + //NuFileSysID fileSysID; + DiskImg::FSFormat fileSysFmt; + unsigned short fileSysInfo; /* fssep lurks here */ + unsigned long access; + unsigned long fileType; + unsigned long extraType; + unsigned short storageType; /* "Unknown" or disk block size */ + NuDateTime createWhen; + NuDateTime modWhen; + NuDateTime archiveWhen; + + private: + static void CopyFields(FileDetails* pDst, const FileDetails* pSrc); + }; + + // Transfer files, one at a time, into this archive from another. + virtual void XferPrepare(const XferFileOptions* pXferOpts) = 0; + virtual CString XferFile(FileDetails* pDetails, unsigned char** pDataBuf, + long dataLen, unsigned char** pRsrcBuf, long rsrcLen) = 0; + virtual void XferAbort(CWnd* pMsgWnd) = 0; + virtual void XferFinish(CWnd* pMsgWnd) = 0; + static void UNIXTimeToDateTime(const time_t* pWhen, NuDateTime *pDateTime); + +protected: + virtual void DeleteEntries(void); + + /* NuLib2-derived recursive directory add functions */ + void ReplaceFssep(char* str, char oldc, char newc, char newSubst); + NuError GetFileDetails(const AddFilesDialog* pAddOpts, const char* pathname, + struct stat* psb, FileDetails* pDetails); + Win32dirent* OpenDir(const char* name); + Win32dirent* ReadDir(Win32dirent* dir); + void CloseDir(Win32dirent* dir); + NuError Win32AddDirectory(const AddFilesDialog* pAddOpts, + const char* dirName, CString* pErrMsg); + NuError Win32AddFile(const AddFilesDialog* pAddOpts, + const char* pathname, CString* pErrMsg); + NuError AddFile(const AddFilesDialog* pAddOpts, const char* pathname, + CString* pErrMsg); + + /* + * Each implementation must provide this. It's called from the generic + * AddFile function with the high-level add options, a partial pathname, + * and a FileDetails structure filled in using Win32 calls. + * + * One call to AddFile can result in multiple calls to DoAddFile if + * the subject of the AddFile call is a directory (and fIncludeSubdirs + * is set). + * + * DoAddFile is not called for subdirectories. The underlying code must + * create directories as needed. + * + * In some cases (such as renaming a file as it is being added) the + * information in "*pDetails" may be modified. + */ + virtual NuError DoAddFile(const AddFilesDialog* pAddOpts, + FileDetails* pDetails) = 0; + + void SetPathName(const char* pathName) { + delete fPathName; + if (pathName != nil) { + fPathName = new char[strlen(pathName)+1]; + strcpy(fPathName, pathName); + } else + fPathName = nil; + } + + bool fReloadFlag; // set after Reload called + +private: + //virtual void CreateIndex(void); + + //CString fNewPathHolder; + //CString fOrigPathHolder; + + char* fPathName; + long fNumEntries; + GenericEntry* fEntryHead; + GenericEntry* fEntryTail; + //GenericEntry** fEntryIndex; +}; + +/* + * One entry in a SelectionSet. + */ +class SelectionEntry { +public: + SelectionEntry(GenericEntry* pEntry) { + fpEntry = pEntry; + //fThreadKind = threadKind; + //fFilter = filter; + //fReformatName = ""; + fpPrev = fpNext = nil; + } + ~SelectionEntry(void) {} + + int Reformat(ReformatHolder* pHolder); + + GenericEntry* GetEntry(void) const { return fpEntry; } + //int GetThreadKind(void) const { return fThreadKind; } + //int GetFilter(void) const { return fFilter; } + //const char* GetReformatName(void) const { return fReformatName; } + + SelectionEntry* GetPrev(void) const { return fpPrev; } + void SetPrev(SelectionEntry* pPrev) { fpPrev = pPrev; } + SelectionEntry* GetNext(void) const { return fpNext; } + void SetNext(SelectionEntry* pNext) { fpNext = pNext; } + +private: + GenericEntry* fpEntry; + //int fThreadKind; // data, rsrc, etc (threadMask) + //int fFilter; // fAllowedFilters, really + //const char* fReformatName; // name of formatting actually applied + + SelectionEntry* fpPrev; + SelectionEntry* fpNext; +}; + +class ContentList; + +/* + * A set of selected files. + * + * Each entry represents one item that can be displayed, such as a data + * fork, resource fork, or comment thread. Thus, a single file may have + * multiple entries in the set. + */ +class SelectionSet { +public: + SelectionSet(void) { + fNumEntries = 0; + fEntryHead = fEntryTail = fIterCurrent = nil; + } + ~SelectionSet(void) { + DeleteEntries(); + } + + // create the set from the selected members of a ContentList + void CreateFromSelection(ContentList* pContentList, int threadMask); + // create the set from all members of a ContentList + void CreateFromAll(ContentList* pContentList, int threadMask); + + // get the head of the list + SelectionEntry* GetEntries(void) const { return fEntryHead; } + + void IterReset(void) { + fIterCurrent = nil; + } + // move to the next or previous entry as part of iterating + SelectionEntry* IterPrev(void) { + if (fIterCurrent == nil) + fIterCurrent = fEntryTail; + else + fIterCurrent = fIterCurrent->GetPrev(); + return fIterCurrent; + } + SelectionEntry* IterNext(void) { + if (fIterCurrent == nil) + fIterCurrent = fEntryHead; + else + fIterCurrent = fIterCurrent->GetNext(); + return fIterCurrent; + } + SelectionEntry* IterCurrent(void) { + return fIterCurrent; + } + bool IterHasPrev(void) const { + if (fIterCurrent == nil) + return fEntryTail != nil; + else + return (fIterCurrent->GetPrev() != nil); + } + bool IterHasNext(void) const { + if (fIterCurrent == nil) + return fEntryHead != nil; + else + return (fIterCurrent->GetNext() != nil); + } + + int GetNumEntries(void) const { return fNumEntries; } + + // count the #of entries whose display name matches "prefix" + int CountMatchingPrefix(const char* prefix); + + // debug dump + void Dump(void); + +private: + void AddToSet(GenericEntry* pEntry, int threadMask); + + void AddEntry(SelectionEntry* pEntry); + void DeleteEntries(void); + + int fNumEntries; + SelectionEntry* fIterCurrent; + + SelectionEntry* fEntryHead; + SelectionEntry* fEntryTail; +}; + +#endif /*__GENERIC_ARCHIVE__*/ \ No newline at end of file diff --git a/app/Graphics/ChooseFolder.bmp b/app/Graphics/ChooseFolder.bmp new file mode 100644 index 0000000000000000000000000000000000000000..bbdaf6925c77f58ebfc4da61ea5f52c3375d60ea GIT binary patch literal 246 zcmZ?r{l)+RWk5;;hy|dSk%0v)(Eui~5kMJ`WJ3dl0+K)`5H~O!IB)}*0@KaI zrapk7$sZGH^#M}btXS0@s9x7=q9Tu!dFAeG%Olj2(2v^Hmm=*sk<-cCTYT=Ty^O0y zwYS@>TciJwzYiXh=NWxWrns^V_a1SBNfod^YV?nRejl`cZAtr=Z)9R(LZ+ssWOjB| z78Vv{adAC+@%$g!k9qWpaPryRd| zBd@;uo(hic>AImYyN;M6=7>3sr7ZEpJ#kOm6Zgb*Ss)&W+ujoo#4!?yN8%AtBoK)w z{DhyPRd!q*^&N>EiyYr?R-TK4!NK5Qa4;AW3=RedgM-1rV3;sC7#s{P5Qo9gVQ?@w z7#s`^2A7Dz;oxw}!2v^M6rQVx#lzxZv0H}4Q`}R?lhngv+rXq};9>Ew*tWpo;qY*H zI6NG-Rd9G1JPaNN-8v74hrz=T?VCO3Eu>EV!?*3#-GPeQ0iA6%wF69nqJaVdray>xrdq=(MO1fToF4(=O zpQ;n<*5Y-h`j2W0`irX3?giDE{^NI{zbTuRU|g-#l|N6E>MX43?F`FBrtiot@gTj* zs{l2(Cx3mu?w2fTHE(?$KewXj(E0-NU(ja~sB|zy{&%hKzyJ5}=!TX}LCP`Kxa#~> z#c=vg$}YWS@7|w*AIF;fz0(f_s_v$Zi(Rh%_eZlf+3RnLynhQ>koj~0wlz$%QmU?lnfBF=A>_FG7Q4(V^AY%eEI zm(_2Nx8~HgM8?%Nc%Q0TeoyI-D&B>88@}K2j>}uF$bV%A@qldHclUETaXLvbwdI-J qZ+g3(_v7taVVASL<>ee*Ds7s~etJ<@UfOj&{Z`n&3QNf1UG{H03!0w* literal 0 HcmV?d00001 diff --git a/app/Graphics/FileViewer.ico b/app/Graphics/FileViewer.ico new file mode 100644 index 0000000000000000000000000000000000000000..b7fcaeaeba16b0087a9e23147127cde06d3e417b GIT binary patch literal 1078 zcmcgqJC4FY41Gh8=tyorsui~gH)A9&Hd}EGBpS+;6p^y;*)w6I%~AnQ?C0-zJ_Z6- zXxj#&+Y0z1HjUf+0BkE@PhU$ukOS6~KxFPRMFts((SedGQc6SxM*A_2(#ZVel6XIj zz4yr7*wq}hBf$e11xhe~?~pgkz#rSvSzzfz6>H_fs^LLp8=76qW)II-uem+XpVrE2 zs-<0?kUr65>rz=h?cWo00rF@3eSRg(?#GQ!N$qNhhU3unyZi1^_jTG_HnB>T7$JuL zxa25bb*jI2L$|bk@oCN*a!W3Ao~SQV^A}gA;u@Zmj+R%Y8$(T!^h!u(ESODT#1sg7v>5~Hpd{HaN&kz+Qrj$*d$H$Ch1x;k*}sEcIXOufnfvh`>47|S-E zZMvOEPo(-UX3={|!GzxBY8?7SEVXX=G)3~8-W%$cPfKQWwBFxE*T9yJ z4c+#W%)m_7;6`sw%@)Pz^=!NC9*BKzvDFy2R?nWT#=l=~t?oAJ7>#EP&fMt?+?~$K zr3-1ybymKZ>kQsM-WeP|-dVZ$`@xmZ!<&rX2JY6w!I@hRb)VLZ9D1MYqwo=9LZ+Fy zjb}{kF!ga_?)NLf%u7{WLYND)6+6LJ1g_^^Rj>cemQX9fGjR9%F@!3 zoIH6_jvYHDM~@zrBS(%%|KlhpPDHsRcP0KE`3lN@NjrItW|J$7(5If25p^( z!^7ZV2=<8;suohG`a`$&YImTbc21nNn%X%`IYn~{L1l#G(KIEy+n&=ufXJcCGkxks-mbP3+;4eGM*;BGB(a;@#67T zE#<4zBJ2CVS=Bt-fotcO@fto({xRb%eDD0JekJyfue{s(ubZlk+KOgX_os>smML7H`F?kYSe5 z_>?1wI)B<#ZL9NZj2@adiL|J#712oic8p=XSu3?2Upb!2KN_F(=MkEJtmAo-wqo>M zNxuu}w9w3Ry<9Unn7a9%<(a#2+T_%Y(~m!4e_50!H;VE>NAvwu{(Mm7^>R(+pV!L^=czxr zR?dA^alG>j5-ydiiMLOiwa=<*-}OCJf1z10bH>bjr_DP}{pv#Tot71YKNk9)`mFD{ zgr`G)W3gp7t1P?7CeEVRLma;;NVk!g^!JOdNd!Li_cDwYuZDE!xo%t#*;;K6wW;s? Wk0L+l_>eW@8tZFUSo6B9{{I1p0YO** literal 0 HcmV?d00001 diff --git a/app/Graphics/diskimage.ico b/app/Graphics/diskimage.ico new file mode 100644 index 0000000000000000000000000000000000000000..0f30d094484b8edc634d0b40759f0292554c089f GIT binary patch literal 4846 zcmeH~y-Qp{5Wwf+!8;C#+E__)vC>Xh2-pRSbV3?yQ*CW`O(2`bIuH=Hvq(Zxr13Af zDn;B{n+Sr1K@Or-4pMA>v+usW+=oVxaCPz)sRFv1M?@)%0SF@qx_~{u} zpMNhJ$#KOdl7cI^?DV>*z2hsR-;!>7yRWhJp6f56g0${=jsD=`xZ{j)r&B{scj9}l zYqhvkUvHb{@tUkU$2Qn$U)#6Zp$`;{Z)&-uah?ZGMLMGHHBb78c!?C6m)Dh11{T*` z?3#XPG3oQ5_Pr*T-#$vQSd>z!B(t-#vaqlqi;Ii0w6rA4%geI5x++^+Te7*iDH|Ib zvcA49?p2f*FQUAYBZ>7OUypvu(fPT&{rm+2gIL5PAy*AHgUw(wYGV^R#bUQ~EOv|C zV%KFJyT|TPJa&(rMgsPLJzx(Q0``a=(PK!F4Yk3!!I5E+;rW)rQyCZx3v^kFfbSx3=9Sa6S2Wz;4sR;AwiWWEVYHj!eU`bTZY9F z+!Dy*)WVX|z@VjLVX?5Jw7_BEuy9y7EF39Sa99{D3>F4WorS}~U||U9$sY0)5-0zl zr}S!epdz+sUP?8wJxrdUo&X*uPi)ViC$=ZHCw4lH6dOD7J@Gy9^@u$2J-P8Pco;l+ z&|@PjY4=!Ysr4AiK_Fg$Ex-|omyT9B$U%T3CF zfQ2uDAI>De5;Px)MQ?+i0yY>T3=xJ1Lxds1kggL90fq=egdxCy8-)2VL>M9r5e8f% z*kOn;L>M9fk@x{;k%f^-79SSf!V!~ionz&Re30$!ZGG~~?(VMa?d{3o;h`KH9LVwU zv7DTo$l2MMTwGkp)zy_W8V$Mp9p(N0KEGo}cJ())4gQj*epQW9{y$5-KGDAFlt~s$ z=MT@;UFWAf*>MerosLd3FwMX{%0SjBQ0c$^L8SxSnV#Kbe%JD@ZtV77&NBU$$Mrwy z{B~UBZ!0~WejcS!q_OgoPaS`LJMx%3J@W@07PjtuPBy=@^56Gz*!=Z}7p3(6b3a;M qdlOG~nzwV8z3t;huD9C$@z%PQAO84_+TUDr|D)Pbe#RJY<^2!SwT7<% literal 0 HcmV?d00001 diff --git a/app/Graphics/fslogo.bmp b/app/Graphics/fslogo.bmp new file mode 100644 index 0000000000000000000000000000000000000000..df4b5622f07d0ae5501a1e3775f9861ad9d80461 GIT binary patch literal 10294 zcmeI0y=ogl6opS9ML-YXCPh%A!r)erKBaMyLaFi^GPv+Fq*98I;x>MR$hb%#QQ?5f zfL_5MNRx$|Z`U@gc4k+zKZXP}_9xBJuFl@Qb7yw!*KbZ4%(o>z(I}% z0|)7}_%U7`bc6;22Tjpn;Gh{A3>-8^gMotCk2Mmc~E&~T0 zp~1jGQ#2SjXody@2hGu7;GhK>3>>sXgMouKXfSZ#OT>c)2N4O;6+h0aj?iG>peY&* z95h3NfrI8~FmTWU4F(QcqQSsH8#EX=$kAZnAgzgKO(bjbvnC!i7&vH(1_KAp&|u)8 zIT{Qcv_OM_gO+G8aL@(~1`cvG7$M7CLzzTmV8w`qJT%f_gK}$3kIB0_g1JCLf7y?>T%-zHf!p48-@Wuwc+FDD#q_Rq4ux3JGYJ5C6);gr4pJuk4|zh8JP6cyKS_Mtam+Ybm!_nFOS13^hnT-`Mp?& z_b||E{cy2$a0h*+K;#b2su$=!z!7&g3|0TW%V^~)SO2;HQ|ISxH9rH7Z!iJYl>zD} zFRJ19S)s#Fu7dC<;YmF{2^XM2%Rd<0EE{t936h@;xjcJh@yrISo^Wi34S!TEP4s}- z4@;;27PM5!k~1F>Rr#^p>VkoPWxhSgudPf=Cnx%UD}AL^_u${0u0wtf`C$$} z_5Av0*N5MUt+`4LM-LhJKQnN(z?yBlW@SiTPX>c)Sksh+>vb(6rFQy^_ZJC5Ty) UjW2z0af)Kycjmrx&&NIIH|M_hb`@#J zn9R*hiH<*C6L}9gHI?**hR7RZA`c!!x9q$w^1-;s0_q&|eP%;2Fbeh#9|cZ4MUezo zmeT2Tkm?BWDmU*ps1ImdO65rmT!+fx>L&QStSb>hCE7i&&!KA6g=(S2>U&-UH!ST91xp z1NrIYf0Ngj#|`eLaDDkqMfd}Gf;_!EYyYH%(jGp;|2g`Tvvzw=9Sbn&lQ753_j!l) z@KMB;Nl|F$n0#2g)P0_AZ;{9C_6|9s)Xu1$W!>Jd?`&x!AGdc3jVE94C<}BRnk_wu zf*5r~UHc3t&v|E=`A#|WI?H#lf6RO$^7~gJzrKY$MPgqdoxUPIVm8W~qL@G#I*L@E z=CES&VWEBM{MM1~FW<@d__*GBGC4UZ)6>&3GczNzv$HZkKQD`mi_&VfWOa2_R#sMI zX=zD{Pl7yr801SikZ|th=YwZ*aC$0V{O}_c4AU|l(&cb0~urT=eX7{Ou)JY%o z*k0`pRMd9F#a2_>!Q?3FDBxgn)OI)>wH>t`wc~Nb+SsY@sPCw+N93sQ=#7KH!QkkF z9vfYWza9&cs~#gg@YM6Lc{n`v;?XJxJ@9aN<=}_K<7ShG#lzx(@Dz-<04$!3o*<9G z!@`$I9?rzW;K|fco+f<0R|5PX_&;r5MT%}1QN4%J%lQ?Co}aXw^BoYy`T zlJn;S<2<=b!Pe>+9p4M=+b}vlhh1L`tvxX*vzpZohWKQ!s#FQ25cE%~u2 ztvNKxYW!WE-xy!Z`=b11_{1Ia-yS|uf4%j>N93>gYs<`?WpejxeOt0C%Rc9zlEbpR zE&tGS7);r-3_FJx<-gh7Ta8?hzvsJIsj2=?NvT(Zs_PLOsv*_&%^~*64=(yQ4Ek%9 z{_uXS7Mj#MgyemF=N65(Z?2wO&T*U6_Nr`~ENC95$8! literal 0 HcmV?d00001 diff --git a/app/Graphics/toolbar1.bmp b/app/Graphics/toolbar1.bmp new file mode 100644 index 0000000000000000000000000000000000000000..a0118cf685263ac12f8b484fcff5f3c894306a11 GIT binary patch literal 5086 zcmcgvJ8mOK5S@h$4Y3h9r>zx?%GC;A=b_jo_P>AFkQZ@O>A|INqM z85O&(!_)0{R-r_*dwP08&4II??tEqoFMed9$ge>OPG84UK|_xuGOUHNZ+qyJ0f=di zAwxMaq%m%D%qe|yb#+CNFobsMa-TrC<%dFByXjDk1zL!f#iXrU>Sm4w9!b=mhdIf( zJ^E#{PblyP+w2&xa4AzH*fvTZ;o%3gLzy)Y6AbMi6bn(bI(WPKO_>yO1^ zx!MOwuHfpqlT43V~KTmIc?lWVjfxxr5I$%D6q&YZkqeJR?1ayGy#~QF2FUsH%dDC%;yZQ4G`QZXBmS}HJ zRJ9U~wApYiVDs*Hd;kNgsZ+#yl<1FenJn0(a6@Lq>o5oxpe~62F!5XF7gyEa}!`-!GgmEXpFJJrKw`bM z+D@R~@>}L)rJTt%3gFV~R@Mg|0Hhb<Q&qG?FS;b2>nT&4xD{)~V zfll$dF1S|Vd^N&_cv03@Tul}b9^-;VpP})AlMIo~xoLkE7ZdSwxSBldbyr}GEMCpI zkv~Mda(o}|bHs`@;$=<(n$Oj7!3l=BxgN^5ZM~N%gjsoHq=G~& zs_yNDbEAkkE?ai)FTG`|(P;LUR)>7vwLuk(9TYL55HYJjh!#|&#dxLJuJ87p=Ue@b zqH6NpWX$tV3>~2?A_o$|C zhq;;(Me+^cM4kcT7>Rm5w^1rxCuHES9e&5(x2xO@Quml;Usb3Z?Sa&xsJuG({(#ML+P zN;5mucIJpuu0u!$&QvuNxSB{IY{-;|RmD|ZkMhRZjZJc_3tarO<05e$dbd=NEvr(P nhRfom;c!qSN`rRQ36YYAJU2iF4vhi0njgnro9s{X|LXN0MY4!9 literal 0 HcmV?d00001 diff --git a/app/Graphics/tree_pics.bmp b/app/Graphics/tree_pics.bmp new file mode 100644 index 0000000000000000000000000000000000000000..2551bf7a69e6caa441e624a76435bdfdd1855b4f GIT binary patch literal 2102 zcmb7@u}&L75QZnnlC2X(XsBpH73!6c=py%)XsGG-6=+hpqK*^^X*-G}Qi?P@fvXf@ zYiL46frL&9bU2nZd^3Bt?@S7^=e^tcZ)g8+XV1gY{y@C#_e9<^`apk6eZnM@`!`8|<`U*DwH>&aj+kj>3a+1lEY?d@&Z+1ZiZ-Cfz++moZCBN>fG zG8_)&;NU>UA5{(yRX)p=Xr1Nf)gQUKyOU4fe_#~ok)G(egaIv}1+-wAmMIZEqDSl=p(pf2kkAu456S2mJ)>uYjIP8=tl6Cd_ke$3p+Hg)6}RUWUvFgPX*0fqoWfFZ!(=r9Br0t^9$07FnTI078OI8?m?C_L>-^h`&$EyEHqj~GQ3 zjj(tbco`W)SRyQ57C0gt5snB)gu_b(M}#555Mi*=MK~f15r&MHa`zJAQ!ZjJuT=*s zyc4s$)OaVD5~c}*1XIF0@iMVU!aL#Z$Kl@S_$T}m{&qwO|3qyP3<-ur9qibsitiqU zCwDtWYLM~Euw^(hetxvZK@Bn-**MskA2&%EmJCYMB)gC`r0-p$~*Zg$H&KVa&jW4r>Amub|#mXmvV7&A=lT}a&vPdx3{-) ze}6BJkB{>7^dt{6l`rS#{Ex>}*Zu?cU_Fm)T?aaC6xI(torwDY5JAU?lwVfG@G2hMnR`P8)b%rLt>%TBlt;r?7-InpR zs4x9H=VAL$om3 b%fNqZ`}JP#?!OZBVAW#+iab-)!mfS*@J7q> literal 0 HcmV?d00001 diff --git a/app/Help/CIDERPRESS.HLP b/app/Help/CIDERPRESS.HLP new file mode 100644 index 0000000000000000000000000000000000000000..e5b1bf5d902acdb7b3ae5a8da4f1fcd3df2fd00e GIT binary patch literal 295476 zcmeFadz5AORo{8KTSAr}*b!iCN7%<#vbx&ts(z^@SvKJ6dUTgn-BqrtR!ar~x2kSc zU3b;J<$J5tT^a*75;mA;fWXRPCWL2TSThU^i+ETJU;@FyI1b}5$s|ljGFl`IYX-*) z$Re4=Xg=S)_wRSkz1<~=N7np9+edZpJ?H!$`?ueF|Mu_8zxr75Wk-tQ1^zww%pJu9 zpWnt+Y#)1Taikc3-CeTfBfUc-Nh^OlXu_os^U&fyeLiqg9o_oc-M)ecNBL#{a}8m_ThyWjsU^C`Q=-V z-EDyW(A2_}m6<12iaS4WNAaPll_wT1Pc9aJ@TGj2Ii_ouc^A%EIE@RI$UzKD4qj@9+Mz_xU2#M)Ox6{|W+s z1%dy6An=xl?tdkYFZZ@u<=X!CPP^0GYj-o8qZH6DS>;Vuh0gw{V zHS*2&(>q-nKtgUdk&S!(@}RqaoCh`-vsSM*-hR2!?UfBl@BUt|)!b?5hn?o0fU`nt z-408z-rm?~^;(^^mJO{_wsv;6ntg@|e0EVITD_-y`sx)xJ_X1Fg%Y&sonfcOi((n1 zdyPsK_j7$U zm*G6LB{C|n41Q5fzOH!g`Oo}8?;aZc=rhm%Q$F$M*lVx6kv~`L8}#r#A!XTR^iP4h z0%#3CHd@WSedb!QjF-AMM6vszdi8{ETx{)uM4RQ7v?{H4C@y^bB%|5l(qu*Xs~X+k z9{grC;pZMLW@g8}W@~S6_t8_QZrr#r;aZoFV*(63wI;Pq@a~`5+u!T<+Rg1#$4`FE z@snpxFe@f7w_z~h5!MSFN>6F9O{g=i;!d*zah)uSwXK$Byuas2AB`4TSK(5n^_4eT zt7X5v*J37OYWjDaSu{{ljLH2zKZxt;pHxo$)y?1L`Z>OT50|E`@Avb$eEr}ZN2=$3u)YN{_IJ0N2k;Ci+6cxct$Q2Y?d|RjEdcamWCA{d z)-arA+23vUSTJkzWZ76=oLpL-X*f2m*4q^=w3_Q$J_dRZmnQP&&x@Jv-s`jbnUB2Q z^hV88H`ksiij{76yBsg4+x_e13=E{(JJ0K{<7=TNV3+J1Ydz1@WUb;}0eyIwxs z+-;Qmd+lvl91q88CsKv)i_))s{OtnZk(QK!AS3ae=Eh~iI_y(YBQYk~*s&An!@ zrM3MY{AjPNF`9}H%k?gk5_5$uLaBi~RnsyiK$4TNT|@K6jUJfYC|CFQ_JD=h@Ai%6 zL0|K1G8Uo2viOWWEto)E$b~-Qj=6(w?Y(lN*WI!E7$f_d3=ga!7q07mKzI-ISY;vF zfRkt8W!v2@aN2HPhozV2C(fRS0dIGAcMq88vNeyuY_S48qwE_^Ce1Q-cU$mrUk3w} zqdUDevz$P(?(c{|#)Kv3dT{2gZX0ya%P!??9sPar(ts))f2;YFKPP#i^*3trnbRVC z2Jmj#%t0U8v*nYViT5DSM1n4lPg*k?ML^lOICQt!Yl)cQK@%qs$XnemL^?DzX6189 zGuH?@B~oq=B<;7r9St$o#b?*v*HF%(}R!RS<%z; z-}w0mgXh0K$pWTW!zgIsIC2>ic(1)7ZO9?gKY&x9(=fueb0GsEUFBzX(UB$8P~R&&bkp|dW!19@49 z_TF*vnllfce$0mIy~68Oq0R$TLWrVq(K{@s7nU3E(sKA)o$`^>FXy$zURQ6L-qd(p z;$Q4WOw0LG!?3gRo9v(h8>CiSB1Lg5X1Iy|$WWb8IJieAnfZiBsw34F?5Qb7#+B0W z#l-q(IyBwb@5tZLY52tQ>{nZm0r%0op*HjAX{yVuOKo$`}Aw=%0A^k08 zfDvUAH2@V5l~(#UOhXj8+S}%Idz9cEjgbGj$?5+G^b_*Y|gJ zc~zZFE>F$Pr6qFD-&S*bBT+Ky6qXK+o1!n}OszX{Y`|z}uyF~0p|{hhtN&wrt+lO= z_&IR)Te-xjzvx0yjEh;14c<6}x6eP77nMc^8RZ1xG6aamcC}rump=x8!|y|BNgdVG9@$&%ve;J`jA|i z7*SH_-W5Sqlw!Ws>`DD#N)Ro!lXz4$V6zZk0Rlgd9VF!Vl4;!VLgF_JK7qc^C)eOH z*b?#@&IG3gzT$*77dL1atONujpAtT104(USS?=Wd^X{sN(*0dS29qnHrou-+2pU;A z*mdNTi-A?y+j24FlG|PZDv~6SB{q{Z(2gOYjZ1(v^vKSi8sBYbOcN6mjndR$)ao62 zoCT$uV7)|aNN|{UpJ$rVL=ScoIpz_$TLYl3x6wA>+Xtlv*$4GNBlJ4d1Y}beS=TSe zjz4-lM1T-9q0$~NWnjX<`p^#QFJuOmIw~2#7#N3cSM(!nxw5b_Blg9B!wFDX7?sL= zgJw-OU78JvHF2USe(+dP+~g92_{ED@b;5&k@VQ}2&iovgje%|Ty8G}*4a`;|1Nmie z60I%o)p=SaDV%Azxx2p${|0-91U+jm=!q4<1OhYB2W`CXuXd2 z4k8RE#xdtMv}FvX%kbK{GEwNr!tP2_)(@VjEcT|O=eVBM<)Bl?LL6^RW?oDsk;_K z3Kd3>VZxJeeI~O9!tc6t>ALmokc0S|#wX5AoD1sN;ziAGSdA)WoU#b$6xKO<-uk|5 zcve+h96a6M-jg2e9+ef`{}kjB>Ocu{y(~9SiEAT+^ohxPDp)lDZ5EVr1To$m5kNe8Gu4}_=Z8U)gZ<+3e)Ry*ZNG0i2sgDvPL(oAL z6oGyp#M$Q(lYZi{tR?^ZVFHC!i**TCYh0{vJ`zEoY`Av`>s^E*FE`F9$EKvVV$MMZ zM$b>TdU8>K#+sz#E=v*QC55kAfsl`;A()pOeJgclO}oGf)sSVAdY;z|wQU>Jh<T-cWt2VViTIx7JH#nPA#6gY#_~5kStb;MkQQjj9(_Hq@FPNOnmjDP$y4#fHS24d} zN}B&k`De^H)}Y1HE=(@ZO{FM|2$t~y6rtcAsJDeyYR|2=R`)kI#aV?a9$Cjq5wnnG z4oqBk5Pv{3Ff89P{hdk7f(Z=+>bXW*>1ihaEa}Wcc|KsfIcVL+5iYPGQbT_l z6-su1#OB>svI;e12SArs>h4093~iX(!L5$Nxp3du2V;z24WeSksIETw7@)xKvdh`eQ_bx*%NkR)wc6fV+*w<{rihMit#DXk-#d zba`&#tWDQHpR=VkM`;7h?5txQncA!29F*fcY=QmC7io2%s%9wIccYD{^x8s5Vt8Pr0aFl~kc)(0c{17ZgVr^2uYWxIs zS2kcwH~N*lN2LbYq2m?lomjrmd%}TXOcw57>XJl*>>`Fc zwtyHmQi4F#aqDJNI>)2o-wwdKUMDWH+BJ>^`FC&!s>`iVn1<27oNjw4J(c3ZQbbU} znaJrhLwQG*nH%vRL(E3tQbX)JzMOT#To!lP+$0dHdzy%}P5h3`p9!HU4BKAQh(I^H zj5o2|vi~KzF}SyVslZ2z0f8H!goS?e@GcLVJ@}@ky#5o`=p}Ah@m#2-BoDnMvRa%}=#G#A2 zvlC~^sfEQSm*y^BS}A4wf_11nb%HjIapv53fY*el^fsB4G6N**(YU06qkynB`<)~P z7-f#Q3UmH8OKuU@Ha8S|)ISth`>dOv;(CPd%0m%;==*(qCgS?`3f*1+pTSaB-QCAW zPbsvkcK(#hI{I&;admV@eAdnVXR&Shew9m|=zDbZ6ZMX!LnjqKM%#e64X~$1Wyr8w ztGQ0FV8^s63&(0f;-}g`HIBbezc$dPYXgNC4I;YvS*|(0EB2~E>id0s1|l^Ae4$tp z!#9365_k5%McdSp1R^zDena&9^U<3?M2yLL*3F;b`ewdwa;aB+kDfnW4EQm^zs_6C zW`NKv9BBhdaZVSqFnEWVi34zl|AR{AnKCWh@X8HokX0BoZpwnp$r{i!vft)1pnZrz zXxRF9SA5pZ2R`gvONkj8w!X*E7mEQu#?WVMMaeY~u^h6dwZ7H#HJY-%zk^o1=NDdh zMSRxHpXK@+e18|0`qlU7`>A^0*W@=C4f>xn7lv4-ma>EEHFT5;eBM2c+fe=;t=n+E zn;+6}?vBs8dFdmbuJPskrlIJ24Cm|qjKe{%e5&2W0vlb?YDntwfF5i}|6OfJ%39U% zbn}n6{wuye!KLBodkpEP>O(T$R(5c#h~aecPavoCNnwv1)^InUU?wy|@|ksJ5l)XT zM9@#VxyGh{8p_+gF~+*j4+(s)iqE>)=9=dFTe&n&eUGtTsE^f&00qm^XWYxlC<%}d zc&c@(HouR{0Q9k1rvwaa0J>RzQ&G@YvBsqi^*uWMwR)!kpvohaXsZH~97YwC+U_P5 zj66rdY&_{>9ynE`spjBzUVIm$(5RkZ6gKSNeG_RgoWQ7& zf0XM1eZF__dK>*K26q%nkb;e;-00%LqebzrQ4G}1FTi!gm&==jUrPEXj}Hi}dRJ;<%Hta+m(bk2Rhmj4`P{O+!+MZ`L_@L$Bf8>Qf(=(* zQmP|B zX}LSw?bTkhci=P1HYz^aUU}y9>652VpGH&fAjg>=kk#s-nS`rbb2WUHhVcPlAip1q ztD<#S=!-?9$<3z~WkK^To=N_w% zXT4Pvm#3%dDTky-otZcvPZh7=_+@@NDjR3TUy$DZ`R5!tMI3UVS*fIdhYtDkYN+dF{&n#!XQ0Wcf6s zKdC8fZO8@KY4&&9=g3*z-GXTNibI9$Lk7&H6~& z#N;xoT4)L;&4ERmEQfoIq_}BJ%i~k!B!seq$6zRD!LoAKu_klY04^9((R`4BmgYNH z#9Lb~uWfB(q4%J^#^l-Ax#@|i1_PM}5B~xDOT6pbPz}CaxcK{noyxEg>$(l^LVbO@ zy*tXg60dva*}><*`3#Srd)p}A>mPL1NypmR8F?LJ?6SO&7^od=0JI6GeXzz-ify4< zrWl&SZg-i`^vW_RO7L8U^&wzZ`a`u$gu0}_sEE^_~=eP@-GBN`@ z-{0O1MqR-ZM$ICuxCzgvr_R8wNCiRNliy46RWOAiz)CLk4)8-Xgm&Ol*~Blp*(j$l zX+V~Duj=l5HgPzZKN*OB8SRlUxuaJi=cteCQGIpEa#>UkEt`5~$RZ+JbE`#CTm$jY z+C@!XkC6_oo`P$?01ln!5-)rF9~K2{ttfwIaJ|mYp2^0s@iXv5*zwGBZym*r&OWp} zf)~#`SB^X;R8?1!(-UF=6W!kCa9-|HXAcM4lG5R)do5-K4CT^+JV`(xnV9^bPWfK= zHDG|!dw78_E5r4GuiM)Id)X2ejf4YFEs(s4>f0qP1Sj)KcOQ1KZP@zW*2V@1Mj$Kc zSi(%Vip|D12QaHh%ubgl7x7*TH(!;j+)gs%v)-vN? zWS)ZF3Tvq}ZdmiFeUi%TQ;L3=v@-4sR+{H?+Gc;*W*?ko6d=Y4ugoifWIFP#>XW?E z-XQ*@+e+!elW4R-+5&n&r4<`V&L8094z0|bKa$VkdW!J~Y`d-APTSSc0Ho`&%56YxvO?NJLlvzb6$lSs-X-RshtFcAa8YvZP1D@f}v#19vR_`-z)`%PsVJs#zj67!45(3J? z&LHrLV#R?j%`KP93$rWNCYNSPJ})jUJU%x)GhJSIa+F}oulbtEWu7^Hyqvr;&Glq4 z^TguP%<^)%uvE@nUYwufJsMh?ys|PkvwX6gyD~L@b$af~#gpZQt1BfLO6C0A<+&9e zTUj_+u3Vx^e~tJ6+~V`N#3?@W$)Y$ZZt=k2HNz^A;RSmNb0_7(YrYdCuT7$FA_DrK^i8 za|>5al$RE+0RhH1$*Wr8>0;rER$6Pdu=J!F6$}i{C(CP>X84`8zoJ>KEKLd+%PUKB zQ(EZ!82zm*EUnb$SYDaAI6rrB=E~HJMzf$+uFWmaoM2hz7}-TNscx@L(kG}xzrq$q z8uG0UCzyxlhcpW?{w+_)L5PiG%_$x zU5X`wSSm3TJg?3bj46joc$O3({?( zvcL}wlT3}W2+0J`{R>JaJlUYhe+Es&Th@;pNfPOgZ-XW;6t{{$v6MfLWGWv^ zDzJ)K2Jl5(TKIPzJ>o%~e?;w;6af2tZtAmc{>{lF#Z`*{h>w`Z`*GVus`%y`xHP4w zcrXG~r)^r=9L$t%`yxSaXy^u)+Q0YCBgHA2DNhbQ>oVG+JkQ^-Ju(u}Sn$_mOR0ae zIb&;w(j>KKMaelh5ZyImS&3-C2mo#+A2flPhG)K)Tf~U8F15xZJqWb0X=*wzytxL4++k)>t9FEpa77yA9bx&dYw>2l z(2`5Y-Ms5a+6FUO!&2eqlYxdYN2Rn`0a_U;Ugi1i}E1v@opvj9HYeXcI$gP^$E=5Bu7b>%~f z6`H@=*`hHxd&Bo&1>BA9f{zyDb=0bOeQp~^v!w=?V!r|?Q@tsmkQbsv z#0NBFd=p!3;^5Gn)k2UzOs;(PbvmC73{$8Ye9*kZ-$K~eJzdbY-pGDb!8$x5w_AQs zR2D#DUe+N#aoc0TxS;lPGo)qw4nj1J_DohiW)O?$bS?(sT$IvH9+39rI zJwSZlUbH;lDLQLI(DaZ#ULgd+GHL_5IH$r20$L?DOx}2+fYk?pq?yi+2fT)SzDkO@ z*;*gJ(OyR|TCX63HX8*j)3oXD%<1vnE;0G~IC#p1hqf*!w$UW)O8A6rE9Q_?Wg@M< zP=(yez=~$MVj1rVVon-{PO&x`NESqZ%DF@Ssjsc~^k1n44UInm)X@aE08Oo>Ef0$) zZ!7y87?9+f*2j1bjj@hxxb&l3;?k#Hll6)HI;TdFc-{%E#ejW|2A7Qx2-tjGz3u7voOm?~2V#7GjU zCIvRwKMr-^CfsW7D(xjUZ~*{Y{8ih~mZS#tZQvkQdP=ruueZO8)C-Nom@gi2LeY7! zXlL5`14$^}LGGVpsI)uF4ezcAN);NihsHQt>{AmxfEUz>QQ%+UL|dAI@ocm=+43UJ zakIU%zXP}2Y41^`qM7o|Z!-pY_{#%AkH~W6Rx2hBX%)p4kW798aq_QyZK(414i<(H z4Fr<|C&>#j6+|js!oYUhwgo4vUu(8u$oIoAN>4#1`6ep@HZkXDz{FNwGjG(P2zr_D zCSt@^Q)PUWo82z3TyHgNo9$Su0J}&P*q8`zs5%w9bhM43CelsX`d&4f6(37_8*E>1 z!0bEMYglVy-ZmB_-e8#r&eicQsDhe>jznFmJ!4A*k`l%z4b2_!X>V)jlIdqBNq?i= z5#ns{f^%#3k+aEUYOl9FKIzX{;~KO8Za1cM!^Xy=K@)7E-CgbaKK016=T6#z->hW@ zjgeqLl$JCYSmPp*TD4o{L+MvnBmuB7ZeC6swumS5a30~~>Ro%E#J2tfH^Qg6#Ij^& zy)IRMhcgS`fZv$%*)IWu!ateT%_5-I=VTbM@X!$ zWyH}S`;(zk*Eo{eotm^*|B70ewT45YJ0FnJx6K6#d)keZ61sHe?<0}kYr+nIk(H9x z*02PARq+mihyW8bOm9N^7=Gr2Hiaub+?>IP+#+-d?r(1cThGUA_)Z5JthP0vBD0u0 zveMptG*=#2X|9&nRV%Q|Pd*n%Hy?YTtav&+-z&z5`Nys%%Rp$tsX!817_y}*1JP$_ z$$3g@mW`2kB{i%Jk94{xmh=FXpIa4&{mXLEpV|&fik(7D*b{k(+v+S1lcxB-jM0~_*Em_4S5oX=IRD2dL6Z?6BwHeA=Mw`{fGDif2ns=jDS`8u$ zY+oUIh_1tRtBb)3d$FX>?X?S@)wHIBmK1Y}ZIH@&h1$SfFjUDUM*Sq`QwYzB5mGjW z7qHyy5wswSY38A}P}iD_Fhcz?9^>$opRo!a%8;l~+Xluj8jJdp)H06%k~p_27mVuw z#wFpAGFH%1TzL>Z#U3Y{*P@qPu%;}NTk_xQOE>~W3bRQ_0fMrM#^yr7@-d)KsK!m$ zjl@>huB)n6EkVHdL)J8IKm_yAR$gW#Z;iUs;KPY#5GRQkvOr%Z3W{0_fEs=F{!9#`Ckfi&a2kX7U4RMmy|s z8m6RB75XLVn5S%lsR<>`fGr3ficxqy1>-n1J%!PmoJyH^$$9Hk-2!W(G zv|iNo5L`(DHV-CrYfZAOmU>ddht|aAgoDkhO#SI z{h$IWWm%8BZZI*>jc3pVimM$MM6VCgPG{Lq%=YYMvPve-&RxFfdrpw%woXSM%bH3} zn!|C5CRfR&VXX1gB%c||dtPEFZyxZR4@Co*(uV2>C)mz9F}?g4Lztej|2$L;LqquJ zp~JXJYhTEvA^c7?jIoy(#=QgH@nI~tcgj9(e1lGCC-_srLWB99LkF}@ zyZ3QvF#o9<(CkYL=+Oc1_<)?!WN=j2QI5?{&uJw5sYaqvyyfy?pm~-CFK}rTKg3f3 zns0cCk&F#^$47Ft(ij+0klrT&=3~rKK8pgX{0$%qIc*o&2l8wzEmM@8(mZ#Z;o(7=-{MMm^kcDy&mj-ZrA;aSSml(tw z2E5~gSkZRSW%gdv{p8{zn0N=_ATD+KL(gVQ{aap9*h>Sm4BS#LOH*T1@-kLFl_i%S zT`D&cUJNxjsU)dX0O?wUE}ShZf)i}mwF8$DQF(9+|ClwTfJoAbdM#O^RT@RrH|j$4s^B70 zQ&Q^|Iq1Id?KXNQg`5ao5K6MZMI~EY6QM0ODAHG`tyDq*HtxS>VA>N)zlY(b{Z7J{gb&*Hin*oNsDZVue)N&2__N=U zM6aeGWl0WR)Hi{lx1a5rr0xK7TUXZC>KxFz2G}PlwUj2SP>@~?&!N?^#Vi_H_dwU# z{T{FSMm^tF2Q)pfX}c*5iDcmo0Z;@G*06lW)r9#1J-C`kh1kxf@~JmUZNV}1Y-G`y zy_d4FZldgNttV6B8K0^jVz~M=y;(q8Ys>>xR%x?rnnim2!f{vs0VB&J-?j{@D9<7o zj}DJT0id`~Nx^<3v3|;WCS$^zP%MKTlBi(#;>X_oWhbN~C-jg82jcz0v7-21E>Y@N ze`9tzk7GB%u9ESDCbOvCZE&J11}RdlocV0UQ$aGT zSda3i!GN4#lm)P}D<6nmsD%L3Q%y<45$_e$*-os}Bv`dny~geTXh#I0?(q<5h|U6N z`f4@!Gcs?qs|8Pn)mC8^*13ht4_dg@HPxiMYMdS^bEVnFZYG~?dQE&i8S=ut6j;+T zieJgP*R-<`s2mu{EdmDg*5k+v)8);N>>CO54o9=_53B}da}$hJ`=A9IVj-;Vx>Xun zp&uWeRO3VjAVJjMa1P;-O%IFX#v>bDRgVJYf<97s15iJ7{_J@sp(;XzYkZFsAVZ#6 z;UTO5TBj1TxyuZC4?T4Lq0u*z8#u~b8m4#(jCiDlCmIvYU93((9!=&gzL18VLiO*l zmClC3q@)%0QW=p=!brj2t7Zjv7kJ`uxj9vwQhNfS$vJ)5?{r#y9pEY)&U z`sIVC#{sFT{V<|O(w9JRtK7owMoN;(n~|Tj+fH~!-sc@BU{TEyE~z>8N-kCxMh|Vyla_L z!;1inZ4C&&IwGGtAHb+l`C-#kK8@9B?wJ4?hg*4#_7NZ&Oe}&w48EE$YFL>_n>*XW zD$ylO5DwyB1NnwB&O*{>|A<=fv4dcv!p!P>_OG`IvucOzK{RI_f>Wo#r&n=F7(D+i zIW6gnYd9rW8dgq_I5D-zaDuZnrdgY_8|!VQ!ieQm$7`%q+)<1l(ZDp&i&%5xJytZB z-#g{G3WGpH@!w|VOXI9Pb zs%Bx(U$b1C%0;-8Yph@u%#aBNhp4!9U||Ia+QxCK3fHOgTgn`ro8S-&y=YtjU!LjZ z6j`pT;ef+j9x<($kf0d|%V>?)vCUrU6hZ1>+6g!#?A-%yiPHwmQFyE7Xro%5z$S5i zIjJdyBsL`_%;^|&LY>Nd17DIGR{ZB<-~O`Pu#+%!jL(n}65q{oYI=NW;j$R2oS0T? zlDVnwAb}-0@#(RFJ*xsz^|y_eTyCx@X4l^uR}k6Q2QY%5wIS16&Jpossjv*Ho@`lE z#bR>N&L&Umg6e62poH1o8gc;|<=Dj@b<74Fs^8zla__bn(nF zYmQ=O_8R!#?;N2@IsET8Kc3yN=*n3s9&y7iyGpyzbC*7vb6yNPvqmwy5S+}>_+3@! zGYDM+R{5j+CSGTpTp~V?3xH=*Agi!gBQ$B_29``gsYs8*vRqQnY}JIyU<>6mVLJo~ zls-kHa7Po#uz?LScaXtNi*|xt65I*(tiUE5JL?6}DGd?%0wH)WRQhq>AX~{xF$4in z_CK?{GE|Q=_VpSeMVn#s$lVJYg~AvJ$O(gN?yQn&)812X4aDPj5hUdl%pQ~@I0Y}U zYNeKfoZcvHk*;QGiwqSN(e)2KFbZIYFPtWHd_7Z52G?@(5NTfjk$K(6Lf`79x(D?6 zZO}(Ef5opHNqdk#X6SRec<#9YjvaKf;Xf}_T*#(SJgRwDODjMa6gNy25`s6KK8`7i+xjU^6-bqJhJI_TECU;6r(W~w4PmTV%;Pp9NTD$? zox})ZWfViheaB(L&E}js_E;Jq;s11+&ftp1yiTKov@1d~qr-ejdBB2a)V{8b3|Zw| zi;;aAJo!;B;mkh+Bk&U!WjVkjK2W+t#t2GQsStDq_&cZ-d|QAU&%R+t0vWiG<|$}z z7Upokl4;6Uj(f&p&2H(cCnGaBuEf=TM|vqs2mU+nO{$Z~CZh$j(GHViT*B=?<+@+9 zd4oG^IZRNftUtW~7#mKP2`KeVTj!n2BWigP652@z~RCTo?I zrg{%P2W8(Ii$(9}9n;vvQ$t`j)VVWlHb_xW7v^9z1G_j~V4^!w@o?|8phA-8s_4&O<0x1a1a-hDln zI{aHzrzc;c(^|at>eTXdl;OX%b9MX=4()tJ9doJUSAH_j_<@(-xx_Fv%1?2khp7-1 ztkvsj+PwX=uh8pjxYX;{RXxAyC3^n+0q;1hCDlm3vi~N$Qhz^kXrF|$Eu&BU{pYIB zx4%T6U|Qy5&~+peJeH9@)E9InPZtxP44jDkwsRwU-YTNCY=^p-LCOeO!}o3`E?b*7oM*A z{P8=ew9KXczWB&dOCBvJ2A>;^on}X9Ih37eTr``3Q?s*^_8p3PD zS1n7*rW3<8%d)w{#s-r?$x`j#z=^>$5ZBtOWXDS2N>Nf8%7TXA=O6?C^UVBLtN89v z4h1)5k>e)uBhe#_QAsu(jDw11+e*^0m)XKr%{_WW6;i}Z0b7;#H!#K$@pkq|$nnM8 zEj+@Il@#U*50S%~7A8b7pETNm85P3^0o%}k-IMOVKB<#WBP?ijNq<%m!3WwLj)Y!e@kD`l)nGHIOv<5Ptdt~QgRr>O} zhEm36n0(xeYa_%3(@6(O$z^aNZxN;Jf4U8`GASj3Gk};yQ_^liwicV`UhsBXmf=~- z9qw8PKA)F*`!hN+2HEmu_eE@lHdDzgT=E+2iP}NWrs8=ByS8^E_wAh3yN9AOKIoiv zH%=k8Ax~v9%t1C=TDkP0zFgUT7=>5hQNlSWty&=6A%!=Nag4mx+Ct$CH6v9Tn`w&G z9!7Hv3XQ}mY?h%FhixAxV$YR^43wwe$s$K^z#Td~Y2$M&_=lstSp^OEdf%$*n6W zxWk-O1dlejPj?oeDpOzh_YVQGrTuTA*C^Hhckg<$CGqI9ZmzK#d$HQ5O6Gs$dVZ-| z;L)n3=4_;*;pKg*>ae)mov0x8PZhflQsyzO zAFasQNmwa5fKH3N$6+?`LQmY)CoyK7t}36sPRW85krAAS$O@;%8ViqEbT>ATs5NiK zWo@;k#*b5Bi?#SRg-+FrZ#t>JBNUg#qxX`aOmB)MH>R5S5w`7h9lF8MFp_ljv|~}W z$D;80Yd0u<2X$d#&FEdUI;E2}JAFPal8T^VbD}8HTmf4vs?P{*X#SjY4BN2p$EU!d zb=#N2jbYEP($7?gP(?>31LxI^kFQ)i9 zELD?Nlnv3f)#hMACH{zS;Sz$dz$Uh;4IRbq$U!O@#9d9JiiFS}1&!?_EY)ZJ@8M)$ z%O&#oE^1?G`wkk||1-qRD)SvAHx$qs1t1#JzKxoACzh?wnUCSQY6M@2-J5CJ0be9i zI^ixs(f0ph8*f)2s&>oa+!EWdIqjoD2}N=B0+Dr6ku0^{anE)DvkP3MAhqBlX@eNc zR(x6*uwX|9XrOYa*r@@&2|YU*)59!bxNSnnG%mxh)~e(ZX+zglA~e6MEGi`WYMWHA z10@#8OUv9?O4~uf`msh)0F3sH%B9L^id9KPird02%&2d&Q1^0)9sMCEvCe3*o)}Qp zYydVN{m#Lo)HQ!)@$5kU=|ld$P<+fi3AJ(}DrRqr9EcVZ_My5$MxM5vYIpYZ>2YOK zusT-WLuzhQXIueg8l7ZrribW0Y(tgjX$Jjj`hbKbfHnXAYo{z}=hWE@3aK;Js1l%5 zHd0N^%CS@SIVD8}xh<~DnTwvvVa|?@l&)GoF@luNZ@n(hOrk2IvML`k&m^avC`Zd@ zEqLQ$qKz>CdW=hy@YnA?nocwFOjbXE$h@Q9&m!F9(jxraD?_lnY4ACxn*893VjW~_ zpxyZH+Z?H@_Rxrhxt~X;L&`U#;Uu(k%oL+Yae9mT8+(Ny05WnKgU1<8dIq*|arsn2 zM4_(n0;}4}4&83Iw|y|4tOx+)33*ViAEil&SilkV4IgvVb|N)64r4YdEJGkLV^FW@ z(rvS+$VikxAx$%|-p8{`NXn~=S|S5JYS~BTiB$M#0}xmGR>EVO3h(2Had z$czA1PYhj-YSE&-Z6S4&fIc^i;|IRUE@2!Avuo6*HTo#1k zpG)n3=(QQ0FMHrS<9)`g#=Ooh94CBV?X495R`&>V%lmX;WWR_@IeQNewXJiqDP%tI zaAbl;IV@B5U@ABDF1yb`6X`QxkC-?8!c#!*8k^8*c_fs!NsSH*Rk$j?nv#}B0W@jn~4Uo3xUGR$NCx?qnI@8B?6U$6T z$8ont7q!YYBcRGMMsRW%jd>)@l)s4m3Nkjsap@q6EG0pt_5Kmo@-V!5BkTQ#;E2}y z*WZ)Z{NEqRh-C=RF$0P zWH)AEZ^FsraxPA5jT_t?{Etj60(fo>@j37DGD=a|r8z`Ot;-^8U!pPS4?`e24-ok*1tqM%E^ zyT;+WSjEyOQ%~yUD`?@co<7L4GI7<*r?}q1=kkHUx8|Nm-}@(4`ANswg@SLAAXMyC zMdX1D_8Y(~WG$An(5{X#sRjg4bTDEK1RB&IaUC|WcQO#gX*8(u3mF8BJg@|U%M|)1 zsMhSX$a>+0G;=k6l@|}|;6H${_{**F84gA+S@-Wn)dnht#%hs(BJg8IeXA)wYf{ZaAN$sA)K<$epFurbbj+#+AY~vNkwA37%x{ zDCSSs8ViPys-)oWNoQ0lUrD5snyl4%-=gl6B(R)jg0T(5*%yIq2d$m4jHA zzztbbyj7m{&7>2wJ48&A4g|}-8nk-Uagwg_ly{(kMRcmw1yk@X) zf!X|=%A=~V&(GAlrPtc5HDh_F4x6>K8gU%W_$Rr{%~jmtZZ5gEzJQzhtD9?cN8P79 zXKLeADzT^iL2Lc@@Z^_psk=Ymx{uFfH$7^UD_<9S&$K+!w|(0ST)YX}M?HNhJ)PJ4 zj||Sc?c>GhzER~ZD08t?FG?p6C*&7ZQYaX=5Q<-WYS$XJ6H1g?h4mQ5x^Q_ zq?K6-7ocyD z&;kqDt*S}VIcrU);-OWoFhRyZ>2Wr2O;8bkiz15pS_`9cIn|tE z+$(@0DgFkbSY?)vk_)JoNnNuYfu1**GHG3$9@xpF))lPxjGm{OsaU2StdJByC`tlr z3WoNi7`2xFq*+NhAZyj|v=hb%F=lHoK|_!fPBlYyHRDLq2aFWAObM2T+y+@8XX7O0 zqQ`t3Cr}ru+9wtq;7jAPfnXrw#lzJd=9yV#RU8mxS_F}F7??QC&%>7<6f!+DcxOVU zgg%7W5>CN-EMvteW~_GFqi|?N1Dr(Iv`8yA8d`Am5an&zGdI!_7%k?L2NL8JQ<0`8 zYJE_sG6M7rt;`r^@P5~xavB0-98a`>=guZ+RLepYD6JeN48(*VXrgem;4J?p+xj#P z73#86p}+--Xk=9v7RAT{=n=a!2?yP9uj-Qh!`C_YSdOIvuC~S; zTj}`(qHryYgbCPX=0NGE)*hU2Hm(!C*vpC+)AA~;ZM2`(4cq!S9&1nUsCh;=o_AD^ zUF}FkFxuC;839*91m7k`jrJ)rA6s!|JGX}?oxM6{2$^o7;o@O$@5zg5EDHhDRzE+D z6dQ193KE-=tHf@P)FhdZ>ea!aj73>qQaPqlh6-0-;EY0QUJ0{6Eo&Hl<3b1w85f3( zYutUDHvGReZj{t#raN^2ag zSvyG!>y9zbO5sKWdx|IfwDuoR~v)G_qSOxxiL(u>vUmqd2TAz0Lxq9q90bP_*3q9b&bgB3iRw8Q5V_E1yp`&oUbws{ z$`duSFu4%?*v!KG35%}d@?}mc+Zj-B)`h1)5%aK?fQx?u#d5x$VWV(8gHahqC_U_U zhw{Pt{a%M62~$h3B{<9z^(|KbqeBKm3)~!=pXC8*0+*Lf4WSvyL1P|Pxtb<;hQ8CR zGLOVPV5eR)3<|F`BdE)8-JMjQmvO;N&m{mzYn|hJ+G@I;UR9rTc^Dq?U=3*Wn8xM^ zl*)IIxP=>t6~M_^{7ZPBkp9=cF&pwfn|PmM!|uhSC{26T%7%~T7cvZ$2jFrCH1dF3PH`f7n)d|ph?cxc2FTy^F}P$J7{Uw4Zk zz@+4%QAUXzE{fk8l0aS!dI!IT3KGLc_R2BQj_n_(G(5nF6R4pMs~PR`K|4|W^;+9$ zaPat421YJyz$u|-4zSAGZQ~F$jP<*PVJ48dgO9HqPo6$hA5~OQvdTWdF*cZJ7R4Jp zH}B>M^fuC9%&s&YDlrs2S*NTF95rS*#A+#b+R1n93dghY%NFRMbI-)XCcgP-I!~pa z)GW{;qk-+suqJAYELv0ZMo6r_EN-j+>DX*z^lSw&c=2CkwbQnBN~sI5E+*VuEghN(Dey5paVHAtc?}@NIFN)U~;v3_pd@aArtIdMvK@sNkaY?q-lKEq47FV9o($sG_ zI(ayIguuZ1G@c;&G0Z09y~z;ZtRpGlaZY{)JxdW3A6qeRk>nD?OVG1a4_jg8fV!4k zG|0~#CABbg+C-~~{2FmguM%(DQe;%HkoHW3&6wtwA0uSWC6=@EZTJwGV0me9XYzR5 zj_uT{?hgTDO1Uy)i&H8N=Mv>+i(=@eW7?6eE=FTorsbMBtOTu1*-oN<>E;a6qixW( zVhFya$qMXv1YA_>V9L{+i zlm;loFxDzFBzVaup4s$N zP2H@dw%IFlAd*fr@X>Ijnu%)@vkZ15+gx&ARV@4`} z_>9RxQH>~a%z-S_3^ERnF(_nYYrF&zLdUX|^%{j`uQQLUecxwb=5J$u;quJoM}r_P zai*)j#ORR&+NO@MN*L!`!oRaQ?$uo$Qa}g^P9Qqb8 zD=*tmF4MZ$t`B%5;Ev@}h{Syr;^IO@Sx|n+ke?jkVwAlVV6s6{xfy+qZ(Uvq>SG(_ zuGo^KE_m>jh=p-3HTa{yl5zVh>Y~9nAbOcZIAb4vVke539;)+lS6rOmKl@-33d`eD z3U?!rl_cm(6%af=7e(1b<&Nl@mm)gSKLqGuG%`N#_+E}heK;8oYzc3}uE7EjBjq$^ zu!qfb2@lHym5$7-tLGp7YqU?&V5Nsj8o8Ay$Nr zSh;q5@0t>&EDIK0$W~cM6DK@;y;wPSNqA%^84wvA;t>$Y2jO`?EGVm;A#X$q*5{Cu z<6cUUq;GMQi~x=(woK?rA&roPd1)M1jkur;P4_VR%!x-9I!zt2&LMLRG4r?@dZ3lL z6c}(EneiX7_C%cx`tgn}XjT5C8Ugla)g|YNFke$}hrq!NWmlU8lQe^dgK}jzMO2nMxG`JTLf1 z#RUQ{odRom>l(I}xxQEpSfhL#k*pM}6s@IYK%3Zy4H_bPR@AC0r%B2gYtPFnbPO%{(n zcbmj6xJXEQZ(nh`)86ABpY$NkA@&+0(zTO@Y7IU7Q5LgGqU^)*cY7rFfLc)2u zt{|RBnvUyqkN}|Ge*Hyy{BP*t2A6tk{dykRALJe_M{>$Hl|XBGNAcz6CX&+qZRIH( zlC`fmb2*$&PG__OB~g0%aAd8bWMuMoxpRT@>4p1JWR1ozUlWn1IcWUfR*lV7qnxwr z`Oo(;2aW%K;|i$F<~gLqJVSE#_>O{Z^4g-wm^xI)3(NvctI&NjXC*C8}40a7X%8omLhWU@Qmcl4P#9RxNt^F!Mm(D zzX=@MHa^0H$}!X_Ok09bXjLdqN`H{%h#=Wt#eve^C3g^vf@bZDJkYW(=!CzDp_)fM z1fpdqjH&qCDc?1%+(1j<$IO%>Dhu4t4f7bDEGJy05_@s73&ut5efG7H*bIB}bJ@(Z zQBN=y0^Iqj!;ydxjHQ(c0p81Vfp?x(CLBoATEn|(s^F=ZSUEO1eW8wV>Ztc3oqZf^ z`#vspboSF3JE4H9SL%N&e`vxJ5pSM@d!*SLb)P+OKiCD));9^#?4 zVm}qi&Y-Jo3yVFn*sM0=r2KbN)jT81{RvW!P4}?f7R+9QQKzZPwi!BzF#@P^xiAHt zTVg>bh=Tsj<6^EI5sOL~%0N#Rq))&kygXo>cx}LuvcbxdY_*Lj){DNQFQw~OoJDK= ze%9|W5pW5lD!H`AKg1PC^`6YkZi7_ISYHGfw_5V`Pc9E}GWGSz7wJ!hti;XK*S{bw zD>&KDkKk3U3+5+A4X9YVJ?)?2V^e;tHHGBs2kSX6sbL_4OU50zNEtXkyav%JZqimr zRz5#p`Cyq$ZjVr*oy>SE%hZB!=oaQ3y0V+Ia&?Ij6oOyn_~%XVcKg(LS~fkx=AnY#;tK|+e6H{ z;6=0i>82HM(r!Ab#y$BQD}+I1EiWC&n}t0pU{hT&1bprDQr=dDkjO z1@1)?xJllstKF)ZTjcE{VCX$u>gvaFr3aS2ZDc#ziZ&G*un3L0Kg-QACtJKw+!wG} z07w=(-ANpbzjo|S-<$wQGEm&ZW&pAlAfH`0u7%tMMn3#myZN}fQ_oI!`{d4@D6oqP~0A54mgSP>N~7Led8 z*#0JPJ8U^Gu$;<}7Hsc1mY=IcnW=$*Hz|MsCBYQj#ESZR@R93W>gA{JJeE?H{`nnZ zTrXlp4Ihh4_8ThD{pTNi@O<*#HS7sokrn%wCIT43O0tDW+dKJLTc?~Ps*n*N)CtuK^<#)k6UMnU7 za{phScg&6gEgu-X#TdvidYW;c%CiU`wOV8)?B8^2#YL9)iaicI1wG94e!4iF9&fsjThbi&p48NAR+*Z`AV ze;3z~PTNuPs=L;)Si2G zxl$$F--x9%7dmk>z)rQ4*z|A8_740WIjOBC%o$utD60v7g2@~)qe^akQPa^In z%d~kDPpTKJ!dlzwk%CT@e;A16+v-s_?5ldeX~^=@Y9EQSonWI;KN^%06NS%UY+3YG zUvswE$}mmz!>V&b$NQA!rXuT9-$C!n!qfGrpdWjl3^Dkk#rb9A=fmEa!X&WFn+TYl zB_`*|*5)DoIp84)2`RY$Kpwd03&N-utVu5$&#O^|*6H_HgToNyD?yHjxwKBxug=eP zGJ=?zd#OFnYLyo$s6lmk&}4RDMR0uNu@cwT$@1(Yr*EElCxL@Ln+Yty-LK(``roeF zuJFb7qpHC%gIs`q^`d*04b&D zoN$bU&=XLcbQjD1HOB{1-K^8m$=MR2F}dbXVlp1|TmgWUscUc z5QAweOBXLBuRn`EmL=z&n~&lJQK!($FC_z8AU}X1;d7BnN3sz`25DU{IG&|4MBt30?$dYRi-cl6BEggKvjBP`#bl5ji^66Td0_#B$}J z9Zz-8vL%XR!SvN_E26uU9;>FWQ9Cy7DCLh7)Ob7%*76~f6=uGWP!{C+=Lnw~=?c5S z1mZ~DHS$4Ob7NIQ2|!7l3+mqs)DK(aZ8-HbmsaNJg{*;oQ|8vMicNmg-F@Pgr1ajN zZ=ZQ67SBeGm64s{{`Iu}Cxf&6KQ{0%bT*E@vcPzS(jtyZRqL|RFh1=9nF`XxDa#9O`8 z6fDs)vnElZB%$H+2K`YFmn~_|LeMrqn;zDOh59uHX{ZeN$#9HDOf_ZP@Wf6Ut!e6M zk2BNeNHm-!DH*TI#cB+%ifC3Qx^E+mna@h=_v)_5Ig52I=CnaYX>o-Y-_9jOx#!Zc zLR)mp_YS_q6Z;fFfi|8UyvC?@^sn;riwnV>uZcTzut+Q;Fl7uQEEdVMO_ft&tk`na zSZp5<&{=S+EivD;ZX=90tzr~~VHU-*3=-Q%$OyvdW4BKT{umO(*mj=U?#MW_96FIF z$^o4V(ao@-%9jaK+Xv9-u0ExsbIkkI>yj3zXj;_=ZY!c*EK^bgvPM=%JXOThW{l9G zQnuF(84D(NK^dFJcH7b{ZlK6_Z!njjHP`dO1eqF!)2dLD;KSj2EdnSCgqQc73tgOw zLQ0f!?eam}@6aD=J*MpAw$heL5=*cb&W;kCX?4^DTv?u?6Wd!gma-3kKuz6qrz$vL zksS-iuWl2;mXQN(tMOPAsU@8uxtaiI;5UcA+6^(A;g!$Tx<^7#mZP ziOnj8gFDh{chrZ^gOb|D-$v+Scqu4H2QTs^cWIYm))tpVSpn7iAX60MRhb=6xGRu2_Y!3ytrmDf;QUhHfap3FJ$Sgu1LIYOO*&E5;(hTHbBK)) z?GD=`mDz5dVD`bV+%YoFxx>v}ZXWD(R7Gbk`FSfMbb9v~Yh)(qJU4$|c*2g-5~B$^ zdl&NJZZ64-pS^sn;BbPXJUjRTi!)eZLS)7>&paFdvu!&S>X?3w?rUKp?ZU3`2rOW6 zNFpMds1yzHA6Ahp=d^-ug7o)$Z1^zN#($jh4pcAW!JE>!3={PsCvmzZ1cYnOE~#t5 zU0vtT>VmuZ#5luILu{qEYR6Lp`LPDn)F|&?J9FwZ|Dc^oc@53XQp8;*Ok3j7Cy9dY8&ga-i5b8$WP;Z!1(Dv!^Dch59TlU7{Od`HS$aqxD6!y(D z@9l1>iUTG4VnaM6}f;tcI0vu)-w&^pV(p(mmqNTV*6sN7w2Y8 zki2(s=3>HN6Bh)iv=55&g)!Md|CGfe-;bq#a`x4_*q%W&aRv}p+6;!zP7F;9$DNaL z*pXJ3Mex1Z(wMCK+=d3?)w zBhpr(iF0Sgd4xi;|4*ORCK;qSC?Iyg?6E?8qr5Qqjv>bAY*9(iuS&H@Y{N{& zV1l)fSjzSp0MTH!%&_7NqZ}kZbf)&uAsk^W7?MgHlk3p9e06?hZgFy+^4K~~fKK2u zhp-a>E5AvgL*gi+|CDFVgcm`2A`BJv?nCIHy8=;;Nj)wnn|!)q~Prn>_P(XO^)VPco011++UY?iACl@*EOdM zNO|*NR76K2Uo#fD*OKz$+j9t5%I4h_ZNPvn+Ytmq`%_hfzVGRyA1^bm78s#Bj25bHMt*;S{V(Z3V;I?FyRO z5p@)=(7yyA;$f<0VPWJq4}NyYVdlHBQSp1IDC}qOE#Z+&0R_1WoOR70cZP0@>JG`A zY|H|{e-5CM>VP{uxM#_+7O~U91$K3#uy|TRBKqTKSQl$ssesvNXX%7cfXbR}}hq$Q+_0g+9VjQ)`PC$&V<0j|EXrcYQQlwf~E|>8vhu zeGAR=H=;1>=y4wsR<&`#gQ^h&dJoczhF{jPA6XYbiB1(@K{$2NsP)2$7|m4xx+0|^ zPBXB_P!U33LT#{3?0PlIE${?fs5^ho=w0y&IyhvO`brPg-Svunm}g2DvWA^F4cdDM7!N}tU2 zfo#Ye+aXS%3<`s{RTWY@sM^YIW(uYmMw`K#fgCIjut@L7rNPAYFkVs&OQOdG(s)F* z(!fwX=&#q>ZX58e7t{DGYNgzOw1ZFm8}kvDv9&`#_qeEknZ|;c-Vwct zH8@t-^2Yfb{AhZu<1835yrMxChpnLr0~%&M8p}9BHEE>K(~yOS5~~U!c3gx2iiNq3 zM^Z$TYX92A-D@1InDR-=hE7e1-qiSNlk8c+Q$7z58$l=jj-akieEp2T5IwjSD*(xY zNYv@wG4@{s0j2zkKfM=j7}XVy)whSo7TVa$GqmNeaS26YTgpUEMu0#p|GnWm;nG%3 zO&;Y(LLnXy!4%r$;Kx2G2Oc5HsO3jBVgpM2-vpxt+oY@6*&WU)s=$nNNOhperlbfL zHMi^m0Z_4ti>BdVI)zpBSm2{pJ|39Zi8`_Wx4f|Q*r}<7MMow%F1A}f3)O#71_@p& zxtRSh6_5EP&uSsYk~Z48#CD`lNAor&^6D5+>ACKM8py+Vw1+9 zZccnhjN`+#aVP;$4(DXrUZzKbU{1pRwceAdnH=W6m@p)L(Y-(xr*I%GI3LzCQR zFc?FZXoGPDxsq-@5R4-tZZgbi8dr5}b@+d9*@)hM7pY8l-esebPNiP2O4&!9~iZ)}NbQa~p~Zjj0M%~@8s z0;&QyCs!P9vMoSY>y{@lmqUHiBcTJB$|)Aqtj@EFlUt~N;-&&SAoEX*K0XgspoU3k zyv9_t)izj)ednH?Jg$T+hCzWzZL^U{XzvIZ2{;m_6sOlfr7SS2-X`Ua@q!3%u!uR# z8#b>xtCF0R9wl_1px20Pb}eqD;vnm!w)x)m)L($1^oGa=T-|SP?~MaJxhZMI zyp*9R&2#=yz#t_CA5>G;784`PdON99u=yzOX zHa56iCo>h5QYb{?!?0S!S{tT7$Ngh(CirQd7ydFkkE&(TD zn_PA%(xzI$3)F*@sNrny%LkY(J1o1lZq-ap=QU+0(iDsG;|Vb$SGsK0@gpYLTY6g) zb489_voLGcs9ZfsiL-5zd1^-1=51r%iMB<8*W?Vjg}a2sV(3Px5k-jAZia}Z0f4Uj zpcFcg&ZvEuOWG=m3jMZlNevjmXQIgcw{GI$s%VQ@p|6MXHry@@$r5Kww=$|h!Rnw0 zx#?kP>7WHR#cdb^0YDiU@T!d4S~Z(ctz29RSQr4SQE8fSqGWrBV;cV9H1EW=FOb5Y z%h~ryVML+5&YHHG9YWW!L4}1#dYGh^MB;rSsY(xnl0-<_pfBx0M0er2q4j?TukcTYMI`%r9}Jp5k=MuOD&EJpcC3 z_U_?e3PvwF72)_^Z-f6K%}cJ<2&E?C4?T*tMg_MJOs$v7Cj<6|69$Cw!;B*@Nr;h{ z###k2^g(Js>vZlFaq7f$O-4&)6TZ4^vAPyU5fX=XqNNWny))EH>SpUGw+7xcN*B84iIb!z2O+c{~=9LqQ$>S|_^ zy-cQ2KZ)|JZB9wJO48J5f(AnzK1Pu!s1@Ni%T#=0X@15Skcu9|SvcPkuq~URjV^0G z8#ag#DKlLKHswcPO7^n488*^?Y|}%8h2dqUmZzxm+#HZiz|q`4&Rh*RzY+iuP~IG$ zb#v)c#|o+ET`mDe--FtJ=jFiW%m{2$H7qg8?7>ffWzH`LGSw6`!e<$gO<|`tg}20K z-TWmkl`Rn~((v^?rf{}Ch3T$oM+xZyd#ldW)_>r#&i;7~nab!)H^2AO#|oXD_Y9Xh z()U27J8R751;g*g^85B%t*1!_ge2uKR9Z_o_f?WHe2P|yG>E3r|6pwO+t()st3o&^ zA>W|XuPbdjEwN3Jdw72=UQ3>U@)wvZL}CZ*1`nc|v==uH#$f)h z)QTsqt?%O|WHW__#MvB7_MthCYQz>iJOo`t4UvY`vTD^n!7ACZo~teETVq*u^VP*2 z7Hjz(e$$fbdo1fO4Dkr#whl8F+I?l_M-(2wSXJpp!ZCcc3i*fPVDUJCF*CkykCjCR z63a_^OK=F)3N&T^HoQj6HPhCW{rUE4s^*x||WC-f>ZDNk|@1Gh!!??9c z+;l;&bp$XSREr!Z1DUBzIkB(U7G5!2e^IcDx}#iiy74r)k8a80)RM#Fno9Ltik*m1%ZOhUeVwxe>aDs8r73;~x^DqFF(sLD%B zfK;hUQsGjSsKu7UROlyXDn(2pb zW|{#S8qe?lKj+@}eZQ)bY}}nEpCx_Y``&x*xo1D;o(l>rRYL8SHNk5$AyBFx5F!S4 zIM9D$|JeT4#%W)EgMBbHep_qfv@zN^{kUhxj@}Hvy@`)DP5;h~``XPLH_H}a7!=_I zkRcBaflvy)MsX#Of-C)L=RKjz{>Y(6rsF2Kw0avG&IVM(tId`+2qJEa18>)w)Cx{A z6|jY3f}SxZ?DyB!A9^ zs#>(a|BfiscLN_G{HsB*cjPN~#4czjbVfpd>c{tAzoU0JF8m7jRHqML!AD#3Q9kJ> z49r$d3b*qPkKc$-!cC{1@F#ZeDEC!;rlqinzu^>Ex`RPg>dE{?`=l9OooJb=WXE;q zh@B_d+Af;sYnV$FhhQlJsA-#JxYN zeFrV$x#`8+NP(@y_bkLkjxctYAvNNj{_fG%lLRpqXD+f~2cqlds$I#LS>4+7Zf5XonMMk(Q@VLm_aK5fqHb{ zMUf8hsTI+jrWL}{^_hZ^(d?QtIxF!zMo^d)Wlp1z)Lr0CdSZ0n44BE{iw_GT5p647*V!g_NRL0L(XiJSJR z;JTY5k(G6qic0nwd{LX_vi*_oiDWHpR)~3UqP68LcCVftoFMo*Y2ZckzHT<1*CB>; zIK|wgK3ncnK+u-7o{6i{C3kJ*a^oRZ+8gI{6CW}8Z?LtNF8AOpoagqupZ(_7T3|@J z=*Pc!*AB&eed>~M(xjBV>I6TVkLcpM7gqr@G9+MzU*nB0K*szmRHc#oFWu3b=ez!Q zY+UG2)hF)l9hn??`}wuCi}&u`t82s0$~|9FpQkaj>E1KSUfR>#ySA=tre+rR?%wnE z-Fv7;R@=q0nYLL<;28y|MSwD0sAK_OLVH5lg(jA#-_WjP9`o)z6NQ3lL2VvKQ4O(g zLGSQ|vC2!GqoRF&?u&U1FCg04hGG$Ks&qhhM@pImPfmU*Jc<2BdAce~n%owbG?my2 znTs(d^EYW1Ge(6$gt3(L(IaJ~*?FiRoUw-BymmzX$(coEvC7375K!W@sJULCe@bn( zo}8aWzH)FX8SO3W%7Ia1;OK35^1(Jpv!+T!)*F#jg_3+^N%eE=Bq?JGZm5<73Gae5 zbR(V!>IZJvQBK2r_YPB)t-(?jLKbM3zlcz^X;NA!2@aZiD~zu-re5N=vi-tOY(nLX z6x4OwaYrIFlpaYuo=>HD*-bda9*~5LN1a;WUR@TOXuINxyT0}#>;k8xmma;Du-6;y^lyooeE+BLYZac z%%UyZtfemOgRhElHK^^7l>LX?23GG;NjS)UFjOnAFCiwvI*2?=jjBOuxcDeZwZp#T z<6((rtOCGL8A3_p4Dk9mt!69~r*||rpq&`w@Iu+v=ul(pSl2}>!Q~MLNL{JS7PwRf zh&}a)w+*EK*|l^!;v>=Yz2lVzm!URxBuTEJzRQaFI@u+1S+VvM+MT<31dJn zox$y~^_nzoGm(R6Eya|kCS0EVy#Ph|fV_GYqOY^G`_zMH1WL=k}=?}?cO8tVCA zYwlREP5-K;c4>IV))iAC0=6w%!eI{8^cldsK~%;H2$D<_O-X60Cr{=DLmD+TgRH|i zr)s%ejgj6{6TfceMX+(Pl!TG48#8p_*Or>K066rG^;Qsk$!tW63HXPq?9x50)sv-6 z1Ph85u%2``7(*at#QmIG7*dlzA z+NJ+Ev7_AA{lz6cN@}#l_qRJiVpj+TN&#)z!V*^dV+W4kucOV9$Sn9gNeHBMTl4^9 zH_X@FA-46DOYTQmDz6U4PMypkhW45ZPZbZu8L+TS(m!zY`28u#!U#s`D#C*>JV%mw z#Y`0YBC$stB3x7*tEi==^X`^G#Es1^MeQ=D9hhaO)~KZd*>YO&Z!z_eO!2E!Pg9-P zEEeSUwDUd=Bc`;KZ+654Sl2OV6hH}}xWA_5n<&@sKsi&P+>m^Oamobs6X%}LT$mc1 zRFZ+T@`R0#RIUgiHyll6uW^FmpyI7xWV^_*La)I;W^pRwIZ(k=ss-cxs?w%lMpANd z{v7CM8w)3yod>sFx}noNT0P`zn?vG8^kEgzsblu$KA&afn2I75+SJlZ z2(b;qfHQ*#%vw$(rfBVD>Z6lhZk4=EgEaXBnY4iEe+JR|0X`C~Kl$bwTl)O2VWsWY zt6H2Q&c;I(O*SOAx~7tQIj0U&J|({E?>|UC?1R^`NlG?&ga%OR4~ z&=F(>>-0>$+Oj+7KJ^*rwZiQoMn#B20LgyQc0@S24yoHT<0{OS>n|qEE!9eaU;xPV zu1|W6KaRL@{>e8676L3lsKgK3$hEkpNL3LVXGS1A2t7h$CCA;Sc2R&8q#upNNv`SX z=KCm;Dl7saF1JG$f}g(LVmj^;5Y8bw_6_BZKoXw>9Wj-rbJLNOE>SVrUTq*ktWHag zvuCj-(@NFsz*kH`$ZTdQ;C0hwN4(r)Ol~ot40xN68H;iK3)(MnEKl5`%uf%HDO_s@ ziRMLy?FWH-JNvMq&o+QgVPT^t<)&B}Sb~6x!PIGvU<(Nq7EKWZJ{V_h^sHOMTA*uG zp@*N|x;eoa*Y;%My}1cL5o*OV7jQ*4HK0M6q#2-V+72F9~>qU>oF zrcDto5N+xEn$mJW!95i1%jeS5RS67!T&J_M#3oY{?VFq7SfF8OF}PiBgJPGZu7O(A zI4-oA0n%V|nMfY(31WBt0{)A5L37x&t_;T0Vd54}VMwVIs1Crn%~n1X`$Hw7~4_z<+soK-Ee4-q;J78}HYxk`z3pYsq%#3a>sa|setg9d4Z zcc0wRJBYUE-?{N*o6L%NfCdBGJC3HIDYI{aCdhFwC&EO8^q*02AEv(-tmQ1wQ-+Wnx`d?jNg_= z>fMnsp+J>mcVSUiR?yB_Ci|2{s1&in6=H78IhgWxWlY!>wBjgnt&m4Kp=h3yRp z+A$2CqwA)`K{3IPG?G#svmke1?9QDh!9cc_ePcj0LBn!fb4APAB>@6jP;ytE7^bn zXsMw?6>$Y)38Eljlwuf-Hoc;7@kc4#RS}9kv8K>Uh5&j15sH9Fhq1a7CA3&*DKwr} ztu%YO5>{#Haa<5~W~ADEzVdV@1QwF6Y&Mak?Y%@n(~+(LX379-t&u2T76A>32kc7_ zglnZpQq&g8b_?iDz}_pVS|n}~vrIExNl$Zi-U`W6tV!Edh89ee0`bcHWImK5I1AOj zk&npe2j5jGcezEmO@Kl&vZh2^TO_lLnj>OoC^O=-aBLcr_|3)3+h$vMRo;jY(g|Hc zev`s<27Y8Ew-9Drh6*yeLYZx~Shg5;2MSSNb8RfcE65R{p`;X^19E~aNnW6*cTzOb zKjFKzd`h8YtniKnUUHfCL0zJeJ$SB4h(MQIjhH9w+AKL-V+v@V14L+3p~NDP8h0LO z-wF@QCTmg;&0`C6A6R`HWvailzYZeD_Kp+ZVsOl;Mm*NnuKplFH$^+d<6iiX5OiDs z_;gQ>o}t6Du>&SR<0&wC2OkM4W&W}k^zq}pP;?vZvysYU%y6#IHi zN2eNpnW%u$i~8Sge$=1(@#o&Nqerc2Z=R1P*T47hd;j$tT$i-L-xPu0$b*^C_z|Uc zy$UZ`{!Ll6c1Qks52ybOU21xY%V9l+xmKlK%UCnAqy&YDjD=jFSaYZ1H13t9n{V4p{cxid?!NL{Z+ezzwCNA7G%Rja|(1&g`TXyBc1$nv8oB( zkJ2iOGdled8#ydQIc5bH-4x}kcyiyq{r4)%(Wp^+o102YoT4>MR0)|^#rEA$M$tRO z0h(m7Ht4U|5bxm8*EKt+!T{}{e*8x}uj?sxGs(JE;alaeWzeaXMZ0;&duy`zW^ZUy zjZ$CZ(QiP`J;Fyr{_y*1wBRQ?vQ9GYA>=M@9^RxoQ}d4qP6@A$j1V8I=_J|5C5u#) zOWEk;dqyHXNshqjd!GTVfk`N<_U9@dxX-7U%|=#lFA4iva-fM80jcl^2!05Zh)?aM84TP4z|~q>Z@^vI3$v3`yBaZU1Buh zwk`3tSuCGX8Oj3o3OX|eNjFwg8wJS#Vw!=>1H7*4TL7fUz|9mjvd zIja`y`=Puud_=4VKTxUfDKtAzC`&CkW%w_jA#R7$0af6h zxp|hPKL^4b-k9VuEzu^03zoIzan4HJtgluCgn5d_JTh%P3)&=}^4n7Lh};anKAI$N_%mV{rc%w^Nv zF+(;)SVk?nutgv%lbedz$S8&B_k^77hOQYHYrlfVtZzwa6@4^IA~V7kGnjBAic!dx zGo3e~uE8z)?xq(R&B!boIfj~5VAjE20Vag%aLi=}&*9DxvUIy`8x>(nnTTPqaRTU4 z_?_I4=XZA_x?6IGr7hK>F6x>rz}&{i9VEj^v$7sIgH&=+((e!|&T!fkXmq7p*rkV{ zjWIrwP(S&LRZ4xuCMlJMMV6RI!)2WGsLPW=+-{Q-TLZUv=9~8w9%q-%%A)ekEe@y9 zj)+cIiLeL;gQiKgyje#`0A4nhPnF(ROR=LUah0}oGN|DTq4KCP(ZE-6m zF)=lW6$5LdWCx|Nu>Vd0)RssNu#rvz+xP|93KLmfU0|Wi&R36y5*}I4f zc2Ug-c<23TA%*|zaq6XK=oT*GumK#-<|`pE!To>za*hAq)+R7auxaUEEmzC%&YfGA zYw7*9C0XU*fD0KxdTqjU77G@qEh(ThCpLL9Xe7X69Ug3!{1)UDLG!{=b#PHyTVo8y z))X|4eclca#OGu7!Px5g$A_MlT<+gGzYp?LGr9FkhT>M5@EvF&Phqi6WTQ)9%iXSk z*oK)_XXip?CKYzzRmVmW|jnUE346gM#-a1T416Y7+&7_FdcSUBgGD{&~Mv;}qBKtPWf|nhOC% z$}i-bj2?Z{0@~JcQ)gZ}VzKYJx2CZUSRRXrw+c!|u9TlE3BMAmQydfVRTG9>xU$@o zMqF{&ZBmZD#%^ubSKu&UedWFPO-R8#ptMnT5$ll7-*5ZO?5vIy9e5NY{R`I~Q<_A`S?@d=9+wXR`AUv3ovQ_4prM*YHc^JcQYb zf@Yphp0qB+@D;$Sa}fFy5>sMQeCxbqU^nVJjV2@vfO73DrW(B+vKpWzkVI9aD{VK) zAZf{E?_ZnOnAYVb zI>m~y`LxMNt?{T-H^XLKxqlL-kkUM@_Cr+bw8?Zg+)#+1NJxz&ER`Tui1oqgBjXdt zk3c`<5Epg8gjW+nbo8OEzweBJrncfy}6TFIU{n zHm*Mm`6w&|;jhe(!WYfA0nXgQoP?H&VHM_!k|+(`B9YjotdmwLB*-iOQb)3p>4l5; z);2Aau#tVy5VKTiC@F4H4N{%Ws}gzmGg8QnP=lT=_L%liQC3LYzZ!C0gD-%{aTPPM3z(+uy1tK|QKy)$L;@5U-6Ay>PDEe{`%d+O z?b;70vvbiRdeihCC)^4T$UYd)CPx|05slf(mH!nhmXPAW&Kj4yOUSeT+8`?>S2lDU zS}nR&6vApZrOlz3!O?6eL=-ziQBEql`KML`uuE+n;@MQ`E2}Qc6Unn@dAYd2^xuTK zI>cBF2Zkj;#1J`@RHH`=d?}`0B4g>1VLVB-RKV)KZQQx?YO3{68QNNy@Le=DKol^OIXiORYPjHvX$kGY=lCD47k9 zZUWe{odxqq8Lg%Lb-5dot?`bVjRFqfnS2wkCqLivK|A z_Flu2hX^PEmzofq3`cjW(bP)}utWRfLJt=wC&B|#5|6#0%7DI-tJNNYBF zm77TX7JC3|*hdatdD>x95u@1>bNy0_!| z33%reop&q`m0tjGWE#M8Di*eNdNP)j@XwTE8g@R0Q#Nd1@u7?VR6-!-usnsfsH-1^ zCBB}I5dWGT*Ogk2zi9<9s>F<1k12Rj-6O%(*@a6(jwryHlN;#TWXH-{bw&;SA}hn3 z)?TQMoONhxQ%KIWnr#W2Hlgxt(iZ%nlRq=7@@?&!EeFbhPT2%)Kb!4CyqP`8`@oPB zKD~}nZ?b5TMuh^arMJ8sQS+vnyXb6OJDsqPa~H|7CZ_>ct|N{qC02&p)%yOcn6IXk z2AOhbBJhA3JD)?=k`G)IjaXz1t0TMxowd%DS!jjM%o@BzmYJ`S%YsV0(;7RH>=wz) zKu3)8oc%UhEMAi%3qEK-%wdjtR*VyGEBo(deEvLeuYL?Oq+&I2p_LE(Dr;k%X?e)p z4fGjIpD$o*lyAD5j|9ZV&g)8dz6&i{efsIkpDM@A-U>0rJ{>kXTu5j6cFzPd` z+s8sPqaQ(cP@PZ$HY_=^0jAkfs)xu!bQs?yUxWpxYyQ$$?Vb#dwZSxb!3;qAKRs|=XBlb$ ztAphl{_}TTR~k?Bhf@8Wl~Z#9-S6h35f@&3ooDFxH%@kFzr;W!2{PBuUx#8_Rba^z zvo#ux)p$}B4)cx#LHg9#>qNE+swEafT&d)Y*DmOkOdycG2yK3`hToy)ai&$#gXaqgE!vB63ysQr(|qAJ0`dj;X&!$Yj?i;!TtWk|`fITfO(dC9 z@c^=k5GR}7%{s$;-LG|tNpCX)wTQvr{v~4zltEgw=Ej zLXy5034O_-C^T-7S4w4S`Kh7@7eTrX#zvk%joU*Y6;`Te1nbIK-U7O7T?mLI~MG zNJs=ffQB^*TtjL@G9<;3Z{bohR3&##o#hB`v!LlnX^7|{OKl%j0l%3CxhK@Nhms7* z5%$J3(l#*+y{b-5wVsV)+kE<7Jd<)f|H3*v9(@BLRr_wt*1iFT|uo;kord-J{< zuPgmW=AB0<`y;chZPmyOH?^g<9p01JH{r~R8%&&3A&gO7xbQbI zNfB|lYWLnqu&PK-hyiza4YyXigcV2DZD#Wcm{m-mJNW4SELH)s!J z%xuDhnNLwB8O2R8ba7Girs%ppX1!Z}Rq%pW3-PlgYkEc6T#X-It$Zq5>*i*FuI-M4#sq_VUnrAAZ*xFt+)EvVoLO&xRCUna<@+ClV zaHv6Cwrqbr8|>Bf!u8*5~TNxAEZei$t@n)sp; zWKytXRco@AEzJzk(@IoT`#YJBwKiK*Eh@3B5N`pl+<)c*7tQt(v2~LKC|?{ztyL8- z+c8iGji==IFd?X-h8^u=RGBM0C%hE>>(irwDg?0+s5+JA-pV)|t1!XLwY}awe6&A* zJyFfiud$-R5cbLcJa%+e^~Oze6^2%zBczA|Ecg*89}MR?N}p=S=`;%?rgxD;+cL$B zR70pCny56#gt0aNw%kp*5Q!D?}MNm{uW!T1jGr1XUYl63|~vnpb<_2s&sNx3Za-Fnj(F1%?jBl_b;P(DDu;$P+-wwTU6y~{gSGnu*Bdxx|_KQ+DqDm z3{4P>Zzr-nd>&&=8JKQ_+EAfUn>kf6U0SsWwXIBDZT~KGSLwn@pE!y@#3YRycBz)w z!o{+7;~7>s9$ks-X%Yv*@T?4>tx-s&$!9+V>V>}&Re0Dl^}wecYy={jaYo0XF5tR7 zz(*2WVF({odDX_ZHs?ef(sg&mYJHo6?aJjIHo^q53SnCaMwG(t0FY2T;GRYqcwDb} zn#2R`D5_l{hZoB%(g`AsRW&<9=LkEj4!Kcs?{ZwR2t*mV_dz$UsV(ed%2nuGjn9_uWX3?^pHSfDh+0ApE#-;=0}uMJEosa%Qarg|tg! zN4E_MwO?Vh6$HjnKAP_PAFNvWBORf!MNr75h_d$RKuED;iGqaM(}o=rv9or+VZP!T zv*Ho8?88hdn3gtM1l2$7{ zB?5D(p$JV0OeR7rTcMEabuN_7i`7GlOu1j{JTxrX9c=$&TL zS}jwQQ$|DbRn>D7x{=y6cv-O=$9IuFZI~4h1vIUmHKkEx{z2IJrq@KUUV?Lk8$;p* za)!amyx=!c#zQO?j-xiM8cOH+wwN#m&9SPy4hwf8_KNyI+++ay1*q+Vd_<#y5*%Iwq9J_cSI)opfj)c8}x>;tQ^aq5`{b7QNuxTqm0i*RMc zFD*6YJVb1z*hDKh*Ri=aYPkbi-Z%T~{-371zAmA!0;5n=N*!|ZGidS{-TEL=>TkeH z(XA3YGWytXJI1nD7w7`HxQuRNH=-dICwVZ%*)6wq!o(KMN0~#@!cJ&Nm(QQ%M zYX%7rnwy0n->4z6T^vEH$_PRSI!FYpYWsqD(TE7j!m-7%RxqW`V4&P)ZUnoW22~h( zrmVqnPJErI(F2NrGrlad7$Ut=vzty~SL!u0hvjp7opM$LtE}-07uV`4gf`6*5#rcX z5r#Z=bBo;fVloSzgc)wh+B zqXb(8Vo+!b=#f%zL1<$5?3f-TV6Q8mibkjP@FEF=p`N}B7uTGHT2_74;=GWh1q*SE zldOo5uuOVM*IZO^Scb?&WKqiUtH19h3Dyi$5zCW(A{UV%vXfU{5!J#S5Cw(Ff`S<( ztoZ}0Zj0%e*#=EUV8S5@ncv_~Xp5PbvQ2~6^j3IM8>PUWJ<%fLQFaCMDczcXtA-+F zd$X6Sd8h~0(n4}oijooSmdq7zu)@EB!hVL2kS!V(YxHm4ICqW7v*{V@Xk&two`Lb5 zm42MtMCw(&XSv39Txi@Yw(kMy7rFaLr-2NJr!r$21q2EL>QJDXABLnollO0w;Zn?6 z_ZLMad4M#{tPGUH%dD&SARn=vDCm7G%-s7sfejBz`0}V~_KfJpJ4!@$KA%~bY zXdeYYC=7oP!N*27)|NO|wBCfc(gKr#oU*#16Se@d5{YC5YeV56v0(~!u9UBi;1D~z zzcoL5Q@7^m@Kf@-!e*kT6BYdP6k8Jw+znGoD8=5@o;t&E&M3lCij#3xZbl|b#}F8d z9eVVw%z#q06zRkvrd?QP^6S&D%512!AUUW4177MjCjc^ z?o}>cj%KwUrOHDsxKsabxTTneI{QJ}lsl19Wc^jolfL~jzz)?A>zx{w?)0AXGMmLBZeM~Bl1MooC|rw*c)zF zW#!jH*t+>vobcK2sru~4+X6#0JuM~eT$v@WOnjn5xTV}zE&k?bm2HAZOM7Yh5aDqJ znaJM16uB*??!AwX&d-aR{sEKE@b_E3ZR6Cn*NESb;NN~09}d*KdL!Yv-ZJdjYxLEP z8~SY{zj~3r+SB>!Sz07q(hbYK3F;te;v@|o|BTaK@(27)F(HJ3U*WU03GofQtA-2$ zb^N{L{`vbYYa5Sl0{3$ZxUxNf^`?aN>1ToRrj30YyvQ%Wr&g&X`qa9#kpUb!6RBdL zw!bSd3chp9zBP>B&d7)O2)@5*V0_o}h4F1043aR8pQ10uMAJwBhMJ~~`0*+A`CnR? z6)j+OQ;>LN$OZN5zG*wipJYCTF$DE@@Kj>=&CeI|TQ?XaAwP8Dt)Qb^Fxx=a`l_H+ zfc=YYfqR-E_wf;6_r0$I_x9%t+`StNQh<94z~M(J>Yyrt7EopXTVEA81>;#D*n0cF zo6(Q*5scTozry)#&lk=oHW;M9Id!DhyMO9|2m4dpCprpCmEJPr#V_+s_bdU*Xn|Z- zA^)&3(*^u)K-+o&1cDvtFi%d6MswyiOl8O0 z-xb0YjIaH`c5uG4M@E*9VElfbO78#4=S#SExAA$H;M)WUJgj`#-D6Y;t4raP(J2sr zZCfxu1c4dxWuPGAOj{ShvjK>Sz0?;JZop6*Jh6};~NnypFT zgTVX6d<5^V4^^0-cn&akNZ>>Z=KCM(fJ{uHNHIH_x_a!&APd}4fY};kStWPy5x76n zfc)@t06DCJ)U;zQkRO@>Qa9a?C5E--r?`^+SG5faX zfY}bY-r2(J)C5w&O3U<`AU5HFn^)}^?7U8i8iPY6olHx%Mf#kEpG>>VEpZE;Vhz6FwXu;Md@=9 ztsRt}Xu-)PjvXN|EL;8im9&At{U(6f8ny53^?bX7zMa zbon2mNv>stB&l$jF9DYZvu(lqiWl{Ie}Z!-Kozf%wC~@#@u|*j|ICMG*ZmPi86G+Rlsi?No^=5G>UqQUE7%M1|o~qi5Hb91`KM;Gjm~L^2uhrxr?- zr$8U<#ln?8ZW&aJ#D)Zwhi@p(kzAmbh?ihX_emuSs!X6A$$7hturSAORm9E-E0MXw z!kzSfA^yB5c6FV5*qDrwW_6jWC=35HAqA(7+$k5XU(C{0rz2%yYGmu1?gQ#LFnM6_ z!LftflMwsC2Z8VUPi(x?GVRoh zK5_5U4{W@o_sj-Sq38PNv%Q~VM|G2ykQ!m%ZbmY(D8AzkKV1Y3#;5YK%z{5|ZIMKw zp5_UXpMkVhg)Q0WylT87V89;p9*GUgskO7E=7rFsBF>7QDGUtv*Os4xBlkR-Ahj>Z zNg9x^z>PJ^!XTa49SQIZ6wU$GBE%IyO2)CNzLfevKs7!mf|*-jCF_76co8{8$#M(C zDAgqL=x5?>Thdu$n(wyn{ws+sGA|crm&uLCTc@Llsx{68r3p~~@a(Ok=cxYr5okb> zEpg*Ve**y^nz(!8&&0!9lZ3b2`9lqE972N8eko`%k5Z`CZGpbmQX?!fE1X&|p5olU zsvokes~!@f17@(Qe_C&7*mIX^jK!$YcH@& zoFp5CNVQ6p-s780J$j;C>7HzTNkO0juZ5MqZ%Tt((b}8{?^IZdIyq!wS*VxJb9w z-fF*o5M*z+w@0)Ie6%5d+B|pProC+txOr!Z0{f0tP4tzYt|b5TRY?AD*kzkIi1i)D z);d=U@1kf-;wY=TS_>4=?_tjE7Wl`&_6vLj*V)h11nGJtNGtW8u7~83*G5Y3Z%vyb zSAxsD6xC&_5~Nx|-?9bXC;ooB?1H-A)>bt_T4)p$Iwg2)VM%w1HrJ&;MAsN4kIGI4 z2^>DP_qg@!>EY`5hKYrP53#=O@URRHWN`@xFYE0r&wYD$qo<2qoz2Z9zEjkbTnhD3 zw6gRt2eTJ2ehRMO3pnnuRAbN3XeJpmQj|Xr?+e2Ii+1+j#_#?2Y<$fNP?SYBSSF+> z%&r?M9h_OAjua9iD@ofE?AfL&aiGhEiuE7-6sEwzPG64b;lX>HA`*8)1o5!9!&7bX zW7}+aF3gZwD(Ew6^ge(kY z0%(NwHJw&Q<`0e?o#G}hi43ThPN24hBqp-FiQB*V*XmQ;ZgLL*~Dug%QPvR}>kci(k*-+{e%-*wM^JWvkZ%3v-7;JhyP_wL1x zUZMW-949DfDYd|9d;Bc#U~KpCnR66rg>};3+wc67vP}+uEB%H&T=M80piL}ML(YHc z2NI_jn3Bg%FHp!nE!vq32Y95Gd@jzPUE3RXca!MlUOpt%EN4K~!GPwjXDHrWJEQ9( zrKy@a^$G6eQ&%*Net7C(y3=B+J7X*%5hvU*;rR8)CH3Lw%W!jNl-0)Jlu4sub?Jk8 za7spe*Up{ApuWw;QqRpsC~~3vY5CBCbqZT_W^VQTe5A+$Hs@!= zdDrc=sA4OeZ9>8QQ~e=K$58j|+rKZI)gi3M2oEfbo`!czKS@n>*WLRr9lYnx{{Fi$ zQN*WX+*QhjrLTWPc#E&h(C24J&Rtx%1VUYZ{b#xUd|PL+!4A_3k0LCL17rbcc(vwtt`iFLi)DWobFy{K}T zulJR&nS}tdZh3%3oEf29g z7;ZkY7Y+Utv_bT$=v3L!#s86@`6Ohpo$2&-7%PwP5vd$`aeXc-W~v2w6di#vC{ZTL zROhYertQDWvFpY|&?&l;F#?IyC@Qi-{X8&NAX_jBz6;IVn|!o?0es)VNATV8lAWav zaQ~2*+X@1ylG^^@Y?z!vVYDz)qX^s!nGYCrjp6So4Qw5ZC!0?YqeoO7kWOYj1>zbr z#=p>|cHsfUw50>qV%Z6;a0=+R+X>at{uGR>6<&JT&Ym2P{?Bflc^-^g)WUQ9(~LfJ z|L{>Im62Lw2QHkG7{WEoHCZuElms++TppTReVkCaoEjyi(cPzr(DMBPFZ=tcg$p(a z$X6DY7#TZ1_eA_UT?=nAfITvX){NgKbi{Xw{%e|2|A79--*5I z(@=DS({|?mkBnWUM?5_+^`HF*`7gusUzWpv7gm>t2Z?H;;X(M8$CqZ62bZbQ)XTbo zs%Udl`WYb4y@Th!2clTuBSQM@8+Mie%x_)A8@=irM8KJX{{~Do_%I@$|MoUW!dbJE zG}>k@t8=1)Lg*siF9Ejg2>o|}|0nqf{#V{u*?DG&ovXVRdyp+*-E$$BG`0vf&1Src z3Dz1&1Y)PF9D@Ysd(Yup4POg{Q+nxi!Ae|`#!;80c^!(KSy73no^F*_8wtr4lGQCX z*UIl@Rokupb+Gx5!RA`|x%(=cfBAoFHg5%l`y(RW{dYSkeeeKiP~SpCUsw}&?c*X? zOq$i8g>XMjs>F}|XUh-tzvVRgFH=)TVOV?e%t0Gc4Xml^U!_F%AB3PBVnuJse7QSj zqH~9!+}j_&5PzP0hyEPVjd}V@97DtzL>n#jv6CJ*2UjpD=+bTKqw%u-dt8U_B`#WT zZgvM;v0R-N$Ns6mixB9JcvsU?%@$KpP zAhdh~AA$F#LzSDqz>&{hV?l&k;W8#U-!7m&OJMw>WAEjFc5Xw1g9hFdIdDE)?(|L* zhk`#(oqviBN(*P_~tKca0+G_^@>xW=qDFvI21~JWsYaH zsl$6x?3@Us@nW?JM(}~xxqIZCL&I^vqB-*GeO2cVS&Id=LhC#z+fG!dYHXB`(E4-D zbB}eyS}n6DT@smD7^-Slt!SUgp_TJ2`P;XyN&uU-jJe4u-0ZdQl<^6vIyp|`g9N@HsBgvqqPuph56#iui zz@@*@FL-%5f(DW^ounL3wK|%1K&Q-G@vj&?j!y#vJQE96+YHPz1}Ni?aBX)zdKe&60Ted|$o~YD z{5&5~){Dvfs{kwOm`Zd97MJX2&D-(vVQ}$EKElfy9~T7t)~-mEzvr6Qxm;9fbNzVj ziJiUi+BX&1q)_Lo(D}K1G|f-)$prcyi-YE{-sTs1XAEZE$w#CA>-%>a?EV*S+z?W_ z_C=uzAkOKi#kGqevZth6n~d8NNIbhHQ3kVSV+|sCk6}Q9YyNh6{IqLWmg2-?w!nCN zi2?_^UWg-KAg&Br>5JrFAuc6I8+jIX7xG>*Ul)%fG@>69h8d>=iLnsZEXTl`_u>Q~ zr=%@AL;@PuVWql}C9%laG2Jv((s^@A=Jl>e{Cxy$3Cn+SbZ2j!@A^N#vD@M=b=otR z|H=m&pSk>(`RBBoUOaR8J^5$vKdij+eNR7g`LFZ$|D1pRP5$|#`P>it-=IF57fOfV zT*B$mCqc03%#155PR7BgJ)EXn79)n5vXIexCAVVb)e?p6cl&rPA%4@K@{jjtTl{u{ zJ8vgJ)J185A5`R3QyHIvQ>b)S);~L*QkKyD>EXO_HWr6Br=}W^gZBdI0?=h9(NY7x z2PS!_VdG)x=H_>2K$@5RxD#-CK}~(SX*mCWq$&L0a!5V2A3FGH=s+CvdB=A4zEO1W zn;Y|8I{4k($G^>ejmECeWD}HUF8`PO{?qyAqc2x;lxHsg?hWzh@8zH0wKM+x-TpI5 z#pG6N5n8X>BUKD|4a$`28xX1XEL{}(38(s7k95B@2HQm7I)LHFmA%(@h5w5r_E`1D zH9+8;B~Y-V`6yKn(8j7CESc?Q*QAd1dwII^So;S(*u3V)is07pR}|6hrz7RlmF!Mq zRVZ6B#E0?Sh9?7G7?(DqWW26fV5pOfFX}?PV3Hy^{yoGp%SS>(Lj3(An18$Rv5qnP z%;i7+gFhV?1$=5(?D!o?K@TToJevRhgZ%S%{3oSO&t%JjXD+`tfBJ?bs!RFj*ZWVM zam%vH^peB)GBzfvg;b2D`4#_gNH$J`=%HV20mzCEd%8OEgy%Im=U1(6P zqbjn9<0v72^AnSz!8A@wM4N-A}?bkPUmU{zU z>g*B+3#oUBTDfueJ(E)5$_SSjwp6a^+I6QyJLcsvB39=|QMGcdh=#EJ7W}-gy1-V& zpmKp|YzG1*;oFL&1WfA~rI$07KQV4%ergxO+Yj-#mFU{^1pjV28%d`}t_@9y(&Jya zIat3JRphtsEF14n_zDD6z8Xd&Hk33LFz27T$jMh?eY;0@Q^cS~uN+F@jHd*f9_TFP z@p+vEhBBiDfOv6sxI<7N1^8aOBrI#k-1HZ6Wh!7|(lh+62yEFbpi;eqDiS(#C05&* z3z;OlQ&Z&w()haU7ufYJC@bxxkU{D?oIkWgSR9ED3qk!;4zH~&jy`O(C@!MD1-;%^ zKr?dpw4&Wm>27F=`qB%V=_ahCXwrvz_HA|o#o#V1MNO1xM|U9U7hNaT>RU4KiGRH0 zL20GvN*I=YeCBb4LR>kE;Um+X$FfZ!3p2Ih;ZYGtI94niZIQ46qduTol$u%`ZW+SJ zQEILB4{E3B$FWruQbDetRXs689iigYe{ByC{i5?g8A7d3Vei_Fdft9U5JB^==V z0tCtJ7)GwvUl-!ekxlEWIKJ z<+bTLpbU@3Wi%2mL?p6%r0%xS(8Bp^RX=8SFZC1$w{3C^sBV0wSK~2l+o`5MSBy2an^)^IpV`CAQYira9 zh>f3Sp(=jV$SxS=JyTREULxkReqqU)@{_<%fX_Hsc=yiUEPwBR`^KlGQRvHj`B}^a zRKok7zI=zhl*@zo6aSHE@KTeCE`SVmH(G_jwU*kp`e|Zqm^o4x15&!hPJ4+*{H?_o5+C(;( z4+jcLnC^T8^O06jDh0-5OP1?VnJarw1PO~nO;5d+6_K(8rQfs-X5V;Mk6!3>HWB;G zaq9@hCDhHuSkX3#GVdp`YLL0}1Ql|c*swg6!SD~T(XgL)QRA+j$6m0r<(cM|pQINH zg~8TAj%kMGgwvGa$>FfARGPl(as@^`Y`u}tbfyHv2n^}jfLHbK2B7_-RJ0;<%PddL z$E@U{ic#Db8ctGKp+>&Ya4l@Pdy`yn{Wh55N9uawR6xPRKz_skUAN>;VcF}#VN6I` z^CgJRSv0++Vq5jQ#$KknN4Hk+7hRQ+(4e#(s`&}X=$rV6AI`kLYPwf!?s#GZz>{-p z=OZ*9lY3CbBAAuf1H?3QW@TAz;Fq3ILoAtwEIYIZ!rx_{#)q{L>QIfO8=})sr9^uW zJE`;e$d{%+!1L! z)uI?EZEa@RQk--g@m!Xw)yK+X@8)}ZP^7_-TFzhWxGw=A-b<*HteJQW$+v;&Pru5d5)ca>2 zsFLr&w&Xh*-k#jOD2ob=rSx&QEd-9Ofc8Z~v-U)k@MA@ou+;^UnA>8knh`D$raet> zgyO(M01#RA{TokGLY9q)JGL~T=VMzDQ;uh+BGXS0!o}iniEL7BLhaEE+C8r%w&3o@ zSOVw~?M*(-pv^UGzCLyirY%>6l!#%4Bw49FwJiDb9K=DU$(l!(FGH{V+003-m?9V1 zamn%RzB-bol|L+jxV>0{SUq;I)-`vu-@~fIihL6LneV#Id8ogrFs6eFT)9Ruw8%T8 z+yxnCQ$3gd?{^ll=8Ez@(=$^2Bv8dn^4T+IOC003D$H|FEYYXD?n1457baJ zX0+c5AW*XlqlGY9gB4vTxy?R2gynQAAIXjX#7^6y9(hHt_hlcrq4&?Pc}-7;@nli! z$8Y`gPWNN5cXPNMR<1GBNvY5iyrXo7NaxUpt8INsHf68LKb$NP>MIPz3kT7}H}Vk# z@BF1qoV;q|H7)Tbv8K^Xb`gQ@5{+%7kiyto13l~3rxmY|(jpFu%nImeD%4hGDFl#s zDX<(m$@HgjikjuM@ycX$i$)|VL$CDPiqdmYciq1oUNk~U96LfS^H zC1e{KS!68RcB`4Sz^;UB&i~XYt}`Os@Uxw2XtKh96H= zQWMpU9g^OLEKLSNHHN2@3(kubSoh`;VOhR!MpEi``r2Oab$lcT9{qS0=iIq*II1Ze zCn(KNGbe~VlU{1K-j^UyL|nVjD~@St5ZaTgYDdGGR9xhpB1tU`hKXTy&}LjV&Wv9C ziqg|mf-~1?WN+0yZ)j&vN}RFkYB+62EJAC22%gDk1*EG6t}>+XDw#YY-udrncwMQl zNhZgsvpp;w#cG`d=x$gqp=7Spp&uw1TBY}~%CKdPBy}{T+${iX2zwK>62g@WFzM(E zCTW#+<3LNpoXd`&iCAWf>jEL6DzteiJvU%u%Yv5#b{!tM>?0_Rb;T~0Uu8gmh(>NJ z#*3?gZw2lp192K0R4ii-P8X}z0#uhAOw{yFVmdra311;>Mp+UCoIqe{tDJw_3HHe-yI+Q@?%bZ!A&h=T)eDxw3)PA^vS|}9Mwg%;sjMIM7e7%z zzXOrHU@XE(z9{xH`a#tOt|mfS#&(pA6S*8baQu+Qwgqm!rq~tmePJG@L+rM-wAy!rhyn(_tx6(L>av%mI_$n_*qBZJpXL>#`z8W2d_9l1pngVy50ejvCJM` zz7OuW#77$E^FLLcxIb?jJ`RN*#R2tEhcHAG(GNitCQvv=s^_xAEK7qX8qmltsPQmV z=CVrNTvE}ZJEvsTYL`=exAwXj1HPh)OQec2sXMf%?`|!H0tG@~&jbx#m90_1buERD zxaU)HFufn02GwUdbr6|=SRzqa(yI2^F+m~7rZK}{N*R;HL`EDl`a~^IMk+R`4n_EM z#!GrwFrYE?hhj*Q2pMT?)dxr%rHX5CLH==m?A(~eOO#zi_+v#?p>mgk%!=i|^`3A!OXdnK1?lZU;xRiw{x{Y;C<-_AaXy%Tz$0WoNuJ zF5g7wPUZ==sNynBgA`*jy?v2dXG5Xkj=i0miQ!@QQg%l{jP+cns?z9plA8{8CTd** zP$m~qShW_M2eC?ZbAyESQ^7h=NDBFsd6Qvwvv9C%O3dsQN?vc)L(iI3qcnRCLcz1@ z3ndc%H}Gzc|Mnf><^SnRZ*b|jJbwN7y)W9;o2pjE;jAybhu1%ckGAb)pQ*{ZH&p)~ ze|+;>z22Ae&fR=8@;Tn~mfyUw==Yj*8H)p$kM_V7!HJ<$*rStu{27tZ3LB6 z@bTw-l*$q~bG==qY?0c$4d<~)VMSHL{;__5hKxaIs9v&hc*D)jt4DP6S!OAa`UDJm z2d?z4Seh$3(TTcgNm#cA(K-c?aMe3F7Jao-lqQdGO0Wr|*?0@XZd(X#Cey!0kv8Ok z`wFP3nC7~~j_JGp8@f%!E=Q4}1ir!)f!X46!`QIr+&|SuFWwJ;1#I&4&{`Hb zEa;{pYhZ80y|}{bn06VA$6ST)BryvaS}-W^geoXRjb$i1g6#ByG|D{WOS0My3%)>T zPVtJv0Z5(YCElEZDz1>*#ZL5CSYwM?z;S7}%p;}TmVsMt?8)2Zx0NipO`2E!di5xp z_myZ9khyOb^m(UriE7rMiUV&slxW<>qJ0xY31AN!FcB+nCY6Tm`XN=)%Kz{k*Q~iD z)J1#Z;#))$n-c1!N0+GPcllTt{Lhfm7rp3)-WOhbWA9F0lQ7VakL}piD@B_M3#vz# zhJNw2yF4AmAj3Vn%Hurw7DiR$n=+CDgcKG>k(_z<1$35S#p$E`O|4Mq96GGq~P>)Kul&w&bDb$~$*)%zs;usHln z`P{%q=>HD3#(VcG7#9{>BraX+U0&)GQy+{R^P~*ui&Fi{S2S1e;8C5 zE{XYU_Z=SBjedW_E^5rb+_|e{+5a*MT_cpy$~8?#rM56TP*Jb3mtt4v^mL_BGELdR zFgnYHgGy$Y;hlTzj$>(U`2yQSxG-JhDvMoj!u!(z5VZ$_Vt%D(<3zGyq9w9~97eD) zjJYZ~RBDdRe;uuyg@6oBM?DoJ))4I`DY%KJZ9P=)Tl=hJD-IV#gA8WYmQz<)zuqll z!=j)|Vk{4KqwH<#)^LNvjjY`AyvkU=-ZnL{oMI2OevKQZwKOCD(xoo?!9nwGY^!Be zj)y7^okyaph#!%bXm4xrb3YWNg9@UgIi4~J|1Cr<(R04{T3ZVuVf}dPkzMKwRVmn- zazf$l`xxS8KAPwU_#`b4Urx03%e*%RQFC8`LHMl~@A3(Y{+^9zTSWcz)0cm?oINWW^q@_bTSRK?ihU7=$t>(o#zF1QN)bgw3bjKxY$i-6Mz@|xJb$MrIPQp&Q zQkzPQVUeafu8B*#rf0A$4$9pi9o1RYFf(Cp>h>KtCY)V)bxfe$h?#k&+h@#Hii@M0 z8?l}HhgOCs)W8W$DEKU<~! z-5d2-MqNY53ayQgj*}LfGtF(*Ur1Y&_E%8L)}*}?n)*#>O7!%1FWu#y+5JNs-)WlK z@`%&rcW1DQqu$frP)SFbS{NYO>vnaHYw*}cu!uty#r^FCI{&F{KNSa7(}|1%9KuGd zlYMMS8FCVeW?^#)sc|Z?9N=mXoT5Ut32Y7y5tFRUbC$OTz48!QCCSJ-U8MYFLEVMU z5o{7*K{Z9Vu?Ltn9t#wMfuJ(TPX2uXry-;wvPz9afuhO7$OsEia823Fm@j}43u(d@ z1@YB6s+hxH8oI%z*J86$+6|*9O~xIIa#7fcTp$%OwZf-4^d&lZ{mXV0k7uW~Y(4VY z>#3`{s_;Z9Kixd%D!kUwST>t{4mQx4nbiz~boK@~v6#X_RZ*rg@#z8%6kHLzsSIf0 zl7qnIH(#rjg|H3Gg7^0uST?~dgYFJKg7=OaQ|jKl@kr+g!?V`9xDK(u!!kk|N*fb3 zH-i%=Pejv&?EBciRb6t3d^%Lxil6u$8b8HJ60J#%HB-3RfU;xUn0yl_eV9_(l12`l zX#TuaM#v`id6Kr!3W-%Q?^leV zVnVmJC`XEdW-u}`jrzDUK$P3a!2+B;zJ%`7^rM!?$UkMmA`~a01-8yr_sVA2H`DP} zTWwlbtN8$3-pDbl^u>|+Du66TeLegoOl`9k>uK^V#tNZj#o`7+=9o5=s&uj!@oyJ4 zJ0)Q7iV3A>5sGV1X(P2svQ0p+)}fG7AQFD&m?91^R;P2pIOItAED&rcLW9gR$aWO* z{~|g*&PNkHI=-v7hrjn!;&>LsW00t*WFsL;(b>m<4P)GZvKo)a?yu=Jn!li z&&Q>vK0#3!T=3r6z_N+KmtpRsd<5@5JX{(4BSVt)5gF&~kgaoefUq+bxy---x1R+ z|4!76InuhaoXIUxzz=5PfLY8Eq6Y&K8BSYo60+PikND)NmHEcLsFsfwjaRhfkwZhxM4n`aWPS{5#{l;X`_`(gS7H{nm}(V5FeQ6`L1)a{8H@2^$=b`fFfWm` zuzCR@`sQ_PNzyWl?Og+%PVP845j@?|q^sOq?%OPz9A;-Zsr|Nzoi{uxq{@5S4zc5i ztDE=;sq;K%H}LchE*AOb*Yv-0xC<6T1d^E}T@ks}8 z#Nw|hI2(SAH+~K?{ag5GucGxzKHpQAH5X@yRYQlQv7IPMOVP7tdElg^i8OA}4y|I|APVPIuG+!3 z6wq+$mjaBiuiQA;tj!9P(K3=i24&8+a>2t_9X;tJ0zEA%tc0+A4+eI>S>5mZ`EDp>+iC%>%HoFNk1y3|)2 z#~9P>n*_b$jogZ`&LKnCd^z|j0U$P1`^;!tyh?U9sU)mXuTq(j--vIc&P z-9=>e5kr3i`HI-Z5az5`ZTw{@{79t`Nu7kcV!w?Md|2bS5FZ1Y`nHherRLok0z!_8 z^3(E)=Y!i-QdAp*%SJ5tD>Bwd^cp(dvGN=`$Sunf&N-<9_QWjUTE-oyOgYI!RQN1w zSOwYgDh)39n7@c~yh8Y|LjYnF@bhKh=0-kZ>3;@(EK#1h7Kws1`P=v1*i#Bi5=B4$ z^|4*tl<8dFkvN~AsP`-Q2(|x?&n^6&;CT}qMIv>RY&!j?9m+HVkF#-^+6=$IclCL@&JeeS{9gi*5| z%0CWbIT4*IH3WGJtSp*5QmG8N*~B)7&mjuZlH`!WZ-=S^0G;oHN%WiV3K{68<0voW zZ$>Rg4!J}0$~(IQH)*ve9SJ!Y_cGc#js!mxEM#|jkw0-UsO%FwMP zq*q_$T_MvDc9EkMG7I%qVGw4RcstDxn5zAtrE2jPOR-0<_xn)EKjkAT6CGM}-Mq2T zW@M?K1UTbcO2jSt;1b(dLOcy!0BoCly!r8b*lC@9cZjwa6`2ESgMcJ^yzP!T4RVt} z>{~?(bk4zrN#tqtP0=M*G{NVy_=p$YbUaz``s;hWH@x@8-VeR!CB7ds!3ZOK*o6zle{<|NWCywXb#=()p$qD@w|$ zd@Dl-M#(TSu(d_UH%O=Gvx!v|X$VdWDa(}IRM3jFgREy39c2vZj)hx}pTM|{jSbZ{ zRGl0um6u+zwn4GN5E_~+NwnMkMAJ3ENr3p|;8DE0w54i^lUXLihxatkK_Z%~o}lv` zC0<4|ZkV=%xu>w=>EC_qu}Ajqr!Fz7p|+mr{;9nc6sCxqPqrLZv#+{UySE7w_Y7$6 zOm<+9q}nc^Eh?>h-$bLu!n1AIi7Uk?4M0b74$ z<6B$wb@>hdL6oOoMZ)UxV+C-CJy&BSFo~~J455NQ=~g%qhuVd9GCpeSXm5(RyYyCK zq0=B_R##yZD}*rM0^S{@ycsJ95+N{FWN{GWE{V!SNrRrIp*?A&*pcuohtZ0F$lE)b zd;^VMn?4%ji{~Jiwey^4RU$L8435>dn?E!cWL47w1`#PYVG&a=0g~v~3sW3%v9nH1 zuvzY2Zc)qPOI2n1y!EORsHN%Q=0Y`LFd2tUu~iB0%|^=r4l7pMFw}s|4qa6`%SmKq zs@a2-&xl5Kb{B>zsL+vaf=6Lf#9rI}LRA&3+!kpWPgZ5xwQ5-&H)iXjktTksX?;l@sE3@ve%K zw45_Ml7`o_c<(syoW>1UIe7$=(*)O-GDxsz|bmkn2o&tHZ4#lusA_=X4Md0Na zD{fp3>IN-#ws%Qt5BZ7n2b%v+nEFTfh^b%qzFoe}xj(V-Q07qEw3MwKHu9c1Q&Orr zNVzt^=|Saf`|i*) z?bb#!@a8NDH*2vH6qm3#$|i3YBvf_hKjb4qc;)-6r*!`{)(th6DvC`GNv}k!)3Rk| zxl@gVTp} z&g4Mz-s#=3L}3`Ms(!x6tzO~~CSgX`Wfomm{SHD5ZD+t_Nd%hj_O{dS0sFVm&plB=9Y%cw=v z3JWz}OWSCLSX^}#d;y@K-be5=z%KxMLUR_3QF=Cf(4g1uTT7CwiG_?~`pxxsKe{`% zq>bwm&&_2QmCnYstI)x6%Z&HUwM!;cR9$o%LO9A!HH`~QW01Prrs36$GD~8bb&d{` zsnh$Wm)XU2^4(vy1#qkDs}~o}DDoU6@WtSWbf{QxeM$DU#t3VIsTB5;jit&?g+v$E z&a16jkqlyzGcdq*j3V3bK0aa+*_0VWd1pH>gZVef;;hBV_19`6R$I;pRJCT6-D&<_ zB#M5)1%St@Aeo&Pe?tT{CNtu&u~nT*QwLv#BH6c(TWUa>sRWWER-j;C5L?i&peq5Q zCQ0PJO+Y-4#8MD813$H#81s)lcqyDt=Y89I z0YjU@J7`k~Q6RdC=jMIPMNX!dkoJ7OPnUGAyMb<3Wt1_<-re{sveHa|MV91ChU@6* zy$Y%{wXUX;TJ$th>=PfwLcUX7^*R9t5(~{jiy_RxLv&|u75}j2rqWLuhqt4LCwJ$N<3c6N3?RX0nD`l^kLh`TWC-7 zh#YLe{!u=M`TH#oZ+zj4uGxbG{1!gF-Zih<&~nOK*YvJ=<;G{n7f-{VJ=wKXFJaw3 zI^9gHNk4J*i8blXzw!bmKFY*LPqZf1q~na3Te01WHR(rQ$i(AJd}w^CS+OSlg{x1j zNniG>FJQ%oH1W|B&BU7YAR}I7#hUcpFJ$5=Ccbr&qZ<5a;?kr)di9Al>EFGOiBB`( z=_ALQi8bkMAKP}t>O!Nik0yNwk0n*#dNr!Pd6=rlclU2$y|+#tZ06RyAHDkAn)m;E zA#)!pbKgm69*fz@NAte!*Q)9AvKC*Wt@?c)c`F~)z-#Vz@Mx}m&(+pG*f5KmqqElT zp(RP2xn7St%4YqV|D$}ip|&&3-RGmZcYM6gf8W*SzkBoik8;UZe`J=Nd|c8&4B^oD zL<5PSxPvLSf#hMJP{v45yt9Gi^;d)B)tezXOlyf=@6_Se*6PoyT6l%5^oSSReK)@=gBXC2N=Wvc;~`C}>1DAe zU%p2V_KGniLmaQcwef>nCpla!A(Q=tvG1IwvK5_XYFzA|{b(?`diU{>%_3M1GmVQe z`a3v#_L7(P-tz93`}!Ao4*Kzy&s5Jr{b8q>ONSIC7X;$h@kz(w^#-%4?n=j@yu}M2 z;j_d?MKGpHd~Uj}28+X@#q0I)$N z+L{3>Cu{($sTOIce#d$>`OKxWJ`@rIW{5d*2Hd}<`&paO!+Ox+jPckq-J{MUb_+0)pPyt z(Gy%FJT#dAxky?ECqT)Nx_5E7P#4QJ-#K!jwNT5?aN|=kTsB6>u#Z%H7WCh`kBf`L zF_I@k=|3&MPfXu#S4jr|MlhiXfho2z{3Zt%#^_b7a{I^&-CK?c#GwWaSOTy=!Au&HoQrAeR1zubk&3Qh8{dm=8t9$7MraM&jMzduRP}f1_ z!OHMNtXu}!%wC?2h>~P@nOR}Kt4&&y5v|Fab0w`=^2_HmO9T+yHJZg_?xkc@wQgS) zsbh$W<15i->}%yNgkPeDP7^`PHA=asKlt+e{wwIpNl1Z1QEDJi!f=!bMB5L-rj4Lx z1m>$qD?NXBCXSIPP8T(x6ohdB0RtWl`I*V10topp?u+NbZyz9;oS$7;mx?*Ke|3#` z3xc-9`)W9G@Fn@qmnsS*O1_qlh)9HgiwJbnA^vg@Tx9l9KIWenqhF<4r>?!pC&k4- z`f+h* z_T5IP9t(oFw(GD6cGW{LItM@$OhKg_7{S>9sHl7y*wOcy{@1MQ1`liw*gQ|QHFY~6 z$YI{pz<}17EOqPJx>!S~Fku&Jj*;iyLs@osrt+NANO8cO9CPUB}8RdTa2fb`=evDPeaGi8S_b%Fq z{R6-4XliZDk3Yl$!qA3Er9?09xXESF5(N731Jq(BQ%|w0s=SdB*24d&EdJFSU%0iA ztv7i1C$MDS$)}7bAZR>ut*zPcATGyc5Ji7NVEkX`cOec*_j8=uX;Y$W#ygBBVLa%O znWmJ0>{!GWgd87UKm@pF^)zFeJnP{MQvL24an>?RT%&3Phw?)pX9QQa4)TEJ$=EV3 z4e1{`e(HAH)gr#mi!Qg+8-%z?!jBS07r7IyUs*+_=lNU~R170^yj)#hBwUXAAY|{} zh)b%qX&9nO_hs)|~)yxt@_E>#+@+q#@^|`Xd9^^_26l7%cPTp4b}M zQ7_}8{rx1{Z~AyO`zIyw#C13I?&C+%hkpE_7gzc?EfJ)~P0Ig>Zl2|ndZNEIf%p}k z{$W1P@F^oEPwg7!siKfG6(Ry*wF~rJKHE3B3`BA|-liLj5J!4q5LbcZw+N4M-Fi{f zht8QOa36$YW)khem`V#FHI>(p;FhMX-Bh%+KVj*{&Hih9=9|CXb(8a7!i|3XS1+r$ z2@r}B71Cbwl8T&dlxV!er2jp@oAV{1uZ9>Tt;}NSxXwr?Qqo?LuF{F|7I#W-V3To1C%q!+w0d}LRaADeCZJR0 zq~2|E*V;NmtOJUny{r~PEHEO#PU##(=$JKKql6oYlDz2aX>~wX#pxH+H49}S6OODq z)%{^x$HBuv?7K8IAT^RX-LTi_50IH*rlnR9`_=*xPTGO`d`b8O&Ci#Lv20;Nc zl0=_PnVE|Lvc-a?@NoQA4@hxqmAkkEg(yX8*rAmQHMw3K3U*LD3gMfOMd`PQDz~*d zMYo@KLo&=05VHh{808SJ=}-N5^5vCb;=&%{^OhzW|IIv*b2QQfMOy+odBcycI9G^R z<9&>eecGEhezfJ%)(mi7UL&(Qm?aGXq^Jmq269G-vbt!8C>sv{&DDdDoASRlFqO9c zX+CnN1?iJFUf+8IfA0^{oz6U4`iOH>vl{y)LFw}?bG@aZ@7<@ESp6*DtB#mX4Z#k* zh4Li(hcmFCwi)w1+Lm_44L6hlbi;FdLTx!VOhPS03Hd2^=ZEEPQnei(6|ctOhp6~c z2dn--9*zzc`yX+cvTOWOzb&sIlP>zcANtO(d9 zPWe=*R~4=BfKi^O;{95m(?SV?#PlP4v=;5cKVn@L)4xjH?RX`-nYE`g;<@z{868gWX(G?nBLhI}m_q3UJ#Rr&p;L(1(6Bk=N;{}UVheZM;-}EtZu$AzFaPZ58L%bs9a@q^Y9{=_vZG#z8qFEc0T8X=$m;qb_kAiVf_-U2+-vIm8kZaCNz9bK*_3Ok<)8|O z3#4mYS$>9Xo~{+fu3oFKr|iCT9Mx+oS22ZqRhVcU3^63KYj$JgAMw3ZV5%Q z^73s5&gwhXV7NK1u4Bdp_Y$U-6#T)8Q z8S2Uf%6MSz)$&!Ig|;w=63^=JU!J=hDtTk3veSPEA^#X3N$2-JP@`I3-XZ2;xhsdm zr-v6+I|tX44K?EK?H@jQf|Sc?{&V7?V|)4_eNX?`sc8}m{P%$Wd(i*A6Vv+SalYrj ze9wRRp8wuyVi1_LYVXguWsFTY2h6YqgI|q)$!AA*#79(uE)xq0lrZnudAswYA-P>P4trzXr~YaFo5_~kEJXt#9Q;LveFlPi#Xe@TQ8i9xgBcN;D)@nYI1=L~ajA*Tcsu>2idntsD%87pZ|0o3L2#e5{BfBwyBKfh|@vx@a0Qvz4>P&!hjZ)qvc#p}|Q0^Lo9 zw(9Y_qLou8TP!zas{p|QPPf%58v*detFhq$ABOId7reI>3%AhDJwHv_zo^ zsOsoUQVa2Aw1wTKOPvpjjTZ==p5HBD>+^kSL?K;tgu!pc>49Q&KzlqRO&M7hCQvq@ zl9u(0U}%w+k{$YL>vdF|m{LTrjY(o2(W^m9bck)E)N(S6Gy~xCNrktOQ?ovrQEG}o zH9IOihSwgS(5R~$IK9YhrU`JsvI=k^iDFsO8+oE6`bzi4fmhN5u$B# zX7v=YfFL6xi-Q{eGTFvg6LNpK`s6OvWt-LADa>;Wah+55GL> zeoLPu)E-A_T*cW{-KNuw>{WjVU#4+Q!j80<*=(#8PTK5z!j;pkY?X!_3U}ZHvDY?4 z6?yQ@e4s>&Kj-`G6$lWfLL-ZGIrei7tTS_SM}{Cs2@Q_Q?1LxH&a9!B2mEpH0Dlp$ z43^f;a$lWUAq^D0oxC2?oTVvVdm8mP=;Ft3TID;#g4ZpTpO%Dg|3Lne)31>Qi61#< z>GkhyOhp7x?=+v+@e$d*n~#0GkAD`$yDz?}_YYq31-%J=l#iz$|NVzLKHlLi4;|mf z3pex8oFC(3Z}`<4!wq7$ewp_qHvb86Qcd{jlh^mYlJENO-*}`I=YFn^+r~A$%ij>4 zWH;zs7G=FHe=9eRa}xxseRf4M!dE!eL(%Ty5h_o#abkV)QSWAxQi}F!rIQ?ct>#m> z*!&s&bY!YDBMf2)%-y9qUAm4(HtfpjwolP&=uY!jflFQqASt_@V&ug7*-OV3X8F>7 zqHeOx7Re$y!JNMoPM=+0AVO{fMwGA6yCMP^c@#5uCc_O=qD>uqvLOZbFxH`K5h|KO zzvK}=xwYrqMkWP%uf5x+>aoDpUbzyUJw8Eug=TKP_*MvEmXC<&@u#a){8*8SF5by~ zwNyMJa?z6_y*y(Ze2vw{;e;sVz`j?EF#s0dlv!ho#Bua?fPs2bk$~*SArto|8ce;# zNy&lgz8pu#4kNKEn_F2qE(+k%iIp_q9ptH&A_>hB+0oyaRu>}RB4l)jV3NpWF!q+W zM8g*T!rv?}f*@pW_^dFGWqh*=2dHggd1(e&aAc8t-zkz|zJtw#Top(|77Q7;1r}-O zs#~v6RzQJ2ebGt66LVZgwDhFHxf`Xi?q{#az3 zj>wm0k|^bQVEyb_7EUNPzC{zMN$8TBI?;c3i8G_Ro&eH0_Sk5g_2j;g^Ggc=3$_G$ z*_pHAv#diBK0%7&(lb6jUu#2Tz!uXL@Q*_LGYr(?6S+Mcp+ecU9uNc3a}~`8$tTx4{gV;y+guDhJ?Xw50Lb>IV|10%zAV;sB%S7BEBs zwvx_HMC*!T)tPeG92-*(3U<%>$;On7;uLKAAsDezRlrJ+zgY26pTT8(!5eca5VPh* zTr<&bM8dyM=Jnr7!f`>n5B0FOh3n|)OGH+UVET8B9k@$fCmp1e;Hnl&-OBQo;#gJo zD4jSf!;OZoXU?xLxjTg%gjP2RlZAZcE9&}H{|KxxABj+ zrkAJNVr?86-=1)61yocFZ@_^frfk^OPA(iuU!j4?oI0Rc-6h2rgzJp9D*8&?kSEbL zE$R^#o(EWa)cY{V5X6+5o%C?OVRLV3bg-u=Gb~_sh0)7*#WxYG*+8rG*RY;%S)*HR-?qfC@I`oYE$( z_s7>#e}UXO^v&0oqyE2yi3EzH7P+B71;6>l+Sb`o85*JsSx(WbGfj&P`wwae^XTLu%Iwjq&u^^&omLuygKsHN8 z%N`bFo1wuUB&6paXbYPD9sSgiZ zPm@G3f)LYd1cK#fj)pZOZVuKPjE6A!? z{XhtqJdeU0Ga{tTiUye#Wf|CeBJ42`HUxt;U`tcJ%uw;NNn>DUim>2?hg3^W`d{g% zP^xf)j_$X6G3)J)P7A+UKMh8VZVaj|TDUvl%zYV(BFQ3Gkk!QpV>I!8ZlPQk&3wmg z6C}N#r;g`^g|XXCP2)SAA5L(_(1L!0E?k&sMa4ptClNI{{1S(bjv<7n@$)#7jsX`u z!1<*lIs>a#1!9R{zE~?xCIwq!**NkSixOvL*c3ASGT&hB_9gKcgrZT{@k0a~hi#8%@FVpe5*kI(giQ9Ao0{|bB zA?wb^R-MHdby8R`2p0(db*5K4lqCV>*vP2;)=OF`h`e*c=4EeNUz90FjsCPszv0d4xcM%=kh4aA3bX59-JGGZ zL)5#KG*(?-%{7@ON4nvqn1p((;@lR98fqY`ywfA6}tv5w`l)&6m?k z-|AP_kG}7anJbK2uYM=Z<9=iIr#=tnXWjgJ+P8D9Er#k>*V*@ci*jV8yhC<6V!>06 zZnAHDN|Ezf3Yc$k$g6{>1rh4&nvWzf|GEL@P=40U_tRd>^%PBj=sJP<#OlCEvh7sJ zYu_0qYmWDi%s>uKa^Rp;LF7{=zAAEMjYisg*`26IYJKqnx4`;invwXy2JXZ8SvN0y z^zy;?aeam+n01|S|85I6QL&1`l6NLGG{OvBl&Yzdhvi7>7LpW84pl{JNK-G3h2EJ> z%6prvRYA)D1m8~rj|cFl%>ceFKkMe?$1WfI64yUS6Y#oT$7e(l2OKT(DIN9g2vBPx zAyo#R)q{r_N0XsIwUw7q2Tb-~EuDs%pmR>FoP&^7H$=oSM|sdu$mYnZB6ZDWXv;`7 zknaa@kMw&1S+uS{g;VseZhnRKVyAVdc1_{c+bzps z`@+H;&Pn+vL|LM-Mh2^^Z~XWQupCWTbn|XnfkklW`r%+f_{yaQJZCVAt%cKb*+!xr zo6t$PGvrYycu`mHr5P`+GQw8v;D6lw@3f+f2o7B*EInmpmsx=32NblMTlbBl4+P=r zNRiUHI$r;Y>|AR^6QMsfo!_K!|TQQjS43%iUEB`&4uR0!pHw^J%0zYCx|il1plq3lqNLO1`0 zChbNfrV;2mNAdpEM&Z~~NHMQ!O*5%=LxN&wwG72S>nJK{X%PE-?s->vV<9(0AZRxJ zMuK}^11<$346bgzlBU&hElr^6I>G&p7TlHy@}SXEGh_IOtKnYJ+Y+L>_*t4!_Rks^ zZqLuU`HWAo@ri4xKk8rC3B&idFtC#e>n&CR{(gcZs!|uTx+GF57QHf<4T9A70s1m< zzt8~pqWr9z|Ar>yP0|FIt`oQqwZOGh+3wNla`wcp8_R4tRXVdnHT=Yod3a=g@&YGx z${_8QwqUyS(+PCTS440kLft$_D||%&>pFp6m9IRS>D0SlNi)XYl^v>6DKq+4H~%Y5 z85ok)>R8v=`8!s_*wW6GW6)c!$4;y0^|Ryz)Mi`h9Jsp`M>#P`n4&?DK)Cud3FG@4 z7+;c~b@L}_ui^R_P4MYDVf;u7V@vj)y8;x)?uI{7GLjR~N4%}i*;iDm5L*#L%BNM0 zu-moOQA7i^fchPpG5d-JYQ_A8S>3$)vz?}Hh`COvdzyL+b)P3z@SCpn<_X^9uD{%L zE#aW9b@TDRdHLW2T)&kjFm%0+&*&;+JzZr&j;*={73Q1UT(x1X^SWGM*PCuxzAINo z>0DxaK9YVoJ%vhH+@_NIu51a;S=Jn*BOjfg_H#nC=%uZGNkZK5dAvO&%z8`Uvi~i{MlgNQRia10*9)A9r-Ayh(4!L}q!D z?Q`r{c0`qWI$^FvA+^syW6c$^pChg;FSpDSrZwtavoxsVU%T73bCg}m@lqq}mw(46uzJrX@`-*l;#ESIMTlg=b z8qZIIE5Y$+v}J%!C7`-Fb!i8vfYkN>+kgs=AEaLcDy(TD^*5cLb@Pif`I$t@0#err z=-w8i6X1$zi@cO%0c=4fy)H1?AV{yh>ay(jRZYJq^0RKfgeFNeMpK`<&VFCi_KN|l zBkw&`bRQ86bEs6n>;{IhHvs7UZ=!kN|DpllWPaAo%O7*aKu#YiiRxU}3BdbW0QQhZ z8~aP+TwE(V((&Mbm~G~(c)3YvNvqmkfyzW#iCT>m1+c?BR(7X02$JW}vqAlr4XCH` zvu=KXCOk^!2uxiksK2r@s7kwVx!vw8pRX2NDvb~;=mLm>P=5mG%QQpW^6D9Gin?_3 zIgh=f@@he=>x8(+t3_3_=g%CYA7~0%a{Z+0OE1tjbE~)C!Q0&Z4`-L^_Ky6lo1dcz zfpRdbYh7peuWu37Du;v(f)lgD1*yjjGwlTc&vs|ABqA2|B1zmL~S2#Z?YQfjM8l?b#0C?z~ULqp74K8i*&Ndz@ENNb`=GmH>N_+}hVR&-AxJn2EFJE(w!U%1|q z*izcFj)`$j*NV6cTk-{|7nk|T-R9liI(#Km_K&%_BuqJfaiaT6Z+7GLT8Be55!KDm3 zyAAGU^iI&|u%_6dC8tk5gG30E9uEbvj<$tp}*xu9Vq$og%%s$YxH!|AV@Y#Wn`!8C2pX z!=jAh6NF7~*!&zSVzKMGR#vOd1#%IXWN*P@(@YrnR{VO$RCj=@*nvW?$LO{=!RpIu zJtQHj0H3P7Jdq>&=Vaef+b#y5IYH1!)4}q$MUTaNPV^K1&~7gXt!mydR@q@7Fbrv4 zDo0Avs&=Z#`5CiHZfP6@81mHV3jjTWb43|{-o4FrCbfrA9*{-KhDKME&D@d0FgP1j z5agU$f-}#|fL~j9g`7M%oO3B_(Mr{TJ>|}!z=w{P{QXPV6HXJ8d;e8el$3<&CLg@! zC&lK7;BYT_@y6jxUi>W^hu2)A?TlV{@JKD$<5c3-mZic9ox-OLZ+WP30V^`42_1)u zvipYNkwXWV!zfCV=?RHp==ju~bYyOrV>6%l-lVh0i< zjuf=qW~hn2e%m-gVf^r|<^`~29*0Ne=$#%;X-xK}iw(0N69bOg``|5a`zzXBEzx}= z1#Os>^E9!dmp=ZA*wlJ`lnFn4=_gh=!>B#G=!W5e{rj97#C}f!Ix<5>$*cl{%m{>s zet2wt?oN2S6NNRIb*Gf=^9??I=P1VA0u+F9B5Iu)#%vF%;16j|Q{V2N+5A57Hyy4J}5M+mM z7&q|8iS6n@H?8$>xD@8At)}upJ)0(g4n3g)HT!U#skEff(tGn6^VZ6;^=`$OG!PBq zSGspU>_Di7Z0xPuy;-F1B)WB=a#??3TJ1mzb~T8#H`Rf3*nQkUBI#lO1f+f|bxKO$ zlWF?yX1>$=hr`8<6)}<1_>lB7WbwXzCl?wnQJ~S==2wCy_L2-f&0F71QxE@Wdn#~G zUpx>#i#pk2I?MY(LezCO*=Xuojb+`kPW9u^#Ju{nPZ|m)AcfkbM^$iNCm}mL1Scq) zPqtj=jqj{G?dM3URuNU%P-En1ktOJ$*(dQ|AeN=xOrDy8rOu} zu1jNQqYpwTLFm?ur5)nryE76tBd!Mse1at=pok;;HH_m9QV` z5z49GOFst5Iy|7GB8f zT1Smzl`JE*DZ-1wkq~klk0}d6I+C~B#2)x=MREANRHwT8!3b35R;lMuz(ip_cR>f1 zitNrz&!5yQ&(2IM-MpMq)9?aK@H~^Z+9aQ2g7?xi!GE;-iUD@;VEC*>wr;WN1~JJG zUi22ca$=U0L2s=e4kDf8$r1@3r}A>xq@tJ2U#gvln3O;PoHPs4&R<32sJf4bYS;z^w6<8v_Ja* z`xKC!yB$m3P_i0XH+o10Ku`U$9=$Mq(vG0eafHeiZ3rq=L*ZxPVWvin=D~rI{AEL& z8vGOXu8shOUgeq9;D4%n?k^hJM5X8}VuyCzOnHcX=wL@TU(e~H6BLoy^+I+G9p1Nl z<8VU%Ikx|;qoafrJ+UvPw9?Bs?{wbZhLl`m6wPGJ!D^qso^?AvG>z{tKjh<835 za42rT)mIf!69bplyq0Qpt7%z6$IV@@otBk)jl5b{&;;2lXsJDYZH2CAPj+MRU?EDq z^v56O#s8LPf10M3-gR>&$`>yoN_>a%oLrcUJuljD>intdi!!?tiFH=ng?SnL@<8Ou zYTqDt!g5bmxL|tqK9xry^O@4)CSfwr8Yj6rR1acTBZ~DjjQzcX|V z`vbSsp?_uh(ECG$VR}`V`Nz75p{A&EfUq5F1F(Ns>%ME_Ly@DF5M->g(H}=7jug}} ztuz^RIKM`F=lR(q1L4J@X2(O*g(sol#HIyHKp8?>+$A;EGEIw}9e~PT=)u+CQ26^2 zxLFN{p9Y8TrU`%VJ5X_WV2M&Op{^VKkpV?AJ&r}j8=7*g)KI$QGK!r5DaZ9Sa30)` zdc0|;_Y&qdT^RE*o^5M%HI}-#IX`tDur`&kn42$~X2aQRqD4JJT`4OJmtX9m%89SjR5UdV58L#g-h{@{&~r{fIQ!60D8G4QfVt z$EisI{I*$qrhuXcoMEgy`UMNsdf<1u_9;;oz68^Q1S>oo;E?g>#`q0BX2q_@+D4ACt6(A2XWWvaCU5Rkpjw+0GtDHqZNsH;BVx++pn zo&IpwNheM{zQKTFa$N9PfPAKVE($)^xgHLOdpK9;(1{bH*mv^kd@W7yeax#W?$-4O zz;BMaB*?_%!kzkA0ZH9#?0&Wkr1G3zJ@GKlDV(*3rguHiJ@=+Q7!xsju9d1ObU7y! zYn>eMXy#H1Q)Gt!1Nss?|D)?`wBMIZ8CbKoF3KO;%!2^@hxb-oEgST6c)2>z%U{$z zx1Fsh5qfu0y%M-_v&7^fysmjAUrEo+uyKAv(hg3XVXz!L?cBT1i@93q$Bp z5#3!h!SdFwpNQaAl60Bc;pi4B%TdkFo8NJBH#3T-SG${4JHg%@pQPzk%ywhdxQ%H1 zT6)rGKHl~8)c%M@H@lDa&}M=4=iLvMp{kIuUiy^#Dyo(}SB9#0y{da|$xxS)aGwh) z-jiHNv@QMqVdzlMyr=7F8AOn*cq~gxiNB*F=dC zsjL)>Og;(CLH3p`5$TfK!IUQsnzb}o!(rWao@PPxto!SzI#09tC@k3|1>+yRO=)yl zRDg(+c(PQsjZv&xHA~N)=d{(RVvYLeen~wbUtp49J&&egy|(Kt(!cA_P$gI}u4tu- z5P1m3Hy?8Wf^YWQix{1m)h_*v+cZmEBl(%GgJn1_b0HwU@G#F!0qnQZ^seW=wqobI z`*XhCn{R~)RXee~H2Wo_j4J_}HGt9rjg1BQ|B-A~+Hjll{_Qlu@nc;d_xAgU@)+=x zj{IRk!Y%9p4#TIrW22@+KAHk%83yG5TGxw4@wo4)BU#c58Z^!?s_yW&V!UY6niX2$zWnjpEmd+z4`m}38ld$tZgMxqIw#X+q!Nd@2xIx z!r*Z#^T=L_(J7VhlA1T%ONSWwqt^wmY3WHozp{JdGDJw64r%K3Bi(a1_3;ip?Rq(; z!jY4+qqNO?H}c;td*RckCqYrcraJnw?j_4Q(-1GGsiUX8p0414L@X0eK(bQ)6yvnh3XqYmLU4}?p@F8BdhnWYpDCnAI5%YF=bu^ z%6;7{mLZ^+b?WA~y62XIy&Xj=>Pj|rUDme(2puh`zrXI@wXDxV3-0fY6;&(KLg7Jr z;TyV#m+)YGx~2Y<Tk7<5^|J zVxyLrQbG%a!E3EXBq91F8l+bP*bN&y3sWO`(VObfmoODcsH|voj%rsavm`N!7^x~P zE>f%6)hQA8z-|pVIY<4C3 zpH5T1U-#xZ#3-m(`-WBz-*DrRT?e-x*fqN4#<;`!armvvLvv$}X! z_uLXpc5zFS#caZBg>g+4sb4!e6)QE+JvCS=-0zKWR^W3Flgy&}+^>bXYSr3#_ zpue3o!SjL-R)l;{e{iN|>k6zLJ6dAim@pDBNWOFeI5e6}NNTTEO9iDHlm_&nuA?P` zijGDtk;j#AiWk1~nA#$=?~JsNx#6?B_v;`h$#})b;>;1n9yFk*ey9%WSbtE_%T9tt z^K*BrI5fUfHoqWFkladKJIn4>K6*@zLMZ5O>fX9+`ji4L7lC?yd-vRYzjx@x@%kHM zn4_D=o{e?3ar^P(8z=5QyOCn(*Ic*p=(&>{cOBZzr+NRwLR3b|(`bU=i{0C|tn&68 z-somt-YjFmrLhM3%4ABd>4F=~P`?SFi zJ@)Gchzmocn_FLdtQ#ah<6&la*QR626uAU1zWXL1|? z%Lu~`(l6&mZQi==TGu;Mjb30w$ys;c^X_Oq|udk@@c*2d444I*vGkbj|A?_?;qE(CK}}*Ldzkb|XP``E zpy+BG2&yN@TXg_RGoWkw5GB}c;4i2{pbQb+7tm1OjtpbwFTCXVr6Zd7DRsmVG_;-p zl%xe9yPo2p`3f!DsP2d6>RJ%IUcXQ>b^99<7zjzoe{$Uo{8|*)->lG-r@A2XGs;UH z$NdEBGTux2D6{*Ypw9Qx#DKo*Q&$XN>Vx5M@l2%MOObPRH7DNJatB(DircKT!(Xmu zC;76{p=0t?k%Or@blB2tr#h;$oy5eIN~D3+`AJMB&nT*P6V@yT6KMGOd83MZ|Hbf< zCcLa^%(oH)#XBUv1)-IyRv8w|pj($%GksCs{hV(>*n+N98-=QsO0L~gOluI5t32!J zb_8{#;NI1DE2CvKJF6w)6P{qN&AGFgM+H|?6$Np+lDQ*a89u4=$4@EgX#123@2Y;N zgKESy9Ya;I)Pn8G2ad}1*t6Rq;%ydvdnGp#WUT|QNV!TwfOkr~YLFXK4c2p>x zzy%oiJv?WQ4R(y&iyj^U&nAOOb+Vl~Pf9x|-hh1lACR<|@4G*Jg^Mc;H!c3kBMQ*n&+{p^(9k(m@Lj^3#aRJ9C&l=krPDsrfh%K|~QE14(9`$6AzVi&z zTM#l2PVmmcvD_^Z4@7vJK}`PT;c@A$Oa*(cBjGNkbU;vKI4{0c8Yet~jZ{!O`O`2^ z(YOd++ucai&1ov9?8=ie_yp~HY2pF1pSi-m=;7|grx{TXpQ-9h)a_Gquqok7L^mAO zF<{h|JvOa_%bx2q8eJ|B{zBsC9!@#AO$xG~JIU1ruHw-J>K9E*A&EzMG5+WvlOn!;mw!gsnD$GH{$xK$6Wyuk4%p1~^+W&3cXsyD@ z?)RM|M#Fp+Ls)I7&%cZ#6KNXeZ(MR^Nx8eGvoxGvm3bIxw{XH6dIL(2&jK(Fwsa{8 zFsWnUcxVQvRH1O_Gzjnb3Iv=z1+$aWQ&9`6 zdn~FEN$9)-C{;uPPOL-&3wBFpPn?8|fTt9uR#jJI9G0nN%9)_k@M{jqVpWWs(E(!G zV=$@mVaj_~7;$!*h)sE82VBCYJZSOwZPz|i>&WM%ade|G)ULT0F<)zHgmxJTX%)&n zrEwZg;-X4+h&^G{)EMsGd@c9k=Ix5YAa(S_>J+RE_kc>#XiSyRJVDY^PtT!CM-6D9 z3M>*f0_a)HUw(mtahhuFbY;@?JEB^6gkvw%Eq)D{^mx0_UX$MupAh% zGYt7xU}(}$l7dCQavTUyC9r^gsp1U8ijnrq@LT7EA-H6+o#g>GQikEK_cA~RsVc$n zIo#GrJn;|je?#OP!_L+|J{<#7PkcBveflgk2QuI+nw+H=b~$i_wd-%YrY^SC`6G{k_3~7}Oc|yHuK6smXd72zWQ?P}h=gL}j zdgL972&XIwi>z9uhNvXGVYEkjmzE34ziO_C32FWtDGz)H;B7&{T5wuyXvRgsvUOE8 z6iqt6aL8H@v+6jbfKM_}I2H+3fsuLIy@YaOrlD1vDs)3O2KUbC>9ed#Y04ss9MR0V zbEqv8d2bew7SC1|#%c@b=Ml_rq-pW|QMZ7;eu;3Nm^sVYdZ>`|hqajqA(U1CDyn1x z>>-^9sevcpOzac|j5}*_?oP2%odh?fFq_F;*iA|a@``Cjy}?Z&Rw|`R>xi*3w7&=3 ze^1k}{@`osn*X_#KxXdbSQIfR)Ga@Y5Z3evhYP$($u$p6d5iVnc^&)k} zyMmgD)nu)G-0Bm%4s~3Z5{9n5ykhC^mSE|qn7PnVw55%>Z8+EcR5RP*5^!O{Nt`0M zpzhoUxo0tXq&q46c*|6tim^sMeQq>{sd^cdx3m7Cd6G`Kk$g4loIp>}L%fz39d$7f z12=-TBwtMAmdY>43*oA<3nv%o_S^!8=6${axgA^xM~qlmP@R+!9$yHKR$C;`2ahV# zAUyscP10#NTcz`l9`lj@jZ)6JPsVY&uq9PCQ%nV}Uh9tst~e@~O=Mn=9Cr&_SHWf5EBi2HE)H&l zeMaT;WiFdydZxK)b_a%bI|rB_Mp1eW?_u^smBC(?>`x|PZaG?WjW^n+t_}~9`Oh8q zslWSumIW^?8}`Gyv{4+|QQD1;G=*_rmU(fUG2Mf46^g=%#w$b>+8TvS@Oj$c z-X-B{wE|G>X*|i83(&ca1X%E_u78-{Bq*@HC}vzVzsS0fKx6yt1-u!Q5y9OHcoPiK zg#hxcI?EDNmg2!;>67WbNurP@!5d)HJ^x?25R+C)RV1;+>gFt4gCaV-!-X(q&#6IV-uSXWFi++g*V1<7*JAyERM8k zsylSuwrw(k*6rMuh(WZ=D9JHZN4Ife@7vuvy{l^5BfUZtP{Nrxm7W;E{}Af94v%TP zqSo$nn*-=)B=zhI-!c` z;G#~2H7-*+Dg*9;`N?~lM;kv~y z_jtI25JZa`bqe&|g>t3pz9k8(vS%@FR*2wLcayk=@5YTZ@*=#9!}0k!Dp_c=sU6>n z1W-3dPRfjpbipc{0>SIa$&~FCA^{=c{fJU;E~o@fZ1QM0Rj4sZC*V`^H{ABp9SHBk z#N>MhKNB)2`G0Zqvc$z;P~I@Wq$$dX2T>!3@eSw;|9hMF^o}0I9d&kti=)^y6ooB^ zjvsmsRmwI7T~#=-L7`pte=2L{sCrSD%37~cjlu^zLtQ$6VYqV~wV7LSB)uMoTj&_{ z9D4oP!YTHY@GEpR={>LrOP@~1L@VTA9>vg^Gbn^CVp$rHGoBak!q+}diL<9dEA0I; z@BZkHN-O`NrIi}4S`lAIWRZX`@GX#G?5I{4^2Q@)lfz*w*gB%BQGMB_Vso2?xoEV(4HOJ zNHU7LCyvjtbvam0b%0m1%lJs!>-Jnd)e}~%c}e9?CIw_gZ4$j=*@QEMu9VuE z!bIPSj)}U_3llAgY9u$3nF}9~%A#B>pwl5hCdlxqSCW)XC4XYW_%t73dD}TEVk-ER5zTI&=IQwLW7qoCL8^kQ{zX#2^Tw{ShWs#;*h|&639%p#X*t} zs~1bw4)&DN#e@MvXtunVWydcT=3oI+lQMqrCF9r>K^o762OhIBF<*&a(UW)_?E2^@ zrQh1Lfw@aW37D`Y!ECx#k#VypS#w&|LU`3rshq?TD}Ekps~%lfVk()KqBRQ~zDX<> zIFah&tyopSO@H3TJ~>=}?Rt3-U|Mr`h794m&Hk=+3zp*`GmHo0_DiPbVm&)0!Zx>I zjR{l5HXu!ZP4oxt9Y>|Z9z&@ILnyvh8pK`wG;%ybb)HDGIUChxdRXkeF+fD}+5p4R z;KFuOh=v8rlGWP?xQa`mh;nZ;luQI9P&9cOpd1uO>CLlIwV>ezbjGoyrjRUxHhPpI zsF3`=ed6zv@ORmC;_d%=Z`IEy+k^zv@>;tJjewP>5nA^UtLf+O)1iWrT2wPV=YE{m z2E3xf$`ghydu$}=)=uaM^tLGA#*g;y=;+8Hs!2|n7= z%oLmjc{Z4L`Fv`4)XE9ej>u=1{?<)fML_Ah=mOYAq2Qf4uDu^(4b)#&q_V6|@P&9n?#=j9?FI#dLn=Tw>lhGVG0Vya6UW)pj?BuBx) zcOxWjrAbJf-d_d9zi17>H2lVW#6)sqa~j5E1lhf$I0Y^(qQvQFkv(Bm?wH7fF@|d@ zy)b$1Lpi&-I~RKCu5fu9bd)G`7~AsI8>1?t*PSk4;kD)k5W+dPw;L4Y0q_>PFQf^> zz(Be=1nf0esgxhNfnjLs#N=JiW3<9zBu8D{CH^L>bCncg9{Uk^UXG-4eu8FG@ZfK7 zd5&cGg2cr>gQKdmCJjQcGi#C^Ir$zw8hMw5nL`FCn4db=J!A)lXwF@wXY&YNC1%jp zf9mfAM?}hZS>C4QD^)c(fGvcP8?L=})8_5h?>^FoeFVD66<(Hys#YJoV%w&g6qpvX zRp~NZx^3f3QdZU{`Bo!oYSC`U)HG5311M`rE2nHXZ4QZ>sQ=ERt;cnkCR zBv#&^!rsN&UwNpmzR&j}PvLec%4g%m6varGQyZq`w-ZVjF;!2V4jCG!9|16wP}K%% z!doy{00l6|x&*gZOx0MUbBQuxp{|@PV(5(@Ocq4$+a^A3e+vUZuLAwW z#h~h}S_5a;s3aFeDGUp8DYX~%^1@6sA494th{Ng$&Y|!huMR&=;eavGTD!FzM|qWL z82`JRX}G4LKd4@`v1X|yG35v1tm_u`#OjBJ~BFQCyE7~d()(HVRDws7B*lB~k zURr3k!4@| zFyJIFm|~@Bmm3k)XV!tzYWUMSRL8RaZ(?mehNkcK-+N`rvbn=Ofd_}16a$s;gMN{vJH23R)RotxS7D@L z`&cI6n~AwiTp&`gXUH~f1E`{>bI{Lfv7zt7g>wf@H1?SLYMkx!m$p<-mC5kHdSunc z33cC#s4lE#0W_fSN=Of~QlnwSc+2^V(`|ag)(OBZ<#Eo)+lAMK) z8>K`)D;6GhnpnLYCt(P)eTvxq^a%?KYedR!=XT+a!Z@&zd=LV*4y{`Pfp(u}PIN z^tzH9C8l@Cn3kW1S_U;SMX!Ft!@T}pNaEXRdi7QJSF-q}C1fGhK&%+)j@UVyws`1H zebcSDA0!(N9NsEyW9u_h*3)zP<$>uk4;xa#eIx0ao)dnL@s(01znqG4w_5Kw6;Uv8Im7Uv}YGkPjK#4SZ-O@X7BCQi1UtWu-ZkD7RU}Db6p^1kE}wbWfvmZLA=tG%*aMbAS^I%_Ae;aT+}cA0vBmm>y>S@xCf3ce7@T*SoU4Af(ee4t~hdaztw2coZR%#|{8S05P)u zJbb-qg^KsQTL=rV%w6cTp0U8)^!~$*WHp&;lsiL~!EofT#T(W$s@+9r7M<4Fit3%w z_6Typ$%BG?(8_#x8pr)fKrRd{8!^y|;U?!z3hqruCZgJ=kYojh(@L!?mxCQefe_!X zqYl4;rX~6x-*}~~QVp+Pd`fff!-I!ht5s{xe`xDse2Of1qpw+v^K=r=-I0@G>r00K z*nxB3@j~!=89z6FdW-^^cBFb#a5jHAUgeT@m1EC@3!$6-OtUW=G4gN%&&1hDCG}+Z zqC_Z?rl6ie+1v4pC=m z%W&tq=a26_vTpRc4Z|CUhmWYP?hYs&LNS)E66Z;PC8EJ3ac4Xhgu6k#%)h6n2 z>(-51xAK^N(LO!C|IpE0`u)VN7wX2YgF6S;4UgLh%96 zh2|^OWS;*(eHNhn;|2B$Kxu*cLB7zQTm7BQ&$_wm%~#gboHZm}U(J1l=kHp0dQ=Y$ zVyChAEFDkH&Cjy-H*XzNgbH9g7dFrxQfB7lU6jPIYNXL`+q7ldNb^)`Xq&g%LZw2u z#g*8n&f5^c`V)M20gngpX1)mUAA0EJ`B^vDK5(TE&eB0f0$$fSfV+q)l~{8< zW`ycaK`X$ZQ`Vf?G5 z!>~5$l6x|WP-A1!bd!KTZWE3nW!-2s6&F})>{9XbC&k2RkS<`pOf$6iG|8t%8KT=4NM($$e3n!U|Lo@dYvYGP?rm z>pzrGf4+fQ2Z{)4-TZahySQGY31VHZ<1?$QWgd|;8HKE13la9$3>Fx@#j|tdCHwEl znm8~S!&;jXg#5=d zl4V&zubz3k_rRsB_*@cFrF_N^_`>bHW!v@PJS*51ya{M1}zNHEpskBwz2x-`0?L|EgopcD8Kp$#nR>PKlt z+j|;V?#|D;`IL`hUvYgaO>pQsVR>&03)+kIM>*=%7iKNn83_uqifl#dOY_j83C1)+ za@F~36f5~bzi9v!9DC{4fVLdHFdE%_H|?bWDUL26b)A6rIQlL};M7&gDK=!76`JHZ zJB-A2WZTRk9OHnn_GEGO_63^3(H?}D!G|7tMSj-JtslG6M|r3$hd|MFf)iyqiiC9d z(k^5=KimR9{oP+rYfH0p>gNvu^$#O_TZpngG#t0`pVtBsPo}V4_m8 zXY(9PSIALvslPy_6uyi?IkkgzfK(-8rie8b@Sb{AL3eU zP*CbRA^p?UAvKw68b=ON2W+=tlj6WpYA3Du2-sBD(1#EqXh9X^982f657obWK%;L4 zA~?T5Gs@rE4CK4>vu-}`6Nn|QchEElUFSgVYX>r+WuZ+pXD82{T7e=THB{>K44oLN z_cc(xGC%9)pVErHAc%CGQ1$hNx#1mgoPD^R!;B3?vA$hMei`5<;u;dlxcsh*?3SFO z*4gy5YR(Hf{Rx<_|71e@YkX1G&|-ep&7Y)ceW?zrAk}q3`|tWVgeUWVQ$Wt9=1kcD zLDEp<`fpjUQ6y2vQ%+b?svN(hbTXQBCUUPI^TXJorzt1y`#nJiAiqO1u3OIes)SxQ zzwI|V&MC-sozV9l|2NpB)i8$g5zHLHK1=u;DqXX z^RsS#ktPN5*)##E>jZSRMRUhF#m85m)mHKZcnv7(>8ej9D4%LTd3Ao)&7YvXjcW;R zfuZXJWuC5wmGp_zI0_c@vY`e*_n zfL@!Qb@Rk$uN(-0FQ*9@T_-?aS{)$j8Wk66$y4gvJjw=%P*Oly$k;jN8{v?05nWfx-kbiQ@3~<8=9_XXaZB$3F_xsjI~@jPOsd4bbQ}Ff?gF%RlrofS|kcscIJ-uNDJS|^rrAY z&YYNG;U+aTzzepGmnQIS*}Gz3=;j+~lLDM3z;&I#_sZV2z(<-J29=Has@eufMMNg! z>si(6@YiTYXRGGEiQa`)-Msp;HC4?uG+ZY{JvFz5sAn2}wNR zPt$G(%Ij%@P}d37>D4AVWl?`UKOBD4(SM{Fk`E?*Jf=;76Jf2x5&y zH?Mr`8Y|#?_)gI4I!ExP)ke@uHtmL@-5tdZrND&}6 zTAXfvnpW&lfvD>Qv$s%J*4)g^3NScf9ghUckk|p@632u({X3eW`dz>j;`H}}`B^vL z{kSy)$!yKAAky_ZKBFYBxnwo$rM@Nuq@%5QQgszsjB^g8M+CWBJF07ld=KxRBP~}F z2NPGm%}&aDspz-)3z-pVh;<@SfPVo%J+fj*X=DZWx;c9J8XuDfIeApNPJDlRJFp2l zv+Acp?&=PHT{4z{B7$BXN<*0H;C5bU2;?eJ@A~^E`B^vr8Eq%m|B$BMb)69GSPDUF z%&pd`-uYRYJ1ssNbt-?2{?*N=U$MrAI&I)Pb*Ss?^vKdqR~@#kU1jceIM=&x;a%>$ zM55H`4`=7P`Fh&7a{UaNI@Wb|e#6qv+YztOpI-ONH1}8ZV)ZBMRR8Mc*Iv2C?&RzE zPJQV*`#aV4*Q}|T{`r7)8`^3}S2yFl)^HTTqW%lJ*3Ea)UIq+097HhaI^l@JL3+MM zdkU7oLAevh9mt49|}yE(Fg%&Cj~Iea#vRyj%EAu;@DBdFu*z zQ9iz>Lkq9YMWitFz^Q!FS$yo4n<8*&2$0ACD1i5IrM@1b8JMD;2$;7gFuM7YwQH)|T7c*}f$6!elZIMg zR+z4((6ok%8JL(4w2UqkNW%+%|LxJ0TEC? zo}YE|_!HJt=e+>Zbpq9M-nYg;nnq+Z5W&iR>pHjR4x@@6py_H`E>c_2*IzY5!K#(B zS{rtX+Fs4@PLK%B9{?^7gG-j$zq^9eVhsdK%bra|aB2l7KLc9+<*Z1B{%aY2KR z+y$ceSv#NbhmgPKPRd z4hO3Fn)Gu{=aY)0h}bHsx`6m2nxSkx1n*8Lb@My6cOC*ksOyBX=OG9mv(5{EttyX< zl0J$n6wA8IEVqaPr(d_z->R!ubg_KJ3zE0cvq3Fti>SCL_qzGlw3C3olO`~AouGbC zi_j&L?_DQClNONO$(ee-gdD0Hx0z^lyJ1Jd^|y&O!S$2*SvUV1?NeL}R>7p}b$muw zKGf5dmjL!Ku&nG~&P+BA3ike$k#e4j2cpQ4StK2M8&j_z z?-l75q=xp)@f>?=C&(@k4s`RgG+oP1(71J-WB)4c1SL&*t5P_4tA8(23kt(=5zo8)Yb0h_3yc+Y@Z;H_)l`J#=R1ekCE&+`cbA z>*nWZZ{YeAO_1q2q5G|+(A7}$!n6vjC>5_pinGrUXd#+bY^enEMUFWK zA(rVZEUHh$cQ*j4|98=h++Rwd1mMr+XWiVmcg;W=^E6Gs={f=YjpYD34i1mVL{&i; zyIbn6m#qI+gju@dai@fJ=wxM`M3CaFc|pfF1=mY}!+_t@0RD6NSvTKJ+sCys?gX^1 z6YyTfUAOk^-cB_>p$-Au^{f|vf##n7y6O2}=4ah}`@S`Uk8}OwH1(?Mb$muz?O9n` z8B3BBjz?&-HwBn1Nrb@=ECplp(`+t@y)&#JSv}C`_R}W`9GyEw`kMTr+AtkeLMR#G zNhllWDeOs(Y@Ga*rSlJ-KXGyY%(2iM^=mNDCu5DQ(*Z*rzu*NidLhc4X?8ocaEc3a246)>x_$r zv<1#9>E7_R_UFG!cy;slXfNejGDxuMI^pfvpB;q=@!AIMRc9QPgr9j%eL}v5AB!Yy z0?!RN>h1BjBsi^Y{tF3?Zr(vFwz)vjb%N8g%_p=bvMOScz$RTp1?pn;OpYh8L|sm| z`IqYJM`*@Pap!6--=Dzf=HJndas90{0ix>!CeAQTze!q@T;D(wIJ!=d&bH*$frJ*Cd0JluEb8h5G^3}uH3iGRPFQsFn+|j? zWx=8Agr(TaN#89=dpTf6w^F6fNf%1BqAkuY0)rY@ z>3Ie5r1c5Ftq0e{wt(d)L#^~vezoH=%srYqdey6V1K$m=Tl}W4>8RkrBe5su!Ph?i z;L~2GI+|8%*$p`*O;RTh2P`VzSBlA`Zm+;4+L!X5z=O`~PrH6`vO7-SsQ5R|fIhk)mW`^tSA5j$pSGbsTRp}P31E~eF?vJ|Q z8Zs#qYgyNteodlCp+g+~DX~#lj}6zkZNjkYXN)&RRgImK4~5Q_dW3DS*fBPLrkUfM z*^v@DR2XN_&>2Fw*}rA6qXJlnME1vYn|*b9I;S_eR?Fz^$~x9l-fqRCPB35yDl4Zk zEt}NXo^Q!Uj#6aRtLg_k3&Zj%VkM@-S^@oMp)62>kIz*lRHK6qTO6pvNdF9=#QKIK zd$gN~V`E~r^TeSMeiWvrkmS-2ENyOq)YIK!cLbo}2&&*lYfO%K!JuT!!Jy~gS z;c`J&#(6m$*>v(A2)lSCgz)mX6c%2z+(J~2$>eEePepAp>7bfECtOVr1%S)rD#tW@ zd^#UzyU#5zyZpm(8r(?CLgN#s#p3MvO=8$8o8LORiOO>)@8RSQmB)+SZpwx65hR*V zEk1}>wNs36Mj7xFWGQ4A%U#7@D2y0q|A@}8>w-sTw=J)gZwkDeF=BCL2pYD=N~2N+ zHv@%tIl)qsUP$9Kb>a%Q-n!+$4#3W%vpNN|mi=;4b^%iwS&&!z4lRD@;Yx((gRDNW zz$;!?SKr5@a-@~4SbhEDFsft16Ko=fcSkv}L$@gTw=$)cb!j~Mh4t55TUvU>fn#6$kasQ zI8A602I|s`KpMMA5S>UrrlH5i#4XNF&LDhwux>?R_v?_eHBxZIItMD4rv{ikxr}7y zJN6d309OCrtjCxTcv|};7|v?3n8OE5b^LgXq5n~m&Yag^HrtFGJ zrx2U;#VqCT9dhdc^>*9UA*_ru7=|X-d&T>MiPLi!w-$;`Y$nS*lakHK9?<8>Mq|)< zI+7#Wks_5xHCsI=`CjF4sHCrWzOcd}m)euaL>OLVRjIQmL}$#BnI@l+O`I`0br6_M z?S%JDZ(`Fz!l^e2E)RZW6Fr-Z_;hRwu15|i+C-d-5@-m+W*cJNL<&|7OdUumBvV&z z2LUV$IHM*PHXJFWf>Dtf3NT6GlVi7Nb%o$Nti&+nC@gfEB1G_`VF*o%C_tZqK>FkV zXTj51mE2aC`M)7|KSPrY{=1G7{KzGpRvVn)cF4DYK$TXTd;$(3@$>B@ivUnpWEM7Ty^&&@bE&bQ$ zs&(6B7jA5X&8D4)tPG9U5B@_G8=RWZw1DcHLjky4&z@3Hp$ z2=~-#qf!quC*2ZqT15skt2-OJCNxbbe9N948ea;c>K>N3wBNv~a-lPIb#pYH4+HCJ zW0H~n7MjL$&-d0bUA<&X2fbI+Yr&~}?k2Q_kk^ytM47122(4j)p+RV0n(~fY`gI`= zwumFjdj%9ASeG=73TVwLES1t@smhaIrc6V7Ct$BOJh^Valcu4`uFdEE+Uv5;7~Yns zq{Bk<@EkRXL)Q_*v6A;Nz#C+rD~qRUOba)$~AB1>F<8pJsO zv}OKZ_(ImW{(d<>>*jTDBC(I_e@fG6biIzxI47Ueu$pfFx)dwMR5dvPWov$vRVIVf zK!bOYXB0rJ)&68dgYf0M+4)&)`IiybDf4>Pw3(D(iPGOR-^O4xo@wCsfQutU1O62T zq8N5DV${{(Qs^^I>%!2fvyD}YPtwr+ku zO@X6tqX}qTC*bewWWutEYWZ0+SBE`U%b{K!yMp1vG-J2K(gftMCBVA5?}0A3EdX_$ zK=y*$;|04*!7DG(k4;vO4%w@Ntry+{6b8Gvrv>(}C)m3Ax3qh?ei=<5>pH>ywTHoN zYCoobXyDMJsN0|gVr40+T3&zsw(EVmOiRatXzYUtasg8yKae2n<}cIU#Pyvtfvf8T z`L|aDxdk=_e4m8>SAj3+{)}dfw;{w2CiuE}`-i#^qCnSmg5L`vRzi{d8c)NyC=c+| zmC;@WdOX=8SDn9}&J3Lt4k1>5AIi_VdG&|a41Snvg^vvx*9qO9t%%%eNp}j9W*QcV z%qeFRQd(eA#i)xVD1)_VZpbuo{I6y~3eX7v^GL3|q#MbHb0oSs_K`J%-{D%31&vDA z>-Y?RzjI~&ew5?$uDJ$7_^ug>q_B0)mEou6Nff5Wk=Ep+f>Q9sCgCYLOPrj0;3t)YV8fDIgL{Jq$jk z^Ca~WaYLzN>}nYGuZ1UO>@T18kbL z`raAXOY1hoNtaoe&_X;oH?HW(1DK;<2KZn;o9G(r9kKbAxLt7d2) z%g?&`hcr#;J7^k~u5)PHdsDc=(6oUW^IdBWJvj{4P(GJbtpNTbT@6MLRZ8d9O*FX-rT4cn(^FDs-!Gu|DhOYiK0{PXpF<@}gb_ z-m)rg4uwhgUp_CWs&Vi5k{<)?kq@RZ~!Q+(UyUzvAA-OHp%Zdi{6(M)iXI_+$Lb z#tUX$)A1XYlvt?Lf7mO9P_YDjVnG+pDci&|20lW+BQ|L!w^{PKLkp^_1>-d!HhjfOVnTjLbb@7c3`L*LxdH#QMC?q-pom#cfS>Jw-k`YfHXuoo%aw zWP|n3V~cbC(uo+3!$b`??}9pxItSp3R!MQ=U%n zS!tX}v*2XL?8r-RHe}jgi=d0LhGu_upb1(#x--#5>XUKR___{3t5srTuyb_5+vO`iRuykWwsEcB6pU^ z!o5siIDPk6S>CZ|TlZFA9RU}%MBlB&8z3R8Vb!&zFoIs}S3SIAE0jDON$JYc%Iks` zW=TRuEOGTOzh9@sFl~fZ@Nn8?Y2Ap~wJ%NpgiZv*qSCAt1V|?qU0KL=^+CRqmyyg! z6U%%Lo1imvcYayuZr_R4d4kRDXj*k{ou5r6T83`7F zMs}CY`S1YmLJ*fYR)|)s1VZi29o>Mi@>3V=iok|- zJs}!IRp&-G9o>7&HJga`%R_P$$8s#HAd4nFcvTPT#s;+YT>;Y)lE@%_@BF2D5LYPP z%$IA(n&L!9Fg};r%`@r?=P>^bH}ndstYk?y@<5$X)F-K6i&>qOz99NCt0JL?iN=fV zigu+lD9TbJ^L^rjt=H(g^PIwUJvsq#jC9K@=a!=t6_2k4>v|z@DR&XrSr~aPR3N;_ z_@MXHYDHU+tOZBx0wYfTr|2otMFp@_pl9(J5AW+5J=9@x(VbO)43Dk;#F_zR;(f-8Bt>;In)&iizk|~f(%o`;W+$vM3tG9Ig!ks z=ms%2;R^g&H_JXEz;tHj^c2x&=@4V7L%@E#Uct#OM)wbB8u6DIp;2-L-~8nLR}R*H z&r==A5K44&Vb|KhE>1G_vD(A!WjD-1$$AosHe8kzv^?u+rJvIE9D2$QntJ%5$F3cW z@!fEA@ijdi`N7XP?+Kk{0wNI%(j}Baj{4fw5b2fp_3c1{SgrRmhwg`xG;^1fMtwhG zxVm?2+O(+_lm_*;gIGo3EK?&q&F9QtSM7Id73uJm2n^LUY$?IZH^UaG0{V5{Arqk- zfhNco=}ZzuqYN~Yn*?Qfv2wWs?m_IZ9F)rAWL}w0#5AIuSLrQfSV){HLUUs}EL3IP z0v(xsl6~tmwl0kcg2uQuDn*AgXKyT-ND-A!Ii|O|PMpT=T4L1Makgl)M75h+M5^&P zGd5<(@`}T!$>0se#$RNvsm25s6X&vW%FI(BTP6uFit`!F+@n)KSJr*6c+}*vV-yOG z^M9G8Q;^!X(nP3(%OO~i+i>xfJ#veH}p=Br(=pOw$QAHMt2_l8Spt zoq+t~GsVznIBYl)#i|3Xq^vkNc0dOXSP-VPJ%agJg{h6ry8l908QFp0;f%OAd<|=X zGqAzOxYA&Xo(&4mVXnrBKP&^ZD)NKyXj-n2aD=&l;SiRYkD6qVSxi0O!O0`QejFwu zgLgP`ezyG$^Y{Sz>kre!(jHh-b=a-T5@5iaBh;yalpJ?|S7ZKhoZ-@WMTH5EP8=PG z!fDi%VUC0qo`?@rFbn}2Q7xgh8{Sy`F;v@sh>lA~!3QG`pP@Tps!}2lh!*|Kj(rEM zFHB++P0GY!D{ZAZUbuJnQ3xTncPhK8Q69VZnZceu4L--oyn;o9p;RA`7FW(s+;UAl zLow5J)Z2OEwf5cS@LptHKd@PX>A)3!=?+>?cz z!rCT(tjNH?^7rAAagrzrD#}IED!afbnSj%%>Qx<5P8bU%GhXQrrXMtrj@A{i!OQlY z;E=<1?MpF8Il^RVwF+1**a@n%iyP}RlhrV4^Hp={a!6O@#MwE1VtGcJb)zm5t>%`yC1(c zV(3p@e6}4ePaJ$=(T>5}wjVi2ZT(JkYW6ZDn{3?2KW!XRk>B=(=L59O%RQF|mkTF0 zw@^BS(ZpY*1?WI(G1o}O>833Sw5S68Oue93ux=UKs3DE}LT)T@!!g?{iFcD|ngbt~O-Uu?*8pK?t-Xg{r2+NgM+|j67WXQ2FPe-S^T&;rDG{8z)bF@3C_!@=m!@0EBW=Fk@q3xrhLf>yNGAq6!~OYDdGmgsEi^}m_U&EtW@}N;6tq7n5Y${ zfKGh&TCYDT+)#2{Vnpnn`nQB8XLO8q5`v3Q+$InA>Nx4M{P5VpRhDGiHI!Al?p8x= z*932NCfJ=?C46`)kid_?nHtgAy}0Q$*jut|TYk1%G<}++xb4+U`2cUg(=GO~Lfx-{ zvR_KmTwJlU>R`Xx*Y)A!q1j;qOblvW)`Exl?5i@l-D(WEig_PQXbZ;-(X-?wdS7cLE0tNm3Q4GRH0!7EGX;(6CHo_;33V} zh|a$aKMmjQ*uGQW&CH%4J_8oF>I!v{W4KR~Jz%`@&55~ly1)BSncWfO?fH2y=4ZC) z>~$T;xRWMkAP-9MuU*>40HHzY!wi36cpZv(>M2F%lSmc5tCTQ-j5t!DTZ}}x@nMpc zgCC054B0Tn5jRorBiL|!ia{Z)tKf&i7!^`H07cq@YTPkrT#83 zZD-bZb3+a+{e3Dw>*jx<9pL(#X_^*YU(IK9yoqjR;vA0Rd?}i5_${KmMA+|iuio^h zmyhhu-tTX`i=XDEpwP_=d)5wa<=!zyIS&4d-5D4*|Ho7-_i;qX*JCG-g=2V+>I`DHUu5%U+tuhNz$NMfJP^knVIRU%SeQ^$;Q3MN8x<+gu6grH?(Qsx?fthh_tI3(V_H=<+$?Cl*_$M~S*m^L-d&S6&)QU`v8sTsAK$%aJv5^v zuYfNAUl0%5u7x_RTxYis(UAkcL}(MvykL@2s# zKtexMacHS)Lf;q1zkM*U-lguC27EzxC(sytW%>$%`unZ?teYRA-NyBGG=Z+`1pm}m ziA2Y^S3sk>`Aga|IQ~t-p_@0~(!n7Zbp1aCM^jq(lr)OB|Ip|;!YYzCQ`rWjD;0B@`WIIu((3(Q^%S)yN!YdlC}1Q-O< zS7^r77q1u$?!DxxgL994+TgbnXx;qn``4BuMB5$zaGl-qrEQOj@tR5r4h`u!2UCbV zV=?NnbO~bPxe0xz{1%a4w&X>;r?xLypKUu8tu&?xtmInuaKL54d{{M3LERGgA zxX98WMi{3Y$*h&VY@eRo8dk_NAw#Ny_ozsfu+w9i$DzS<>hF^2*^z}l0cowEwAM#@ zg~6Z|)aBy(S9g?oLB#2Y5Y?H?BO~e6l0eAdQ@yMXkVCkkmgD3U2A@wzH3Nvz+5XBDgY4BXPUAu@ z-lY5!H7;Hi7bYG3E8d;Qv3$jNZDc-QLqIapCEvW*?tCX zA&(9*$Tj*MFBly-Ppnl&&_|RaFmN7+%YpeIS|bzunI|Sb9TBa`R@ygJkKr#R+GhriVwAZxSG^PH9mW!()D0YFZWzcnm zG`nyeNF}g2d8ddlQtjH*noNsqY_NN#=jS+3$H5HGuB;dZUTWOU3c$#wb?bI%dUnT= zTQPjONa?e03@YfWW5UwG=Y{BnEzhMFV4V$xxxGk&=w%#6Vv0k{f+9UUh2)aEXP#Wf zbC`U}SZE78JfSd^@b}6JS&3ejkcaco5M2KU6#FH#eyz}-dCl6vH89@c#>J14%F8Dw z=05n{_4r||bJ1_zoIY4#1H_;*0ncgRg!!>+QB+%}w+xY&$}t|?h2_{fp)~bJ^o7t! zevKzgVo&~~4YjL|->^iAk?kxnPaErLW)@a=7x;}LiJG1jj=oUxZdqAiXE!T@^fE>VS7ABdWX5eK@%)r5Ja}J2LdSq*wd}q#S;^J%Y@1wk6VlJtX zbEH|t70tI0a-BrxZ13C!F$e3V+AWC@jRH()!jKBkM_i zCM9d?;^CtpxN1NEi&>Ym6M=2X5W$kw>Jk3cn%VH$)Scc>CWUf^9(dK024CQ&6hGa( z_QRd-B)k=EN&I|gRe_4`B&?t3SH}G6y3^_}qUYybHW)mPrrv+<^=n;fdU(U)6TYTf z*7x4~;AbAE+DZ3zI@E2kb*Cflh$U)%6fQ7{Ne$OVa%jB?%V-+ljK@!r*{fkix=c^W z2(>En@lkSA_~bUb<|Dm7YTbGHRc*%1Gje8mY1>H^mWr663fBw)o=YXCp)q9ygo32y zv-!HVs0%;nS9xK8I#7GKbb%QTIoKvx$Z?U<1)r~k!fB#yNbY(XbO4Y$&qvvMy&;$! z1IO$VmoCYAR$q$d*~#AUJam&F;U_NA7Bm{q?Zxv0s|OJPbKE0Ux)y78m;NtM>i$2d z$eEOi$WdzBYKokaRpx10gEzma221|1w|2a1pvhH~Qk~r29YIvNAES1o4gF4wV*06* z6%Ju1Hn_He^Wck>oH&~a;giTh3Gf~m^pr#MlFhe}d`@X`b#d^^*G)5Z)( zs?}CBsc9b(3CJ$CGP3E8vnNLqbpO`*gPM})&KRaV#Cg$8c&Y6?MacoI^RqJ?%?Je2 zg=!+*$eFvJ^(?(Df7WiN|MXXPcY)$6$B8#eDmeG#IT&J#r`(16P`ewUshc*Ncr zCq2Z^H_N4c5MA|ZngrUr9#}j076jdJ-Qu3cA<&frOiD*XRBhOLnES9!F7xxYbRjSxzhI>y#@s*d)iG}xKog^2vyl(%x^Coge<6)WB@_0f`1}{j^>XG0nD%gU+j>|RO#E{WCqd&1w&6d=f(Bc6w#NX1Y zUPZi_gXWTj!pH8~yy=FL>@D1vk%nMQn@FrMOd(HIn$k`pLekEML<(1nVBvG zG#H$Ks-901tXHC}WiIS18p;9Aew%)#N^G7Q`ql~8~!MThGgDI*yf|iVFohIz8O>wDv z=qHU*)F^XRkCR2hBigVnv))6q;uHSj?s^47jCEabvP=UK+h$3%R?7bmOe-n$m(jEu zWD;C0{Rjal*VBudiLkqk1k&NetULd;_ENC;Z)NOpIB#V9Aow>#(Y`-ZPB|#h> z9c(wL!;$(5)drmOAjVxYr)!Y`0SdviMURDz?_=~v*HapFsaAr}rJHwsbnW1exc(4L zsM2+!>ot!CU72< zY@6tEq%QT@1^QO@s2P>VGa&mcJI(=_)x== znYGt911HLib}3R}&ee8&hM`<+xVvyn6^JCQMDJ`*MLHK`Xsq`$HY4McNzx)~4PO81 z<~M$l4OLv9rwI|dPTt!)y2IUyEFRPphQZM6)+c?h^XkT*30`MvzCn?FT+3D*?X^B{Dc13A}D z>bTskT2pp(q`;OPh&>{wp9#2E9#@jQ;Tr1f_h`$Y`Hu;WZr=DC9W;VO*AE9xdj3`c z#|3_x>Ie-q>g-NlZfNdE2sF9>DL?DxyJ#v;p(+=GMAr$;#uah14ezO8X?oI&K2LK` zs>7sS^!I<}XWe|lrx+{OPob$7U1v|5R_tl>^(%CxH{HZ9-PQNy>($lo=V#sgQQFJ6 zejZKT=sLT)xMEjZt}R`WVZCI+eNPx>b2wYDmQ1<2`z@N``B($bm-4f2PJeps;0L*$ zrwJBaCp>?!B0P2rG#aDR?y;Gxx2`8BV4qQ_ zE$hK^C7lS{F&!Be58-w=f6kkL<(y6pn8*5#l@4#ViDh@sbkz~glQ%uD;pcW*1zH*^ z));?;F&POZ#Yd#|2Z;pTe9mVHQ*!+Znh>DtoXY>+PUQrAIDAU!8ZxNj&egjk#}>d8 zH3m~xx63g>-S`VSmwTYZk(l!1+GQYHXr^V2g7yxe^KhQo%;_KIaCGyfzq!^0R+SQ@ zq3Am26wPLlZ?iTH#!msf?nns8#iwn+GaJBTqseA%-%lU`(KqFbc1H)UYJA@Whc?5nC0HK?Zi?fP)%*)$HfG9$7eBI#!31^9y*4&alL_EsCv!D>vyO0EF!Ol1p|8Fx$MPAupQSgaQXq2{eK$ICiZ zLU8^Y5Mm4#?O2%mZmA+E5zXg z&^cZ!x+?8(%$jM~^7hFoi2O*dN@(NT0DMRj;GY6ekL>y6_x$|*Paf>N?)SWP#7 z@ye@8(Wf2fPv6Xi z)O-4PMPU+!C7_dTrxR?cu*ck`FdSf-pF0NOC>Y6H!5}gyc>>m~ebEt=6*MaY!}F+- zO5q9p5jssD6;l;QFUSOLIYCJWsUd_B7x>W%nmyla^J5j(0zdWlRyymj#9+hbhv4uG zmf@Q46feUDOFJntXYF6)*`wh&U<25>nR3KjlPYnB-Dm_CVo(dGW=@Qk&Lyu&EU#n)!L<|UB5lH4uryV?L#W0al?tJS z#O!c#!OS@aAjSp4c91`N*2pZ1$?b z3D33*c0%>z-Vhh{ba)q*n1cf~tty8F;>lMjwahy}m)hfUHu4zE+U^{YK%OvA_6MuL z50etfURqXN#TpGnxX4lSV~gOL{Rjisl*_#=3vFqX<18Cca8sVcEmWjN**QaXv4uNF zl)p{(D(FMAEOf+JlzU|?s`HpU=D;?T$j6avwCN^OOLD7mWAXF^KkX(pDASYeA^63d zauJ^KNC8SMsSbn}de;{AupWS{?dDYsAdF$)?1oKq$EVn5fT7>t?2@tfc=pqvdOtDG^c!5uQ}fS0(HQZ zwI2r(6;twc^F?o0dV{kpLo(f|eW=`lReKDp3sLQbc1 z5x{9Tw9*$iN60CY8G!FI{7>Qw5uS`3UCmz`rgn&@IL-FI=(cONcs0i!uzmYH91O15 zCIN*mYXTs3$d*C%LtrLOa}pSE(8 z#z%F`?A*DtMRSJ&<*9H4p--p?D%GQh=jTA~A^XSb!AQbOcRWI}O~xlk^(VmI2_T02 z^?VU_<00e z1Bp7?xHchqPXo!9^RsTgi*}LgTWNwq*9pmcSArxua#Vr!%bAg6X=yhou|DjEpy<(~ zW8<8D;af^3+rRJV_KibInX(PgyU!K-XaE%)pQjmRZ)^blv;6FvS6x+)aTJi7xlTZP z$2hKnvZJi6$th|=U^PE1*3{QA-fdu%l2_*MD+!ElZh8Dw1KGSkMH3*pUdLw$`u0m6 z8Epq9j~^zucW$`;@WBbTJ+ZU%EKW;QQ^j4ppjB~0+=T6NbE;tmN89%7>n}cg%e6&P zSM{_eTi4gZ3RGJpUL+xfs9k?}-#B}Jb9KWn#JqEdanFyT@f~M8M!=p10slD>pqqC; z;i|!tx%XfBPJ`EVBH#~u2-uGp3XxG|uE<#q?l^<$7)+cbc(^OV*MbW0Wzx8%jmhj;BMI1DS=J$IbCLY+?*%JHX|DAR>>sMRYqGpuZ{;W)u!rsHdyPh@`e{l1}raN`*qt`EnBVKMwVIJYDri@o7HVw zHcrATbxUg88{KM4a>CG-7cd3_3E4v+fg}(JWCb#l88(L{7(zlANJugyCJQ7$2sms8 zf4~3#RNY%|x8ykUJ>NGo&$#WYdhb@Z>eQ*T*QqSw4NsC#3f&)bhs_nt2y`k1|AQ~W z$d(kSNW+j5RsdtAafv1ODJmxb4y%x?7hqHtt7_HqwHSp9)4CY*V5@KQ=3XU*VhJVN ztPDE017JXcRs zKuWj8wat+3p6B%G=F)6*7D&7d56O7|TJz6?>%wSY}+Ian!PmcMf3^Ji*tDHg$s z0t=-Clqwq8Pb0}NwFk4zLGMxO;MqENiiUz&f~AmTXZtk_vGrIU6tYa3OgL~WCAFbJ zH>kOqw0Q-W#)n2nLS}}m)@ylqfYu--DTl+b_w$lvlV{9*oMkP2IE`Z@D2}N9u=7d2 zi9NBCw*j+e3h(O`7D*QLG>YiDJlTa$SefxdLUsAj>`ti>5$XsCqQ3kH7_A`^Hkt|$ ztuu*c&T58crfSL5=DcKL_OK}?8xW!YlVg&3)A4yv&FE&mY=On{Vh&7X+nV{Z;S>75 z))mQ$Lq8oa)eKe|9yLYVbTSpOYXmJc6W=@JK(}iQBk*FBDO``VC|Lm&ajeE|MwPvg zNs~uUNDx$OC4}g*$CGqmh`iuXX`x#ns}QN(YJf;vaAUD6h+V;s=*yXrjQc5$(~A;$ zFE8Pk0}CeV7n!6V;1H`>{ruWYbL0 z5T2;{zFCWrbJoX%of$T(SFT(cEG#j6>EjJBo0*@f;njy%Y};f~#@@rX^ukh^ zvtKeVwX}`T&jzWMONj8(g4NGYrmq|e#!%6P`u^4H%sxHn@dY-eQ~+o8qB<)ay$Qov zDOL#slq*|X&4MrZml=!>!%{3S?2G|&tX7sb!t+Ay8hBgfqO}^OBQ))asXr_9| zjWj;J{{Tb*J!~JV8mzHr#DqRDRrd11{v6uK6=S&xk`^e}+F65j5)c@;Bguc3ndnBC zsJL0(_^a1}*eL*^BY;oS6~5BjzjI4`ZnD+gt1U0~stay>=)5@W_ZMwXyE)ipMRFzq zk@QGB0zGT?Q)0*D4k_HoJ~BRjo4Hrlp|Lh80N1QuJ~%RxasaA52m@dFZ7R=tBTWLj%m&avx z9yCu88`sr39I{^)WkkU=KC&o)TM`B|f6}%!66mlNDTvOBs273;)OZ-oBp*3j$M73u z<-IK%H|Z5O5s`$c8EGk-iEECO0373%1|d2SOvUp+Q`nHwSU}6h@V$hbmUCMxVQ=Wb zdcsJsAAOIe%!U!Yml<^phdR9B&f1W$Z?<@LT_iL0a$mJGnV$y7u|#a?Iy_fC{k~FL?2$sQ{X#mczBshzD z(vf##gw>-Wimh&-EruHWFU5kEl-M1gLg}Gj1&SAH1vinA(ruzlnJ2H49$~X z@vyA*voWwSvIwhfP`MqWY2vOl=q9bF4RJ&jr< zY4l+vKgyIP?0Yka@(dx$`N@=}jcmb?M@t;D_y&eO z9Sr+C7$)p`-#xV@>-~=d!y3#gv8Z#beFx4DL<&L3)TxdnI-m%Iz)lZtGT+DxDR*`n zg)F!wq0`&5$=2g|c{zA-0*4UlEBE5a5mH?? zbCMSZbW)!h7an`^nUn|lNj$_sHWBI7IcEhdRD@dCuzkmKPXmOzUny_e8Q6u@(^DxF zO^q|tFo*2VdMMn*3BC4Awui@h2HOgVkcBkpK@#fCKX4)mz_kjte_Vsku*!(VKy?G1 z`5c->i(ghx!4Tc+DHwZJ9^g4a^sPMSwTzd|eA3J!o_pwRwjDoO^#-i?NJCGP{j6Z- zdHUJvgq871kYwlRnc@c`%fOD>2lirA%~j% z2QN>2?);hi=M`-inF%#+dMwiE8=Fhpsk7JCjWjU`otF9ONkD%s@CcdGJVEh}m7z)p z2ti!Ff4Lv+kfk(blBmSScCw>JEP_qH8ayg&WU2OkhWywPRWq6b#~0{oBB*ml^GVE) zhX9Gd@#Xt!kbHO+NCxPM_cGE%4WnH%q8>Mc!4v|VM<9>w``A4L!7Entu3MGueS%8@ zM@KCXw*f6g;eQC2#4riqImp2)YgXfo9sqkIJ3_cIl!1o^O&vl= zgZk3djj_%(*k-VM9;mw(_V1#0fu89zYBEuDLtS{VY2KW9{!zN{CJuE$zQlyK_@-yL zS`RW(d=mN;yd_R$Rdy(}7;sDh9w6Ao=xjnfvk>N_!~`zWJLOhMGPRl4s+G$y&4s-M zHC9xOc`!Ln)YX(MP#rhrz6KWQJz*m`yLLv+Mm)1RUbImX~? z>9=lP@oF?T&NpzV*E+w1-^3W)?T*imZdtWf3054(2icmn3}yC0A0rjyh+$AG^rw&B z%17L)GoY=xRdVv{w{AZ3$Ih&@|^ir}r^uBXFNeXlb zV#3VI2G#8EOjA-=bJdksuDLR=yXe;Dk65U5@ha0>IZe}i!7t`G9y_LDK0>c$H`e4W zkdZ4Aav;~M*g4*|H(;SqqANMn)z{NC!(tGyKYah9YRlahR$u3)V4<6TvtU7mMvW9W ze7MzY@A4@@JvXO{+1CoBu6O; zZl3f6+((UGTRUE2PX}+;<;^)Z&-PP6-P1;1S)zpr2Qi~d!rP`rl4+G)00ZWf#b)ha zhIku+qM|jiQ{eP)7^~DC>$ucZ@0gXVLzhMC63f>|fT+$vz5+Y{|0TrlKe4KQoI?on zl@Fbn)6qTSNk&Ld=o$Q7a0~rm+^Z5WH%ffcsbdE1J#MqZ_m$xR3Jo9;M~zrU37DD! zEzmdxxugZ8MaU+yf=dXL9jvdzYz0bfcVrWWtYjd|0LB&w`HSR$Rziv>>O3-p{L!Ky z>5N1Ot2_G&J*6R;p+xsKJec>Abl;BMt2f#N9+3dzCzIMGU8znc2bZ$osV{c7R#dA3 z;^2}4)A*(kOF>F63}`|qXqdI%=9x4h zC|nF0^-8MK{KA&Ppf{O}s0$*)gq6?NlCazmhz075d^A{ZHTcdoNZkfXn2l6rc`Ia- zdhK6QOC71v7ZExW$_`|wlRps4`7FJohr#yI{Of!;42CLC-Ox6{lJdjK+3$U3XZd&i zr2H!QtJE|i+NDAPisEI-7LsWrJ)k}iS(YSdlo0cJkPbk}L=bAGt)ajz7;0Xl8REEx zI{A%&pu7s%<1L`tp#XUmrHSkN+QZuNt2f5UGETGagdh^0I`GATC22dpV4q3cNA9hy zM=Tf=zJ`Za!d88Ud^R=*I`smgG_wVJI5)UBVHWkvRBauEWr+frQ%>C6{W)2V> z!5fe`qg<3SLNYJh_Ri*Y@`7VCPc}#ige&Hnm6(`WSzfP$=ortO%C@$Fn1G5+6okPO(hyPLWv&2BNFum7#jaNzAm6l zO#rQM6;oMbuTSXDt6e)&EM&-S)G|IvwAF;N!GW^b&ILf>XL9~gadK>G6r?=0R$4yR z5Y;A(6i$^fgBojlm}V8P&f=*Zu~MmLJyQpdDA*)BHltGeb2I0W90QG!Pum&?jTHA0 z)5GM*O0w~>p8RYce5mQf$n<_0oQJiAT-VkdhM2(8IA)0$?E-TcfJ`WXyHGC0>X}T- z6TBJlrgUBDmQhA;#)4_Vt&Wa3R*$3}8Ix_guwYz<-(@R(Y9fumfi`QbfRjigQdOoO zdHB3$9J&G$etSOZK8JYaB_}M%{PI-=J$Ps!VyK(kep{6z2Hj^34Or{ z0&YSjYx9p3>pD>qEG)x+a7g+x^FqOSv_6aDrkVjs+it`s(wg1cYbuy)ngi;alXVB) zkR^mnSddSQg>`tZ#fl6%As!qF24FFKY>2st7cboSvTv8R*L6YBil7r4Vq-4@(eZvH zuCBR3>S1IiSl5Z6p%b%7z|^$kR4n6^0)6U(I+_px+rjYuNMtRgKrn;s0=;1DVvdN- z5d_woUjj;^@)}v`|5m&j;$rf%wwY1pi0i;+TQV49YF7(j9;4P_iMF$9m##N7KMBp5 znjxnlH&XclIOa#<%ggjV!ADQ zi8)k}`4jUsRM?6hP}DY!-hr>~gYHZbzX3%w4U{yKB*kPOfe3sS0`NN=LY42Hv>*q( z?JtOcbp0LH6uo(Iy@PBZQ!5cW&JrcBWjA}*mBtD*HNF|fdvU-_w!?xWb-@$KM2yEG zzgiouoqQEyJ03-Ardd@`+*{y2VC^oX)1(X3Zeyww5g2Q2>+fM*MyqRc?*Nmsj+=u5 zFqyDF1W8e=WN8=O5VLTEx#S$Ha#pa^ksKM76g9I!Q{TPpeJQ_mzfdhaB-V!97j;E% zgnN^QXE#wH7K0XQ12a28mKZ#Sm$!xoB_e9`1aGcekoC~}%m@u6Bn|bZ79)vu3tCM= zTStP4BnOaFC^c>AJ*Z^D#w3~(1j{ecNho~FI3_^aV#gtHJhE!};h+|qvELrzkeO^--)g8doN#$$YFyKSIT%Db85X5p0O#mwTI1qYSPPU11JlZoWRiU?La}?+u0*3< zf%V6XKzj>fqY65j$2SG(MmLfiXly`I)#Nd)_0WqB07p>%nYmg%cvF7HvJglzt51Tw z>c}J_V25!xTu?v4c3Dx!Ov&+7R{I)ms&SHJxj7l$jjw#ML&Km|aNKNW45l4; z>c>O(r>pV{3S&a|qaizuYOEfHh5YB!7UawV&nwWa)-7c^4j)MU!z8aZI*4cwHuew= zy$DSfywj|$`CMMG;cy|15}=HYK_e`ej@P!8LUq)bn+iit!F7^%ZvqQRI>eyneg7pE#Wn#rbDWW{d+Y!%Hy4c*joCd z*ZK3{)8B(n@8l5adgGZ$up(d2n)%_vmY0@uLd7~TtGABW2_8(nF|M^*tSB^`WsP>v zBWaA2PDL+o7;})tmi`?*S6_A2N`nO}mYS$~(eWXAUhh_OulyMgGPmHUqgZM}k)Og> zAjSkhl3JeJ74Ebh2TH>a=6Oi4;@&1mIRlEEqE;fn3rMiWAix>aEVN!B@C131MzqQb zPoT`+Zf?TrWd843PM65mViS>jqPZ5lRgzkmz8Ha$w$zTOkg0(|Vao|Qfwt;Ha&Sj1 z@<#PZ=As%t2#Cj=hhUPGliI8R>WY|&_DNcIfp<-WR9Vv{Pc1gKZNzSY z7V86&z3PD2&dPn>i$C^h9HN-=Bd3^#XBLTn?P=RwYJd7@<3O+_&Mnovdx1>$g(V(s^4 zE^=N%dFplZlXou2Y1Xokd1Gm{eJ&N9$DxrJ<#~_B(`Qy&Vrm@n$9$L9T9B(RF_`+) z&-N~GT94NHnNN2(*k@pfjP8PBF}it>CD}UXze%R0e3pLNmLp$L{&1280SdU9gb>YS zpy|5ObYi5{3pGVOvJubnspq~&wsA@%X9iYf$NC%1b#kK$|w!;?kpqxRgzA(!K|5p^!yFL5XQD)pJw014!WsqLyJ5Xmusj zW;~@|bla5?n6nf^rk>0aV5D3kGmsZ>@2g`cK(Lg9Oc3S^Rf=R51&XSiBA|AP)V{B# zHL+^Eh&>Gq6OD9CiMdi!U)?6g-(s0PxrIm?%g~j%`WP>%kC`+FS4yTd8&#qz(jwW| zXYqyQ3V8*j*QaU?rY04UyETA1wu$Np;w@r$&8kE22*5BnG%y*c)YR;JvVXr$74=wy zr74|kb+sLj`kxq(z#001C`Zg&r$uL;h z<{Ie$Wc`Ergr0GTkj%TT)+DbeEkv;>63a`Oj;vl^jp>rPoeY=RxKWHBjB9udraygf z9BWZHx08|~L{Xnv?%R_f+wgKLGqpe)T0=u+3R)ESSbR}Z76HxFjwmNUr?4dBK|DrP*iz-5MR*-`;aX(SNn%mL66+I=WV)o!t1c z`Xe)9YUextcrtCIx4W@;v4kb*LtzfsKdCJ}**0mZl_=IW5RZUN&{>z2i{`_vh*?Q% zt3lg6BcxSRC5(Th4hAv>{Af<`wi}&xrDf6K@Pu;-3m26qMRuB+V1!I7h(idxD;5>A z<(!Q<9*M4u!-jlva@I~%Zzand@9sK$`0#S_4lXUHgLPl){;q$lh4(rdyk(y%A4rwWSc>YByLOr0#@u;*p@9Z{bK zvVh{|Y%Yejc_x(A8T)7@f_s6oDW8*LP2n+TaEQmO{(;)MdtSk3#9wZbi%f%{>E@m# zDA2ZcBK3vtlLJ*cLP8o?HLb>8;5pKS4SA%A4pWpsht3rMZxb!iWXz$W#MIJj<;|qR zc$`v&o%En!a>)i1ucyz}bi?yx%}PgUKu=kVE7L~KcRZ^B3a*tcdCl@Q|B(h@;gQIc zW-6Gc2*QRyAyg$NHV|`~VIzON__*zFrdbc5ow|6lPz9svL97qO==)6>YRo^)#~4Ke zFeUvd2DPQAcj^gu3X}bxK$Z`1h|piKZ9(3R>7GLGR`lN-qzU2^EwlnFH0@{?=5$#Z zaB+A}2VqaCJxiQsN-^&0T-4c3yzsUSS1iq9iSQ7HA%)7})j0+`mSaHHr)ohth==$> zP9dtwYvwC?DqD`#9AR)6%@U-+h9u;gA(s091Ffo zDf`D12i<(D#zVW03SBePsT74@+x1f8tnJn2ykJ4~*aOg-VfmScrid&I&{L^UxH;t^ z7a(SFn7UQX9OMbOv(0|Tn!m6-W-sVezsi)(@RKRF0~_4Z$ddtcGB?({(KqZD+5UmM92BWkRhGL|`>#ulz{h#6R&^d)KF?L)WW3ef73m(FLsFo)>kThPdoh5WZp zo^2t&PEMdO2E+`mRsJ$8Arya88iHD2ZS+g@K1mkID}``V2UgCp@C}HN52Ipr-l>IR zFICidO(oj7Oz6hYC+#O18KI@px@Et#RiKG{WjG70tnE6D>RlUK3JXt@g%+0SMo5zB zOZ-BpY>T5NCetWUZ?~KNrF@g7`P@|YkSzu#Et?m(@9}vx3BFjv5XyyI-wY` z9Ko2*=QhwHW~#&#+tLVsL|d^QZ&;Z9+8Mw^mQ>-c$#N*kLaQT0 z!G+q=|K${sAShMB4Oz2N!L3C1m|mjL(o^BNk*&SL*ZF4F0vD^KQ7S1-CN!QwNg zVS_vUOiC)W3e8MIh3@TJtOTggrtVCHgZsy&q(1( zG<*cu$5WzG(1dK9i;C)Pam~#wWEMmoDhdwIQHYp!{SwJU3&RZ2$UvEE@v)+!hDl@% zBZ+blKJnnN%RwzeKwiBpOUik(mk?5J=<6q|avP#ClDe)=Wj)9Cz+`r6Acc>fP*m)r zisv-gLQAWy@OI|xlTlxCjby}FYnc|~T5Dlu4kaM0kfxAESOCsqOtNlCM(WrS9asfu65$b9klUSl{T!p& zv159`G>JNM-${dFovvQuCRK#`yn&M#tA_O@7V=e9e3god0nhya z&Tzi$m47^+Vtn>t2nsDWzLzTtX)<~8a}yv2Rp3x36f*!ZcF~AVfm#+Y^swXZo+AOG zO_x3Qj=g0L`+7&?GTISdb=+;e2T~|s-I3h*LebC0TC2Nyuxsb89e_8&zLIG}IyQn^ z>J2fE5Ox6Ydm!JBa)_M&?fna!(WQ0m%(v$t;EV$0LC1%#$csp-n6r-wyO`AlKtr}L zbPzC}$*^{8WFFCJ(poYgj}61ABcpr9F+3Ia=Bn$`XFqq@3oco2+tlL)TUYbFRUXp;AeI;c!+n|Z3CjD$)Adx5xGCQFucO`Q5Rj-%VEQe3=>Z&y)C>Aa=7Gk66k$`IYw#TZhFA?;b`twq<7sWaeniBJxo>4E8 zLBW#Q#~NIrQID2ua|l|jjXs)0N*!`bPVip=E1W*;FNM_nCJs?d2|1d`t;=WFQfG!= zPD~mro|4g~qj|L;0xdEVCl5FaJWa)=}m`4&f<$kH(?EX_81LTWD4`@@Ho;G0uV8%;3BSR^mTp-zY+1D zQ%d;EC57SEz|j~5@~`2W`ID;gCTj2}CyP&hEB)5ZpW*1`{6Y?WPUq>9&-(X18E8>_ zQ5Ala!(V)P@x^ba-@4iQF`C8sS`K|p=jn?Fj`PJph#CbtB+gn^DcYgWTu-%b$193< zd?)?Z&0ptGo;hvTp*H9|wWDLZj=oSAYMeJ|Q>1b1k|z~>mMmMeL?8Mbhg)(h6FUa| z(MP|VTB4i3_Pzzx`#Apv4z)t(m-3rozj|H?lKik9V&*q8iV=<++MtBQgpgw-#MdYy zi%BE5^hj|uQ&l!8Qnk(A>bxqBVS(4jdOq=Q2yKIaYVK$0i#z`p zbSQWJk@QT<59tox3G$()!it(EqY+0M!%yso}p(L*28Fb0POE?+Kxs z+dr_tLwbb!>XptDg8gebs>kTq9N{81qG*=Lh&Hi>(a}15l6h=hOvJ~XZ58IFC ziV}?^(F~K?`cm5F&KxQ_^ZoQ&H~)}hGw14nI->K`nayQqM250Z6nm?7=MZ0s=>>ME z)_>#hXMef)tn>iEQ#bd2a6$EAeUd|;)Oq^sFO;8UHn5;&u1VTfbNryr=bBedC|p>H zZ}PmwLuX&VleE&pYx&qh4mIG{_@G<(z4^^3S7{>a=HQ1HLD`XkwHev20-5m%qjp?Y_Hs5W8$bQW17R|D`#uhz30`j1B-yq{XiMNgUU zx@{4lwm5660f|C@OE;4RAZ&rNeAg+RFFOGZ>7~qHvfD<2O{agZUQKBm9pX`Vvm+uy zU&BV$p1{Tn^iiBGTm+^7@{RC#+ektZk--xN2Wd%)P1d4BhdR9E+75Kd?j<}Q8IEg^ z3?pXKQ(J4qv@Pr*sCXd?lA9i!R7QY=tq{w?WKFH6Zqxv?ZTy{CTm_H`vxg>e>qXpYx2RW~?xAE)dEUw4yZRoeyS|?=nNOH7>)n375 zIbftEBje^`REcsUzdbk`**0`=9h@zMRewD`C z;u`z}xtqOJnlRaC1A#L*H2;1Hh^4cbo0BXEv$ND`!}Lb_l--I z&<;8R5=6x|@$G!{Y2@1CN;PXo_2KB6N{M`yH%H!#pEm&gsNW2 z*pfzEMdwF^aNDOh?qhb-^@#_xU6dg{rB~xh%eF4 z?&bZPdU=MCd925d_r2vC`ZtLi)2Cf{jaX)%!BOvs9t#Cbl?A6}=r?KQi^ftx$gx-$ zvMrMsje%g(sJ(lqLmNV&ihAN(+Z2sKM0SJ^r%#T?#S{lwEY#9xjU3IlgPw;Ihbb)s zu-Q7qR8WEH)z)3DyyC(1u8exg-hn``H5X1Ao8p*3&J5{<+csQf2AP{i9joccg<#~L zGifzhfA!;pd-t2D@8ik$zmL7`nw^f&=j!N&RHWa_UwCctx@#Jr#vjeaqVy zCeSMS4~zNZuZ%1ZGSJ0qQOOTXV>|ALxPz;65W+EUEp6N>Ojf8xG_v*=N)OeTg@Z1s zAI;cppWb(P>oB_qQ3D)K@8fz;lR>K`7z{?t>Ng_ozJq?`x`Bb6trw|XmI?5Oj@rX1 zN+ZiR7%p6-!jvj-GtX;-BJs>^GCzjVClIOFAppO;lAh*qUzU>o*s+T1JEpdL|C-r{ z4iH$`AXDt!ZYn9;c;k-&j5%?q=+!5{jyai5p;VG_#Vi(Z2!l@i!~(z9YRx>|%t8qe ziOBS$5{_z5Rjyq%FXOG_)&6<#^EDi*`CP==L^&`1kKkxQPyb(2&K;ynoRlzyRy=)q!bEZecr9ahG0+05EF~kAVok^>=99&X{BhmVfz*-n)aNzs%6c( zoid2WdLCL~UL~0n;|^KXW~B}_$Jmj=LLvYNSzWfh6sZ^5m)K2d{tbcN&20i~7`qrH ztparLZZTXMs;@E(#~ZG{fz7;@Lqqk+6YD8_iG%kZC!3i)h5Lz=o|`8|0E}gBFAnv9aBE1fHUsLeE_4cX&4hlPNvMp z%^VI2#+|xaT?dZrA<;t0YSWn8c?;j{iZr&P!-s_}lD3+SeGyEw`G`L^PJE1`lA4xg zRI?0Q^y7++Nq+$cGU=+Nq%@1+T3Ii#6}ahCYIbUuzU?G+U`EeX?uKmOPM~mffW?Do;%r$C-b#~k&Y96 zixtJ;MOi-UfEK+ObS#`?F#0pvK0G`+J!)^jF(@xotxXr71#*8AdZbBk#wiQ49QD=z z2#*TJB&%mXK$1{z5NSng<1!65eGQ>BBr!Abfe(}ZNi-ras(X?h) z4Ui5L2?h-S&lR%<&fkP<-~QhkifTwX9#% zGOme-X%)Y)v>{+%>q6^c@|Zs~0-#X(k2#J<-w)E{EgWio->D1p#sxoK3xM;ghjx8s z^2zrhE^fp7)WJ*|t-bL;dMGkhV?0yKV$FP|rU^z_d{nz`fJ_~fxZk|f~Y<=G$M&3!bYRE+NM{6JSMmAf%a^C14N;-4O{5|t$^kQ9Ei&Up*D5SAk|bAkH6MX3 zz@S7Z(>`wef>xt_949fABcak_%OWZVsuE^CwW1@QA&0zZ|K7b*6>O_bjy78f9K0@h zIkQspWUugqWeiB)*22|@sf2W~&n4;TrpGi1u(f*uDRR|jyawkzxH`IxiZKztZZsq!0m#nvFm=~ILv<5vZr)RlkiTUf1t2DO&W ze7PW>nRv#sGzj@PsvVErq#z-4OD3&1F&~uadq=;6#4yjbm5L2+alvqZlT`b)mHo3^FOv$nGcK5lbi7PqYo^YKT z+~g3Gcg);mL@b3o-9nE9P}HGB!8OPIc%~CMP)&)GNwq|qfs|wt=85OJWSL7eNfytX zY#AjPg`4;dO{3@Xls%PwsM8XI^fy2K*3IiTEwr=uD^#T_b$)47`&aYIdO?Gl;~yWL zk~SRHxn_<00|n1>(sxGG|K2kil3*cI227@d4LoJADSQ z$0T7)P+%?ROesbYCSbF3Mif8`Wcm|?jsg*b@LgO806zNYi3xMikHDK%ubnx~?Kd>;bzkGMbTHa3!jj=T}z*gq-0rI+fx37MH>P!|`n9Hp$NonN9}ZJ@!- zu+bMVuYSJvJ>>Ip&564sZQyh>M$dT{86@bOT^EhC#T;*%7wGP-A0#p`TT`wd*RRbi zMA_-HC5#MuxrsuogiekQTS+#D9&dTq!DF{YA24;Ix^PM&>T*F}IMy5btROZTo2k(5 zo5P5O7{LFLj!R8J$Vbf@lIssfvmco-r)o>Vk(nVVnS9!`XznHfw~b-VmAR777UN$I zK9d5*=22t_;3|g>NEz}Vco4;9%2Ut;eNL_M0Hw4o0IQIW3k`WE^cAicXdWZ3WT>%CWdqs*_dV+)k z&%qF8t@_lrXU(jjIB*%g%LwjOfa|{LNj{(a7&l9l4>e61Xp>YaW`wwW2fA!lq>C=; zzSxvS^m=ULgOR~nVzLG2uHJIM(KzjE3(vNy$}4d96X7)H9~#YPR9QLLUrV+>GB6<*0$7~;I0?}TnvL=APCyj8E8B+Gz2NSC*b4U-F_a# zY4Ej|90TKMI5MO#wUmULbw-nrANKpimL7N+%`phJ$y+$t+OaR}DG;|3fIzl^ooSd~ z3a4Rf4!A)1x~H9(z?XB?+!!p*Sc?WQ8ru7G|eLORdu*RT_ zTDd838_RsX@_!P+)Sa)= zo#P?c%fJo^+v@V|h_UIp5Vqxm$Osnu>DJjVg5zt@#-Vf*1OV;x$jlam5LVFMkb)eA zVu0@&A~d?^;X`G&jj z`JgD;%}20B_WVL{da2`S9QxLhqqQi0RT9OSE4GOr=WSj*%y)Kjg(o@Sj*n&4jR-0U5rb%1jDRq0LCeJ1Al_=GCF^GO^5|=_acqQJu&}f>@5A$`vdI3azKdSTKt|(DquJ)S!Km z@ihYc8aJ{wc}hY6-Q0XPNX5@goz26{F0{mAT5 z%!bCjLN4WivO`t8G(-y2Ql($S=BG57=-aueYROypvYYy*qN%5)rt0ST_b#lo*lq`h znyK^D)L$%{S^$h8E9{Or%4jp(LXj|*g8nTUhN9##;?azKLsiTMl2f_oq3M0U#&O$W&*}d%U8(2^6HQx=+4u zVNNm@LE6O4gU9|u`AAbQaV{fr*>$T{$s9!Y)52syAPi1cZHv)u_H=0HXa~b9Ki!cF zqswj2m{B;=LI6}m$JjKmR2%cGku^e)PZp~-25 z^3FiXAXuv}O17%;2+g|a1wdjcj8^A#1gSxMnhcr}ESl90v^D}99oUjmo<+(oB$H^8 zL@eq8LS%?9j+8 zO%iiLFc~&%_02mi(plSZ{1RYEC|RItV@VDNj^JYw0$WfVQZjxDB+7jT^XRIqofdY? zs?&Hc%Yj)7!5THmhhQ6gH%vlrYY;PVYFJzWc0K`O2Q>u8|Yv^yb;Rm{@`Yfx` z;4(=8fE{SaW|w*%QAD$67Mu{LSO$fMG{;TBR9ozthwy2C*(w2rRewQx@sANtb4Sn| zPw7}1mZJRv@jI$*t!y0`9vWe>b?4=%vU8Uk2^q7Wx)RdwoMOSQyP|2%E!8Euz8~7v z$03^aE)eNbez(q@xwj$HvX=-dYGY^ZvIb=zLbBV!hovOT?jR!J)l0-1Ro?+uKh7as z{moa``1EVmnJd1;d7@HROm z;o6;bsnPhZ_9$bjDFkW^!WYR}lTguFEsr-{Q-koZ0a3-lswH|!;*;_XDQsEFhn~{d z^6fRtTfI^FoO(R^H^#gikb6f}NlAkcu~FlV13UT@SUo^xq?#5gCg_8atWUPX$V@wE z#8m4NKGxFX8mO+m-htk&JDw+Fk>yGW|2#uth>%hvhnvZ&wHl$M8s6D^6XJp>WpifC za%DhZo@?12wL%3&&O}tf0%k!@Q%keN$7N|P;SN8yt2;k7#R3OB-+4p90Au*1XSJ%^ z$!?rxJCgny^n9jhDf3g20q`#3R@CuWYjMdM>pPFpWlQ{JI}wDB*szv5?Gt>T9OPSpS3uC(x^**_W17Rg@9h<&D~7E&`>S#dH`S5latwTbYr0EPo=Qwg+F=Dt)&x z2DKD(!zQd$)r-MIJ;xo4o+*6?=Z%uo;o_UX8f+Iv_UC(e>x2RRdXPpS^7#KmJp@O| zXjv%LwP$n965sDoSC;s49y#6o362}Nw}wL%==>6XqipPJs2!=}vVzByg^)2%J<{~A z@SYuHv4&}rY>~Mc`pN?wZbm7_pP2+t-F(jb8!=u@(0OXcvBmgk#@s~WcmR-1Vm61; zCeGG0EZH?9y^6+mv}meYGR~LX)Rz_zTacQnoBx$#JNK^OP&0L&ntJ20AO_W!dSf$M zY7rnTA=O7sN^qyzs}H2lKM$|nKc4>d>L>4RRSVPSb@RkeU>svG)1FLZ;N*tOyf9)1U`=VctK_Mr!Azudwi;vyK2UIXB52-seB96A!N z{4V7>hCJhy8R`P&@4g;C=ZIhJ#yP-ziuesyK1%#z z^_M8w)B#awDnmw{999%d>_oCm>pXVnc%bx*s@#X2K88437fs>kR3LM)!)p{$C)<6z z8cop`L7Kd}sJ&;8OsCS3;UK%P;&pA^%)e?U=llkREV$NP23AxsipeM?q>;d!Ry{C# zh#&^@62qPg5SeHj@X6#vHM|BRkNGBTT^kn>8bKT!4O1YO7B$_$`bC*|^J6&~y1Z_& z+iAK(pT&9V2FuVPfk+Gwnx}0Jr=7yd!8tKN=Bs%NWbH?c=$f*JOBC@|;mwONL1RHf z@(^h4r()6-cqz8NYgudS=76KX7{AOZlR+iiAwy%tlaRZSN$`6dLhtu}sK(aYY$2|7 z$t0E)hz{hKEj9rT9fmLCJ{E&dSl8%@0iW58s?9u%r#r(?Xy74$unyK8xgI)OEay=k zTQ>fxcc`$>Qyx8HPe5V1F8;ybDOFECLmWp6AwGqX=O1yd$Aqu&cFZK8p~0z7?r@}6 zl+PAzV1l(V+I5@kUR$;x{WO|@gi>wPOluO>D9G3}Ufr~`F43vR6p{7;?TW280H~5R zf(CHMk%O#+bRsWHae0xD=C)(xkqak>6uL?6?!RUr2!|m2Ca927T!;$o`DjqU*2d8O$W&FWyK0RmKk-6r2?!emE4Oau zN3Trd+MuG;w>>;rmt*kIr8HrT%dPGMLx)M@DXC%Q;gy52JaFsgS=ZZ&N8B-MgEs{t zG3LplI3}~AMYz1)z(!8xV^iaJzqhkdY%iXZ?l!{|Rl2cW73A2FzFmSnctam8^Lp$& zT&B-RCjMO?H;g-+^jy^V7BfXNcJ|7q>(!xZkcwuH2lIRRWC z46*WTLv`DeOowI$(z?cOE2L9KP1A2vmRJcxr26G_*$T z4KhuynEY8Y5Dp5SX<9>#NT30JH zw(azj1v@MxifBMO-OP<>s|b^g)2v<=k(ae6&Eq_4pe9hvJ=%>)@MONMnv48bUbS-F zmB&N=D@g%8i-m&YD?3nsWk!8X*rJtIGxtoPlQ}0Vp={>eo-kH$rpBe~87)(CIa4;V z%VZr%2nLE9AJ=Ft&DuE)GSHj4g-cQ2^jJ;+Y2l=1?TSx& zM_pk*Uk3LLV2E}!4CdAFEz1Kn|BNvTwE@CU4-&o35`!t_Y|w%(Q=p4>Wfe7($R=sA zb!py}HKU7+pKz>xoPQFX27Sz(Z6Sb4I6soibJ11r<9`D-~@XHY|`)M(> zX{5cM2JOJY7tvY1P$y=$jU{0c-|Qek4FjbW z8|=~w@Qi*g;^XQu0mx~xm@tOAJH4A=0!)lT)9Mos& zqbSsKL9qoKqE3CFoVA#H7vmSf!+8H)3#vtsFX@1~dCkT}RiErT;__G!4-T#4RcRr@ z)*sWr&{HYwO^#kO%NxLg74c?t?{9t)u2T>1Y% z3{XLvo@$LCD{{)!#UN!>-4De*%PHbfp3!0BW~zG(Ga4G9W;H@l7!Vp@Fd-PIL2PQO zT?%HWp2?m?KkVRmE5Ob7Fi)jW0A?H9t8;N>VNXus!b5|$WTn2k8wl|^#wmTS!?PM) zm1JmphWjiCa$N%^;A`+AE+~2>AbT|s?o~!xYFhUWSX~6XgppmnPkybg{Ll{QCWzH{ zaB`%lPt0{7z5~9QwlE=J>NY1QAyEy{cz)hiWngo()fmw&7Vli&EU#a8aPELxJ(W?j!lvKrPznA>^iHMI73i>PeUz)$?;9Uqe(>-?O_iHzzKpbKtO1aOln5%E zg+=50Sk{IJ46LgCBUn=TO1%cNqAsJuF&5-yTs>|(g6OB)Yr3*~DPO4DD*>p!_!ynq zEW8V*VnmiY!>dgFtW)AF@iaET46Eq3GAMP}HNidxBW+VH+iV4sp2ruVeGCLgz5Q1Pa**!%tY6(Vo_By^z8^;FK6C~n&xMWQo{DSp=z5>vL zek2Z>EJE~BD{ZP%sG6y-H|A)pfjF>4*H(!@J2al+TSRpkts@c3v`ER?frz(+klsmS_}*O0&ddhoVaf5o6mLpp<5IoI|XnJdCv}ktWTn&Bs7zjik*9%IFZw zabzff%(E&b`~Yr`aEE?*@p>9uX|$~+8^{mh^W%AsYsb zNp0=w-_?iP&n+X`%T*36;l5WW#PzJmc@B0=zVt@d&W*iYt#13*kkzGS$wE`G(>i#t zk?vBh(m4dnv;}0Uen8fa?wtd>muiY&B?2pULW7^mAsYPEGZt0P5*^+=vve*UKAI_T zE=`FwYK3XXQl0Ev*(8!a1pU*;qwAi|?zh@l8}c&kuBTbbG!M~fajBzn~SbE5l&5Y zq8=n_a~-iTIQu@Q7ZPT`1+_vO9ob7#6<5GiF9n2-2E`C%(YQ`4PY?XHbJ6%U>XaAiT3Z&9%m7#Y?%?zieRgQz!>vz zw-}TNESnf*I)jEWeb_S`CVy~mvCujifpj9xRro;w0zR<8sRKhw#hhERZ5VIT)HwLE z>G0m65o(F_kpy47e2e0^T`JK_n^*(q$DH7>RxroG=0) z!8FAOXsK)c4eEmNm90(ahwH!$uRS8jV&4%vN8AEgygA}@d97)mvW^w+scax-ABRVxt zN&!#H$zzi48X`;gBqk_m1RO`aLSHhL9Z$FlxJe2MWMnq-GOShT3v;QneaVcbTefLw zc{Fc!_cEx(^Ri!Mt`l6(d>1I?x0_%t>zH=x*Mn(Vs2Hw}D(PeiQ>`XX!ie7%2vO&n zj?=D;RUMttL~e_zXd+1eZ;Yg>5jLtq0IS22QoJqI01VNk^++uzbEujl8|wLVrj_pw zsio#mF`vYA#S8X3hRy-dO<<51yuOf~zY;3ghRpkPuo{9;i~Sky0b zif!>s0ldXW0!{5Z)5kRd32mXSf^{oIB&M;yj4>EFct7fc-85k#0h8Ow@ERgj2NKgf zq?w?3-8_mPn4AYPWnd>(I}#F_DDD}ud{*us7@KSHVS8Ql!Z@ZEG|!PDIc1=9VzHtr zsZvb?zoC#SP9my-OUO$Re4d{0*F~r4ewvEVrHKP= zGI>vAHgwp_+>rcejHIc#bPCM_rsvNg>+a(a@;wEc;#x^ISIrRBHe18&#GI5{Ea69< z(g_B9M2)L0f^R>oQLyw8p$-i$ih@j{D@L?n^tVwM3q^vfgi{luZW?Xdtz3PTtNy6GA$ct z-KxUf(pOvlP=LJK10`P>jJP(B$H#+}EY)kXA(j_wX+F!^J7;l9%Ab_ubToP+95x$2 z%rNF~p|g_NST|R0T2wv2y*F~GQk`GQZ^DIo3scCWvMHf*(j&q#GIBMxLo4`B6^ud; zw4$Ra1w0)Ig`l1W^iIM|onf5HLBCYH`kg0L!#6QsqA>AB@~tA7$vHAKNk|6hlsm0H zwdsR9{nK&V3Gn%L}XJGxeaL=8qf%jzP)2mjXI(p65Do&VIi&wU77C5^s6_EoBB+S zH?%pw8$-IatsGF4gom!>t4H^f%>;we^3}uoIW#734Gpaw(NJwVI4&~R)u-sQao8;aR+~+XV@DWhn{wCiaXl&_+-5`jNJ2b7 zvf_@q+UhQNs%h$yzQM_zOC<4k-58p^tJez}H@}os{_yW>n}x`$vQj-Jl)2TlMd>Bg z29#usM1;Mhr8g-rrm729jz5Z=(c?t2aNm^KH2YZQUgsN=?L)~WC@HvWgVUnc?QFsh z*mk~0&d8i3reLbT{j~XnrGRfj)xoVbzI}L3kX#q*pPj1NaIPUL@2GrT7F25`7JZc7 z5g4rq;1qB=OfXlDH<5ttfGT^MdL|}O+O!uo9Wf#_;lXtntUe?m9yGBzhPHR*4S$WE zNg$7o1Bc_K^K56)J%OlhOS4o?X|WXFL~}gN=<)Po!mMFQ6F$rpot>&@uck_hDyFvDY8^Au(c2{9 za9n*)bhx9Q0`?Kt5_=wKN=HT{?SKG?2rU?7OeD)Rcw~Yl*+_w@d9?y&7eHL^`O*2c z$jAE$2)(GTh&+0O5`VtFGXT$CkO9W3>&CRa#tT7YsFPmLI3&3syJBF=u`WI5L(?-_ zZ{BzYKrH48A&agm$X~sLB&`PM3oB0K+EV~UNPfwe=1(|d+ zbr3NMqQ`s0Stj|c_R*Tm3ErBdh{;Gg2z~5i#MF(nFjfX<=e3n6pUphoT4kXvh{}K^ zRjImVQLx2HZVyJX-Rj%evunpT7=$K+KcCHbY7{g^wg4y3P0-7?bKAxI^lmm)sM@H@ zj73VS=+O|d?7ChCM~nEql3_RV|2xJ$i%MrF=C7OA?pRbQcdJ}w!V#S(`LJ}AAs^16 zK57O!V9-!g%^DAmKesfXBCwu6d(q7_!KtaCtO&+pfbONqAe7ZaRiXHas5bJT`tV}f z>`rRuo!rUAsgt^S^~|E`54iU;9O|IXQzuu=D+NN3sNOPfHdR&UydAr8YbTZy#DJmW zMXRBCt6>bB3HQs_LQW9eZqLn9bINHKBNUz(grK zs8SFW0JaO#1(Bmds2LN{gQLPu6C)U|wA>mk&boqXJ_y_zKzEAKwTIG~-igbTX4<;RGH zYT_rZ8ycC&8}+H-_i-d8P9_+S!hSZdlC4SB2ryo2pF>zeP#_O4-`pminK%dLVH9oOL8wrs zA08fI?UhBRuBaWpLdu{dZ+K7-Dui59A?(A$sqT&fu1IE#=yp+i15E82# zuSrrQ3HCvn=pk1)qu6Y#d&zT`EY0LJ(os6psYiZn5TBh|6sa1(Z?XbO4l+|MG6fNR z8i|eoN+X(tNzR_^Xiwo#4tGizISDY?OAtd2v~+BrrL9z{yo&Vi+n~yW9Kx~RdikQf z4(?~Ob|G|`Nvq@uq3b0%k&xWU|9TxHbG=aBA^W!Fshh|N4K^Q~hCnecHk;QQLlr8d zB8bH6d~BgTw1p1X+|cYpbvl`g|KOB$2BHuzhzH@TJSQ)=PxlM>NtyLkAZYIf~#>0Xn?HvLbZ#* z=Uap!iJ;Es5RLo{h<^#c;kPH3+#zyDoW}If)VSdO+HZcgsp+x(5}kf?6um8@`+=-H zI*DS{mXDyS0&Qcy!Rg45gDV*^IR#3hasYV*x)gwKIuxv4a1Ssh$3XKyhZk1!bKAzg zR`)iow(8kP1KGrdIP3BK$s>^VLi=~yZ0Aw2H)1l69;XA+4|Y8@JDPfZN3SQZ-m`*&{9R!BImXloITRqL-pcrg)B zK_Le&;A=siq?afdOME=$`eq>AfI0aBtjX6;&GInVMT_QGX5NeiHD2ftx*ea5FER+( z1)!<(96EkH7r@&PhVJ1IaK8AeI=J!ljdPJgJ}JQFS*U>lfjc6j0!LCb1`Ez2^4Q?l z7qxImf@9xC$UOpqMXzkFV`RINY>-X3U>c1}s@mSnf;2}mnk*k2A$B2{jTV-}gVe4D z2Kk%8A8LnqV-h7z{mw+hH%sroHCV2g@~urZx5`tShg8FP)~k;{q>APMRwKuxzbgpAt|f@lgyYGBoB@6)q;rW3oYxqLAd1T)F| z$D0aSB@@i<;IQJ*pOghD3{OwLb#wB4i>iMH$RFWQr8>V<#AWf!YXi_p9~S;@*@9#Y zi&d5Y%^2kc9>t_I)!MM@`L_7??bvBlnM5$LL?`zUQQlrDlbN@8cC3rFr8m`+TccL6 zS%dAV+H)<;lx+z$=3rPCAAOT7@uX;a9Bljja1SFj&j{o_R<7eH~^JnLZZQ&gYO_rgh7dsTVVfs!V zLDjI6g&i||;#|x+w|#4(Kd=wkhQO;n)hc#h_CZbDPm|n-*B5=bF!ezSJ1F3PP zi}Iq>INf{^$37a?$DxMlJT>m#V*%K6sX;=Pk2ft@u|h`C!A^ul(kCTk(JX!S=Q#{B z78BOv-b&5V&A;Nf)2Calr@EC%;&H z64RnTubUs@cn|kp$Dz;Z{8D~1Czp22N$7ly{-P?`)&O(Rz8gX=2~Cf+rw90G<%OA) zy<5~YSzC9tY|$Mu=fO8@Ari3kOKYG)_*1DcOi2Sn194;$dCJvu)g@Bv4XmdY*O1d#n>3R>yfgIGWw2qMz!q>d2*V*`Nh=D>oi z=CDDkEdZvXwj}LqEm@C2Kq^fpD!r^2Ej_S@^P4%U6RW3Gr&ceow)MCA+t{J{(a}XN zXaDsd8Qn83B7dB}ep}s+bCb(`%zoldTHA*X9rK}iE9Y0$ym|WH@!-Wzp4Zizg9dDmAQuWKvP+0 z?p!8c!jr1%at;zYX&`Q8R}95=yr?>%(pu76XHEd}u@&3dlq>E%v0_0gbK@i@#C_&T zr#P_WIqd9n0oS!%+fykYK6;`h){Ez#>h+{r!Mc>|AD;iD>T=3=op_p8RKMlk)2z+C z_WsjsI{TgKbZ_@~>0PH+D|l|0awVm#zQUHT-*<)=?tkg@Gaae++*h9I-1uLdx4@fj zUpRk(w{25i-gP{$RkJb$U;eTMUOV;3eG4k3!yLo7yqfar6V9x(AL7cRq%5Mm{vI}5 zp`2gg`l9TgkNbi0p%a4--hL7^k@Ih}4qzkY&l119iSprO+qmU45)g6zzB`{%Nmlxq zlg_UG3+3;yjCYjsRVO^Tl5=Ls%g?PoOL_35^D5Eed)T<{Yn0W@`PDZlwB3h7xRgNX zo0Pvl?*e1j_?;JcJG|ez`+`b)!2I1Q7drRwmb)*kl!EpEoLbqTzJJ0+UM;Nn9)*9s z>;8)>x%rfgNM!Hcc^6k7rQVHqpHqE|@+hOf{6x%{VAhqC{{Z@2MfnKj63XjA#BRz% zlq?Lc{+e_R9$<|nXo089P;{-6Ns+Li%;Ce6D|BY{)aR$Lh+_{!g z*!N<}i#5>T(RaB21NWU$eT(wDZeo9)atvG zM_+zQ^#Z=L=Y%I!U*Y_>Pd}^rDrLnMlm9|_Ipx`<-?R; zqx@CsIq%N1sxNbXANBkZ=kMbDW0aR1J*)a{%J)#VX!pNRF5tZTuCuDAP+m_tpYm0d zCs000c^2g@cb`@5I0;V`^;DdHit|O3YwtO$I*IZJDbJw%4a$X-3-3Lvnn!sRCBeMa zgOrOYze)LIO0D=lner9XbDB>1JCox=uKg9qznp${wTbcv&p5lob1 z3}qkX-%@U-+;ivI)kTy?!QWdb|BCZlDbGH7cJ)J)FQPm^`74xzlz&aRpK|S8XIDd% zZ>8Kz`A?K>%1w8lUG-AlOFg5MC*E^*^~1}TwMJ(%ANNtu0BqAKjnKVAE5lNl=GoipP;;k@dYBsdt`J{SDJG{sl$TPj;Q6mmE_vCx)mJI+p!`?LH&K3_@37aN%>{UuitxabvET$_nudMlk#55Z&SXH^1GDZp?rjL(aX-OzDK!)^81u~DbHbk zf12`K%6ndZUiDP2-+a$`)%l!%^1kz`r%`I7(!b^Uubp~6D-D^mPddLkm$HxYJj$P; zJfHGUD9_;emz>6Ry~q3C;rg4O#CA}We^0rX>nFe;E~Xqj{er5C@-E8HbASBw^Q&&o zKh614%G1xdpt^#xnz^7_MtKkA<&@v2Ttj&={Dn>Et6$>z>nZ=?t_!OH%1_O^sJemj zGWg1kl)EW!qI@UiZpsQivy<}MM=z{yp?n4Bdnm{6YE^@j3-4}KdntEO4pF}4{#Lb* za@{Li)yVz8(dy#rX|(&#C||<)GWgVqoWJ+s$oRx=k@7f?=6UP$@-lr5fn zgmOP6J*Zk-f9IVSSHHpeS1Esr@~)#7S8tUY95HE?V#*S1bWZ|? zgaI*fF?$IJV)A16xfHWE<3D<0CBaF;i3A-9KN55#EJ*;8upvQ4r35YsD-uv7oJi1+ za3R4$-;p3EAx#3CgfIyL5{e|~N!XEKB%wrto`eZ~Uw~B|x+VcZbxEL8-MRhhtAs%v zs#k)Bgg5<`@Fu}6w1s?8F(1Oo|-`o7vO*a+6@uf|{ZavO8MRhOR2W3O-Md+MA1ghM(I7Z7#$ zvs6X7g0stX<(j47;;EZguhrdIe~&&YG3++3etF()R=E_Xoa!KWnO}@`6@D~ELG|G* zHq`wq%yT@Z=<$wiwYb*OHOyjrNv$YzCzmeOr8)?k3*EXfeOPhxoGs&QPkU;3FTW-2 zIXx7=)%HEpBe(HWQakYwvbE?9EwtS`*u$cDPOjC-KH9m_KlZh0=vMvA>F{u4XmjFu z)ox#nRR7*rh-UTK7rl|WsI>o5M&51Z;PpTuF9n5EW6Hu-l zPS@n&;`B$0Q-D&PRL%H`Z8$*tb^POuVdH7C1yNxuU#iW+JlCBs z5<`hakt@sc6@^D|zA~R1r{-5{^SSq*oTTs9rYl-r$qQT4sd~3F|Bm%U$-8d&4Ct1E znUB!6-8#vL^Fp7b<_vNY$(%>1?*L~9h?ihhZTrQXk8mFFonKA(xAG-Y;g9Nse4jjX zH$PKfUY>qpOyb5Xb>{d_IcG?(v`Y0y_BbqM;GZQmP>vrA69?= z0Kd-F#bP%YE@_7XE~PyhxX|K)l5q0#QvDRMJE7|4Cn?RFq;9G1mA+mN^je+DOT*ji z_$@T%=d;pJhy-e~P8=2r#ZXqdjvz{&-p#c`?c4cvE5B|U8sW3*#|w2GhqGwjVEjHj z!S4h7e&X-{A_%NF7jc0ji3?lrnBDhzpc(rL^Y0R7%>3%pobiLdgiZ3NG`?(GiYzIn zT-MmhZ_6pvWj(U#$aW(ejchTpxyUvk8;oo%vZ=_TS#n@o9K1mj!tNyb(_1%cYvIXC z)+C#fEDH8&ef;3?VG?OaIGUTVMQf6#EYIz1C=ky)kEjI#fGo*2PqzAI$qo|6ikdgE zAE5W#mNzTHRHrNe7SFhe$pe<0HH1M}lDWd5@4i>~oYg6G^frYwwhUhiu{ zYHahe!OYe&o62k_W9X3QWFfP8%p!1Kq~qEgtWCoDrpN*(aI1M!Hl100=_UJ?FR=r+ z!kaAT?!wV9I@H49KY7FoiL&OmXpWpv)^>csJ?$-JLkak^VaB&ZoS3pKeI0>%-EBNQ zHyt?Q6|uWg+@H0H%`5^S8^@GI{NCHyOxCe?`ZVuMSrVsNk4x3h4wpz$b2#L5!6~{-MTr`CS7ZDPm0)iu&u)6WDS&n3;rop=|S( z)Ft3qU$8?*8-`nxTL50#UG^%rSo0l=GLatg`FWV zNY6=EdL8?0ZE7|G^k!9o6f& zu4|*NExI=6+LmiWZdhGq!nOU@##>u%ZML<|)&^T!Yi+8vUDZZfTWD>bwQbghSzBdo zlC?dS`G(6xmM5F9D%q}VxU$vCCM(;kY^<`S%GznsQ4k_a^Um@zWjm~O!7ZZ_Bx7sS zzN{We2~IaQ^!9J*Q;G{klG^NRn=hcjZ6j-MB~!2Myf*SyvdG%JYum03ySD1uq-%Sw zjk&hu+Kg+Ps|~oe-r96)yRD73w%FQSYg;X_z)d8ps!g=E&)PU^%dE|^w#nKcYiq1c zv9_Dq2x|+h&9An-+VE^tMr-{$f#SXwx8N~YRjq3rnZ^dU}`I;O{KPz+DK{(sm-IdjoLO+aze8q=whr+oV4N8R#Tfe zZ7-#9=Y*xyW>VWoZ6LLE)TU9}MQwDlQ6YT*!L$|iZySP+f6I-mp)YJlA)_+k^POL zB$g$su}#Ld7u#5DLz#sL@w*z%vLD+BY|F7NX$fRS%4HkufEn>)@a3J#c4z~$t;;qo z+jN#*Ja+y88J=x*26-+Hd$Wzrwlv$y*vPmot%R`Y8EEscjLx<=+uUqhvJK6)GTX#l z{m5NB@)G^?1pj}6|3AV1pWy!r-Gj!c3OU2=y6C*`Yp5 zsgT1zr&P${Ur;LK@DWOd9Da{dA&1|mRLJ3@lnObl_N{$bA&2uQ6>@k2r9uwpQ!3=} zL`sDmo@k3r9uvGq*Tb^O_T~b+)b&F!<#7;a(D}+LJnU*DdX5nC>3(} zpC}b__){7XuO9DbitA%~ArD&(-*zxH8;9L}Ru z$l(c;3OSrlsgT2yC?z?cOsSB=Qz#X3xRg>MhnG{zigpF1LJpTvD&(+-QXz-SDHU?K zf>I%eD=8InxQbFChpQE-jZ>Chp;VqO3IeY=7 zLJohBQik8tM%O;9ki*j{6>@k6r9uwRq?9Rb5v4*7&!SYw;gcy9a`+TVg&bZ3(}OiG0uuA@}Q;nkE1Ib2Vvki%;z6>|72N`)Lgn^GZ%&!JSv;R`7ha@eMnv1~7; zOksnR3OO92RLJ2zN`)Nmr&P${0ZN4&4pS=R@RgJbIeZnRLJnU;DXZ0wQ_4d16O;-$ zd>y4i4&Ozoki&OVD&+7zlrmWTKBa6{|Cv%Dhwr6SsNt)}zyqw9uc1^(-fJlplK113 z3d#EkN`>UTj#43cKS`;Oyq}^}NZxk#K2dbn4oXFn?b12ULhXs7$gbNd<=8t!sfeq? zx=y6mODN@Z`%jdL_TMJY$pw<+a0`VOU>M&G5BtLPC*MUH)sQW1LJ zr0fW1^as|%5Z>E z_S2gv6}`8cQW0&pP%2{Z#)aF@Q&ik;N=5#C;-0_0P)iLyNhxRXpHOP4!JkrUsllfx zwbbCxD7Dn!)0A3jFmdFCD`qHPOsS;?layL&Fik1v@a>dZYH);7OAUUJQjXR)P|DZ( zMoKL;_$5ktTt7sqr3N3S)KY_wP->~cM=7<`;EyQfMg1719H|ddYN^2=Q);QfcPO>g zVDgR^uFz70DM~q`4pPb=HBG6d2Dejcslg#iEj2hysig)-D7Dn!4oWRG_z_AiHF!Ox z{8Yb4sig*Qpwv=>H&SY;!7ow%Kkb`KN`e6tMt^J?XqXFd6&2=dE7ihWJ5eDbV%ynrr|mr(Wi3#uM}L)GIIbalLjs>jFVf$DJ@svc*c>hTFwJ%%NTi_B@JuXAld)$_?U+`t>C zdR&95$B}b~opZP3o%VMFdU&~m-di4^7VjhUVA6#e!|zaQcnY!M=Z&@%du{F@cysOduxk-wFHxe_1^V literal 0 HcmV?d00001 diff --git a/app/Help/CiderPress.cnt b/app/Help/CiderPress.cnt new file mode 100644 index 0000000..46dc98e --- /dev/null +++ b/app/Help/CiderPress.cnt @@ -0,0 +1,56 @@ +:Base CiderPress.hlp>main +:Title CiderPress Help +1 Introduction +2 Welcome!=Topic1 +2 Features=Topic47 +2 Getting Help=Topic284 +2 How (and Why) to Register=Topic46 +2 Credits=Topic14 +1 Using CiderPress +2 Available Commands=Topic48 +2 Commands +3 Selecting Commands=Topic52 +3 Opening, Closing, and Creating Files=Topic51 +3 Opening a Volume=Topic241 +3 Archive Info=Topic258 +3 Working With the File List=Topic50 +3 Printing=Topic53 +3 Viewing Files=Topic54 +3 Adding Files and Disks=Topic55 +3 Creating Subdirectories=Topic263 +3 Extracting Files=Topic39 +3 Copying and Pasting=Topic273 +3 Testing Archives=Topic56 +3 Rename Entries=Topic42 +3 Delete Entries=Topic57 +3 Re-Compress Entries=Topic58 +3 Edit Comment=Topic43 +3 Edit File Attributes=Topic203 +3 Convert Disk Image to File Archive=Topic215 +3 Convert File Archive to Disk Image=Topic216 +3 Import from Cassette=Topic111 +3 Import BASIC Program=Topic112 +2 Tools +3 Disk Sector Viewer=Topic3 +3 Disk Image Converter=Topic187 +3 Bulk Disk Image Converter=Topic233 +3 SST Image Merge=Topic201 +3 Windows Volume Copier=Topic245 +3 EOL Scanner=Topic272 +3 2MG Properties Editor=Topic277 +2 Preferences +3 General Preferences=Topic19 +3 Disk Image Preferences=Topic259 +3 Compression Preferences=Topic29 +3 File Viewer Preferences=Topic23 +3 File Preferences=Topic28 +1 Appendix +2 About Disk Images=Topic18 +2 Embedded DOS Volumes=Topic21 +2 File Format Converters=Topic22 +2 Disassembly Notes=Topic109 +2 File Extensions=Topic45 +2 File Attribute Preservation=Topic68 +2 Compression Algorithms=Topic69 +2 About Removable Media (CF, floppy, CD-ROM)=Topic244 +2 Administrator Privileges=Topic262 diff --git a/app/Help/CiderPress.hmp b/app/Help/CiderPress.hmp new file mode 100644 index 0000000000000000000000000000000000000000..a26964fd706306c569d91b143fcc3fd6b393f3d5 GIT binary patch literal 329108 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 &{sug%NdRX3b@IUrN&x&>N1s> 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 + fFileFormat = pImg->GetFileFormat(); + fPhysicalFormat = pImg->GetPhysicalFormat(); + fSectorOrder = pImg->GetSectorOrder(); + fFSFormat = pImg->GetFSFormat(); + + if (pImg->ShowAsBlocks()) + fDisplayFormat = kShowAsBlocks; + else + fDisplayFormat = kShowAsSectors; + if (!pImg->GetHasBlocks() && !pImg->GetHasSectors()) + fDisplayFormat = kShowAsNibbles; + + fHasSectors = pImg->GetHasSectors(); + fHasBlocks = pImg->GetHasBlocks(); + fHasNibbles = pImg->GetHasNibbles(); + + // "Unknown" formats default to sectors, but sometimes it's block-only + if (fDisplayFormat == kShowAsSectors && !fHasSectors) + fDisplayFormat = kShowAsBlocks; + + fInitialized = true; +} + + +/* + * Configure the combo boxes. + */ +BOOL +ImageFormatDialog::OnInitDialog(void) +{ + ASSERT(fInitialized); + + LoadComboBoxes(); + + return CDialog::OnInitDialog(); // do DDX/DDV +} + +/* + * Load the combo boxes with every possible entry, and set the current + * value appropriately. + * + * While we're at it, initialize the "source" edit text box and the + * "show as blocks" checkbox. + */ +void +ImageFormatDialog::LoadComboBoxes(void) +{ + CWnd* pWnd; + CButton* pButton; + + pWnd = GetDlgItem(IDC_DECONF_SOURCE); + ASSERT(pWnd != nil); + pWnd->SetWindowText(fFileSource); + + if (fQueryDisplayFormat) { + pButton = (CButton*) GetDlgItem(IDC_DECONF_VIEWASSECTORS); + ASSERT(pButton != nil); + pButton->SetCheck(fDisplayFormat == kShowAsSectors); + if (!fHasSectors) + pButton->EnableWindow(FALSE); + + pButton = (CButton*) GetDlgItem(IDC_DECONF_VIEWASBLOCKS); + ASSERT(pButton != nil); + pButton->SetCheck(fDisplayFormat == kShowAsBlocks); + if (!fHasBlocks) + pButton->EnableWindow(FALSE); + + pButton = (CButton*) GetDlgItem(IDC_DECONF_VIEWASNIBBLES); + ASSERT(pButton != nil); + pButton->SetCheck(fDisplayFormat == kShowAsNibbles); + if (!fHasNibbles) + pButton->EnableWindow(FALSE); + } else { + /* if we don't need to ask, don't show the buttons */ + pWnd = GetDlgItem(IDC_DECONF_VIEWAS); + pWnd->DestroyWindow(); + pWnd = GetDlgItem(IDC_DECONF_VIEWASBLOCKS); + pWnd->DestroyWindow(); + pWnd = GetDlgItem(IDC_DECONF_VIEWASSECTORS); + pWnd->DestroyWindow(); + pWnd = GetDlgItem(IDC_DECONF_VIEWASNIBBLES); + pWnd->DestroyWindow(); + } + + LoadComboBox(IDC_DECONF_OUTERFORMAT, gOuterFormats, fOuterFormat); + LoadComboBox(IDC_DECONF_FILEFORMAT, gFileFormats, fFileFormat); + LoadComboBox(IDC_DECONF_PHYSICAL, gPhysicalFormats, fPhysicalFormat); + LoadComboBox(IDC_DECONF_SECTORORDER, gSectorOrders, fSectorOrder); + LoadComboBox(IDC_DECONF_FSFORMAT, gFSFormats, fFSFormat); +} + +/* + * Load the strings from ConvTable into the combo box, setting the + * entry matching "default" as the current entry. + */ +void +ImageFormatDialog::LoadComboBox(int boxID, const ConvTable* pTable, int dflt) +{ + CComboBox* pCombo; +// const ConvTable* pBaseTable = pTable; + int current = -1; + int idx, idxShift; + + pCombo = (CComboBox*) GetDlgItem(boxID); + ASSERT(pCombo != nil); + + idx = idxShift = 0; + while (pTable[idx].enumval != kLastEntry) { + /* special-case the generic FS formats */ + if (pTable == gFSFormats && !fAllowGenericFormats && + DiskImg::IsGenericFormat((DiskImg::FSFormat)pTable[idx].enumval)) + { + WMSG1("LoadComboBox skipping '%s'\n", pTable[idx].name); + idxShift++; + } else { + // Note to self: AddString returns the combo box item ID; + // should probably use that instead of doing math. + pCombo->AddString(pTable[idx].name); + pCombo->SetItemData(idx - idxShift, pTable[idx].enumval); + } + + if (pTable[idx].enumval == dflt) + current = idx - idxShift; + + idx++; + } + + if (current != -1) { + WMSG3(" Set default for %d/%d to %d\n", boxID, dflt, current); + pCombo->SetCurSel(current); + } else { + WMSG2(" No matching default for %d (%d)\n", boxID, dflt); + } + +} + +/* + * Find the enum value for the specified index. + */ +int +ImageFormatDialog::ConvComboSel(int boxID, const ConvTable* pTable) +{ + CComboBox* pCombo; + int idx, enumval; + + pCombo = (CComboBox*) GetDlgItem(boxID); + ASSERT(pCombo != nil); + idx = pCombo->GetCurSel(); + + if (idx < 0) { + /* nothing selected?! */ + ASSERT(false); + return 0; + } + +// enumval = pTable[idx].enumval; + enumval = pCombo->GetItemData(idx); + ASSERT(enumval >= 0 && enumval < 100); + + if (pTable != gFSFormats) { + ASSERT(enumval == pTable[idx].enumval); + } + + WMSG3(" Returning ev=%d for %d entry '%s'\n", + enumval, boxID, pTable[idx].name); + + return enumval; +} + +/* + * Handle the "OK" button by extracting values from the dialog and + * verifying that reasonable settings are in place. + */ +void +ImageFormatDialog::OnOK(void) +{ + CButton* pButton; + + if (fQueryDisplayFormat) { + pButton = (CButton*) GetDlgItem(IDC_DECONF_VIEWASSECTORS); + ASSERT(pButton != nil); + if (pButton->GetCheck()) + fDisplayFormat = kShowAsSectors; + + pButton = (CButton*) GetDlgItem(IDC_DECONF_VIEWASBLOCKS); + ASSERT(pButton != nil); + if (pButton->GetCheck()) + fDisplayFormat = kShowAsBlocks; + + pButton = (CButton*) GetDlgItem(IDC_DECONF_VIEWASNIBBLES); + ASSERT(pButton != nil); + if (pButton->GetCheck()) + fDisplayFormat = kShowAsNibbles; + } + + /* outer format, file format, and physical format are immutable */ + + fSectorOrder = (DiskImg::SectorOrder) + ConvComboSel(IDC_DECONF_SECTORORDER, gSectorOrders); + fFSFormat = (DiskImg::FSFormat) + ConvComboSel(IDC_DECONF_FSFORMAT, gFSFormats); + + if (fSectorOrder == DiskImg::kSectorOrderUnknown) { + MessageBox("You must choose a sector ordering.", "Error", + MB_OK | MB_ICONEXCLAMATION); + return; + } + + if (fFSFormat == DiskImg::kFormatUnknown && + !fAllowUnknown) + { + MessageBox("You must choose a filesystem format. If not known," + " use one of the 'generic' entries.", + "Error", MB_OK | MB_ICONEXCLAMATION); + return; + } + + CDialog::OnOK(); +} + +/* + * F1 key hit, or '?' button in title bar used to select help for an + * item in the dialog. + */ +BOOL +ImageFormatDialog::OnHelpInfo(HELPINFO* lpHelpInfo) +{ + WinHelp(lpHelpInfo->iCtrlId, HELP_CONTEXTPOPUP); + return TRUE; // indicate success?? +} + +/* + * User pressed the "Help" button. + */ +void +ImageFormatDialog::OnHelp(void) +{ + WinHelp(HELP_TOPIC_DISK_IMAGES, HELP_CONTEXT); +} diff --git a/app/ImageFormatDialog.h b/app/ImageFormatDialog.h new file mode 100644 index 0000000..1328599 --- /dev/null +++ b/app/ImageFormatDialog.h @@ -0,0 +1,81 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Dialog asking the user to confirm certain details of a disk image. + */ +#ifndef __IMAGEFORMATDIALOG__ +#define __IMAGEFORMATDIALOG__ + +//#include +#include "resource.h" +#include "../diskimg/DiskImg.h" +using namespace DiskImgLib; + +/* + * The default values can be initialized individually or from a prepped + * DiskImg structure. + */ +class ImageFormatDialog : public CDialog { +public: + ImageFormatDialog(CWnd* pParentWnd = NULL) : + CDialog(IDD_DECONF, pParentWnd) + { + fInitialized = false; + fFileSource = ""; + fAllowUnknown = false; + fOuterFormat = DiskImg::kOuterFormatUnknown; + fFileFormat = DiskImg::kFileFormatUnknown; + fPhysicalFormat = DiskImg::kPhysicalFormatUnknown; + fSectorOrder = DiskImg::kSectorOrderUnknown; + fFSFormat = DiskImg::kFormatUnknown; + fDisplayFormat = kShowAsBlocks; + + fQueryDisplayFormat = true; + fAllowGenericFormats = true; + fHasSectors = fHasBlocks = fHasNibbles = false; + } + + // initialize values from a DiskImg + void InitializeValues(const DiskImg* pImg); + + bool fInitialized; + CString fFileSource; + bool fAllowUnknown; // allow "unknown" choice? + + DiskImg::OuterFormat fOuterFormat; + DiskImg::FileFormat fFileFormat; + DiskImg::PhysicalFormat fPhysicalFormat; + DiskImg::SectorOrder fSectorOrder; + DiskImg::FSFormat fFSFormat; + + enum { kShowAsBlocks=0, kShowAsSectors=1, kShowAsNibbles=2 }; + int fDisplayFormat; + + void SetQueryDisplayFormat(bool val) { fQueryDisplayFormat = val; } + void SetAllowGenericFormats(bool val) { fAllowGenericFormats = val; } + +protected: + //virtual void DoDataExchange(CDataExchange* pDX); + virtual BOOL OnInitDialog(void); + void OnOK(void); + afx_msg virtual void OnHelp(void); + afx_msg virtual BOOL OnHelpInfo(HELPINFO* lpHelpInfo); + + struct ConvTable; + void LoadComboBoxes(void); + void LoadComboBox(int boxID, const ConvTable* pTable, int dflt); + int ConvComboSel(int boxID, const ConvTable* pTable); + + bool fQueryDisplayFormat; + bool fAllowGenericFormats; + bool fHasSectors; + bool fHasBlocks; + bool fHasNibbles; + + DECLARE_MESSAGE_MAP() +}; + +#endif /*__IMAGEFORMATDIALOG__*/ \ No newline at end of file diff --git a/app/Main.cpp b/app/Main.cpp new file mode 100644 index 0000000..fff6b88 --- /dev/null +++ b/app/Main.cpp @@ -0,0 +1,2710 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Main window management. + */ +#include "stdafx.h" +#include "Main.h" +#include "MyApp.h" +#include "AboutDialog.h" +#include "NufxArchive.h" +#include "DiskArchive.h" +#include "BNYArchive.h" +#include "ACUArchive.h" +#include "ArchiveInfoDialog.h" +#include "PrefsDialog.h" +#include "EnterRegDialog.h" +#include "OpenVolumeDialog.h" +#include "Print.h" +#include "HelpTopics.h" +#include "../util/UtilLib.h" +#include "resource.h" + +/* use MFC's fancy version of new for debugging */ +//#define new DEBUG_NEW + +static const char* kWebSiteURL = "http://www.faddensoft.com/"; + +/* + * Filters for the "open file" command. In some cases a file may be opened + * in more than one format, so it's necessary to keep track of what the + * file filter was set to when the file was opened. + */ +const char MainWindow::kOpenNuFX[] = + "ShrinkIt Archives (.shk .sdk .bxy .sea .bse)|*.shk;*.sdk;*.bxy;*.sea;*.bse|"; +const char MainWindow::kOpenBinaryII[] = + "Binary II Archives (.bny .bqy .bxy)|*.bny;*.bqy;*.bxy|"; +const char MainWindow::kOpenACU[] = + "ACU Archives (.acu)|*.acu|"; +const char MainWindow::kOpenDiskImage[] = + "Disk Images (.shk .sdk .dsk .po .do .d13 .2mg .img .nib .nb2 .raw .hdv .dc .dc6 .ddd .app .fdi .iso .gz .zip)|" + "*.shk;*.sdk;*.dsk;*.po;*.do;*.d13;*.2mg;*.img;*.nib;*.nb2;*.raw;*.hdv;*.dc;*.dc6;*.ddd;*.app;*.fdi;*.iso;*.gz;*.zip|"; +const char MainWindow::kOpenAll[] = + "All Files (*.*)|*.*|"; +const char MainWindow::kOpenEnd[] = + "|"; + +static const struct { + //const char* extension; + char extension[4]; + FilterIndex idx; +} gExtensionToIndex[] = { + { "shk", kFilterIndexNuFX }, + { "bxy", kFilterIndexNuFX }, + { "bse", kFilterIndexNuFX }, + { "sea", kFilterIndexNuFX }, + { "bny", kFilterIndexBinaryII }, + { "bqy", kFilterIndexBinaryII }, + { "acu", kFilterIndexACU }, + { "dsk", kFilterIndexDiskImage }, + { "po", kFilterIndexDiskImage }, + { "do", kFilterIndexDiskImage }, + { "d13", kFilterIndexDiskImage }, + { "2mg", kFilterIndexDiskImage }, + { "img", kFilterIndexDiskImage }, + { "sdk", kFilterIndexDiskImage }, + { "raw", kFilterIndexDiskImage }, + { "ddd", kFilterIndexDiskImage }, + { "app", kFilterIndexDiskImage }, + { "fdi", kFilterIndexDiskImage }, + { "iso", kFilterIndexDiskImage }, + { "gz", kFilterIndexDiskImage }, // assume disk image inside + { "zip", kFilterIndexDiskImage }, // assume disk image inside +}; + +const char* MainWindow::kModeNuFX = _T("nufx"); +const char* MainWindow::kModeBinaryII = _T("bin2"); +const char* MainWindow::kModeACU = _T("acu"); +const char* MainWindow::kModeDiskImage = _T("disk"); + + +/* + * =========================================================================== + * MainWindow + * =========================================================================== + */ + +static const UINT gFindReplaceID = RegisterWindowMessage(FINDMSGSTRING); + +BEGIN_MESSAGE_MAP(MainWindow, CFrameWnd) + ON_WM_CREATE() + ON_MESSAGE(WMU_LATE_INIT, OnLateInit) + //ON_MESSAGE(WMU_CLOSE_MAIN_DIALOG, OnCloseMainDialog) + ON_WM_SIZE() + ON_WM_GETMINMAXINFO() + ON_WM_PAINT() + //ON_WM_MOUSEWHEEL() + ON_WM_SETFOCUS() + ON_WM_HELPINFO() + ON_WM_QUERYENDSESSION() + ON_WM_ENDSESSION() + ON_REGISTERED_MESSAGE(gFindReplaceID, OnFindDialogMessage) + ON_COMMAND( IDM_FILE_NEW_ARCHIVE, OnFileNewArchive) + ON_COMMAND( IDM_FILE_OPEN, OnFileOpen) + ON_COMMAND( IDM_FILE_OPEN_VOLUME, OnFileOpenVolume) + ON_UPDATE_COMMAND_UI(IDM_FILE_OPEN_VOLUME, OnUpdateFileOpenVolume) + ON_COMMAND( IDM_FILE_REOPEN, OnFileReopen) + ON_UPDATE_COMMAND_UI(IDM_FILE_REOPEN, OnUpdateFileReopen) + ON_COMMAND( IDM_FILE_SAVE, OnFileSave) + ON_UPDATE_COMMAND_UI(IDM_FILE_SAVE, OnUpdateFileSave) + ON_COMMAND( IDM_FILE_CLOSE, OnFileClose) + ON_UPDATE_COMMAND_UI(IDM_FILE_CLOSE, OnUpdateFileClose) + ON_COMMAND( IDM_FILE_ARCHIVEINFO, OnFileArchiveInfo) + ON_UPDATE_COMMAND_UI(IDM_FILE_ARCHIVEINFO, OnUpdateFileArchiveInfo) + ON_COMMAND( IDM_FILE_PRINT, OnFilePrint) + ON_UPDATE_COMMAND_UI(IDM_FILE_PRINT, OnUpdateFilePrint) + ON_COMMAND( IDM_FILE_EXIT, OnFileExit) + ON_COMMAND( IDM_EDIT_COPY, OnEditCopy) + ON_UPDATE_COMMAND_UI(IDM_EDIT_COPY, OnUpdateEditCopy) + ON_COMMAND( IDM_EDIT_PASTE, OnEditPaste) + ON_UPDATE_COMMAND_UI(IDM_EDIT_PASTE, OnUpdateEditPaste) + ON_COMMAND( IDM_EDIT_PASTE_SPECIAL, OnEditPasteSpecial) + ON_UPDATE_COMMAND_UI(IDM_EDIT_PASTE_SPECIAL, OnUpdateEditPasteSpecial) + ON_COMMAND( IDM_EDIT_FIND, OnEditFind) + ON_UPDATE_COMMAND_UI(IDM_EDIT_FIND, OnUpdateEditFind) + ON_COMMAND( IDM_EDIT_SELECT_ALL, OnEditSelectAll) + ON_UPDATE_COMMAND_UI(IDM_EDIT_SELECT_ALL, OnUpdateEditSelectAll) + ON_COMMAND( IDM_EDIT_INVERT_SELECTION, OnEditInvertSelection) + ON_UPDATE_COMMAND_UI(IDM_EDIT_INVERT_SELECTION, OnUpdateEditInvertSelection) + ON_COMMAND( IDM_EDIT_PREFERENCES, OnEditPreferences) + ON_COMMAND_RANGE( IDM_SORT_PATHNAME, IDM_SORT_ORIGINAL, OnEditSort) + ON_UPDATE_COMMAND_UI_RANGE(IDM_SORT_PATHNAME, IDM_SORT_ORIGINAL, OnUpdateEditSort) + ON_COMMAND( IDM_ACTIONS_VIEW, OnActionsView) + ON_UPDATE_COMMAND_UI(IDM_ACTIONS_VIEW, OnUpdateActionsView) + ON_COMMAND( IDM_ACTIONS_ADD_FILES, OnActionsAddFiles) + ON_UPDATE_COMMAND_UI(IDM_ACTIONS_ADD_FILES, OnUpdateActionsAddFiles) + ON_COMMAND( IDM_ACTIONS_ADD_DISKS, OnActionsAddDisks) + ON_UPDATE_COMMAND_UI(IDM_ACTIONS_ADD_DISKS, OnUpdateActionsAddDisks) + ON_COMMAND( IDM_ACTIONS_CREATE_SUBDIR, OnActionsCreateSubdir) + ON_UPDATE_COMMAND_UI(IDM_ACTIONS_CREATE_SUBDIR, OnUpdateActionsCreateSubdir) + ON_COMMAND( IDM_ACTIONS_EXTRACT, OnActionsExtract) + ON_UPDATE_COMMAND_UI(IDM_ACTIONS_EXTRACT, OnUpdateActionsExtract) + ON_COMMAND( IDM_ACTIONS_TEST, OnActionsTest) + ON_UPDATE_COMMAND_UI(IDM_ACTIONS_TEST, OnUpdateActionsTest) + ON_COMMAND( IDM_ACTIONS_DELETE, OnActionsDelete) + ON_UPDATE_COMMAND_UI(IDM_ACTIONS_DELETE, OnUpdateActionsDelete) + ON_COMMAND( IDM_ACTIONS_RENAME, OnActionsRename) + ON_UPDATE_COMMAND_UI(IDM_ACTIONS_RENAME, OnUpdateActionsRename) + ON_COMMAND( IDM_ACTIONS_RECOMPRESS, OnActionsRecompress) + ON_UPDATE_COMMAND_UI(IDM_ACTIONS_RECOMPRESS, OnUpdateActionsRecompress) + ON_COMMAND( IDM_ACTIONS_OPENASDISK, OnActionsOpenAsDisk) + ON_UPDATE_COMMAND_UI(IDM_ACTIONS_OPENASDISK, OnUpdateActionsOpenAsDisk) + ON_COMMAND( IDM_ACTIONS_EDIT_COMMENT, OnActionsEditComment) + ON_UPDATE_COMMAND_UI(IDM_ACTIONS_EDIT_COMMENT, OnUpdateActionsEditComment) + ON_COMMAND( IDM_ACTIONS_EDIT_PROPS, OnActionsEditProps) + ON_UPDATE_COMMAND_UI(IDM_ACTIONS_EDIT_PROPS, OnUpdateActionsEditProps) + ON_COMMAND( IDM_ACTIONS_RENAME_VOLUME, OnActionsRenameVolume) + ON_UPDATE_COMMAND_UI(IDM_ACTIONS_RENAME_VOLUME, OnUpdateActionsRenameVolume) + ON_COMMAND( IDM_ACTIONS_CONV_DISK, OnActionsConvDisk) + ON_UPDATE_COMMAND_UI(IDM_ACTIONS_CONV_DISK, OnUpdateActionsConvDisk) + ON_COMMAND( IDM_ACTIONS_CONV_FILE, OnActionsConvFile) + ON_UPDATE_COMMAND_UI(IDM_ACTIONS_CONV_FILE, OnUpdateActionsConvFile) + ON_COMMAND( IDM_ACTIONS_CONV_TOWAV, OnActionsConvToWav) + ON_UPDATE_COMMAND_UI(IDM_ACTIONS_CONV_TOWAV, OnUpdateActionsConvToWav) + ON_COMMAND( IDM_ACTIONS_CONV_FROMWAV, OnActionsConvFromWav) + ON_UPDATE_COMMAND_UI(IDM_ACTIONS_CONV_FROMWAV, OnUpdateActionsConvFromWav) + ON_COMMAND( IDM_ACTIONS_IMPORT_BAS, OnActionsImportBAS) + ON_UPDATE_COMMAND_UI(IDM_ACTIONS_IMPORT_BAS, OnUpdateActionsImportBAS) + ON_COMMAND( IDM_TOOLS_DISKEDIT, OnToolsDiskEdit) + ON_COMMAND( IDM_TOOLS_IMAGECREATOR, OnToolsDiskImageCreator) + ON_COMMAND( IDM_TOOLS_DISKCONV, OnToolsDiskConv) + ON_COMMAND( IDM_TOOLS_BULKDISKCONV, OnToolsBulkDiskConv) + ON_COMMAND( IDM_TOOLS_SST_MERGE, OnToolsSSTMerge) + ON_COMMAND( IDM_TOOLS_VOLUMECOPIER_VOLUME, OnToolsVolumeCopierVolume) + ON_COMMAND( IDM_TOOLS_VOLUMECOPIER_FILE, OnToolsVolumeCopierFile) + ON_COMMAND( IDM_TOOLS_EOLSCANNER, OnToolsEOLScanner) + ON_COMMAND( IDM_TOOLS_TWOIMGPROPS, OnToolsTwoImgProps) + ON_COMMAND( IDM_HELP_CONTENTS, OnHelpContents) + ON_COMMAND( IDM_HELP_WEBSITE, OnHelpWebSite) + ON_COMMAND( IDM_HELP_ORDERING, OnHelpOrdering) + ON_COMMAND( IDM_HELP_ABOUT, OnHelpAbout) +// ON_COMMAND( IDM_RTCLK_DEFAULT, OnRtClkDefault) + + /* this is required to allow "Help" button to work in PropertySheets (!) */ +// ON_COMMAND(ID_HELP, OnHelp) + ON_COMMAND(ID_HELP_FINDER, CFrameWnd::OnHelpFinder) + ON_COMMAND(ID_HELP, CFrameWnd::OnHelp) + ON_COMMAND(ID_CONTEXT_HELP, CFrameWnd::OnContextHelp) + ON_COMMAND(ID_DEFAULT_HELP, CFrameWnd::OnHelpFinder) +END_MESSAGE_MAP() + + +/* + * MainWindow constructor. Creates the main window and sets + * its properties. + */ +MainWindow::MainWindow() +{ + static const char* kAppName = _T("CiderPress"); + + fpContentList = nil; + fpOpenArchive = nil; + //fpSelSet = nil; + fpActionProgress = nil; + fpProgressCounter = nil; + fpFindDialog = nil; + + fFindDown = true; + fFindMatchCase = false; + fFindMatchWholeWord = false; + + fAbortPrinting = false; + fhDevMode = nil; + fhDevNames = nil; + fNeedReopen = false; + + CString wndClass = AfxRegisterWndClass( + CS_DBLCLKS /*| CS_HREDRAW | CS_VREDRAW*/, + gMyApp.LoadStandardCursor(IDC_ARROW), + NULL /*(HBRUSH) (COLOR_WINDOW + 1)*/, + gMyApp.LoadIcon(IDR_MAINFRAME) ); + + Create(wndClass, kAppName, WS_OVERLAPPEDWINDOW /*| WS_CLIPCHILDREN*/, + rectDefault, NULL, MAKEINTRESOURCE(IDR_MAINFRAME)); + + LoadAccelTable(MAKEINTRESOURCE(IDR_MAINFRAME)); + + // initialize some OLE garbage + AfxOleInit(); + + // required by MFC if Rich Edit controls are used + AfxInitRichEdit(); + + // required?? + //AfxEnableControlContainer(); + + SetCPTitle(); + + int cc = PostMessage(WMU_LATE_INIT, 0, 0); + ASSERT(cc != 0); +} + +/* + * MainWindow destructor. Close the archive if one is open, but don't try + * to shut down any controls in child windows. By this point, Windows has + * already snuffed them. + */ +MainWindow::~MainWindow() +{ + WMSG0("~MainWindow\n"); + + //WMSG0("MainWindow destructor\n"); + CloseArchiveWOControls(); + + int cc; + cc = ::WinHelp(m_hWnd, ::AfxGetApp()->m_pszHelpFilePath, HELP_QUIT, 0); + WMSG1("Turning off WinHelp returned %d\n", cc); + + // free stuff used by print dialog + ::GlobalFree(fhDevMode); + ::GlobalFree(fhDevNames); + + fPreferences.SaveToRegistry(); + WMSG0("MainWindow destructor complete\n"); +} + + +/* + * Override the pre-create function to tweak the window style. + */ +BOOL +MainWindow::PreCreateWindow(CREATESTRUCT& cs) +{ + BOOL res = CFrameWnd::PreCreateWindow(cs); + + cs.dwExStyle &= ~(WS_EX_CLIENTEDGE); + + return res; +} + +/* + * Override GetClientRect so we can factor in the status and tool bars. + */ +void +MainWindow::GetClientRect(LPRECT lpRect) const +{ + CRect sizeRect; + int toolBarHeight, statusBarHeight; + + fToolBar.GetWindowRect(&sizeRect); + toolBarHeight = sizeRect.bottom - sizeRect.top; + fStatusBar.GetWindowRect(&sizeRect); + statusBarHeight = sizeRect.bottom - sizeRect.top; + + //WMSG2("HEIGHTS = %d/%d\n", toolBarHeight, statusBarHeight); + CFrameWnd::GetClientRect(lpRect); + lpRect->top += toolBarHeight; + lpRect->bottom -= statusBarHeight; +} + + +/* + * Do some idle processing. + */ +void +MainWindow::DoIdle(void) +{ + /* + * Make sure that the filename field in the content list is always + * visible, since that what the user clicks on to select things. Would + * be nice to have a way to prevent it, but for now we'll just shove + * things back where they're supposed to be. + */ + if (fpContentList != nil) { + /* get the current column 0 width, with current user adjustments */ + fpContentList->ExportColumnWidths(); + int width = fPreferences.GetColumnLayout()->GetColumnWidth(0); + + if (width >= 0 && width < ColumnLayout::kMinCol0Width) { + /* column is too small, but don't change it until user lets mouse up */ + if (::GetAsyncKeyState(VK_LBUTTON) >= 0) { + WMSG0("Resetting column 0 width\n"); + fPreferences.GetColumnLayout()->SetColumnWidth(0, + ColumnLayout::kMinCol0Width); + fpContentList->NewColumnWidths(); + } + } + } + + /* + * Put an asterisk at the end of the title if we have an open archive + * and it has pending modifications. Remove it if nothing is pending. + */ + if (fpOpenArchive != nil) { + CString title; + int len; + + GetWindowText(/*ref*/ title); + len = title.GetLength(); + if (len > 0 && title.GetAt(len-1) == '*') { + if (!fpOpenArchive->IsModified()) { + /* remove the asterisk and the preceeding space */ + title.Delete(len-2, 2); + SetWindowText(title); + } + } else { + if (fpOpenArchive->IsModified()) { + /* add an asterisk */ + title += " *"; + SetWindowText(title); + } + } + } +} + + +/* + * Handle command-line arguments. + * + * Usage: + * CiderPress [[-temparc] [-mode {nufx,bin2,disk}] [-dispname name] filename] + */ +void +MainWindow::ProcessCommandLine(void) +{ + /* + * Get the command line and break it down into an argument vector. + */ + const char* cmdLine = ::GetCommandLine(); + if (cmdLine == nil || strlen(cmdLine) == 0) + return; + + char* mangle = strdup(cmdLine); + if (mangle == nil) + return; + + WMSG1("Mangling '%s'\n", mangle); + char* argv[8]; + int argc = 8; + VectorizeString(mangle, argv, &argc); + + WMSG0("Args:\n"); + for (int i = 0; i < argc; i++) { + WMSG2(" %d '%s'\n", i, argv[i]); + } + + /* + * Figure out what the arguments are. + */ + const char* filename = nil; + const char* dispName = nil; + int filterIndex = kFilterIndexGeneric; + bool temp = false; + + for (i = 1; i < argc; i++) { + if (argv[i][0] == '-') { + if (strcasecmp(argv[i], "-mode") == 0) { + if (i == argc-1) { + WMSG0("WARNING: -mode specified without mode\n"); + } else + i++; + if (strcasecmp(argv[i], kModeNuFX) == 0) + filterIndex = kFilterIndexNuFX; + else if (strcasecmp(argv[i], kModeBinaryII) == 0) + filterIndex = kFilterIndexBinaryII; + else if (strcasecmp(argv[i], kModeACU) == 0) + filterIndex = kFilterIndexACU; + else if (strcasecmp(argv[i], kModeDiskImage) == 0) + filterIndex = kFilterIndexDiskImage; + else { + WMSG1("WARNING: unrecognized mode '%s'\n", argv[i]); + } + } else if (strcasecmp(argv[i], "-dispname") == 0) { + if (i == argc-1) { + WMSG0("WARNING: -dispname specified without name\n"); + } else + i++; + dispName = argv[i]; + } else if (strcasecmp(argv[i], "-temparc") == 0) { + temp = true; + } else if (strcasecmp(argv[i], "-install") == 0) { + // see MyApp::InitInstance + WMSG0("Got '-install' flag, doing nothing\n"); + } else if (strcasecmp(argv[i], "-uninstall") == 0) { + // see MyApp::InitInstance + WMSG0("Got '-uninstall' flag, doing nothing\n"); + } else { + WMSG1("WARNING: unrecognized flag '%s'\n", argv[i]); + } + } else { + /* must be the filename */ + if (i != argc-1) { + WMSG1("WARNING: ignoring extra arguments (e.g. '%s')\n", + argv[i+1]); + } + filename = argv[i]; + break; + } + } + if (argc != 1 && filename == nil) { + WMSG0("WARNING: args specified but no filename found\n"); + } + + WMSG0("Argument handling:\n"); + WMSG3(" index=%d temp=%d filename='%s'\n", + filterIndex, temp, filename == nil ? "(nil)" : filename); + + if (filename != nil) { + PathName path(filename); + CString ext = path.GetExtension(); + + // drop the leading '.' from the extension + if (ext.Left(1) == ".") + ext.Delete(0, 1); + + /* load the archive, mandating read-only if it's a temporary file */ + if (LoadArchive(filename, ext, filterIndex, temp, false) == 0) { + /* success, update title bar */ + if (temp) + fOpenArchivePathName = path.GetFileName(); + else + fOpenArchivePathName = filename; + if (dispName != nil) + fOpenArchivePathName = dispName; + SetCPTitle(fOpenArchivePathName, fpOpenArchive); + } + + /* if it's a temporary file, arrange to have it deleted before exit */ + if (temp) { + int len = strlen(filename); + + if (len > 4 && strcasecmp(filename + (len-4), ".tmp") == 0) { + fDeleteList.Add(filename); + } else { + WMSG1("NOT adding '%s' to DeleteList -- does not end in '.tmp'\n", + filename); + } + } + } + + free(mangle); +} + + +/* + * =================================== + * Command handlers + * =================================== + */ + +const int kProgressPane = 1; + +/* + * OnCreate handler. Used to add a toolbar and status bar. + */ +int +MainWindow::OnCreate(LPCREATESTRUCT lpcs) +{ + WMSG0("Now in OnCreate!\n"); + if (CFrameWnd::OnCreate(lpcs) == -1) + return -1; + + /* + * Create the tool bar. + */ +#if 0 + static UINT buttonList[] = { + IDM_FILE_OPEN, + IDM_FILE_NEW_ARCHIVE, + // spacer + IDM_FILE_PRINT, + }; +#endif + fToolBar.Create(this, WS_CHILD | WS_VISIBLE | CBRS_TOP | + CBRS_TOOLTIPS | CBRS_FLYBY); + fToolBar.LoadToolBar(IDR_TOOLBAR1); + + /* + * Create the status bar. + */ + static UINT indicators[] = { ID_SEPARATOR, ID_INDICATOR_COMPLETE }; + fStatusBar.Create(this); + fStatusBar.SetIndicators(indicators, NELEM(indicators)); + //fStatusBar.SetPaneInfo(0, ID_SEPARATOR, SBPS_NOBORDERS | SBPS_STRETCH, 0); + + fStatusBar.SetPaneText(kProgressPane, ""); + + return 0; +} + + +/* + * Catch a message sent to inspire us to perform one-time initializations of + * preferences and libraries. + * + * We're doing this the long way around because we want to be able to + * put up a dialog box if the version is bad. If we tried to handle this + * in the constructor we'd be acting before the window was fully created. + */ +LONG +MainWindow::OnLateInit(UINT, LONG) +{ + CString result; + CString appName; + CString niftyListFile; + + appName.LoadString(IDS_MB_APP_NAME); + + WMSG0("----- late init begins -----\n"); + + /* + * Handle all other messages. This gives the framework a chance to dim + * all of the toolbar buttons. This is especially useful when opening + * a file from the command line that doesn't exist, causing an error + * dialog and blocking main window messages. + */ + PeekAndPump(); + + /* + * Initialize libraries. This includes a version check. + */ + result = NufxArchive::AppInit(); + if (!result.IsEmpty()) + goto fail; + result = DiskArchive::AppInit(); + if (!result.IsEmpty()) + goto fail; + result = BnyArchive::AppInit(); + if (!result.IsEmpty()) + goto fail; + + niftyListFile = gMyApp.GetExeBaseName(); + niftyListFile += "NList.Data"; + if (!NiftyList::AppInit(niftyListFile)) { + CString file2 = niftyListFile + ".TXT"; + if (!NiftyList::AppInit(file2)) { + CString msg; + msg.Format(IDS_NLIST_DATA_FAILED, niftyListFile, file2); + MessageBox(msg, appName, MB_OK); + } + } + + /* + * Read preferences from registry. + */ + fPreferences.LoadFromRegistry(); + + /* + * Check to see if we're registered; if we're not, and we've expired, it's + * time to bail out. + */ + MyRegistry::RegStatus regStatus; + //regStatus = gMyApp.fRegistry.CheckRegistration(&result); + regStatus = MyRegistry::kRegValid; + WMSG1("CheckRegistration returned %d\n", regStatus); + switch (regStatus) { + case MyRegistry::kRegNotSet: + case MyRegistry::kRegValid: + ASSERT(result.IsEmpty()); + break; + case MyRegistry::kRegExpired: + case MyRegistry::kRegInvalid: + MessageBox(result, appName, MB_OK|MB_ICONINFORMATION); + WMSG0("FORCING REG\n"); +#if 0 + if (EnterRegDialog::GetRegInfo(this) != 0) { + result = ""; + goto fail; + } +#endif + SetCPTitle(); // update title bar with new reg info + break; + case MyRegistry::kRegFailed: + ASSERT(!result.IsEmpty()); + goto fail; + default: + ASSERT(false); + CString confused; + confused.Format("Registration check failed. %s", (LPCTSTR) result); + result = confused; + goto fail; + } + + /* + * Process command-line options, possibly loading an archive. + */ + ProcessCommandLine(); + + return 0; + +fail: + if (!result.IsEmpty()) + ShowFailureMsg(this, result, IDS_FAILED); + int cc = PostMessage(WM_CLOSE, 0, 0); + ASSERT(cc != 0); + + return 0; +} + + +/* + * The system wants to know if we're okay with shutting down. + * + * Return TRUE if it's okay to shut down, FALSE otherwise. + */ +BOOL +MainWindow::OnQueryEndSession(void) +{ + WMSG0("Got QueryEndSession\n"); + return TRUE; +} + +/* + * Notification of shutdown (or not). + */ +void +MainWindow::OnEndSession(BOOL bEnding) +{ + WMSG1("Got EndSession (bEnding=%d)\n", bEnding); + + if (bEnding) { + CloseArchiveWOControls(); + + fPreferences.SaveToRegistry(); + } +} + +/* + * The main window is resizing. We don't automatically redraw on resize, + * so we will need to update the client region. If it's filled with a + * control, the control's resize & redraw function will take care of it. + * If not, we need to explicitly invalidate the client region so the + * window will repaint itself. + */ +void +MainWindow::OnSize(UINT nType, int cx, int cy) +{ + CFrameWnd::OnSize(nType, cx, cy); + ResizeClientArea(); +} +void +MainWindow::ResizeClientArea(void) +{ + CRect sizeRect; + + GetClientRect(&sizeRect); + if (fpContentList != NULL) + fpContentList->MoveWindow(sizeRect); + else + Invalidate(false); +} + +/* + * Restrict the minimum window size to something reasonable. + */ +void +MainWindow::OnGetMinMaxInfo(MINMAXINFO* pMMI) +{ + pMMI->ptMinTrackSize.x = 256; + pMMI->ptMinTrackSize.y = 192; +} + +/* + * Repaint the main window. + */ +void +MainWindow::OnPaint(void) +{ + CPaintDC dc(this); + CRect clientRect; + + GetClientRect(&clientRect); + + /* + * If there's no control in the window, fill in the client area with + * what looks like an empty MDI client rect. + */ + if (fpContentList == nil) { + DrawEmptyClientArea(&dc, clientRect); + } + +#if 0 + CPen pen(PS_SOLID, 1, RGB(255, 0, 0)); // red pen, 1 pixel wide + CPen* pOldPen = dc.SelectObject(&pen); + + dc.MoveTo(clientRect.left, clientRect.top); + dc.LineTo(clientRect.right-1, clientRect.top); + dc.LineTo(clientRect.right, clientRect.bottom); + dc.LineTo(clientRect.left, clientRect.bottom-1); + dc.LineTo(clientRect.left, clientRect.top); + + dc.SelectObject(pOldPen); +#endif +} + +#if 0 +afx_msg BOOL +MainWindow::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt) +{ + WMSG0("MOUSE WHEEL\n"); + return FALSE; + + WPARAM wparam; + LPARAM lparam; + + wparam = nFlags | (zDelta << 16); + lparam = pt.x | (pt.y << 16); + if (fpContentList != nil) + fpContentList->SendMessage(WM_MOUSEWHEEL, wparam, lparam); + return CWnd::OnMouseWheel(nFlags, zDelta, pt); +// return TRUE; +} +#endif + +/* + * Make sure open controls keep the input focus. + */ +void +MainWindow::OnSetFocus(CWnd* /*pOldWnd*/) +{ + if (fpContentList != nil) { + WMSG0("Returning focus to ContentList\n"); + fpContentList->SetFocus(); + } +} + +/* + * User hit F1. We don't currently have context-sensitive help on the main page. + */ +BOOL +MainWindow::OnHelpInfo(HELPINFO* /*lpHelpInfo*/) +{ + //WinHelp(0, HELP_FINDER); + WinHelp(HELP_TOPIC_WELCOME, HELP_CONTEXT); + return TRUE; // dunno what this means +} + +#if 0 +/* + * Catch-all Help handler, necessary to allow CPropertySheet to display a + * "Help" button. (WTF?) + */ +LONG +MainWindow::OnHelp(UINT wParam, LONG lParam) +{ + HELPINFO* lpHelpInfo = (HELPINFO*) lParam; + + DWORD context = lpHelpInfo->iCtrlId; + WMSG1("MainWindow OnHelp (context=%d)\n", context); + WinHelp(context, HELP_CONTEXTPOPUP); + + return TRUE; // yes, we handled it +} +#endif + +/* + * Handle Edit->Preferences by popping up a property sheet. + */ +void +MainWindow::OnEditPreferences(void) +{ + PrefsSheet ps; + ColumnLayout* pColLayout = fPreferences.GetColumnLayout(); + + /* pull any user header tweaks out of list so we can configure prefs */ + if (fpContentList != nil) + fpContentList->ExportColumnWidths(); + + /* set up PrefsGeneralPage */ + for (int i = 0; i < kNumVisibleColumns; i++) { + ps.fGeneralPage.fColumn[i] = (pColLayout->GetColumnWidth(i) != 0); + } + ps.fGeneralPage.fMimicShrinkIt = fPreferences.GetPrefBool(kPrMimicShrinkIt); + ps.fGeneralPage.fBadMacSHK = fPreferences.GetPrefBool(kPrBadMacSHK); + ps.fGeneralPage.fReduceSHKErrorChecks = fPreferences.GetPrefBool(kPrReduceSHKErrorChecks); + ps.fGeneralPage.fCoerceDOSFilenames = fPreferences.GetPrefBool(kPrCoerceDOSFilenames); + ps.fGeneralPage.fSpacesToUnder = fPreferences.GetPrefBool(kPrSpacesToUnder); + ps.fGeneralPage.fPasteJunkPaths = fPreferences.GetPrefBool(kPrPasteJunkPaths); + ps.fGeneralPage.fBeepOnSuccess = fPreferences.GetPrefBool(kPrBeepOnSuccess); + + /* set up PrefsDiskImagePage */ + ps.fDiskImagePage.fQueryImageFormat = fPreferences.GetPrefBool(kPrQueryImageFormat); + ps.fDiskImagePage.fOpenVolumeRO = fPreferences.GetPrefBool(kPrOpenVolumeRO); + ps.fDiskImagePage.fOpenVolumePhys0 = fPreferences.GetPrefBool(kPrOpenVolumePhys0); + ps.fDiskImagePage.fProDOSAllowLower = fPreferences.GetPrefBool(kPrProDOSAllowLower); + ps.fDiskImagePage.fProDOSUseSparse = fPreferences.GetPrefBool(kPrProDOSUseSparse); + + /* set up PrefsCompressionPage */ + ps.fCompressionPage.fCompressType = fPreferences.GetPrefLong(kPrCompressionType); + + /* set up PrefsFviewPage */ + ps.fFviewPage.fMaxViewFileSizeKB = + (fPreferences.GetPrefLong(kPrMaxViewFileSize) + 1023) / 1024; + ps.fFviewPage.fNoWrapText = fPreferences.GetPrefBool(kPrNoWrapText); + + ps.fFviewPage.fHighlightHexDump = fPreferences.GetPrefBool(kPrHighlightHexDump); + ps.fFviewPage.fHighlightBASIC = fPreferences.GetPrefBool(kPrHighlightBASIC); + ps.fFviewPage.fConvDisasmOneByteBrkCop = fPreferences.GetPrefBool(kPrDisasmOneByteBrkCop); + ps.fFviewPage.fConvHiResBlackWhite = fPreferences.GetPrefBool(kPrConvHiResBlackWhite); + ps.fFviewPage.fConvDHRAlgorithm = fPreferences.GetPrefLong(kPrConvDHRAlgorithm); + ps.fFviewPage.fRelaxGfxTypeCheck = fPreferences.GetPrefBool(kPrRelaxGfxTypeCheck); +// ps.fFviewPage.fEOLConvRaw = fPreferences.GetPrefBool(kPrEOLConvRaw); +// ps.fFviewPage.fConvHighASCII = fPreferences.GetPrefBool(kPrConvHighASCII); + ps.fFviewPage.fConvTextEOL_HA = fPreferences.GetPrefBool(kPrConvTextEOL_HA); + ps.fFviewPage.fConvCPMText = fPreferences.GetPrefBool(kPrConvCPMText); + ps.fFviewPage.fConvPascalText = fPreferences.GetPrefBool(kPrConvPascalText); + ps.fFviewPage.fConvPascalCode = fPreferences.GetPrefBool(kPrConvPascalCode); + ps.fFviewPage.fConvApplesoft = fPreferences.GetPrefBool(kPrConvApplesoft); + ps.fFviewPage.fConvInteger = fPreferences.GetPrefBool(kPrConvInteger); + ps.fFviewPage.fConvGWP = fPreferences.GetPrefBool(kPrConvGWP); + ps.fFviewPage.fConvText8 = fPreferences.GetPrefBool(kPrConvText8); + ps.fFviewPage.fConvAWP = fPreferences.GetPrefBool(kPrConvAWP); + ps.fFviewPage.fConvADB = fPreferences.GetPrefBool(kPrConvADB); + ps.fFviewPage.fConvASP = fPreferences.GetPrefBool(kPrConvASP); + ps.fFviewPage.fConvSCAssem = fPreferences.GetPrefBool(kPrConvSCAssem); + ps.fFviewPage.fConvDisasm = fPreferences.GetPrefBool(kPrConvDisasm); + ps.fFviewPage.fConvHiRes = fPreferences.GetPrefBool(kPrConvHiRes); + ps.fFviewPage.fConvDHR = fPreferences.GetPrefBool(kPrConvDHR); + ps.fFviewPage.fConvSHR = fPreferences.GetPrefBool(kPrConvSHR); + ps.fFviewPage.fConvPrintShop = fPreferences.GetPrefBool(kPrConvPrintShop); + ps.fFviewPage.fConvMacPaint = fPreferences.GetPrefBool(kPrConvMacPaint); + ps.fFviewPage.fConvProDOSFolder = fPreferences.GetPrefBool(kPrConvProDOSFolder); + ps.fFviewPage.fConvResources = fPreferences.GetPrefBool(kPrConvResources); + + /* set up PrefsFilesPage */ + ps.fFilesPage.fTempPath = fPreferences.GetPrefString(kPrTempPath); + ps.fFilesPage.fExtViewerExts = fPreferences.GetPrefString(kPrExtViewerExts); + + if (ps.DoModal() == IDOK) + ApplyNow(&ps); +} + +/* + * Apply a change from the preferences sheet. + */ +void +MainWindow::ApplyNow(PrefsSheet* pPS) +{ + bool mustReload = false; + + //WMSG0("APPLY CHANGES\n"); + + ColumnLayout* pColLayout = fPreferences.GetColumnLayout(); + + if (pPS->fGeneralPage.fDefaultsPushed) { + /* reset all sizes to defaults, then factor in checkboxes */ + WMSG0(" Resetting all widths to defaults\n"); + + /* copy defaults over */ + for (int i = 0; i < kNumVisibleColumns; i++) + pColLayout->SetColumnWidth(i, ColumnLayout::kWidthDefaulted); + } + + /* handle column checkboxes */ + for (int i = 0; i < kNumVisibleColumns; i++) { + if (pColLayout->GetColumnWidth(i) == 0 && + pPS->fGeneralPage.fColumn[i]) + { + /* restore column */ + WMSG1(" Column %d restored\n", i); + pColLayout->SetColumnWidth(i, ColumnLayout::kWidthDefaulted); + } else if (pColLayout->GetColumnWidth(i) != 0 && + !pPS->fGeneralPage.fColumn[i]) + { + /* disable column */ + WMSG1(" Column %d hidden\n", i); + pColLayout->SetColumnWidth(i, 0); + } + } + if (fpContentList != nil) + fpContentList->NewColumnWidths(); + fPreferences.SetPrefBool(kPrMimicShrinkIt, + pPS->fGeneralPage.fMimicShrinkIt != 0); + fPreferences.SetPrefBool(kPrBadMacSHK, pPS->fGeneralPage.fBadMacSHK != 0); + fPreferences.SetPrefBool(kPrReduceSHKErrorChecks, + pPS->fGeneralPage.fReduceSHKErrorChecks != 0); + if (fPreferences.GetPrefBool(kPrCoerceDOSFilenames)!= + (pPS->fGeneralPage.fCoerceDOSFilenames != 0)) + { + WMSG1("DOS filename coercion pref now %d\n", + pPS->fGeneralPage.fCoerceDOSFilenames); + fPreferences.SetPrefBool(kPrCoerceDOSFilenames, + pPS->fGeneralPage.fCoerceDOSFilenames != 0); + mustReload = true; + } + if (fPreferences.GetPrefBool(kPrSpacesToUnder) != + (pPS->fGeneralPage.fSpacesToUnder != 0)) + { + WMSG1("Spaces-to-underscores now %d\n", pPS->fGeneralPage.fSpacesToUnder); + fPreferences.SetPrefBool(kPrSpacesToUnder, pPS->fGeneralPage.fSpacesToUnder != 0); + mustReload = true; + } + fPreferences.SetPrefBool(kPrPasteJunkPaths, pPS->fGeneralPage.fPasteJunkPaths != 0); + fPreferences.SetPrefBool(kPrBeepOnSuccess, pPS->fGeneralPage.fBeepOnSuccess != 0); + + if (pPS->fGeneralPage.fOurAssociations != nil) { + WMSG0("NEW ASSOCIATIONS!\n"); + + for (int assoc = 0; assoc < gMyApp.fRegistry.GetNumFileAssocs(); assoc++) + { + gMyApp.fRegistry.SetFileAssoc(assoc, + pPS->fGeneralPage.fOurAssociations[assoc]); + } + + /* delete them so, if they hit "apply" again, we only update once */ + delete[] pPS->fGeneralPage.fOurAssociations; + pPS->fGeneralPage.fOurAssociations = nil; + } + + fPreferences.SetPrefBool(kPrQueryImageFormat, pPS->fDiskImagePage.fQueryImageFormat != 0); + fPreferences.SetPrefBool(kPrOpenVolumeRO, pPS->fDiskImagePage.fOpenVolumeRO != 0); + fPreferences.SetPrefBool(kPrOpenVolumePhys0, pPS->fDiskImagePage.fOpenVolumePhys0 != 0); + fPreferences.SetPrefBool(kPrProDOSAllowLower, pPS->fDiskImagePage.fProDOSAllowLower != 0); + fPreferences.SetPrefBool(kPrProDOSUseSparse, pPS->fDiskImagePage.fProDOSUseSparse != 0); + + fPreferences.SetPrefLong(kPrCompressionType, pPS->fCompressionPage.fCompressType); + + fPreferences.SetPrefLong(kPrMaxViewFileSize, pPS->fFviewPage.fMaxViewFileSizeKB * 1024); + fPreferences.SetPrefBool(kPrNoWrapText, pPS->fFviewPage.fNoWrapText != 0); + + fPreferences.SetPrefBool(kPrHighlightHexDump, pPS->fFviewPage.fHighlightHexDump != 0); + fPreferences.SetPrefBool(kPrHighlightBASIC, pPS->fFviewPage.fHighlightBASIC != 0); + fPreferences.SetPrefBool(kPrDisasmOneByteBrkCop, pPS->fFviewPage.fConvDisasmOneByteBrkCop != 0); + fPreferences.SetPrefBool(kPrConvHiResBlackWhite, pPS->fFviewPage.fConvHiResBlackWhite != 0); + fPreferences.SetPrefLong(kPrConvDHRAlgorithm, pPS->fFviewPage.fConvDHRAlgorithm); + fPreferences.SetPrefBool(kPrRelaxGfxTypeCheck, pPS->fFviewPage.fRelaxGfxTypeCheck != 0); +// fPreferences.SetPrefBool(kPrEOLConvRaw, pPS->fFviewPage.fEOLConvRaw != 0); +// fPreferences.SetPrefBool(kPrConvHighASCII, pPS->fFviewPage.fConvHighASCII != 0); + fPreferences.SetPrefBool(kPrConvTextEOL_HA, pPS->fFviewPage.fConvTextEOL_HA != 0); + fPreferences.SetPrefBool(kPrConvCPMText, pPS->fFviewPage.fConvCPMText != 0); + fPreferences.SetPrefBool(kPrConvPascalText, pPS->fFviewPage.fConvPascalText != 0); + fPreferences.SetPrefBool(kPrConvPascalCode, pPS->fFviewPage.fConvPascalCode != 0); + fPreferences.SetPrefBool(kPrConvApplesoft, pPS->fFviewPage.fConvApplesoft != 0); + fPreferences.SetPrefBool(kPrConvInteger, pPS->fFviewPage.fConvInteger != 0); + fPreferences.SetPrefBool(kPrConvGWP, pPS->fFviewPage.fConvGWP != 0); + fPreferences.SetPrefBool(kPrConvText8, pPS->fFviewPage.fConvText8 != 0); + fPreferences.SetPrefBool(kPrConvAWP, pPS->fFviewPage.fConvAWP != 0); + fPreferences.SetPrefBool(kPrConvADB, pPS->fFviewPage.fConvADB != 0); + fPreferences.SetPrefBool(kPrConvASP, pPS->fFviewPage.fConvASP != 0); + fPreferences.SetPrefBool(kPrConvSCAssem, pPS->fFviewPage.fConvSCAssem != 0); + fPreferences.SetPrefBool(kPrConvDisasm, pPS->fFviewPage.fConvDisasm != 0); + fPreferences.SetPrefBool(kPrConvHiRes, pPS->fFviewPage.fConvHiRes != 0); + fPreferences.SetPrefBool(kPrConvDHR, pPS->fFviewPage.fConvDHR != 0); + fPreferences.SetPrefBool(kPrConvSHR, pPS->fFviewPage.fConvSHR != 0); + fPreferences.SetPrefBool(kPrConvPrintShop, pPS->fFviewPage.fConvPrintShop != 0); + fPreferences.SetPrefBool(kPrConvMacPaint, pPS->fFviewPage.fConvMacPaint != 0); + fPreferences.SetPrefBool(kPrConvProDOSFolder, pPS->fFviewPage.fConvProDOSFolder != 0); + fPreferences.SetPrefBool(kPrConvResources, pPS->fFviewPage.fConvResources != 0); + + fPreferences.SetPrefString(kPrTempPath, pPS->fFilesPage.fTempPath); + WMSG1("--- Temp path now '%s'\n", fPreferences.GetPrefString(kPrTempPath)); + fPreferences.SetPrefString(kPrExtViewerExts, pPS->fFilesPage.fExtViewerExts); + + +// if ((pPS->fGeneralPage.fShowToolbarText != 0) != fPreferences.GetShowToolbarText()) { +// fPreferences.SetShowToolbarText(pPS->fGeneralPage.fShowToolbarText != 0); +// //SetToolbarTextMode(); +// ResizeClientArea(); +// } + + /* allow open archive to track changes to preferences */ + if (fpOpenArchive != nil) + fpOpenArchive->PreferencesChanged(); + + if (mustReload) { + WMSG0("Preferences apply requesting GA/CL reload\n"); + if (fpOpenArchive != nil) + fpOpenArchive->Reload(); + if (fpContentList != nil) + fpContentList->Reload(); + } + + /* export to registry */ + fPreferences.SaveToRegistry(); + + //Invalidate(); +} + +/* + * Handle IDM_EDIT_FIND. + */ +void +MainWindow::OnEditFind(void) +{ + DWORD flags = 0; + + if (fpFindDialog != nil) + return; + + if (fFindDown) + flags |= FR_DOWN; + if (fFindMatchCase) + flags |= FR_MATCHCASE; + if (fFindMatchWholeWord) + flags |= FR_WHOLEWORD; + + fpFindDialog = new CFindReplaceDialog; + + fpFindDialog->Create(TRUE, // "find" only + fFindLastStr, // default string to search for + NULL, // default string to replace + flags, // flags + this); // parent +} +void +MainWindow::OnUpdateEditFind(CCmdUI* pCmdUI) +{ + pCmdUI->Enable(fpOpenArchive != nil); +} + +/* + * Handle activity in the modeless "find" dialog. + */ +LRESULT +MainWindow::OnFindDialogMessage(WPARAM wParam, LPARAM lParam) +{ + assert(fpFindDialog != nil); + + fFindDown = (fpFindDialog->SearchDown() != 0); + fFindMatchCase = (fpFindDialog->MatchCase() != 0); + fFindMatchWholeWord = (fpFindDialog->MatchWholeWord() != 0); + + if (fpFindDialog->IsTerminating()) { + fpFindDialog = nil; + return 0; + } + + if (fpFindDialog->FindNext()) { + fFindLastStr = fpFindDialog->GetFindString(); + fpContentList->FindNext(fFindLastStr, fFindDown, fFindMatchCase, + fFindMatchWholeWord); + } else { + WMSG0("Unexpected find dialog activity\n"); + } + + return 0; +} + + +/* + * Handle IDM_SORT_*. + * + * The "sort" enu item should really only be active if we have a file open. + */ +void +MainWindow::OnEditSort(UINT id) +{ + WMSG1("EDIT SORT %d\n", id); + + ASSERT(id >= IDM_SORT_PATHNAME && id <= IDM_SORT_ORIGINAL); + fPreferences.GetColumnLayout()->SetSortColumn(id - IDM_SORT_PATHNAME); + fPreferences.GetColumnLayout()->SetAscending(true); + if (fpContentList != nil) + fpContentList->NewSortOrder(); +} +void +MainWindow::OnUpdateEditSort(CCmdUI* pCmdUI) +{ + unsigned int column = fPreferences.GetColumnLayout()->GetSortColumn(); + + pCmdUI->SetCheck(pCmdUI->m_nID - IDM_SORT_PATHNAME == column); +} + +/* + * Open the help file. + */ +void +MainWindow::OnHelpContents(void) +{ + WinHelp(0, HELP_FINDER); +} + +/* + * Go to the faddenSoft web site. + */ +void +MainWindow::OnHelpWebSite(void) +{ + int err; + + err = (int) ::ShellExecute(m_hWnd, _T("open"), kWebSiteURL, NULL, NULL, + SW_SHOWNORMAL); + if (err <= 32) { + CString msg; + if (err == ERROR_FILE_NOT_FOUND) { + msg = "Windows call failed: web browser not found. (Sometimes" + " it mistakenly reports this when IE is not the default" + " browser.)"; + ShowFailureMsg(this, msg, IDS_FAILED); + } else { + msg.Format("Unable to launch web browser (err=%d).", err); + ShowFailureMsg(this, msg, IDS_FAILED); + } + } +} + +/* + * Show ordering info (ka-ching!). + */ +void +MainWindow::OnHelpOrdering(void) +{ + WinHelp(HELP_TOPIC_ORDERING_INFO, HELP_CONTEXT); +} + +/* + * Pop up the About box. + */ +void +MainWindow::OnHelpAbout(void) +{ + int result; + + AboutDialog dlg(this); + + result = dlg.DoModal(); + WMSG1("HelpAbout returned %d\n", result); + + /* + * User could've changed registration. If we're showing the registered + * user name in the title bar, update it. + */ + if (fpOpenArchive == nil) + SetCPTitle(); +} + +/* + * Create a new SHK archive, using a "save as" dialog to select the name. + */ +void +MainWindow::OnFileNewArchive(void) +{ + CString filename, saveFolder, errStr; + GenericArchive* pOpenArchive; + CString errMsg; + + CFileDialog dlg(FALSE, _T("shk"), NULL, + OFN_OVERWRITEPROMPT|OFN_NOREADONLYRETURN|OFN_HIDEREADONLY, + "ShrinkIt Archives (*.shk)|*.shk||", this); + + dlg.m_ofn.lpstrTitle = "New Archive"; + dlg.m_ofn.lpstrInitialDir = fPreferences.GetPrefString(kPrOpenArchiveFolder); + + if (dlg.DoModal() != IDOK) + goto bail; + + saveFolder = dlg.m_ofn.lpstrFile; + saveFolder = saveFolder.Left(dlg.m_ofn.nFileOffset); + fPreferences.SetPrefString(kPrOpenArchiveFolder, saveFolder); + + filename = dlg.GetPathName(); + WMSG1("NEW FILE '%s'\n", filename); + + /* remove file if it already exists */ + errMsg = RemoveFile(filename); + if (!errMsg.IsEmpty()) { + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + + pOpenArchive = new NufxArchive; + errStr = pOpenArchive->New(filename, nil); + if (!errStr.IsEmpty()) { + CString failed; + failed.LoadString(IDS_FAILED); + MessageBox(errStr, failed, MB_ICONERROR); + + delete pOpenArchive; + } else { + SwitchContentList(pOpenArchive); + fOpenArchivePathName = dlg.GetPathName(); + SetCPTitle(fOpenArchivePathName, fpOpenArchive); + } + +bail: + WMSG0("--- OnFileNewArchive done\n"); +} + + +/* + * Handle request to open an archive or disk image. + */ +void +MainWindow::OnFileOpen(void) +{ + CString openFilters; + CString saveFolder; + + /* set up filters; the order is significant */ + openFilters = kOpenNuFX; + openFilters += kOpenBinaryII; + openFilters += kOpenACU; + openFilters += kOpenDiskImage; + openFilters += kOpenAll; + openFilters += kOpenEnd; + CFileDialog dlg(TRUE, "shk", NULL, + OFN_FILEMUSTEXIST, openFilters, this); + + dlg.m_ofn.nFilterIndex = fPreferences.GetPrefLong(kPrLastOpenFilterIndex); + dlg.m_ofn.lpstrInitialDir = fPreferences.GetPrefString(kPrOpenArchiveFolder); + + if (dlg.DoModal() != IDOK) + goto bail; + + fPreferences.SetPrefLong(kPrLastOpenFilterIndex, dlg.m_ofn.nFilterIndex); + saveFolder = dlg.m_ofn.lpstrFile; + saveFolder = saveFolder.Left(dlg.m_ofn.nFileOffset); + fPreferences.SetPrefString(kPrOpenArchiveFolder, saveFolder); + + DoOpenArchive(dlg.GetPathName(), dlg.GetFileExt(), + dlg.m_ofn.nFilterIndex, dlg.GetReadOnlyPref() != 0); + +bail: + WMSG0("--- OnFileOpen done\n"); +} + +/* + * Handle request to open a raw disk volume. + */ +void +MainWindow::OnFileOpenVolume(void) +{ + WMSG0("--- OnFileOpenVolume\n"); + + int result; + + OpenVolumeDialog dlg(this); + + result = dlg.DoModal(); + if (result != IDOK) + goto bail; + + //DiskImg::SetAllowWritePhys0(fPreferences.GetPrefBool(kPrOpenVolumePhys0)); + DoOpenVolume(dlg.fChosenDrive, dlg.fReadOnly != 0); + +bail: + return; +} +void +MainWindow::OnUpdateFileOpenVolume(CCmdUI* pCmdUI) +{ + // don't really need this function + pCmdUI->Enable(TRUE); +} + +/* + * Open an archive. + */ +void +MainWindow::DoOpenArchive(const char* pathName, const char* ext, + int filterIndex, bool readOnly) +{ + if (LoadArchive(pathName, ext, filterIndex, readOnly, false) == 0) { + /* success, update title bar */ + fOpenArchivePathName = pathName; + SetCPTitle(fOpenArchivePathName, fpOpenArchive); + } else { + /* some failures will close an open archive */ + //if (fpOpenArchive == nil) + // SetCPTitle(); + } +} + +/* + * Save any pending changes. + * + * This may be called directly from tools, so don't assume that the + * conditions checked for in OnUpdateFileSave hold here. + */ +void +MainWindow::OnFileReopen(void) +{ + ReopenArchive(); +} +void +MainWindow::OnUpdateFileReopen(CCmdUI* pCmdUI) +{ + pCmdUI->Enable(fpOpenArchive != nil); +} + + +/* + * Save any pending changes. + * + * This may be called directly from tools, so don't assume that the + * conditions checked for in OnUpdateFileSave hold here. + */ +void +MainWindow::OnFileSave(void) +{ + CString errMsg; + + if (fpOpenArchive == nil) + return; + + { + CWaitCursor waitc; + errMsg = fpOpenArchive->Flush(); + } + if (!errMsg.IsEmpty()) + ShowFailureMsg(this, errMsg, IDS_FAILED); + + // update the title bar + DoIdle(); +} +void +MainWindow::OnUpdateFileSave(CCmdUI* pCmdUI) +{ + pCmdUI->Enable(fpOpenArchive != nil && fpOpenArchive->IsModified()); +} + +/* + * Close current archive or disk image. + */ +void +MainWindow::OnFileClose(void) +{ + CloseArchive(); + //SetCPTitle(); + WMSG0("--- OnFileClose done\n"); +} +void +MainWindow::OnUpdateFileClose(CCmdUI* pCmdUI) +{ + pCmdUI->Enable(fpOpenArchive != nil); +} + + +/* + * Show detailed information on the current archive. + */ +void +MainWindow::OnFileArchiveInfo(void) +{ + ArchiveInfoDialog* pDlg = nil; + ASSERT(fpOpenArchive != nil); + + switch (fpOpenArchive->GetArchiveKind()) { + case GenericArchive::kArchiveNuFX: + pDlg = new NufxArchiveInfoDialog((NufxArchive*) fpOpenArchive, this); + break; + case GenericArchive::kArchiveDiskImage: + pDlg = new DiskArchiveInfoDialog((DiskArchive*) fpOpenArchive, this); + break; + case GenericArchive::kArchiveBNY: + pDlg = new BnyArchiveInfoDialog((BnyArchive*) fpOpenArchive, this); + break; + case GenericArchive::kArchiveACU: + pDlg = new AcuArchiveInfoDialog((AcuArchive*) fpOpenArchive, this); + break; + default: + WMSG1("Unexpected archive type %d\n", fpOpenArchive->GetArchiveKind()); + ASSERT(false); + return; + }; + + pDlg->DoModal(); + + delete pDlg; +} +void +MainWindow::OnUpdateFileArchiveInfo(CCmdUI* pCmdUI) +{ + pCmdUI->Enable(fpContentList != nil); +} + +/* + * Print the contents of the current archive. + */ +void +MainWindow::OnFilePrint(void) +{ + PrintListing(fpContentList); +} +void +MainWindow::OnUpdateFilePrint(CCmdUI* pCmdUI) +{ + pCmdUI->Enable(fpContentList != nil && fpContentList->GetItemCount() > 0); +} + +/* + * Print a ContentList. + */ +void +MainWindow::PrintListing(const ContentList* pContentList) +{ + CPrintDialog dlg(FALSE); // use CPrintDialogEx for Win2K? CPageSetUpDialog? + PrintContentList pcl; + CDC dc; + int itemCount, numPages; + + itemCount = pContentList->GetItemCount(); + numPages = (itemCount + (pcl.GetLinesPerPage()-1)) / pcl.GetLinesPerPage(); + + dlg.m_pd.nFromPage = dlg.m_pd.nMinPage = 1; + dlg.m_pd.nToPage = dlg.m_pd.nMaxPage = numPages; + + dlg.m_pd.hDevMode = fhDevMode; + dlg.m_pd.hDevNames = fhDevNames; + dlg.m_pd.Flags |= PD_USEDEVMODECOPIESANDCOLLATE; + dlg.m_pd.Flags &= ~(PD_NOPAGENUMS); + if (dlg.DoModal() != IDOK) + return; + if (dc.Attach(dlg.GetPrinterDC()) != TRUE) { + CString msg; + msg.LoadString(IDS_PRINTER_NOT_USABLE); + ShowFailureMsg(this, msg, IDS_FAILED); + return; + } + + pcl.Setup(&dc, this); + if (dlg.m_pd.Flags & PD_PAGENUMS) + pcl.Print(pContentList, dlg.m_pd.nFromPage, dlg.m_pd.nToPage); + else + pcl.Print(pContentList); + + fhDevMode = dlg.m_pd.hDevMode; + fhDevNames = dlg.m_pd.hDevNames; +} + + +/* + * Handle Exit item by sending a close request. + */ +void +MainWindow::OnFileExit(void) +{ + SendMessage(WM_CLOSE, 0, 0); +} + + +/* + * Select everything in the content list. + */ +void +MainWindow::OnEditSelectAll(void) +{ + ASSERT(fpContentList != nil); + fpContentList->SelectAll(); +} +void +MainWindow::OnUpdateEditSelectAll(CCmdUI* pCmdUI) +{ + pCmdUI->Enable(fpContentList != nil); +} + +/* + * Invert the content list selection. + */ +void +MainWindow::OnEditInvertSelection(void) +{ + ASSERT(fpContentList != nil); + fpContentList->InvertSelection(); +} +void +MainWindow::OnUpdateEditInvertSelection(CCmdUI* pCmdUI) +{ + pCmdUI->Enable(fpContentList != nil); +} + + +/* + * Get the one selected item from the current display. Primarily useful + * for the double-click handler, but also used for "action" menu items + * that insist on operating on a single menu item (edit prefs, create subdir). + * + * Returns nil if the item couldn't be found or if more than one item was + * selected. + */ +GenericEntry* +MainWindow::GetSelectedItem(ContentList* pContentList) +{ + if (pContentList->GetSelectedCount() != 1) + return nil; + + POSITION posn; + posn = pContentList->GetFirstSelectedItemPosition(); + if (posn == nil) { + ASSERT(false); + return nil; + } + int num = pContentList->GetNextSelectedItem(/*ref*/ posn); + GenericEntry* pEntry = (GenericEntry*) pContentList->GetItemData(num); + if (pEntry == nil) { + WMSG1(" Glitch: couldn't find entry %d\n", num); + ASSERT(false); + } + + return pEntry; +} + +/* + * Handle a double-click. + * + * Individual items get special treatment, multiple items just get handed off + * to the file viewer. + */ +void +MainWindow::HandleDoubleClick(void) +{ + bool handled = false; + + ASSERT(fpContentList != nil); + if (fpContentList->GetSelectedCount() == 0) { + /* nothing selected, they double-clicked outside first column */ + WMSG0("Double-click but nothing selected\n"); + return; + } + if (fpContentList->GetSelectedCount() != 1) { + /* multiple items, just bring up viewer */ + HandleView(); + return; + } + + /* + * Find the GenericEntry that corresponds to this item. + */ + GenericEntry* pEntry = GetSelectedItem(fpContentList); + if (pEntry == nil) + return; + + WMSG1(" Double-click GOT '%s'\n", pEntry->GetPathName()); + const char* ext; + long fileType, auxType; + + ext = FindExtension(pEntry->GetPathName(), pEntry->GetFssep()); + fileType = pEntry->GetFileType(); + auxType = pEntry->GetAuxType(); + + /* // unit tests for MatchSemicolonList + MatchSemicolonList("gif; jpeg; jpg", "jpeg"); + MatchSemicolonList("gif; jpeg; jpg", "jpg"); + MatchSemicolonList("gif; jpeg; jpg", "gif"); + MatchSemicolonList("gif;jpeg;jpg", "gif;"); + MatchSemicolonList("gif; jpeg; jpg", "jpe"); + MatchSemicolonList("gif; jpeg; jpg", "jpegx"); + MatchSemicolonList("gif; jpeg; jpg", "jp"); + MatchSemicolonList("gif; jpeg; jpg", "jpgx"); + MatchSemicolonList("gif; jpeg; jpg", "if"); + MatchSemicolonList("gif; jpeg; jpg", "gifs"); + MatchSemicolonList("gif, jpeg; jpg", "jpeg"); + MatchSemicolonList("", "jpeg"); + MatchSemicolonList(";", "jpeg"); + MatchSemicolonList("gif, jpeg; jpg", ""); + */ + + /* + * Figure out what to do with it. + */ + CString extViewerExts; + extViewerExts = fPreferences.GetPrefString(kPrExtViewerExts); + if (ext != nil && MatchSemicolonList(extViewerExts, ext+1)) { + WMSG1(" Launching external viewer for '%s'\n", ext); + TmpExtractForExternal(pEntry); + handled = true; + } else if (pEntry->GetRecordKind() == GenericEntry::kRecordKindFile) { + if ((ext != nil && ( + stricmp(ext, ".shk") == 0 || + stricmp(ext, ".sdk") == 0 || + stricmp(ext, ".bxy") == 0 )) || + (fileType == 0xe0 && auxType == 0x8002)) + { + WMSG0(" Guessing NuFX\n"); + TmpExtractAndOpen(pEntry, GenericEntry::kDataThread, kModeNuFX); + handled = true; + } else + if ((ext != nil && ( + stricmp(ext, ".bny") == 0 || + stricmp(ext, ".bqy") == 0 )) || + (fileType == 0xe0 && auxType == 0x8000)) + { + WMSG0(" Guessing Binary II\n"); + TmpExtractAndOpen(pEntry, GenericEntry::kDataThread, kModeBinaryII); + handled = true; + } else + if ((ext != nil && ( + stricmp(ext, ".acu") == 0 )) || + (fileType == 0xe0 && auxType == 0x8001)) + { + WMSG0(" Guessing ACU\n"); + TmpExtractAndOpen(pEntry, GenericEntry::kDataThread, kModeACU); + handled = true; + } else + if (fileType == 0x64496d67 && auxType == 0x64437079 && + pEntry->GetUncompressedLen() == 819284) + { + /* type is dImg, creator is dCpy, length is 800K + DC stuff */ + WMSG0(" Looks like a disk image\n"); + TmpExtractAndOpen(pEntry, GenericEntry::kDataThread, kModeDiskImage); + handled = true; + } + } else if (pEntry->GetRecordKind() == GenericEntry::kRecordKindDisk) { + WMSG0(" Opening archived disk image\n"); + TmpExtractAndOpen(pEntry, GenericEntry::kDiskImageThread, kModeDiskImage); + handled = true; + } + + if (!handled) { + // standard viewer + HandleView(); + } + + /* set "/t" temp flag and delete afterward, warning user (?) */ +} + +/* + * Extract a record to the temp folder and open it with a new instance of + * CiderPress. We might want to extract disk images as 2MG files to take + * the mystery out of opening them, but since they're coming out of a + * ShrinkIt archive they're pretty un-mysterious anyway. + * + * We tell the new instance to open it read-only, and flag it for + * deletion on exit. + * + * Returns 0 on success, nonzero error status on failure. + */ +int +MainWindow::TmpExtractAndOpen(GenericEntry* pEntry, int threadKind, + const char* modeStr) +{ + CString dispName; + bool mustDelete = false; + + /* + * Get the name to display in the title bar. Double quotes will + * screw it up, so we have to replace them. (We could escape them, + * but then we'd also have to escape the escape char.) + */ + dispName = pEntry->GetFileName(); + dispName.Replace('"', '_'); + + char nameBuf[MAX_PATH]; + UINT unique; + unique = GetTempFileName(fPreferences.GetPrefString(kPrTempPath), + "CPfile", 0, nameBuf); + if (unique == 0) { + DWORD dwerr = ::GetLastError(); + WMSG2("GetTempFileName failed on '%s' (err=%ld)\n", + fPreferences.GetPrefString(kPrTempPath), dwerr); + return dwerr; + } + mustDelete = true; + + /* + * Open the temp file and extract the data into it. + */ + CString errMsg; + int result; + FILE* fp; + + fp = fopen(nameBuf, "wb"); + if (fp != nil) { + WMSG2("Extracting to '%s' (unique=%d)\n", nameBuf, unique); + result = pEntry->ExtractThreadToFile(threadKind, fp, + GenericEntry::kConvertEOLOff, GenericEntry::kConvertHAOff, + &errMsg); + fclose(fp); + if (result == IDOK) { + /* success */ + CString parameters; + + parameters.Format("-mode %s -dispname \"%s\" -temparc \"%s\"", + modeStr, dispName, nameBuf); + int err; + + err = (int) ::ShellExecute(m_hWnd, _T("open"), + gMyApp.GetExeFileName(), parameters, NULL, + SW_SHOWNORMAL); + if (err <= 32) { + CString msg; + msg.Format("Unable to launch CiderPress (err=%d).", err); + ShowFailureMsg(this, msg, IDS_FAILED); + } else { + /* during dev, "missing DLL" causes false-positive success */ + WMSG0("Successfully launched CiderPress\n"); + mustDelete = false; // up to newly-launched app + } + } else { + ShowFailureMsg(this, errMsg, IDS_FAILED); + } + } else { + CString msg; + msg.Format("Unable to open temp file '%s'.", nameBuf); + ::ShowFailureMsg(this, msg, IDS_FAILED); + } + + if (mustDelete) { + WMSG1("Deleting '%s'\n", nameBuf); + unlink(nameBuf); + } + + return 0; +} + +/* + * Extract a record to the temp folder and open it with an external viewer. + * The file must be created with the correct extension so ShellExecute + * does the right thing. + * + * The files will be added to the "delete on exit" list, so that they will + * be cleaned up when CiderPress exits (assuming the external viewer no longer + * has them open). + * + * The GetTempFileName function creates a uniquely-named temp file. We + * create a file that has that name plus an extension. To ensure that we + * don't try to use the same temp filename twice, we have to hold off on + * deleting the unused .tmp files until we're ready to delete the + * corresponding .gif (or whatever) files. Thus, each invocation of this + * function creates two files and two entries in the delete-on-exit set. + * + * Returns 0 on success, nonzero error status on failure. + */ +int +MainWindow::TmpExtractForExternal(GenericEntry* pEntry) +{ + const char* ext; + + ext = FindExtension(pEntry->GetPathName(), pEntry->GetFssep()); + + char nameBuf[MAX_PATH]; + UINT unique; + unique = GetTempFileName(fPreferences.GetPrefString(kPrTempPath), + "CPfile", 0, nameBuf); + if (unique == 0) { + DWORD dwerr = ::GetLastError(); + WMSG2("GetTempFileName failed on '%s' (err=%ld)\n", + fPreferences.GetPrefString(kPrTempPath), dwerr); + return dwerr; + } + fDeleteList.Add(nameBuf); // file is created by GetTempFileName + + strcat(nameBuf, ext); + + /* + * Open the temp file and extract the data into it. + */ + CString errMsg; + int result; + FILE* fp; + + fp = fopen(nameBuf, "wb"); + if (fp != nil) { + fDeleteList.Add(nameBuf); // second file created by fopen + WMSG2("Extracting to '%s' (unique=%d)\n", nameBuf, unique); + result = pEntry->ExtractThreadToFile(GenericEntry::kDataThread, fp, + GenericEntry::kConvertEOLOff, GenericEntry::kConvertHAOff, + &errMsg); + fclose(fp); + if (result == IDOK) { + /* success */ + int err; + + err = (int) ::ShellExecute(m_hWnd, _T("open"), nameBuf, NULL, + NULL, SW_SHOWNORMAL); + if (err <= 32) { + CString msg; + msg.Format("Unable to launch external viewer (err=%d).", err); + ShowFailureMsg(this, msg, IDS_FAILED); + } else { + WMSG0("Successfully launched external viewer\n"); + } + } else { + ShowFailureMsg(this, errMsg, IDS_FAILED); + } + } else { + CString msg; + msg.Format("Unable to open temp file '%s'.", nameBuf); + ShowFailureMsg(this, msg, IDS_FAILED); + } + + return 0; +} + +#if 0 +/* + * Handle a "default action" selection from the right-click menu. The + * action only applies to the record that was clicked on, so we need to + * retrieve that from the control. + */ +void +MainWindow::OnRtClkDefault(void) +{ + int idx; + + ASSERT(fpContentList != nil); + + idx = fpContentList->GetRightClickItem(); + ASSERT(idx != -1); + WMSG1("OnRtClkDefault %d\n", idx); + + fpContentList->ClearRightClickItem(); +} +#endif + + +/* + * =================================== + * Progress meter + * =================================== + */ + +/* + * There are two different mechanisms for reporting progress: ActionProgress + * dialogs (for adding/extracting files) and a small box in the lower + * right-hand corner (for opening archives). These functions will set + * the progress in the active action progress dialog if it exists, or + * will set the percentage in the window frame if not. + */ + +void +MainWindow::SetProgressBegin(void) +{ + if (fpActionProgress != nil) + fpActionProgress->SetProgress(0); + else + fStatusBar.SetPaneText(kProgressPane, "--%"); + //WMSG0(" Complete: BEGIN\n"); + + /* redraw stuff with the changes */ + (void) PeekAndPump(); +} + +int +MainWindow::SetProgressUpdate(int percent, const char* oldName, + const char* newName) +{ + int status = IDOK; + + if (fpActionProgress != nil) { + status = fpActionProgress->SetProgress(percent); + if (oldName != nil) + fpActionProgress->SetArcName(oldName); + if (newName != nil) + fpActionProgress->SetFileName(newName); + } else { + char buf[8]; + sprintf(buf, "%d%%", percent); + fStatusBar.SetPaneText(kProgressPane, buf); + //WMSG1(" Complete: %s\n", buf); + } + + if (!PeekAndPump()) { + WMSG0("SetProgressUpdate: shutdown?!\n"); + } + + //EventPause(10); // DEBUG DEBUG + return status; +} + +void +MainWindow::SetProgressEnd(void) +{ + if (fpActionProgress != nil) + fpActionProgress->SetProgress(100); + else + fStatusBar.SetPaneText(kProgressPane, ""); +// EventPause(100); // DEBUG DEBUG + //WMSG0(" Complete: END\n"); +} + + +/* + * Set a number in the "progress counter". Useful for loading large archives + * where we're not sure how much stuff is left, so showing a percentage is + * hard. + * + * Pass in -1 to erase the counter. + * + * Returns "true" if we'd like things to continue. + */ +bool +MainWindow::SetProgressCounter(const char* str, long val) +{ + /* if the main window is enabled, user could activate menus */ + ASSERT(!IsWindowEnabled()); + + if (fpProgressCounter != nil) { + //WMSG2("SetProgressCounter '%s' %d\n", str, val); + CString msg; + + if (str != nil) + fpProgressCounter->SetCounterFormat(str); + fpProgressCounter->SetCount((int) val); + } else { + if (val < 0) { + fStatusBar.SetPaneText(kProgressPane, ""); + } else { + CString tmpStr; + tmpStr.Format("%ld", val); + fStatusBar.SetPaneText(kProgressPane, tmpStr); + } + } + + if (!PeekAndPump()) { + WMSG0("SetProgressCounter: shutdown?!\n"); + } + //EventPause(10); // DEBUG DEBUG + + if (fpProgressCounter != nil) + return !fpProgressCounter->GetCancel(); + else + return true; +} + + +/* + * Allow events to flow through the message queue whenever the + * progress meter gets updated. This will allow us to redraw with + * reasonable frequency. + * + * Calling this can result in other code being called, such as Windows + * message handlers, which can lead to reentrancy problems. Make sure + * you're adequately semaphored before calling here. + * + * Returns TRUE if all is well, FALSE if we're trying to quit. + */ +BOOL +MainWindow::PeekAndPump(void) +{ + MSG msg; + + while (::PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE)) { + if (!AfxGetApp()->PumpMessage()) { + ::PostQuitMessage(0); + return FALSE; + } + } + + LONG lIdle = 0; + while (AfxGetApp()->OnIdle(lIdle++)) + ; + return TRUE; +} + +/* + * Go to sleep for a little bit, waking up 100x per second to check + * the idle loop. + */ +void +MainWindow::EventPause(int duration) +{ + int count = duration / 10; + + for (int i = 0; i < count; i++) { + PeekAndPump(); + ::Sleep(10); + } +} + +/* + * Printer abort procedure; allows us to abort a print job. The DC + * SetAbortProc() function calls here periodically. The return value from + * this function determine whether or not printing halts. + * + * This checks a global "print cancel" variable, which is set by our print + * cancel button dialog. + * + * If this returns TRUE, printing continues; FALSE, and printing aborts. + */ +/*static*/ BOOL CALLBACK +MainWindow::PrintAbortProc(HDC hDC, int nCode) +{ + MainWindow* pMain = (MainWindow*)::AfxGetMainWnd(); + + pMain->PeekAndPump(); + if (pMain->GetAbortPrinting()) { + WMSG0("PrintAbortProc returning FALSE (abort printing)\n"); + return FALSE; + } + WMSG0(" PrintAbortProc returning TRUE (continue printing)\n"); + return TRUE; +} + +/* + * =================================== + * Support functions + * =================================== + */ + +/* + * Draw what looks like an empty client area. + */ +void +MainWindow::DrawEmptyClientArea(CDC* pDC, const CRect& clientRect) +{ + CBrush brush; + brush.CreateSolidBrush(::GetSysColor(COLOR_APPWORKSPACE)); // dk gray + CBrush* pOldBrush = pDC->SelectObject(&brush); + pDC->FillRect(&clientRect, &brush); + pDC->SelectObject(pOldBrush); + + CPen penWH(PS_SOLID, 1, ::GetSysColor(COLOR_3DHIGHLIGHT)); // white + CPen penLG(PS_SOLID, 1, ::GetSysColor(COLOR_3DLIGHT)); // lt gray + CPen penDG(PS_SOLID, 1, ::GetSysColor(COLOR_3DSHADOW)); // dk gray + CPen penBL(PS_SOLID, 1, ::GetSysColor(COLOR_3DDKSHADOW)); // near-black + CPen* pOldPen = pDC->SelectObject(&penWH); + //pDC->SelectObject(&penWH); + pDC->MoveTo(clientRect.right-1, clientRect.top); + pDC->LineTo(clientRect.right-1, clientRect.bottom-1); + pDC->LineTo(clientRect.left-1, clientRect.bottom-1); + pDC->SelectObject(&penBL); + pDC->MoveTo(clientRect.right-3, clientRect.top+1); + pDC->LineTo(clientRect.left+1, clientRect.top+1); + pDC->LineTo(clientRect.left+1, clientRect.bottom-2); + pDC->SelectObject(&penLG); + pDC->MoveTo(clientRect.right-2, clientRect.top+1); + pDC->LineTo(clientRect.right-2, clientRect.bottom-2); + pDC->LineTo(clientRect.left, clientRect.bottom-2); + pDC->SelectObject(&penDG); + pDC->MoveTo(clientRect.right-2, clientRect.top); + pDC->LineTo(clientRect.left, clientRect.top); + pDC->LineTo(clientRect.left, clientRect.bottom-1); + + pDC->SelectObject(pOldPen); +} + +/* + * Load an archive, using the appropriate GenericArchive subclass. If + * "createFile" is "true", a new archive file will be created (and must + * not already exist!). + * + * "filename" is the full path to the file, "extension" is the + * filetype component of the name (without the leading '.'), "filterIndex" + * is the offset into the set of filename filters used in the standard + * file dialog, "readOnly" reflects the state of the stdfile dialog + * checkbox, and "createFile" is set to true by the "New Archive" command. + * + * Returns 0 on success, nonzero on failure. + */ +int +MainWindow::LoadArchive(const char* fileName, const char* extension, + int filterIndex, bool readOnly, bool createFile) +{ + GenericArchive::OpenResult openResult; + int result = -1; + GenericArchive* pOpenArchive = nil; + int origFilterIndex = filterIndex; + CString errStr, appName; + + appName.LoadString(IDS_MB_APP_NAME); + + WMSG3("LoadArchive: '%s' ro=%d idx=%d\n", fileName, readOnly, filterIndex); + + /* close any existing archive to avoid weirdness from re-open */ + CloseArchive(); + + /* + * If they used the "All Files (*.*)" filter, we have to guess based + * on the file type. + * + * IDEA: change the current "filterIndex ==" stuff to a type-specific + * model, then do type-scanning here. Code later on takes the type + * and opens it. That way we can do the trivial "it must be" handling + * up here, and maybe do a little "open it up and see" stuff as well. + * In general, though, if we don't recognize the extension, it's + * probably a disk image. + */ + if (filterIndex == kFilterIndexGeneric) { + int i; + + for (i = 0; i < NELEM(gExtensionToIndex); i++) { + if (strcasecmp(extension, gExtensionToIndex[i].extension) == 0) { + filterIndex = gExtensionToIndex[i].idx; + break; + } + } + + if (i == NELEM(gExtensionToIndex)) + filterIndex = kFilterIndexDiskImage; + } + +try_again: + if (filterIndex == kFilterIndexBinaryII) { + /* try Binary II and nothing else */ + ASSERT(!createFile); + WMSG0(" Trying Binary II\n"); + pOpenArchive = new BnyArchive; + openResult = pOpenArchive->Open(fileName, readOnly, &errStr); + if (openResult != GenericArchive::kResultSuccess) { + if (!errStr.IsEmpty()) + ShowFailureMsg(this, errStr, IDS_FAILED); + result = -1; + goto bail; + } + } else + if (filterIndex == kFilterIndexACU) { + /* try ACU and nothing else */ + ASSERT(!createFile); + WMSG0(" Trying ACU\n"); + pOpenArchive = new AcuArchive; + openResult = pOpenArchive->Open(fileName, readOnly, &errStr); + if (openResult != GenericArchive::kResultSuccess) { + if (!errStr.IsEmpty()) + ShowFailureMsg(this, errStr, IDS_FAILED); + result = -1; + goto bail; + } + } else + if (filterIndex == kFilterIndexDiskImage) { + /* try various disk image formats */ + ASSERT(!createFile); + WMSG0(" Trying disk images\n"); + + pOpenArchive = new DiskArchive; + openResult = pOpenArchive->Open(fileName, readOnly, &errStr); + if (openResult == GenericArchive::kResultCancel) { + result = -1; + goto bail; + } else if (openResult == GenericArchive::kResultFileArchive) { + delete pOpenArchive; + pOpenArchive = nil; + + if (strcasecmp(extension, "zip") == 0) { + errStr = "ZIP archives with multiple files are not supported."; + MessageBox(errStr, appName, MB_OK|MB_ICONINFORMATION); + result = -1; + goto bail; + } else { + /* assume some variation of a ShrinkIt archive */ + // msg.LoadString(IDS_OPEN_AS_NUFX); <-- with MB_OKCANCEL + filterIndex = kFilterIndexNuFX; + goto try_again; + } + + } else if (openResult != GenericArchive::kResultSuccess) { + if (filterIndex != origFilterIndex) { + /* + * Kluge: assume we guessed disk image and were wrong. + */ + errStr = "File doesn't appear to be a valid archive" + " or disk image."; + } + if (!errStr.IsEmpty()) + ShowFailureMsg(this, errStr, IDS_FAILED); + result = -1; + goto bail; + } + } else + if (filterIndex == kFilterIndexNuFX) { + /* try NuFX (including its embedded-in-BNY form) */ + WMSG0(" Trying NuFX\n"); + + pOpenArchive = new NufxArchive; + openResult = pOpenArchive->Open(fileName, readOnly, &errStr); + if (openResult != GenericArchive::kResultSuccess) { + if (!errStr.IsEmpty()) + ShowFailureMsg(this, errStr, IDS_FAILED); + result = -1; + goto bail; + } + + } else { + ASSERT(FALSE); + result = -1; + goto bail; + } + + SwitchContentList(pOpenArchive); + + pOpenArchive = nil; + result = 0; + +bail: + if (pOpenArchive != nil) { + ASSERT(result != 0); + delete pOpenArchive; + } + return result; +} + +/* + * Open a raw disk volume. Useful for ProDOS-formatted 1.44MB floppy disks + * and CFFA flash cards. + * + * Assume it's a disk image -- it'd be a weird place for a ShrinkIt archive. + * CFFA cards can actually hold multiple volumes, but that's all taken care + * of inside the diskimg DLL. + * + * Returns 0 on success, nonzero on failure. + */ +int +MainWindow::DoOpenVolume(CString drive, bool readOnly) +{ + int result = -1; + + ASSERT(drive.GetLength() > 0); + + CString errStr; + //char filename[4] = "_:\\"; + //filename[0] = driveLetter; + + WMSG2("FileOpenVolume '%s' %d\n", (const char*)drive, readOnly); + + /* close existing archive */ + CloseArchive(); + + GenericArchive* pOpenArchive = nil; + pOpenArchive = new DiskArchive; + { + CWaitCursor waitc; + GenericArchive::OpenResult openResult; + + openResult = pOpenArchive->Open(drive, readOnly, &errStr); + if (openResult == GenericArchive::kResultCancel) { + // this bubbles out of the format confirmation dialog + goto bail; + } else if (openResult != GenericArchive::kResultSuccess) { + if (!errStr.IsEmpty()) + ShowFailureMsg(this, errStr, IDS_FAILED); + goto bail; + } + } + + // success! + SwitchContentList(pOpenArchive); + pOpenArchive = nil; + fOpenArchivePathName = drive; + result = 0; + + fOpenArchivePathName = drive; + SetCPTitle(fOpenArchivePathName, fpOpenArchive); + +bail: + if (pOpenArchive != nil) { + ASSERT(result != 0); + delete pOpenArchive; + } + return result; +} + + +/* + * Close and re-open the current archive. + */ +void +MainWindow::ReopenArchive(void) +{ + if (fpOpenArchive == nil) { + ASSERT(false); + return; + } + + /* clear the flag, regardless of success or failure */ + fNeedReopen = false; + + GenericArchive* pOpenArchive = nil; + CString pathName = fpOpenArchive->GetPathName(); + bool readOnly = fpOpenArchive->IsReadOnly(); + GenericArchive::ArchiveKind archiveKind = fpOpenArchive->GetArchiveKind(); + GenericArchive::OpenResult openResult; + CString errStr; + + /* if the open fails we *don't* want to leave the previous content up */ + WMSG3("Reopening '%s' ro=%d kind=%d\n", pathName, readOnly, archiveKind); + CloseArchive(); + + switch (archiveKind) { + case GenericArchive::kArchiveDiskImage: + pOpenArchive = new DiskArchive; + break; + case GenericArchive::kArchiveNuFX: + pOpenArchive = new NufxArchive; + break; + case GenericArchive::kArchiveBNY: + pOpenArchive = new BnyArchive; + break; + default: + ASSERT(false); + return; + } + + openResult = pOpenArchive->Open(pathName, readOnly, &errStr); + if (openResult == GenericArchive::kResultCancel) { + // this bubbles out of the format confirmation dialog + goto bail; + } else if (openResult != GenericArchive::kResultSuccess) { + if (!errStr.IsEmpty()) + ShowFailureMsg(this, errStr, IDS_FAILED); + goto bail; + } + + WMSG0(" Reopen was successful\n"); + SwitchContentList(pOpenArchive); + pOpenArchive = nil; + SetCPTitle(pathName, fpOpenArchive); + +bail: + delete pOpenArchive; +} + +/* + * Determine whether "path" matches the pathname of the currently open archive. + */ +bool +MainWindow::IsOpenPathName(const char* path) +{ + if (fpOpenArchive == nil) + return false; + + if (stricmp(path, fpOpenArchive->GetPathName()) == 0) + return true; + + return false; +} + + +/* + * Switch the content list to a new archive, closing the previous if one + * was already open. + */ +void +MainWindow::SwitchContentList(GenericArchive* pOpenArchive) +{ + assert(pOpenArchive != nil); + + /* + * We've got an archive opened successfully. If we already had one + * open, shut it. (This assumes that closing an archive is a simple + * matter of closing files and freeing storage. If we needed to do + * something that might fail, like flush changes, we should've done + * that before getting this far to avoid confusion.) + */ + if (fpOpenArchive != nil) + CloseArchive(); + + ASSERT(fpOpenArchive == nil); + ASSERT(fpContentList == nil); + + /* + * Without this we get an assertion failure in CImageList::Attach if we + * call here from ReopenArchive. I think Windows needs to do some + * cleanup, though I don't understand how the reopen case differs from + * the usual case. Maybe there's more stuff pending in the "reopen" + * case? In any event, this seems to work, which is all you can hope + * for from MFC. It does, however, make the screen flash, which it + * didn't do before. + * + * UPDATE: this tripped once while I was debugging, even with this. The + * PeekAndPump function does force the idle loop to run, so I'm not sure + * why it failed, unless the debugger somehow affected the idle + * processing. Yuck. + * + * The screen flash bugged me so I took it back out. And the assert + * didn't hit. I really, really love Windows. + */ + //PeekAndPump(); + + + fpContentList = new ContentList(pOpenArchive, + fPreferences.GetColumnLayout()); + + CRect sizeRect; + GetClientRect(&sizeRect); + fpContentList->Create(WS_CHILD | WS_VISIBLE | WS_VSCROLL, + sizeRect, this, IDC_CONTENT_LIST); + + fpOpenArchive = pOpenArchive; +} + + +/* + * Close the existing archive file, but don't try to shut down the child + * windows. This should really only be used from the destructor. + */ +void +MainWindow::CloseArchiveWOControls(void) +{ + if (fpOpenArchive != nil) { + //fpOpenArchive->Close(); + WMSG0("Deleting OpenArchive\n"); + delete fpOpenArchive; + fpOpenArchive = nil; + } +} + +/* + * Close the existing archive file, and throw out the control we're + * using to display it. + */ +void +MainWindow::CloseArchive(void) +{ + CWaitCursor waitc; // closing large compressed archive can be slow + + // destroy the ContentList + if (fpContentList != nil) { + WMSG0("Destroying ContentList\n"); + fpContentList->DestroyWindow(); // auto-cleanup invokes "delete" + fpContentList = nil; + } + + // destroy the GenericArchive + CloseArchiveWOControls(); + + // reset the title bar + SetCPTitle(); +} + + +/* + * Set the title bar on the main window. + * + * "pathname" is often different from pOpenArchive->GetPathName(), especially + * when we were launched from another instance of CiderPress and handed a + * temp file whose name we're trying to conceal. + */ +void +MainWindow::SetCPTitle(const char* pathname, GenericArchive* pOpenArchive) +{ + ASSERT(pathname != nil); + CString title; + CString archiveDescription; + CString appName; + + appName.LoadString(IDS_MB_APP_NAME); + + pOpenArchive->GetDescription(&archiveDescription); + title.Format(_T("%s - %s (%s)"), appName, pathname, archiveDescription); + + if (fpOpenArchive->IsReadOnly()) { + CString readOnly; + readOnly.LoadString(IDS_READONLY); + title += _T(" "); + title += readOnly; + } + + SetWindowText(title); +} + +/* + * Set the title bar to something boring when nothing is open. + */ +void +MainWindow::SetCPTitle(void) +{ + CString appName, regName, title; + CString user, company, reg, versions, expire; + +#if 0 + if (gMyApp.fRegistry.GetRegistration(&user, &company, ®, &versions, + &expire) == 0) + { + if (reg.IsEmpty()) { + regName += _T(" (unregistered)"); + } else { + regName += _T(" (registered to "); + regName += user; + regName += _T(")"); + // include company? + } + } +#endif + + appName.LoadString(IDS_MB_APP_NAME); + title = appName + regName; + SetWindowText(title); +} + +/* + * Come up with a title to put at the top of a printout. This is essentially + * the same as the window title, but without some flags (e.g. "read-only"). + */ +CString +MainWindow::GetPrintTitle(void) +{ + CString title; + CString archiveDescription; + CString appName; + + if (fpOpenArchive == nil) { + ASSERT(false); + return title; + } + + appName.LoadString(IDS_MB_APP_NAME); + + fpOpenArchive->GetDescription(&archiveDescription); + title.Format(_T("%s - %s (%s)"), + appName, fOpenArchivePathName, archiveDescription); + + return title; +} + + +/* + * After successful completion of a command, make a happy noise (but only + * if we're configured to do so). + */ +void +MainWindow::SuccessBeep(void) +{ + const Preferences* pPreferences = GET_PREFERENCES(); + + if (pPreferences->GetPrefBool(kPrBeepOnSuccess)) { + WMSG0("\n"); + ::MessageBeep(MB_OK); + } +} + +/* + * If something fails, make noise if we're configured for loudness. + */ +void +MainWindow::FailureBeep(void) +{ + const Preferences* pPreferences = GET_PREFERENCES(); + + if (pPreferences->GetPrefBool(kPrBeepOnSuccess)) { + WMSG0("\n"); + ::MessageBeep(MB_ICONEXCLAMATION); // maybe MB_ICONHAND? + } +} + +/* + * Remove a file. Returns a helpful error string on failure. + * + * The absence of the file is not considered an error. + */ +CString +MainWindow::RemoveFile(const char* fileName) +{ + CString errMsg; + + int cc; + cc = unlink(fileName); + if (cc < 0 && errno != ENOENT) { + int err = errno; + WMSG2("Failed removing file '%s', errno=%d\n", fileName, err); + errMsg.Format("Unable to remove '%s': %s.", + fileName, strerror(err)); + if (err == EACCES) + errMsg += "\n\n(Make sure the file isn't open.)"; + } + + return errMsg; +} + + +/* + * Configure a ReformatHolder based on the current preferences. + */ +/*static*/ void +MainWindow::ConfigureReformatFromPreferences(ReformatHolder* pReformat) +{ + const Preferences* pPreferences = GET_PREFERENCES(); + + pReformat->SetReformatAllowed(ReformatHolder::kReformatRaw, true); + pReformat->SetReformatAllowed(ReformatHolder::kReformatHexDump, true); + + pReformat->SetReformatAllowed(ReformatHolder::kReformatTextEOL_HA, + pPreferences->GetPrefBool(kPrConvTextEOL_HA)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatResourceFork, + pPreferences->GetPrefBool(kPrConvResources)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatProDOSDirectory, + pPreferences->GetPrefBool(kPrConvProDOSFolder)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatPascalText, + pPreferences->GetPrefBool(kPrConvPascalText)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatPascalCode, + pPreferences->GetPrefBool(kPrConvPascalCode)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatCPMText, + pPreferences->GetPrefBool(kPrConvCPMText)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatApplesoft, + pPreferences->GetPrefBool(kPrConvApplesoft)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatApplesoft_Hilite, + pPreferences->GetPrefBool(kPrConvApplesoft)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatInteger, + pPreferences->GetPrefBool(kPrConvInteger)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatInteger_Hilite, + pPreferences->GetPrefBool(kPrConvInteger)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatSCAssem, + pPreferences->GetPrefBool(kPrConvSCAssem)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatMerlin, + pPreferences->GetPrefBool(kPrConvSCAssem)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatLISA2, + pPreferences->GetPrefBool(kPrConvSCAssem)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatLISA3, + pPreferences->GetPrefBool(kPrConvSCAssem)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatLISA4, + pPreferences->GetPrefBool(kPrConvSCAssem)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatMonitor8, + pPreferences->GetPrefBool(kPrConvDisasm)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatDisasmMerlin8, + pPreferences->GetPrefBool(kPrConvDisasm)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatMonitor16Long, + pPreferences->GetPrefBool(kPrConvDisasm)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatMonitor16Short, + pPreferences->GetPrefBool(kPrConvDisasm)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatDisasmOrcam16, + pPreferences->GetPrefBool(kPrConvDisasm)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatAWGS_WP, + pPreferences->GetPrefBool(kPrConvGWP)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatTeach, + pPreferences->GetPrefBool(kPrConvGWP)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatGWP, + pPreferences->GetPrefBool(kPrConvGWP)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatMagicWindow, + pPreferences->GetPrefBool(kPrConvText8)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatAWP, + pPreferences->GetPrefBool(kPrConvAWP)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatAWP, + pPreferences->GetPrefBool(kPrConvAWP)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatADB, + pPreferences->GetPrefBool(kPrConvADB)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatASP, + pPreferences->GetPrefBool(kPrConvASP)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatHiRes, + pPreferences->GetPrefBool(kPrConvHiRes)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatHiRes_BW, + pPreferences->GetPrefBool(kPrConvHiRes)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatDHR_Latched, + pPreferences->GetPrefBool(kPrConvDHR)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatDHR_BW, + pPreferences->GetPrefBool(kPrConvDHR)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatDHR_Plain140, + pPreferences->GetPrefBool(kPrConvDHR)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatDHR_Window, + pPreferences->GetPrefBool(kPrConvDHR)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatSHR_PIC, + pPreferences->GetPrefBool(kPrConvSHR)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatSHR_JEQ, + pPreferences->GetPrefBool(kPrConvSHR)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatSHR_Paintworks, + pPreferences->GetPrefBool(kPrConvSHR)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatSHR_Packed, + pPreferences->GetPrefBool(kPrConvSHR)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatSHR_APF, + pPreferences->GetPrefBool(kPrConvSHR)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatSHR_3200, + pPreferences->GetPrefBool(kPrConvSHR)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatSHR_3201, + pPreferences->GetPrefBool(kPrConvSHR)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatSHR_DG256, + pPreferences->GetPrefBool(kPrConvSHR)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatSHR_DG3200, + pPreferences->GetPrefBool(kPrConvSHR)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatPrintShop, + pPreferences->GetPrefBool(kPrConvPrintShop)); + pReformat->SetReformatAllowed(ReformatHolder::kReformatMacPaint, + pPreferences->GetPrefBool(kPrConvMacPaint)); + + pReformat->SetOption(ReformatHolder::kOptHiliteHexDump, + pPreferences->GetPrefBool(kPrHighlightHexDump)); + pReformat->SetOption(ReformatHolder::kOptHiliteBASIC, + pPreferences->GetPrefBool(kPrHighlightBASIC)); + pReformat->SetOption(ReformatHolder::kOptHiResBW, + pPreferences->GetPrefBool(kPrConvHiResBlackWhite)); + pReformat->SetOption(ReformatHolder::kOptDHRAlgorithm, + pPreferences->GetPrefLong(kPrConvDHRAlgorithm)); + pReformat->SetOption(ReformatHolder::kOptRelaxGfxTypeCheck, + pPreferences->GetPrefBool(kPrRelaxGfxTypeCheck)); + pReformat->SetOption(ReformatHolder::kOptOneByteBrkCop, + pPreferences->GetPrefBool(kPrDisasmOneByteBrkCop)); +} + +/* + * Convert a DiskImg format spec into a ReformatHolder SourceFormat. + */ +/*static*/ ReformatHolder::SourceFormat +MainWindow::ReformatterSourceFormat(DiskImg::FSFormat format) +{ + if (DiskImg::UsesDOSFileStructure(format)) + return ReformatHolder::kSourceFormatDOS; + else if (format == DiskImg::kFormatCPM) + return ReformatHolder::kSourceFormatCPM; + else + return ReformatHolder::kSourceFormatGeneric; +} diff --git a/app/Main.h b/app/Main.h new file mode 100644 index 0000000..ed5a663 --- /dev/null +++ b/app/Main.h @@ -0,0 +1,435 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Application UI classes. + */ +#ifndef __MAIN__ +#define __MAIN__ + +#include "ContentList.h" +#include "GenericArchive.h" +#include "PrefsDialog.h" +#include "ActionProgressDialog.h" +#include "ProgressCounterDialog.h" +#include "AddFilesDialog.h" +#include "ExtractOptionsDialog.h" +#include "ConvFileOptionsDialog.h" +#include "DiskConvertDialog.h" +#include "FileNameConv.h" +//#include "ProgressCancelDialog.h" + +/* user-defined window messages */ +#define WMU_LATE_INIT (WM_USER+0) +#define WMU_START (WM_USER+1) // used by ActionProgressDialog + +typedef enum { + kFilterIndexNuFX = 1, + kFilterIndexBinaryII = 2, + kFilterIndexACU = 3, + kFilterIndexDiskImage = 4, + kFilterIndexGeneric = 5, // *.* filter used +} FilterIndex; + +struct FileCollectionEntry; // fwd + +/* + * The main UI window. + */ +class MainWindow : public CFrameWnd +{ +public: + MainWindow(void); + ~MainWindow(void); + + // Overridden functions + BOOL PreCreateWindow(CREATESTRUCT& cs); + //BOOL OnCreateClient( LPCREATESTRUCT lpcs, CCreateContext* pContext ); + void GetClientRect(LPRECT lpRect) const; + + // get a pointer to the preferences + const Preferences* GetPreferences(void) const { return &fPreferences; } + Preferences* GetPreferencesWr(void) { return &fPreferences; } + // apply an update from the Preferences pages + void ApplyNow(PrefsSheet*); + + // get the text of the next file in the selection list + int GetPrevFileText(ReformatHolder* pHolder, CString* pTitle); + int GetNextFileText(ReformatHolder* pHolder, CString* pTitle); + + // update the progress meter + void SetProgressBegin(void); + int SetProgressUpdate(int percent, const char* oldName, + const char* newName); + void SetProgressEnd(void); + + // update the progress counter + bool SetProgressCounter(const char* fmt, long val); + + // handle a double-click in the content view + void HandleDoubleClick(void); + + // do some idle processing + void DoIdle(void); + + // return the title to put at the top of a printout + CString GetPrintTitle(void); + + // raise flag to abort the current print job + void SetAbortPrinting(bool val) { fAbortPrinting = val; } + bool GetAbortPrinting(void) const { return fAbortPrinting; } + static BOOL CALLBACK PrintAbortProc(HDC hDC, int nCode); + bool fAbortPrinting; + // track printer choice + HANDLE fhDevMode; + HANDLE fhDevNames; + + // set flag to abort current operation + //void SetAbortOperation(bool val) { fAbortOperation = val; } + //bool fAbortOperation; + + // pause, for debugging + void EventPause(int duration); + + ContentList* GetContentList(void) const { return fpContentList; } + + void SetActionProgressDialog(ActionProgressDialog* pActionProgress) { + fpActionProgress = pActionProgress; + } + void SetProgressCounterDialog(ProgressCounterDialog* pProgressCounter) { + fpProgressCounter = pProgressCounter; + } + GenericArchive* GetOpenArchive(void) const { return fpOpenArchive; } + + int GetFileParts(const GenericEntry* pEntry, + ReformatHolder** ppHolder) const; + + // force processing of pending messages + BOOL PeekAndPump(); + + // make a happy noise after successful execution of a command + void SuccessBeep(void); + // make a not-so-happy noise + void FailureBeep(void); + + // remove a file, returning a helpful message on failure + CString RemoveFile(const char* fileName); + + // choose the place to put a file + bool ChooseAddTarget(DiskImgLib::A2File** ppTargetSubdir, + DiskImgLib::DiskFS** ppDiskFS); + + // try a disk image override dialog + int TryDiskImgOverride(DiskImg* pImg, const char* fileSource, + DiskImg::FSFormat defaultFormat, int* pDisplayFormat, + bool allowUnknown, CString* pErrMsg); + // copy all blocks from one disk image to another + DIError CopyDiskImage(DiskImg* pDstImg, DiskImg* pSrcImg, bool bulk, + bool partial, ProgressCancelDialog* pPCDialog); + + // does the currently open archive pathname match? + bool IsOpenPathName(const char* path); + // raise a flag to cause a full reload of the open file + void SetReopenFlag(void) { fNeedReopen = true; } + + static void ConfigureReformatFromPreferences(ReformatHolder* pReformat); + static ReformatHolder::SourceFormat ReformatterSourceFormat(DiskImg::FSFormat format); + + // save a buffer of data as a file in a disk image or file archive + static bool SaveToArchive(GenericArchive::FileDetails* pDetails, + const unsigned char* dataBuf, long dataLen, + const unsigned char* rsrcBuf, long rsrcLen, + CString& errMsg, CWnd* pDialog); + + static const char kOpenNuFX[]; + static const char kOpenBinaryII[]; + static const char kOpenACU[]; + static const char kOpenDiskImage[]; + static const char kOpenAll[]; + static const char kOpenEnd[]; + +private: + static const char* kModeNuFX; + static const char* kModeBinaryII; + static const char* kModeACU; + static const char* kModeDiskImage; + + // Command handlers + afx_msg int OnCreate(LPCREATESTRUCT lpcs); + afx_msg LONG OnLateInit(UINT, LONG); + //afx_msg LONG OnCloseMainDialog(UINT, LONG); + afx_msg void OnSize(UINT nType, int cx, int cy); + afx_msg void OnGetMinMaxInfo(MINMAXINFO* pMMI); + afx_msg void OnPaint(void); + //afx_msg BOOL OnMouseWheel(UINT nFlags, short zDelta, CPoint pt); + afx_msg void OnSetFocus(CWnd* pOldWnd); + afx_msg BOOL OnHelpInfo(HELPINFO* lpHelpInfo); + afx_msg BOOL OnQueryEndSession(void); + afx_msg void OnEndSession(BOOL bEnding); + afx_msg LRESULT OnFindDialogMessage(WPARAM wParam, LPARAM lParam); + //afx_msg LONG OnHelp(UINT wParam, LONG lParam); + afx_msg void OnFileNewArchive(void); + afx_msg void OnFileOpen(void); + afx_msg void OnFileOpenVolume(void); + afx_msg void OnUpdateFileOpenVolume(CCmdUI* pCmdUI); + afx_msg void OnFileReopen(void); + afx_msg void OnUpdateFileReopen(CCmdUI* pCmdUI); + afx_msg void OnFileSave(void); + afx_msg void OnUpdateFileSave(CCmdUI* pCmdUI); + afx_msg void OnFileClose(void); + afx_msg void OnUpdateFileClose(CCmdUI* pCmdUI); + afx_msg void OnFileArchiveInfo(void); + afx_msg void OnUpdateFileArchiveInfo(CCmdUI* pCmdUI); + afx_msg void OnFilePrint(void); + afx_msg void OnUpdateFilePrint(CCmdUI* pCmdUI); + afx_msg void OnFileExit(void); + afx_msg void OnEditCopy(void); + afx_msg void OnUpdateEditCopy(CCmdUI* pCmdUI); + afx_msg void OnEditPaste(void); + afx_msg void OnUpdateEditPaste(CCmdUI* pCmdUI); + afx_msg void OnEditPasteSpecial(void); + afx_msg void OnUpdateEditPasteSpecial(CCmdUI* pCmdUI); + afx_msg void OnEditFind(void); + afx_msg void OnUpdateEditFind(CCmdUI* pCmdUI); + afx_msg void OnEditSelectAll(void); + afx_msg void OnUpdateEditSelectAll(CCmdUI* pCmdUI); + afx_msg void OnEditInvertSelection(void); + afx_msg void OnUpdateEditInvertSelection(CCmdUI* pCmdUI); + afx_msg void OnEditPreferences(void); + afx_msg void OnEditSort(UINT id); + afx_msg void OnUpdateEditSort(CCmdUI* pCmdUI); + afx_msg void OnActionsView(void); + afx_msg void OnUpdateActionsView(CCmdUI* pCmdUI); + afx_msg void OnActionsOpenAsDisk(void); + afx_msg void OnUpdateActionsOpenAsDisk(CCmdUI* pCmdUI); + afx_msg void OnActionsAddFiles(void); + afx_msg void OnUpdateActionsAddFiles(CCmdUI* pCmdUI); + afx_msg void OnActionsAddDisks(void); + afx_msg void OnUpdateActionsAddDisks(CCmdUI* pCmdUI); + afx_msg void OnActionsCreateSubdir(void); + afx_msg void OnUpdateActionsCreateSubdir(CCmdUI* pCmdUI); + afx_msg void OnActionsExtract(void); + afx_msg void OnUpdateActionsExtract(CCmdUI* pCmdUI); + afx_msg void OnActionsTest(void); + afx_msg void OnUpdateActionsTest(CCmdUI* pCmdUI); + afx_msg void OnActionsDelete(void); + afx_msg void OnUpdateActionsDelete(CCmdUI* pCmdUI); + afx_msg void OnActionsRename(void); + afx_msg void OnUpdateActionsRename(CCmdUI* pCmdUI); + afx_msg void OnActionsEditComment(void); + afx_msg void OnUpdateActionsEditComment(CCmdUI* pCmdUI); + afx_msg void OnActionsEditProps(void); + afx_msg void OnUpdateActionsEditProps(CCmdUI* pCmdUI); + afx_msg void OnActionsRenameVolume(void); + afx_msg void OnUpdateActionsRenameVolume(CCmdUI* pCmdUI); + afx_msg void OnActionsRecompress(void); + afx_msg void OnUpdateActionsRecompress(CCmdUI* pCmdUI); + afx_msg void OnActionsConvDisk(void); + afx_msg void OnUpdateActionsConvDisk(CCmdUI* pCmdUI); + afx_msg void OnActionsConvFile(void); + afx_msg void OnUpdateActionsConvFile(CCmdUI* pCmdUI); + afx_msg void OnActionsConvToWav(void); + afx_msg void OnUpdateActionsConvToWav(CCmdUI* pCmdUI); + afx_msg void OnActionsConvFromWav(void); + afx_msg void OnUpdateActionsConvFromWav(CCmdUI* pCmdUI); + afx_msg void OnActionsImportBAS(void); + afx_msg void OnUpdateActionsImportBAS(CCmdUI* pCmdUI); + afx_msg void OnToolsDiskEdit(void); + afx_msg void OnToolsDiskConv(void); + afx_msg void OnToolsBulkDiskConv(void); + afx_msg void OnToolsSSTMerge(void); + afx_msg void OnToolsVolumeCopierVolume(void); + afx_msg void OnToolsVolumeCopierFile(void); + afx_msg void OnToolsEOLScanner(void); + afx_msg void OnToolsTwoImgProps(void); + afx_msg void OnToolsDiskImageCreator(void); + afx_msg void OnHelpContents(void); + afx_msg void OnHelpWebSite(void); + afx_msg void OnHelpOrdering(void); + afx_msg void OnHelpAbout(void); + afx_msg void OnRtClkDefault(void); + + void ProcessCommandLine(void); + void ResizeClientArea(void); + void DrawEmptyClientArea(CDC* pDC, const CRect& clientRect); + int TmpExtractAndOpen(GenericEntry* pEntry, int threadKind, + const char* modeStr); + int TmpExtractForExternal(GenericEntry* pEntry); + void DoOpenArchive(const char* pathName, const char* ext, + int filterIndex, bool readOnly); + int LoadArchive(const char* filename, const char* extension, + int filterIndex, bool readOnly, bool createFile); + int DoOpenVolume(CString drive, bool readOnly); + void SwitchContentList(GenericArchive* pOpenArchive); + void CloseArchiveWOControls(void); + void CloseArchive(void); + void SetCPTitle(const char* pathname, GenericArchive* pArchive); + void SetCPTitle(void); + GenericEntry* GetSelectedItem(ContentList* pContentList); + void HandleView(void); + + void DeleteFileOnExit(const char* name); + + void ReopenArchive(void); + + /* some stuff from Actions.cpp */ + //int GetFileText(SelectionEntry* pSelEntry, ReformatHolder* pHolder, + // CString* pTitle); + void GetFilePart(const GenericEntry* pEntry, int whichThread, + ReformatHolder* pHolder) const; + + void DoBulkExtract(SelectionSet* pSelSet, + const ExtractOptionsDialog* pExtOpts); + bool ExtractEntry(GenericEntry* pEntry, int thread, + ReformatHolder* pHolder, const ExtractOptionsDialog* pExtOpts, + bool* pOverwriteExisting, bool* pOvwrForAll); + int OpenOutputFile(CString* pOutputPath, const PathProposal& pathProp, + time_t arcFileModWhen, bool* pOverwriteExisting, bool* pOvwrForAll, + FILE** pFp); + bool DoBulkRecompress(ActionProgressDialog* pActionProgress, + SelectionSet* pSelSet, const RecompressOptionsDialog* pRecompOpts); + void CalcTotalSize(LONGLONG* pUncomp, LONGLONG* pComp) const; + + /* some stuff from Clipboard.cpp */ + CString CreateFileList(SelectionSet* pSelSet); + static CString DblDblQuote(const char* str); + long GetClipboardContentLen(void); + HGLOBAL CreateFileCollection(SelectionSet* pSelSet); + CString CopyToCollection(GenericEntry* pEntry, void** pBuf, long* pBufLen); + void DoPaste(bool pasteJunkPaths); + CString ProcessClipboard(const void* vbuf, long bufLen, + bool pasteJunkPaths); + CString ProcessClipboardEntry(const FileCollectionEntry* pCollEnt, + const char* pathName, const unsigned char* buf, long remLen); + + /* some stuff from Tools.cpp */ + int DetermineImageSettings(int convertIdx, bool addGzip, + DiskImg::OuterFormat* pOuterFormat, DiskImg::FileFormat* pFileFormat, + DiskImg::PhysicalFormat* pPhysicalFormat, + DiskImg::SectorOrder* pSectorOrder); + void BulkConvertImage(const char* pathName, const char* targetDir, + const DiskConvertDialog& convDlg, CString* pErrMsg); + int SSTOpenImage(int seqNum, DiskImg* pDiskImg); + int SSTLoadData(int seqNum, DiskImg* pDiskImg, unsigned char* trackBuf, + long* pBadCount); + long SSTGetBufOffset(int track); + long SSTCountBadBytes(const unsigned char* sctBuf, int count); + void SSTProcessTrackData(unsigned char* trackBuf); + void VolumeCopier(bool openFile); + bool EditTwoImgProps(const char* fileName); + + + void PrintListing(const ContentList* pContentList); + + // set when one of the tools modifies the file we have open + bool fNeedReopen; + + CToolBar fToolBar; + CStatusBar fStatusBar; + + // currently-open archive, if any + GenericArchive* fpOpenArchive; + // name of open archive, for display only -- if this is a temporary + // file launched from another instance of CP, this won't be the name + // of an actual file on disk. + CString fOpenArchivePathName; // for display only + + // archive viewer, open when file is open + // NOTE: make a super-class for a tree-structured display or other + // kinds of display, so we can avoid the if/then/else. Rename + // ContentList to DetailList or FlatList or something. + ContentList* fpContentList; + + // currently selected set of goodies; used when viewing, extracting, etc. + //SelectionSet* fpSelSet; + + // action progress meter, if any + ActionProgressDialog* fpActionProgress; + + // progress counter meter, if any + ProgressCounterDialog* fpProgressCounter; + + // modeless standard "find" dialog + CFindReplaceDialog* fpFindDialog; + CString fFindLastStr; + bool fFindDown; + bool fFindMatchCase; + bool fFindMatchWholeWord; + + // our preferences + Preferences fPreferences; + + /* + * Manage a list of files that must be deleted before we exit. + */ + class DeleteList { + private: + class DeleteListNode { + public: + DeleteListNode(const CString& name) : fName(name), + fPrev(nil), fNext(nil) {} + ~DeleteListNode(void) {} + + DeleteListNode* fPrev; + DeleteListNode* fNext; + CString fName; + }; + + public: + DeleteList(void) { fHead = nil; } + ~DeleteList(void) { + WMSG1("Processing DeleteList (head=0x%08lx)\n", fHead); + DeleteListNode* pNode = fHead; + DeleteListNode* pNext; + + while (pNode != nil) { + pNext = pNode->fNext; + if (unlink(pNode->fName) != 0) { + WMSG2(" WARNING: delete of '%s' failed, err=%d\n", + pNode->fName, errno); + } else { + WMSG1(" Deleted '%s'\n", pNode->fName); + } + delete pNode; + pNode = pNext; + } + WMSG0("Processing DeleteList completed\n"); + } + + void Add(const CString& name) { + DeleteListNode* pNode = new DeleteListNode(name); + if (fHead != nil) { + fHead->fPrev = pNode; + pNode->fNext = fHead; + } + fHead = pNode; + WMSG1("Delete-on-exit '%s'\n", (LPCTSTR) name); + } + + DeleteListNode* fHead; + }; + DeleteList fDeleteList; + + DECLARE_MESSAGE_MAP() +}; + +#define GET_MAIN_WINDOW() ((MainWindow*)::AfxGetMainWnd()) + +#define SET_PROGRESS_BEGIN() ((MainWindow*)::AfxGetMainWnd())->SetProgressBegin() +#define SET_PROGRESS_UPDATE(perc) \ + ((MainWindow*)::AfxGetMainWnd())->SetProgressUpdate(perc, nil, nil) +#define SET_PROGRESS_UPDATE2(perc, oldName, newName) \ + ((MainWindow*)::AfxGetMainWnd())->SetProgressUpdate(perc, oldName, newName) +#define SET_PROGRESS_END() ((MainWindow*)::AfxGetMainWnd())->SetProgressEnd() + +#define SET_PROGRESS_COUNTER(val) \ + ((MainWindow*)::AfxGetMainWnd())->SetProgressCounter(nil, val) +#define SET_PROGRESS_COUNTER_2(fmt, val) \ + ((MainWindow*)::AfxGetMainWnd())->SetProgressCounter(fmt, val) + +#define GET_PREFERENCES() ((MainWindow*)::AfxGetMainWnd())->GetPreferences() +#define GET_PREFERENCES_WR() ((MainWindow*)::AfxGetMainWnd())->GetPreferencesWr() + +#endif /*__MAIN__*/ \ No newline at end of file diff --git a/app/MyApp.cpp b/app/MyApp.cpp new file mode 100644 index 0000000..4da463a --- /dev/null +++ b/app/MyApp.cpp @@ -0,0 +1,221 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * The application object. + */ +#include "stdafx.h" +#include "../util/UtilLib.h" +#include "MyApp.h" +#include "Registry.h" +#include "Main.h" +#include "DiskArchive.h" +#include + +/* magic global that MFC finds (or that finds MFC) */ +MyApp gMyApp; + +#if defined(_DEBUG_LOG) +FILE* gLog = nil; +int gPid = -1; +#endif + +/* + * Constructor. This is the closest thing to "main" that we have, but we + * should wait for InitInstance for most things. + */ +MyApp::MyApp(LPCTSTR lpszAppName) : CWinApp(lpszAppName) +{ + const int kStaleLog = 8 * 60 * 60; + + //fclose(fopen("c:\\cp-myapp.txt", "w")); + + time_t now; + now = time(nil); + +#ifdef _DEBUG_LOG + PathName debugPath(kDebugLog); + time_t when = debugPath.GetModWhen(); + if (when > 0 && now - when > kStaleLog) { + /* log file is more than 8 hours old, remove it */ + /* [consider opening it and truncating with chsize() instead, so we + don't hose somebody's custom access permissions. ++ATM 20041015] */ + unlink(kDebugLog); + } + gLog = fopen(kDebugLog, "a"); + if (gLog == nil) + abort(); + ::setvbuf(gLog, nil, _IONBF, 0); + + gPid = ::getpid(); + fprintf(gLog, "\n"); + if (when > 0) { + WMSG2("(Log file was %.3f hours old; logs are reset after %.3f)\n", + (now - when) / 3600.0, kStaleLog / 3600.0); + } +#endif + + WMSG5("CiderPress v%d.%d.%d%s started at %.24s\n", + kAppMajorVersion, kAppMinorVersion, kAppBugVersion, + kAppDevString, ctime(&now)); + + int tmpDbgFlag; + // enable memory leak detection + tmpDbgFlag = _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG); + tmpDbgFlag |= _CRTDBG_LEAK_CHECK_DF; + _CrtSetDbgFlag(tmpDbgFlag); + WMSG0("Leak detection enabled\n"); +} + +/* + * This is the last point of control we have. + */ +MyApp::~MyApp(void) +{ + DiskArchive::AppCleanup(); + NiftyList::AppCleanup(); + + WMSG0("SHUTTING DOWN\n\n"); +#ifdef _DEBUG_LOG + if (gLog != nil) + fclose(gLog); +#endif +} + + +/* + * It all begins here. + * + * Create a main window. + */ +BOOL +MyApp::InitInstance(void) +{ + //fclose(fopen("c:\\cp-initinstance.txt", "w")); + + //_CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_DEBUG); + + m_pMainWnd = new MainWindow; + m_pMainWnd->ShowWindow(m_nCmdShow); + m_pMainWnd->UpdateWindow(); + + WMSG0("Happily in InitInstance!\n"); + + /* find our .EXE file */ + //HMODULE hModule = ::GetModuleHandle(NULL); + char buf[MAX_PATH]; + if (::GetModuleFileName(nil /*hModule*/, buf, sizeof(buf)) != 0) { + WMSG1("Module name is '%s'\n", buf); + fExeFileName = buf; + + char* cp = strrchr(buf, '\\'); + if (cp == nil) + fExeBaseName = ""; + else + fExeBaseName = fExeFileName.Left(cp - buf +1); + } else { + WMSG1("BIG problem: GetModuleFileName failed (err=%ld)\n", + ::GetLastError()); + } + + LogModuleLocation("riched.dll"); + LogModuleLocation("riched20.dll"); + LogModuleLocation("riched32.dll"); + +#if 0 + /* find our .INI file by tweaking the EXE path */ + char* cp = strrchr(buf, '\\'); + if (cp == nil) + cp = buf; + else + cp++; + if (cp + ::lstrlen(_T("CiderPress.INI")) >= buf+sizeof(buf)) + return FALSE; + ::lstrcpy(cp, _T("CiderPress.INI")); + + free((void*)m_pszProfileName); + m_pszProfileName = strdup(buf); + WMSG1("Profile name is '%s'\n", m_pszProfileName); + + if (!WriteProfileString("SectionOne", "MyEntry", "test")) + WMSG0("WriteProfileString failed\n"); +#endif + + SetRegistryKey(fRegistry.GetAppRegistryKey()); + + //WMSG1("Registry key is '%s'\n", m_pszRegistryKey); + //WMSG1("Profile name is '%s'\n", m_pszProfileName); + WMSG1("Short command line is '%s'\n", m_lpCmdLine); + //WMSG1("CP app name is '%s'\n", m_pszAppName); + //WMSG1("CP exe name is '%s'\n", m_pszExeName); + WMSG1("CP help file is '%s'\n", m_pszHelpFilePath); + WMSG1("Command line is '%s'\n", ::GetCommandLine()); + + //if (!WriteProfileString("SectionOne", "MyEntry", "test")) + // WMSG0("WriteProfileString failed\n"); + + /* + * If we're installing or uninstalling, do what we need to and then + * bail immediately. This will hemorrhage memory, but I'm sure the + * incredibly robust Windows environment will take it in stride. + */ + if (strcmp(m_lpCmdLine, _T("-install")) == 0) { + WMSG0("Invoked with INSTALL flag\n"); + fRegistry.OneTimeInstall(); + exit(0); + } else if (strcmp(m_lpCmdLine, _T("-uninstall")) == 0) { + WMSG0("Invoked with UNINSTALL flag\n"); + fRegistry.OneTimeUninstall(); + exit(1); // tell DeployMaster to continue with uninstall + } + + fRegistry.FixBasicSettings(); + + return TRUE; +} + +/* + * Show where we got something from. Handy for checking DLL load locations. + * + * If "name" is nil, we show the EXE info. + */ +void +MyApp::LogModuleLocation(const char* name) +{ + HMODULE hModule; + char fileNameBuf[256]; + hModule = ::GetModuleHandle(name); + if (hModule != nil && + ::GetModuleFileName(hModule, fileNameBuf, sizeof(fileNameBuf)) != 0) + { + // GetModuleHandle does not increase ref count, so no need to release + WMSG2("Module '%s' loaded from '%s'\n", name, fileNameBuf); + } else { + WMSG1("Module '%s' not loaded\n", name); + } +} + +/* + * Do some idle processing. + */ +BOOL +MyApp::OnIdle(LONG lCount) +{ + BOOL bMore = CWinApp::OnIdle(lCount); + + //if (lCount == 0) { + // WMSG1("IDLE lcount=%d\n", lCount); + //} + + /* + * If MFC is done, we take a swing. + */ + if (bMore == false) { + /* downcast */ + ((MainWindow*)m_pMainWnd)->DoIdle(); + } + + return bMore; +} diff --git a/app/MyApp.h b/app/MyApp.h new file mode 100644 index 0000000..3598eaf --- /dev/null +++ b/app/MyApp.h @@ -0,0 +1,52 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * The application object. + */ +#ifndef __MYAPP__ +#define __MYAPP__ + +#include "Registry.h" + +#if defined(_DEBUG_LOG) +//#define kDebugLog "C:\\test\\cplog.txt" +#define kDebugLog "C:\\cplog.txt" +#endif + +/* CiderPress version numbers */ +#define kAppMajorVersion 3 +#define kAppMinorVersion 0 +#define kAppBugVersion 0 +#define kAppDevString "" + +/* + * Windows application object. + */ +class MyApp: public CWinApp +{ +public: + MyApp(LPCTSTR lpszAppName = NULL); + virtual ~MyApp(void); + + MyRegistry fRegistry; + + const char* GetExeFileName(void) const { return fExeFileName; } + const char* GetExeBaseName(void) const { return fExeBaseName; } + +private: + // Overridden functions + virtual BOOL InitInstance(void); + virtual BOOL OnIdle(LONG lCount); + + void LogModuleLocation(const char* name); + + CString fExeFileName; + CString fExeBaseName; +}; + +extern MyApp gMyApp; + +#endif /*__MYAPP__*/ \ No newline at end of file diff --git a/app/NewDiskSize.cpp b/app/NewDiskSize.cpp new file mode 100644 index 0000000..9920c99 --- /dev/null +++ b/app/NewDiskSize.cpp @@ -0,0 +1,166 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Functions and data to support the "new disk size" radio buttons. + */ +#include "stdafx.h" +#include "NewDiskSize.h" +#include "resource.h" + +/* + * Number of blocks in the disks we create. + * + * These must be in ascending order. + */ +/*static*/ const NewDiskSize::RadioCtrlMap NewDiskSize::kCtrlMap[] = { + { IDC_CONVDISK_140K, 280 }, + { IDC_CONVDISK_800K, 1600 }, + { IDC_CONVDISK_1440K, 2880 }, + { IDC_CONVDISK_5MB, 10240 }, + { IDC_CONVDISK_16MB, 32768 }, + { IDC_CONVDISK_20MB, 40960 }, + { IDC_CONVDISK_32MB, 65535 }, + { IDC_CONVDISK_SPECIFY, kSpecified }, +}; +static const kEditBoxID = IDC_CONVDISK_SPECIFY_EDIT; + +/* + * Return the #of entries in the table. + */ +/*static*/ unsigned int +NewDiskSize::GetNumSizeEntries(void) +{ + return NELEM(kCtrlMap); +} + +/* + * Return the "size" field from an array entry. + */ +/*static*/ long +NewDiskSize::GetDiskSizeByIndex(int idx) +{ + ASSERT(idx >= 0 && idx < NELEM(kCtrlMap)); + return kCtrlMap[idx].blocks; +} + +/*static*/ void +NewDiskSize::EnableButtons(CDialog* pDialog, BOOL state /*=true*/) +{ + CWnd* pWnd; + + for (int i = 0; i < NELEM(kCtrlMap); i++) { + pWnd = pDialog->GetDlgItem(kCtrlMap[i].ctrlID); + if (pWnd != nil) + pWnd->EnableWindow(state); + } +} + +/* + * Run through the set of radio buttons, disabling any that don't have enough + * space to hold the ProDOS volume with the specified parameters. + * + * The space required is equal to the blocks required for data plus the blocks + * required for the free-space bitmap. Since the free-space bitmap size is + * smaller for smaller volumes, we have to adjust it for each. + * + * Pass in the total blocks and #of blocks used on a particular ProDOS volume. + * This will compute how much space would be required for larger and smaller + * volumes, and enable or disable radio buttons as appropriate. (You can get + * these values from DiskFS::GetFreeBlockCount()). + */ +/*static*/ void +NewDiskSize::EnableButtons_ProDOS(CDialog* pDialog, long totalBlocks, + long blocksUsed) +{ + CButton* pButton; + long usedWithoutBitmap = blocksUsed - GetNumBitmapBlocks_ProDOS(totalBlocks); + bool first = true; + + WMSG3("EnableButtons_ProDOS total=%ld used=%ld usedw/o=%ld\n", + totalBlocks, blocksUsed, usedWithoutBitmap); + + for (int i = 0; i < NELEM(kCtrlMap); i++) { + pButton = (CButton*) pDialog->GetDlgItem(kCtrlMap[i].ctrlID); + if (pButton == nil) { + WMSG1("WARNING: couldn't find ctrlID %d\n", kCtrlMap[i].ctrlID); + continue; + } + + if (kCtrlMap[i].blocks == kSpecified) { + pButton->SetCheck(BST_UNCHECKED); + pButton->EnableWindow(TRUE); + CWnd* pWnd = pDialog->GetDlgItem(kEditBoxID); + pWnd->EnableWindow(FALSE); + continue; + } + + if (usedWithoutBitmap + GetNumBitmapBlocks_ProDOS(kCtrlMap[i].blocks) <= + kCtrlMap[i].blocks) + { + pButton->EnableWindow(TRUE); + if (first) { + pButton->SetCheck(BST_CHECKED); + first = false; + } else { + pButton->SetCheck(BST_UNCHECKED); + } + } else { + pButton->EnableWindow(FALSE); + pButton->SetCheck(BST_UNCHECKED); + } + } + + UpdateSpecifyEdit(pDialog); +} + +/* + * Compute the #of blocks needed to hold the ProDOS block bitmap. + */ +/*static*/long +NewDiskSize::GetNumBitmapBlocks_ProDOS(long totalBlocks) { + ASSERT(totalBlocks > 0); + const int kBitsPerBlock = 512 * 8; + int numBlocks = (totalBlocks + kBitsPerBlock-1) / kBitsPerBlock; + return numBlocks; +} + + +/* + * Update the "specify size" edit box. + */ +/*static*/ void +NewDiskSize::UpdateSpecifyEdit(CDialog* pDialog) +{ + CEdit* pEdit = (CEdit*) pDialog->GetDlgItem(kEditBoxID); + int i; + + if (pEdit == nil) { + ASSERT(false); + return; + } + + for (i = 0; i < NELEM(kCtrlMap); i++) { + CButton* pButton = (CButton*) pDialog->GetDlgItem(kCtrlMap[i].ctrlID); + if (pButton == nil) { + WMSG1("WARNING: couldn't find ctrlID %d\n", kCtrlMap[i].ctrlID); + continue; + } + + if (pButton->GetCheck() == BST_CHECKED) { + if (kCtrlMap[i].blocks == kSpecified) + return; + break; + } + } + if (i == NELEM(kCtrlMap)) { + WMSG0("WARNING: couldn't find a checked radio button\n"); + return; + } + + CString fmt; + fmt.Format("%ld", kCtrlMap[i].blocks); + pEdit->SetWindowText(fmt); +} diff --git a/app/NewDiskSize.h b/app/NewDiskSize.h new file mode 100644 index 0000000..dd83169 --- /dev/null +++ b/app/NewDiskSize.h @@ -0,0 +1,38 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Functions to manage the "new disk size" radio button set in dialogs. + */ +#ifndef __NEWDISKSIZE__ +#define __NEWDISKSIZE__ + +/* + * All members are static. Don't instantiate the class. + */ +class NewDiskSize { +public: + NewDiskSize(void) { ASSERT(false); } + + static unsigned int GetNumSizeEntries(void); + static long GetDiskSizeByIndex(int idx); + enum { kSpecified = -1 }; + + static void EnableButtons(CDialog* pDialog, BOOL state = true); + static void EnableButtons_ProDOS(CDialog* pDialog, long totalBlocks, + long blocksUsed); + static long GetNumBitmapBlocks_ProDOS(long totalBlocks); + static void UpdateSpecifyEdit(CDialog* pDialog); + +private: + typedef struct { + int ctrlID; + long blocks; + } RadioCtrlMap; + + static const RadioCtrlMap kCtrlMap[]; +}; + +#endif /*__NEWDISKSIZE__*/ \ No newline at end of file diff --git a/app/NewFolderDialog.cpp b/app/NewFolderDialog.cpp new file mode 100644 index 0000000..af050e4 --- /dev/null +++ b/app/NewFolderDialog.cpp @@ -0,0 +1,84 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Allow the user to create a new folder. + */ +#include "stdafx.h" +#include "NewFolderDialog.h" + +BEGIN_MESSAGE_MAP(NewFolderDialog, CDialog) + ON_WM_HELPINFO() +END_MESSAGE_MAP() + +/* + * Convert values. + * + * It is very important to keep '\\' out of the folder path, because it allows + * for all sorts of behavior (like "..\foo" or "D:\ack") that the caller + * might not be expecting. For example, if it's displaying a tree, it + * might assume that the folder goes under the currently selected node. + * + * Under WinNT, '/' is regarded as equivalent to '\', so we have to block + * that as well. + * + * Other characters (':') are also dangerous, but so long as we start with + * a valid path, Windows will prevent them from being used where they are + * inappropriate. + */ +void +NewFolderDialog::DoDataExchange(CDataExchange* pDX) +{ + if (!pDX->m_bSaveAndValidate) + DDX_Text(pDX, IDC_NEWFOLDER_CURDIR, fCurrentFolder); + + DDX_Text(pDX, IDC_NEWFOLDER_NAME, fNewFolder); + + /* validate the new folder by creating it */ + if (pDX->m_bSaveAndValidate) { + if (fNewFolder.IsEmpty()) { + MessageBox("No name entered, not creating new folder.", + "CiderPress", MB_OK); + // fall out of DoModal with fFolderCreated==false + } else if (fNewFolder.Find('\\') >= 0 || + fNewFolder.Find('/') >= 0) + { + MessageBox("Folder names may not contain '/' or '\\'.", + "CiderPress", MB_OK); + pDX->Fail(); + } else { + fNewFullPath = fCurrentFolder; + if (fNewFullPath.Right(1) != "\\") + fNewFullPath += "\\"; + fNewFullPath += fNewFolder; + WMSG1("CREATING '%s'\n", fNewFullPath); + if (!::CreateDirectory(fNewFullPath, nil)) { + /* show the sometimes-bizarre Windows error string */ + CString msg, errStr, failed; + DWORD dwerr = ::GetLastError(); + GetWin32ErrorString(dwerr, &errStr); + msg.Format("Unable to create folder '%s': %s", + fNewFolder, errStr); + failed.LoadString(IDS_FAILED); + MessageBox(msg, failed, MB_OK | MB_ICONERROR); + pDX->Fail(); + } else { + /* success! */ + fFolderCreated = true; + } + } + } +} + + +/* + * Context help request (question mark button). + */ +BOOL +NewFolderDialog::OnHelpInfo(HELPINFO* lpHelpInfo) +{ + WinHelp((DWORD) lpHelpInfo->iCtrlId, HELP_CONTEXTPOPUP); + return TRUE; // yes, we handled it +} diff --git a/app/NewFolderDialog.h b/app/NewFolderDialog.h new file mode 100644 index 0000000..4c24535 --- /dev/null +++ b/app/NewFolderDialog.h @@ -0,0 +1,50 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Allow the user to create a new folder. + */ +#ifndef __NEWFOLDERDIALOG__ +#define __NEWFOLDERDIALOG__ + +#include "resource.h" + +/* + * Create a new folder in an existing location. + * + * Expects, but does not verify, that "fCurrentFolder" is set to a valid + * path before DoModal is called. + */ +class NewFolderDialog : public CDialog { +public: + NewFolderDialog(CWnd* pParent = NULL) : CDialog(IDD_NEWFOLDER, pParent) { + fCurrentFolder = ""; + fNewFolder = ""; + fFolderCreated = false; + } + virtual ~NewFolderDialog(void) {} + + bool GetFolderCreated(void) const { return fFolderCreated; } + + // set to CWD before calling DoModal + CString fCurrentFolder; + + // filename (NOT pathname) of new folder (DDXed in edit ctrl) + CString fNewFolder; + + // full pathname of new folder, valid if fFolderCreated is true + CString fNewFullPath; + +protected: + void DoDataExchange(CDataExchange* pDX); + BOOL OnHelpInfo(HELPINFO* lpHelpInfo); + + // on exit, set to "true" if we created the folder in "fNewFolder" + bool fFolderCreated; + + DECLARE_MESSAGE_MAP() +}; + +#endif /*__NEWFOLDERDIALOG__*/ \ No newline at end of file diff --git a/app/NufxArchive.cpp b/app/NufxArchive.cpp new file mode 100644 index 0000000..47c08ca --- /dev/null +++ b/app/NufxArchive.cpp @@ -0,0 +1,2697 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Bridge between NufxLib and GenericArchive. + */ +#include "stdafx.h" +#include "NufxArchive.h" +#include "ConfirmOverwriteDialog.h" +#include "RenameEntryDialog.h" +#include "RecompressOptionsDialog.h" +#include "AddClashDialog.h" +#include "Main.h" +#include "../prebuilt/NufxLib.h" + +/* + * NufxLib doesn't currently allow an fssep of '\0', so we use this instead + * to indicate the absence of an fssep char. Not quite right, but it'll do + * until NufxLib gets fixed. + */ +const unsigned char kNufxNoFssep = 0xff; + + +/* + * =========================================================================== + * NufxEntry + * =========================================================================== + */ + +/* + * Extract data from a thread into a buffer. + * + * If "*ppText" is non-nil and "*pLength" is > 0, the data will be read into + * the pointed-to buffer so long as it's shorter than *pLength bytes. The + * value in "*pLength" will be set to the actual length used. + * + * If "*ppText" is nil or the length is <= 0, the uncompressed data will be + * placed into a buffer allocated with "new[]". + * + * Returns IDOK on success, IDCANCEL if the operation was cancelled by the + * user, and -1 value on failure. On failure, "*ppText" and "*pLength" will + * be valid but point at an error message. + * + * "which" is an anonymous GenericArchive enum. + */ +int +NufxEntry::ExtractThreadToBuffer(int which, char** ppText, long* pLength, + CString* pErrMsg) const +{ + NuError nerr; + char* dataBuf = nil; + NuDataSink* pDataSink = nil; + NuThread thread; + unsigned long actualThreadEOF; + NuThreadIdx threadIdx; + bool needAlloc = true; + int result = -1; + + ASSERT(IDOK != -1 && IDCANCEL != -1); // make sure return vals don't clash + + if (*ppText != nil) + needAlloc = false; + + FindThreadInfo(which, &thread, pErrMsg); + if (!pErrMsg->IsEmpty()) + goto bail; + threadIdx = thread.threadIdx; + actualThreadEOF = thread.actualThreadEOF; + + /* + * We've got the right thread. Create an appropriately-sized buffer + * and extract the data into it (WITHOUT doing EOL conversion). + * + * First check for a length of zero. + */ + if (actualThreadEOF == 0) { + WMSG0("Empty thread\n"); + if (needAlloc) { + *ppText = new char[1]; + **ppText = '\0'; + } + *pLength = 0; + result = IDOK; + goto bail; + } + + if (needAlloc) { + dataBuf = new char[actualThreadEOF]; + if (dataBuf == nil) { + pErrMsg->Format("allocation of %ld bytes failed", + actualThreadEOF); + goto bail; + } + } else { + if (*pLength < (long) actualThreadEOF) { + pErrMsg->Format("buf size %ld too short (%ld)", + *pLength, actualThreadEOF); + goto bail; + } + dataBuf = *ppText; + } + nerr = NuCreateDataSinkForBuffer(true, kNuConvertOff, + (unsigned char*)dataBuf, actualThreadEOF, &pDataSink); + if (nerr != kNuErrNone) { + pErrMsg->Format("unable to create buffer data sink: %s", + NuStrError(nerr)); + goto bail; + } + + SET_PROGRESS_BEGIN(); + nerr = NuExtractThread(fpArchive, threadIdx, pDataSink); + if (nerr != kNuErrNone) { + if (nerr == kNuErrAborted) { + result = IDCANCEL; + //::sprintf(errorBuf, "Cancelled.\n"); + } else if (nerr == kNuErrBadFormat) { + pErrMsg->Format("The compression method used on this file is not supported " + "by your copy of \"nufxlib2.dll\". For more information, " + "please visit us on the web at " + "http://www.faddensoft.com/ciderpress/"); + } else { + pErrMsg->Format("unable to extract thread %ld: %s", + threadIdx, NuStrError(nerr)); + } + goto bail; + } + + if (needAlloc) + *ppText = dataBuf; + *pLength = actualThreadEOF; + result = IDOK; + +bail: + if (result == IDOK) { + SET_PROGRESS_END(); + ASSERT(pErrMsg->IsEmpty()); + } else { + ASSERT(result == IDCANCEL || !pErrMsg->IsEmpty()); + if (needAlloc) { + delete[] dataBuf; + ASSERT(*ppText == nil); + } + } + if (pDataSink != nil) + NuFreeDataSink(pDataSink); + return result; +} + +/* + * Extract data from a thread to a file. Since we're not copying to memory, + * we can't assume that we're able to hold the entire file all at once. + * + * Returns IDOK on success, IDCANCEL if the operation was cancelled by the + * user, and -1 value on failure. On failure, "*pMsg" holds an + * error message. + */ +int +NufxEntry::ExtractThreadToFile(int which, FILE* outfp, ConvertEOL conv, + ConvertHighASCII convHA, CString* pErrMsg) const +{ + NuDataSink* pDataSink = nil; + NuError nerr; + NuThread thread; + unsigned long actualThreadEOF; + NuThreadIdx threadIdx; + int result = -1; + + ASSERT(outfp != nil); + + //CString errMsg; + FindThreadInfo(which, &thread, pErrMsg); + if (!pErrMsg->IsEmpty()) + goto bail; + threadIdx = thread.threadIdx; + actualThreadEOF = thread.actualThreadEOF; + + /* we've got the right thread, see if it's empty */ + if (actualThreadEOF == 0) { + WMSG0("Empty thread\n"); + result = IDOK; + goto bail; + } + + /* set EOL conversion flags */ + NuValue nuConv; + switch (conv) { + case kConvertEOLOff: nuConv = kNuConvertOff; break; + case kConvertEOLOn: nuConv = kNuConvertOn; break; + case kConvertEOLAuto: nuConv = kNuConvertAuto; break; + default: + ASSERT(false); + pErrMsg->Format("internal error: bad conv flag %d", conv); + goto bail; + } + if (which == kDiskImageThread) { + /* override the above; never EOL-convert a disk image */ + nuConv = kNuConvertOff; + } + + switch (convHA) { + case kConvertHAOff: + nerr = NuSetValue(fpArchive, kNuValueStripHighASCII, false); + break; + case kConvertHAOn: + case kConvertHAAuto: + nerr = NuSetValue(fpArchive, kNuValueStripHighASCII, true); + break; + default: + ASSERT(false); + pErrMsg->Format("internal error: bad convHA flag %d", convHA); + goto bail; + } + + /* make sure we convert to CRLF */ + nerr = NuSetValue(fpArchive, kNuValueEOL, kNuEOLCRLF); // for Win32 + if (nerr != kNuErrNone) { + pErrMsg->Format("failed setting EOL value: %s", NuStrError(nerr)); + goto bail; + } + + /* create a data sink for "outfp" */ + nerr = NuCreateDataSinkForFP(true, nuConv, outfp, &pDataSink); + if (nerr != kNuErrNone) { + pErrMsg->Format("unable to create FP data sink: %s", + NuStrError(nerr)); + goto bail; + } + + /* extract the thread to the file */ + SET_PROGRESS_BEGIN(); + nerr = NuExtractThread(fpArchive, threadIdx, pDataSink); + if (nerr != kNuErrNone) { + if (nerr == kNuErrAborted) { + /* user hit the "cancel" button */ + *pErrMsg = _T("cancelled"); + result = IDCANCEL; + } else if (nerr == kNuErrBadFormat) { + pErrMsg->Format("The compression method used on this file is not supported " + "by your copy of \"nufxlib2.dll\". For more information, " + "please visit us on the web at " + "http://www.faddensoft.com/ciderpress/"); + } else { + pErrMsg->Format("unable to extract thread %ld: %s", + threadIdx, NuStrError(nerr)); + } + goto bail; + } + + result = IDOK; + +bail: + if (result == IDOK) { + SET_PROGRESS_END(); + } + if (pDataSink != nil) + NuFreeDataSink(pDataSink); + return result; +} + +/* + * Find info for the thread we're about to extract. + * + * Given the NuRecordIdx stored in the object, find the thread whose + * ThreadID matches "which". Copies the NuThread structure into + * "*pThread". + * + * On failure, "pErrMsg" will have a nonzero length, and contain an error + * message describing the problem. + */ +void +NufxEntry::FindThreadInfo(int which, NuThread* pRetThread, + CString* pErrMsg) const +{ + NuError nerr; + + ASSERT(pErrMsg->IsEmpty()); + + /* + * Retrieve the record from the archive. + */ + const NuRecord* pRecord; + nerr = NuGetRecord(fpArchive, fRecordIdx, &pRecord); + if (nerr != kNuErrNone) { + pErrMsg->Format("NufxLib unable to locate record %ld: %s", + fRecordIdx, NuStrError(nerr)); + goto bail; + } + + /* + * Find the right thread. + */ + const NuThread* pThread; + unsigned long wantedThreadID; + switch (which) { + case kDataThread: wantedThreadID = kNuThreadIDDataFork; break; + case kRsrcThread: wantedThreadID = kNuThreadIDRsrcFork; break; + case kDiskImageThread: wantedThreadID = kNuThreadIDDiskImage; break; + case kCommentThread: wantedThreadID = kNuThreadIDComment; break; + default: + pErrMsg->Format("looking for bogus thread 0x%02x", which); + goto bail; + } + + int i; + pThread = nil; + for (i = 0; i < (int)NuRecordGetNumThreads(pRecord); i++) { + pThread = NuGetThread(pRecord, i); + if (NuGetThreadID(pThread) == wantedThreadID) + break; + } + if (i == (int)NuRecordGetNumThreads(pRecord)) { + /* didn't find the thread we wanted */ + pErrMsg->Format("searched %d threads but couldn't find 0x%02x", + NuRecordGetNumThreads(pRecord), which); + goto bail; + } + + memcpy(pRetThread, pThread, sizeof(*pRetThread)); + +bail: + return; +} + + +//static const char* gShortFormatNames[] = { +// "unc", "squ", "lz1", "lz2", "u12", "u16", "dfl", "bzp" +//}; +static const char* gFormatNames[] = { + "Uncompr", "Squeeze", "LZW/1", "LZW/2", "LZC-12", + "LZC-16", "Deflate", "Bzip2" +}; + +/* + * Analyze the contents of a record to determine if it's a disk, file, + * or "other". Compute the total compressed and uncompressed lengths + * of all data threads. Return the "best" format. + * + * The "best format" and "record type" stuff assume that the entire + * record contains only a disk thread or a file thread, and that any + * format is interesting so long as it isn't "no compression". In + * general these will be true, because ShrinkIt and NuLib create files + * this way. + * + * You could, of course, create a single record with a data thread and + * a disk image thread, but it's a fair bet ShrinkIt would ignore one + * or the other. + * + * NOTE: we don't currently work around the GSHK zero-length file bug. + * Such records, which have a filename thread but no data threads at all, + * will be categorized as "unknown". We could detect the situation and + * correct it, but we might as well flag it in a user-visible way. + */ +void +NufxEntry::AnalyzeRecord(const NuRecord* pRecord) +{ + const NuThread* pThread; + NuThreadID threadID; + unsigned long idx; + RecordKind recordKind; + unsigned long uncompressedLen; + unsigned long compressedLen; + unsigned short format; + + recordKind = kRecordKindUnknown; + uncompressedLen = compressedLen = 0; + format = kNuThreadFormatUncompressed; + + for (idx = 0; idx < pRecord->recTotalThreads; idx++) { + pThread = NuGetThread(pRecord, idx); + ASSERT(pThread != nil); + + threadID = NuMakeThreadID(pThread->thThreadClass, + pThread->thThreadKind); + + if (pThread->thThreadClass == kNuThreadClassData) { + /* replace what's there if this might be more interesting */ + if (format == kNuThreadFormatUncompressed) + format = (unsigned short) pThread->thThreadFormat; + + if (threadID == kNuThreadIDRsrcFork) + recordKind = kRecordKindForkedFile; + else if (threadID == kNuThreadIDDiskImage) + recordKind = kRecordKindDisk; + else if (threadID == kNuThreadIDDataFork && + recordKind == kRecordKindUnknown) + recordKind = kRecordKindFile; + + /* sum up, so we get both forks of forked files */ + //uncompressedLen += pThread->actualThreadEOF; + compressedLen += pThread->thCompThreadEOF; + } + + if (threadID == kNuThreadIDDataFork) { + if (!GetHasDataFork() && !GetHasDiskImage()) { + SetHasDataFork(true); + SetDataForkLen(pThread->actualThreadEOF); + } else { + WMSG0("WARNING: ignoring second disk image / data fork\n"); + } + } + if (threadID == kNuThreadIDRsrcFork) { + if (!GetHasRsrcFork()) { + SetHasRsrcFork(true); + SetRsrcForkLen(pThread->actualThreadEOF); + } else { + WMSG0("WARNING: ignoring second data fork\n"); + } + } + if (threadID == kNuThreadIDDiskImage) { + if (!GetHasDiskImage() && !GetHasDataFork()) { + SetHasDiskImage(true); + SetDataForkLen(pThread->actualThreadEOF); + } else { + WMSG0("WARNING: ignoring second disk image / data fork\n"); + } + } + if (threadID == kNuThreadIDComment) { + SetHasComment(true); + if (pThread->actualThreadEOF != 0) + SetHasNonEmptyComment(true); + } + } + + SetRecordKind(recordKind); + //SetUncompressedLen(uncompressedLen); + SetCompressedLen(compressedLen); + + if (format >= 0 && format < NELEM(gFormatNames)) + SetFormatStr(gFormatNames[format]); + else + SetFormatStr("Unknown"); +} + + +/* + * =========================================================================== + * NufxArchive + * =========================================================================== + */ + +/* + * Perform one-time initialization of the NufxLib library. + * + * Returns with an error if the NufxLib version is off. Major version must + * match (since it indicates an interface change), minor version must be + * >= what we expect (in case we're relying on recent behavior changes). + * + * Returns 0 on success, nonzero on error. + */ +/*static*/ CString +NufxArchive::AppInit(void) +{ + NuError nerr; + CString result(""); + long major, minor, bug; + + nerr = NuGetVersion(&major, &minor, &bug, NULL, NULL); + if (nerr != kNuErrNone) { + result = "Unable to get version number from NufxLib."; + goto bail; + } + + if (major != kNuVersionMajor || minor < kNuVersionMinor) { + result.Format("Older or incompatible version of NufxLib DLL found.\r\r" + "Wanted v%d.%d.x, found %ld.%ld.%ld.", + kNuVersionMajor, kNuVersionMinor, + major, minor, bug); + goto bail; + } + if (bug != kNuVersionBug) { + WMSG2("Different 'bug' version (built vX.X.%d, dll vX.X.%d)\n", + kNuVersionBug, bug); + } + + /* set NufxLib's global error message handler */ + NuSetGlobalErrorMessageHandler(NufxErrorMsgHandler); + +bail: + return result; +} + + +/* + * Determine whether a particular kind of compression is supported by + * NufxLib. + * + * Returns "true" if supported, "false" if not. + */ +/*static*/ bool +NufxArchive::IsCompressionSupported(NuThreadFormat format) +{ + NuFeature feature; + + switch (format) { + case kNuThreadFormatUncompressed: + return true; + + case kNuThreadFormatHuffmanSQ: + feature = kNuFeatureCompressSQ; + break; + case kNuThreadFormatLZW1: + case kNuThreadFormatLZW2: + feature = kNuFeatureCompressLZW; + break; + case kNuThreadFormatLZC12: + case kNuThreadFormatLZC16: + feature = kNuFeatureCompressLZC; + break; + case kNuThreadFormatDeflate: + feature = kNuFeatureCompressDeflate; + break; + case kNuThreadFormatBzip2: + feature = kNuFeatureCompressBzip2; + break; + + default: + ASSERT(false); + return false; + } + + NuError nerr; + nerr = NuTestFeature(feature); + if (nerr == kNuErrNone) + return true; + return false; +} + +/* + * Display error messages... or not. + */ +NuResult +NufxArchive::NufxErrorMsgHandler(NuArchive* /*pArchive*/, void* vErrorMessage) +{ +#if defined(_DEBUG_LOG) + const NuErrorMessage* pErrorMessage = (const NuErrorMessage*) vErrorMessage; + CString msg(pErrorMessage->message); + + msg += "\n"; + + if (pErrorMessage->isDebug) + msg = "[D] " + msg; + fprintf(gLog, "%05u NufxLib %s(%d) : %s", + gPid, pErrorMessage->file, pErrorMessage->line, msg); + +#elif defined(_DEBUG) + const NuErrorMessage* pErrorMessage = (const NuErrorMessage*) vErrorMessage; + CString msg(pErrorMessage->message); + + msg += "\n"; + + if (pErrorMessage->isDebug) + msg = "[D] " + msg; + _CrtDbgReport(_CRT_WARN, pErrorMessage->file, pErrorMessage->line, + pErrorMessage->function, msg); +#endif + + return kNuOK; +} + +/* + * Display our progress. + * + * "oldName" ends up on top, "newName" on bottom. + */ +/*static*/NuResult +NufxArchive::ProgressUpdater(NuArchive* pArchive, void* vpProgress) +{ + const NuProgressData* pProgress = (const NuProgressData*) vpProgress; + NufxArchive* pThis; + MainWindow* pMainWin = (MainWindow*)::AfxGetMainWnd(); + int status; + const char* oldName; + const char* newName; + int perc; + + ASSERT(pProgress != nil); + ASSERT(pMainWin != nil); + + ASSERT(pArchive != nil); + (void) NuGetExtraData(pArchive, (void**) &pThis); + ASSERT(pThis != nil); + + oldName = newName = nil; + if (pProgress->operation == kNuOpAdd) { + oldName = pProgress->origPathname; + newName = pProgress->pathname; + if (pThis->fProgressAsRecompress) + oldName = "-"; + } else if (pProgress->operation == kNuOpTest) { + oldName = pProgress->pathname; + } else if (pProgress->operation == kNuOpExtract) { + if (pThis->fProgressAsRecompress) { + oldName = pProgress->origPathname; + newName = "-"; + } + } + + perc = pProgress->percentComplete; + if (pProgress->state == kNuProgressDone) + perc = 100; + + //WMSG3("Progress: %d%% '%s' '%s'\n", perc, + // oldName == nil ? "(nil)" : oldName, + // newName == nil ? "(nil)" : newName); + + //status = pMainWin->SetProgressUpdate(perc, oldName, newName); + status = SET_PROGRESS_UPDATE2(perc, oldName, newName); + + /* check to see if user hit the "cancel" button on the progress dialog */ + if (pProgress->state == kNuProgressAborted) { + WMSG0("(looks like we're aborting)\n"); + ASSERT(status == IDCANCEL); + } + + if (status == IDCANCEL) { + WMSG0("Signaling NufxLib to abort\n"); + return kNuAbort; + } else + return kNuOK; +} + + +/* + * Finish instantiating a NufxArchive object by opening an existing file. + * + * Returns an error string on failure, or nil on success. + */ +GenericArchive::OpenResult +NufxArchive::Open(const char* filename, bool readOnly, CString* pErrMsg) +{ + NuError nerr; + CString errMsg; + + ASSERT(fpArchive == nil); + + if (!readOnly) { + CString tmpname = GenDerivedTempName(filename); + WMSG2("Opening file '%s' rw (tmp='%s')\n", filename, tmpname); + fIsReadOnly = false; + nerr = NuOpenRW(filename, tmpname, 0, &fpArchive); + } + if (nerr == kNuErrFileAccessDenied || nerr == EACCES) { + WMSG0("Read-write failed with access denied, trying read-only\n"); + readOnly = true; + } + if (readOnly) { + WMSG1("Opening file '%s' ro\n", filename); + fIsReadOnly = true; + nerr = NuOpenRO(filename, &fpArchive); + } + if (nerr != kNuErrNone) { + errMsg = "Unable to open '"; + errMsg += filename; + errMsg += "': "; + errMsg += NuStrError(nerr); + goto bail; + } else { + //WMSG0("FILE OPEN SUCCESS\n"); + } + + nerr = SetCallbacks(); + if (nerr != kNuErrNone) { + errMsg = "Callback init failed"; + goto bail; + } + + nerr = LoadContents(); + if (nerr != kNuErrNone) { + errMsg = "Failed reading archive contents: "; + errMsg += NuStrError(nerr); + } + + SetPathName(filename); + +bail: + *pErrMsg = errMsg; + if (!errMsg.IsEmpty()) + return kResultFailure; + else + return kResultSuccess; +} + + +/* + * Finish instantiating a NufxArchive object by creating a new archive. + * + * Returns an error string on failure, or "" on success. + */ +CString +NufxArchive::New(const char* filename, const void* options) +{ + NuError nerr; + CString retmsg(""); + + ASSERT(fpArchive == nil); + ASSERT(options == nil); + + CString tmpname = GenDerivedTempName(filename); + WMSG2("Creating file '%s' (tmp='%s')\n", filename, tmpname); + fIsReadOnly = false; + nerr = NuOpenRW(filename, tmpname, kNuOpenCreat | kNuOpenExcl, &fpArchive); + if (nerr != kNuErrNone) { + retmsg = "Unable to open '"; + retmsg += filename; + retmsg += "': "; + retmsg += NuStrError(nerr); + goto bail; + } else { + WMSG0("NEW FILE SUCCESS\n"); + } + + + nerr = SetCallbacks(); + if (nerr != kNuErrNone) { + retmsg = "Callback init failed"; + goto bail; + } + + SetPathName(filename); + +bail: + return retmsg; +} + +/* + * Set some standard callbacks and feature flags. + */ +NuError +NufxArchive::SetCallbacks(void) +{ + NuError nerr; + + nerr = NuSetExtraData(fpArchive, this); + if (nerr != kNuErrNone) + goto bail; +// nerr = NuSetSelectionFilter(fpArchive, SelectionFilter); +// if (nerr != kNuErrNone) +// goto bail; +// nerr = NuSetOutputPathnameFilter(fpArchive, OutputPathnameFilter); +// if (nerr != kNuErrNone) +// goto bail; + NuSetProgressUpdater(fpArchive, ProgressUpdater); +// nerr = NuSetErrorHandler(fpArchive, ErrorHandler); +// if (nerr != kNuErrNone) +// goto bail; + NuSetErrorMessageHandler(fpArchive, NufxErrorMsgHandler); + + /* let NufxLib worry about buggy records without data threads */ + nerr = NuSetValue(fpArchive, kNuValueMaskDataless, kNuValueTrue); + if (nerr != kNuErrNone) + goto bail; + + /* set any values based on Preferences values */ + PreferencesChanged(); + +bail: + return nerr; +} + +/* + * User has updated their preferences. Take note. + * + * (This is also called the first time through.) + */ +void +NufxArchive::PreferencesChanged(void) +{ + NuError nerr; + const Preferences* pPreferences = GET_PREFERENCES(); + bool val; + + val = pPreferences->GetPrefBool(kPrMimicShrinkIt); + nerr = NuSetValue(fpArchive, kNuValueMimicSHK, val); + if (nerr != kNuErrNone) { + WMSG2("NuSetValue(kNuValueMimicSHK, %d) failed, err=%d\n", val, nerr); + ASSERT(false); + } else { + WMSG1("Set MimicShrinkIt to %d\n", val); + } + + val = pPreferences->GetPrefBool(kPrReduceSHKErrorChecks); + NuSetValue(fpArchive, kNuValueIgnoreLZW2Len, val); + NuSetValue(fpArchive, kNuValueIgnoreCRC, val); + + val = pPreferences->GetPrefBool(kPrBadMacSHK); + NuSetValue(fpArchive, kNuValueHandleBadMac, val); +} + +/* + * Report on what NuFX is capable of. + */ +long +NufxArchive::GetCapability(Capability cap) +{ + switch (cap) { + case kCapCanTest: + return true; + break; + case kCapCanRenameFullPath: + return true; + break; + case kCapCanRecompress: + return true; + break; + case kCapCanEditComment: + return true; + break; + case kCapCanAddDisk: + return true; + break; + case kCapCanConvEOLOnAdd: + return false; + break; + case kCapCanCreateSubdir: + return false; + break; + case kCapCanRenameVolume: + return false; + break; + default: + ASSERT(false); + return -1; + break; + } +} + +/* + * Load the contents of an archive into the GenericEntry/NufxEntry list. + * + * We will need to set an error handler if we want to be able to do things + * like "I found a bad CRC, did you want me to keep trying anyway?". + */ +NuError +NufxArchive::LoadContents(void) +{ + long counter = 0; + NuError result; + + WMSG0("NufxArchive LoadContents\n"); + ASSERT(fpArchive != nil); + + { + MainWindow* pMain = GET_MAIN_WINDOW(); + ExclusiveModelessDialog* pWaitDlg = new ExclusiveModelessDialog; + pWaitDlg->Create(IDD_LOADING, pMain); + pWaitDlg->CenterWindow(); + pMain->PeekAndPump(); // redraw + CWaitCursor waitc; + + result = NuContents(fpArchive, ContentFunc); + + SET_PROGRESS_COUNTER(-1); + + pWaitDlg->DestroyWindow(); + //pMain->PeekAndPump(); // redraw + } + + return result; +} + +/* + * Reload the contents. + */ +CString +NufxArchive::Reload(void) +{ + NuError nerr; + CString errMsg; + + fReloadFlag = true; // tell everybody that cached data is invalid + + DeleteEntries(); // a GenericArchive operation + + nerr = LoadContents(); + if (nerr != kNuErrNone) { + errMsg.Format("ERROR: unable to reload archive contents: %s.", + NuStrError(nerr)); + + DeleteEntries(); + fIsReadOnly = true; + } + + return errMsg; +} + +/* + * Reload the contents of the archive, showing an error message if the + * reload fails. + */ +NuError +NufxArchive::InternalReload(CWnd* pMsgWnd) +{ + CString errMsg; + + errMsg = Reload(); + + if (!errMsg.IsEmpty()) { + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + return kNuErrGeneric; + } + + return kNuErrNone; +} + +/* + * Static callback function. Used for scanning the contents of an archive. + */ +NuResult +NufxArchive::ContentFunc(NuArchive* pArchive, void* vpRecord) +{ + const NuRecord* pRecord = (const NuRecord*) vpRecord; + NufxArchive* pThis; + NufxEntry* pNewEntry; + + ASSERT(pArchive != nil); + ASSERT(vpRecord != nil); + + NuGetExtraData(pArchive, (void**) &pThis); + + pNewEntry = new NufxEntry(pArchive); + + pNewEntry->SetPathName(pRecord->filename); + pNewEntry->SetFssep(NuGetSepFromSysInfo(pRecord->recFileSysInfo)); + pNewEntry->SetFileType(pRecord->recFileType); + pNewEntry->SetAuxType(pRecord->recExtraType); + pNewEntry->SetAccess(pRecord->recAccess); + pNewEntry->SetCreateWhen(DateTimeToSeconds(&pRecord->recCreateWhen)); + pNewEntry->SetModWhen(DateTimeToSeconds(&pRecord->recModWhen)); + + /* + * Our files are always ProDOS format. This is especially important + * when cutting & pasting, so that the DOS high ASCII converter gets + * invoked at appropriate times. + */ + pNewEntry->SetSourceFS(DiskImg::kFormatProDOS); + + pNewEntry->AnalyzeRecord(pRecord); + pNewEntry->SetRecordIdx(pRecord->recordIdx); + + pThis->AddEntry(pNewEntry); + if ((pThis->GetNumEntries() % 10) == 0) + SET_PROGRESS_COUNTER(pThis->GetNumEntries()); + + return kNuOK; +} + +/* + * Convert a NuDateTime structure to a time_t. + */ +/*static*/ time_t +NufxArchive::DateTimeToSeconds(const NuDateTime* pDateTime) +{ + if (pDateTime->second == 0 && + pDateTime->minute == 0 && + pDateTime->hour == 0 && + pDateTime->year == 0 && + pDateTime->day == 0 && + pDateTime->month == 0 && + pDateTime->extra == 0 && + pDateTime->weekDay == 0) + { + return kDateNone; + } + + int year; + if (pDateTime->year < 40) + year = pDateTime->year + 2000; + else + year = pDateTime->year + 1900; + + if (year < 1969) { + /* + * Years like 1963 are valid on an Apple II but cannot be represented + * as a time_t, which starts in 1970. (Depending on GMT offsets, + * it actually starts a few hours earlier at the end of 1969.) + * + * I'm catching this here because of an assert in the CTime + * constructor. The constructor seems to do the right thing, and the + * assert won't be present in the shipping version, but it's annoying + * during debugging. + */ + //WMSG1(" Ignoring funky year %ld\n", year); + return kDateInvalid; + } + if (pDateTime->month > 11) + return kDateInvalid; + + CTime modTime(year, + pDateTime->month+1, + pDateTime->day+1, + pDateTime->hour, + pDateTime->minute, + pDateTime->second); + return (time_t) modTime.GetTime(); +} + +/* + * Callback from a DataSource that is done with a buffer. Use for memory + * allocated with new[]. + */ +/*static*/ NuResult +NufxArchive::ArrayDeleteHandler(NuArchive* pArchive, void* ptr) +{ + delete[] ptr; + return kNuOK; +} + + +/* + * =========================================================================== + * NufxArchive -- add files (or disks) + * =========================================================================== + */ + +/* + * Process a bulk "add" request. + * + * This calls into the GenericArchive "AddFile" function, which does + * Win32-specific processing. That function calls our DoAddFile function, + * which does the NuFX stuff. + * + * Returns "true" on success, "false" on failure. + */ +bool +NufxArchive::BulkAdd(ActionProgressDialog* pActionProgress, + const AddFilesDialog* pAddOpts) +{ + NuError nerr; + CString errMsg; + char curDir[MAX_PATH] = ""; + bool retVal = false; + + WMSG2("Opts: '%s' typePres=%d\n", + pAddOpts->fStoragePrefix, pAddOpts->fTypePreservation); + WMSG3(" sub=%d strip=%d ovwr=%d\n", + pAddOpts->fIncludeSubfolders, pAddOpts->fStripFolderNames, + pAddOpts->fOverwriteExisting); + + AddPrep(pActionProgress, pAddOpts); + + pActionProgress->SetArcName("(Scanning files to be added...)"); + pActionProgress->SetFileName(""); + + /* initialize count */ + fNumAdded = 0; + + const char* buf = pAddOpts->GetFileNames(); + WMSG2("Selected path = '%s' (offset=%d)\n", buf, + pAddOpts->GetFileNameOffset()); + + if (GetCurrentDirectory(sizeof(curDir), curDir) == 0) { + errMsg = "Unable to get current directory.\n"; + ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED); + goto bail; + } + if (SetCurrentDirectory(buf) == false) { + errMsg.Format("Unable to set current directory to '%s'.\n", buf); + ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED); + goto bail; + } + + buf += pAddOpts->GetFileNameOffset(); + while (*buf != '\0') { + WMSG1(" file '%s'\n", buf); + + /* this just provides the list of files to NufxLib */ + nerr = AddFile(pAddOpts, buf, &errMsg); + if (nerr != kNuErrNone) { + if (errMsg.IsEmpty()) + errMsg.Format("Failed while adding file '%s': %s.", + (LPCTSTR) buf, NuStrError(nerr)); + if (nerr != kNuErrAborted) { + ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED); + } + goto bail; + } + + buf += strlen(buf)+1; + } + + /* actually do the work */ + long statusFlags; + nerr = NuFlush(fpArchive, &statusFlags); + if (nerr != kNuErrNone) { + if (nerr != kNuErrAborted) { + errMsg.Format("Unable to add files: %s.", NuStrError(nerr)); + ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED); + } + + /* see if it got converted to read-only status */ + if (statusFlags & kNuFlushReadOnly) + fIsReadOnly = true; + goto bail; + } + + if (!fNumAdded) { + errMsg = "No files added.\n"; + fpMsgWnd->MessageBox(errMsg, "CiderPress", MB_OK | MB_ICONWARNING); + } else { + if (InternalReload(fpMsgWnd) == kNuErrNone) + retVal = true; + else + errMsg = "Reload failed."; + } + +bail: + NuAbort(fpArchive); // abort anything that didn't get flushed + if (SetCurrentDirectory(curDir) == false) { + errMsg.Format("Unable to reset current directory to '%s'.\n", buf); + ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED); + // bummer, but don't signal failure + } + AddFinish(); + return retVal; +} + +/* + * Add a single disk to the archive. + */ +bool +NufxArchive::AddDisk(ActionProgressDialog* pActionProgress, + const AddFilesDialog* pAddOpts) +{ + NuError nerr; + CString errMsg; + const int kBlockSize = 512; + PathProposal pathProp; + PathName pathName; + DiskImg* pDiskImg; + NuDataSource* pSource = nil; + unsigned char* diskData = nil; + char curDir[MAX_PATH] = "\\"; + bool retVal = false; + + WMSG2("AddDisk: '%s' %d\n", pAddOpts->GetFileNames(), + pAddOpts->GetFileNameOffset()); + WMSG2("Opts: '%s' type=%d\n", + pAddOpts->fStoragePrefix, pAddOpts->fTypePreservation); + WMSG3(" sub=%d strip=%d ovwr=%d\n", + pAddOpts->fIncludeSubfolders, pAddOpts->fStripFolderNames, + pAddOpts->fOverwriteExisting); + + pDiskImg = pAddOpts->fpDiskImg; + ASSERT(pDiskImg != nil); + + /* allocate storage for the disk */ + diskData = new unsigned char[pDiskImg->GetNumBlocks() * kBlockSize]; + if (diskData == nil) { + errMsg.Format("Unable to allocate %d bytes.", + pDiskImg->GetNumBlocks() * kBlockSize); + ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED); + goto bail; + } + + /* prepare to add */ + AddPrep(pActionProgress, pAddOpts); + + const char* buf; + buf = pAddOpts->GetFileNames(); + WMSG2("Selected path = '%s' (offset=%d)\n", buf, + pAddOpts->GetFileNameOffset()); + + if (GetCurrentDirectory(sizeof(curDir), curDir) == 0) { + errMsg = "Unable to get current directory.\n"; + ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED); + goto bail; + } + if (SetCurrentDirectory(buf) == false) { + errMsg.Format("Unable to set current directory to '%s'.\n", buf); + ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED); + goto bail; + } + + buf += pAddOpts->GetFileNameOffset(); + WMSG1(" file '%s'\n", buf); + + /* strip off preservation stuff, and ignore it */ + pathProp.Init(buf); + pathProp.fStripDiskImageSuffix = true; + pathProp.LocalToArchive(pAddOpts); + + /* fill in the necessary file details */ + NuFileDetails details; + memset(&details, 0, sizeof(details)); + details.threadID = kNuThreadIDDiskImage; + details.storageType = kBlockSize; + details.access = kNuAccessUnlocked; + details.extraType = pAddOpts->fpDiskImg->GetNumBlocks(); + details.origName = buf; + details.storageName = pathProp.fStoredPathName; + details.fileSysID = kNuFileSysUnknown; + details.fileSysInfo = PathProposal::kDefaultStoredFssep; + + time_t now, then; + + pathName = buf; + now = time(nil); + then = pathName.GetModWhen(); + UNIXTimeToDateTime(&now, &details.archiveWhen); + UNIXTimeToDateTime(&then, &details.modWhen); + UNIXTimeToDateTime(&then, &details.createWhen); + + /* set up the progress updater */ + pActionProgress->SetArcName(details.storageName); + pActionProgress->SetFileName(details.origName); + + /* read the disk now that we have progress update titles in place */ + int block, numBadBlocks; + unsigned char* bufPtr; + numBadBlocks = 0; + for (block = 0, bufPtr = diskData; block < pDiskImg->GetNumBlocks(); block++) + { + DIError dierr; + dierr = pDiskImg->ReadBlock(block, bufPtr); + if (dierr != kDIErrNone) + numBadBlocks++; + bufPtr += kBlockSize; + } + if (numBadBlocks > 0) { + CString appName, msg; + appName.LoadString(IDS_MB_APP_NAME); + msg.Format("Skipped %ld unreadable block%s.", numBadBlocks, + numBadBlocks == 1 ? "" : "s"); + fpMsgWnd->MessageBox(msg, appName, MB_OK | MB_ICONWARNING); + // keep going -- just a warning + } + + /* create a data source for the disk */ + nerr = NuCreateDataSourceForBuffer(kNuThreadFormatUncompressed, 0, + diskData, 0, pAddOpts->fpDiskImg->GetNumBlocks() * kBlockSize, + nil, &pSource); + if (nerr != kNuErrNone) { + errMsg = "Unable to create NufxLib data source."; + ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED); + goto bail; + } + + /* add the record; name conflicts cause the error handler to fire */ + NuRecordIdx recordIdx; + nerr = NuAddRecord(fpArchive, &details, &recordIdx); + if (nerr != kNuErrNone) { + if (nerr != kNuErrAborted) { + errMsg.Format("Failed adding record: %s.", NuStrError(nerr)); + ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED); + } + goto bail; + } + + /* do the compression */ + nerr = NuAddThread(fpArchive, recordIdx, kNuThreadIDDiskImage, + pSource, nil); + if (nerr != kNuErrNone) { + errMsg.Format("Failed adding thread: %s.", NuStrError(nerr)); + ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED); + goto bail; + } + pSource = nil; /* NufxLib owns it now */ + + /* actually do the work */ + long statusFlags; + nerr = NuFlush(fpArchive, &statusFlags); + if (nerr != kNuErrNone) { + if (nerr != kNuErrAborted) { + errMsg.Format("Unable to add disk: %s.", NuStrError(nerr)); + ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED); + } + + /* see if it got converted to read-only status */ + if (statusFlags & kNuFlushReadOnly) + fIsReadOnly = true; + goto bail; + } + + if (InternalReload(fpMsgWnd) == kNuErrNone) + retVal = true; + +bail: + delete[] diskData; + NuAbort(fpArchive); // abort anything that didn't get flushed + NuFreeDataSource(pSource); + if (SetCurrentDirectory(curDir) == false) { + errMsg.Format("Unable to reset current directory to '%s'.\n", buf); + ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED); + // bummer + } + AddFinish(); + return retVal; +} + +/* + * Do the archive-dependent part of the file add, including things like + * adding comments. This is eventually called by AddFile() during bulk + * adds. + */ +NuError +NufxArchive::DoAddFile(const AddFilesDialog* pAddOpts, + FileDetails* pDetails) +{ + NuError err; + NuRecordIdx recordIdx = 0; + NuFileDetails nuFileDetails; + +retry: + nuFileDetails = *pDetails; // stuff class contents into struct + err = NuAddFile(fpArchive, pDetails->origName /*pathname*/, + &nuFileDetails, false, &recordIdx); + + if (err == kNuErrNone) { + fNumAdded++; + } else if (err == kNuErrSkipped) { + /* "maybe overwrite" UI causes this if user declines */ + // fall through with the error + WMSG1("DoAddFile: skipped '%s'\n", pDetails->origName); + } else if (err == kNuErrRecordExists) { + AddClashDialog dlg; + + dlg.fWindowsName = pDetails->origName; + dlg.fStorageName = pDetails->storageName; + if (dlg.DoModal() != IDOK) { + err = kNuErrAborted; + goto bail_quiet; + } + if (dlg.fDoRename) { + WMSG1("add clash: rename to '%s'\n", (const char*) dlg.fNewName); + pDetails->storageName = dlg.fNewName; + goto retry; + } else { + WMSG0("add clash: skip"); + err = kNuErrSkipped; + // fall through with error + } + } + //if (err != kNuErrNone) + // goto bail; + +//bail: + if (err != kNuErrNone && err != kNuErrAborted && err != kNuErrSkipped) { + CString msg; + msg.Format("Unable to add file '%s': %s.", pDetails->origName, + NuStrError(err)); + ShowFailureMsg(fpMsgWnd, msg, IDS_FAILED); + } +bail_quiet: + return err; +} + +/* + * Prepare to add files. + */ +void +NufxArchive::AddPrep(CWnd* pMsgWnd, const AddFilesDialog* pAddOpts) +{ + NuError nerr; + const Preferences* pPreferences = GET_PREFERENCES(); + int defaultCompression; + + ASSERT(fpArchive != nil); + + fpMsgWnd = pMsgWnd; + ASSERT(fpMsgWnd != nil); + + fpAddOpts = pAddOpts; + ASSERT(fpAddOpts != nil); + + //fBulkProgress = true; + + defaultCompression = pPreferences->GetPrefLong(kPrCompressionType); + nerr = NuSetValue(fpArchive, kNuValueDataCompression, + defaultCompression + kNuCompressNone); + if (nerr != kNuErrNone) { + WMSG1("GLITCH: unable to set compression type to %d\n", + defaultCompression); + /* keep going */ + } + + if (pAddOpts->fOverwriteExisting) + NuSetValue(fpArchive, kNuValueHandleExisting, kNuAlwaysOverwrite); + else + NuSetValue(fpArchive, kNuValueHandleExisting, kNuMaybeOverwrite); + + NuSetErrorHandler(fpArchive, BulkAddErrorHandler); + NuSetExtraData(fpArchive, this); +} + +/* + * Reset some things after we finish adding files. We don't necessarily + * want these to stay in effect for other operations, e.g. extracting + * (though that is currently handled within CiderPress). + */ +void +NufxArchive::AddFinish(void) +{ + NuSetErrorHandler(fpArchive, nil); + NuSetValue(fpArchive, kNuValueHandleExisting, kNuMaybeOverwrite); + fpMsgWnd = nil; + fpAddOpts = nil; + //fBulkProgress = false; +} + + +/* + * Error handler callback for "bulk" adds. + */ +/*static*/ NuResult +NufxArchive::BulkAddErrorHandler(NuArchive* pArchive, void* vErrorStatus) +{ + const NuErrorStatus* pErrorStatus = (const NuErrorStatus*)vErrorStatus; + NufxArchive* pThis; + NuResult result; + + ASSERT(pArchive != nil); + (void) NuGetExtraData(pArchive, (void**) &pThis); + ASSERT(pThis != nil); + ASSERT(pArchive == pThis->fpArchive); + + /* default action is to abort the current operation */ + result = kNuAbort; + + /* + * When adding files, the NuAddFile and NuAddRecord calls can return + * immediate, specific results for a single add. The only reasons for + * calling here are to decide if an existing record should be replaced + * or not (without even an option to rename), or to decide what to do + * when the NuFlush call runs into a problem while adding a file. + */ + if (pErrorStatus->operation != kNuOpAdd) { + ASSERT(false); + return kNuAbort; + } + + if (pErrorStatus->err == kNuErrRecordExists) { + /* if they want to update or freshen, don't hassle them */ + //if (NState_GetModFreshen(pState) || NState_GetModUpdate(pState)) + if (pThis->fpAddOpts->fOverwriteExisting) { + ASSERT(false); // should be handled by AddPrep()/NufxLib + result = kNuOverwrite; + } else + result = pThis->HandleReplaceExisting(pErrorStatus); + } else if (pErrorStatus->err == kNuErrFileNotFound) { + /* file was specified with NuAdd but removed during NuFlush */ + result = pThis->HandleAddNotFound(pErrorStatus); + } + + return result; +} + +/* + * Decide whether or not to replace an existing file (during extract) + * or record (during add). + */ +NuResult +NufxArchive::HandleReplaceExisting(const NuErrorStatus* pErrorStatus) +{ + NuResult result = kNuOK; + + ASSERT(pErrorStatus != nil); + ASSERT(pErrorStatus->pathname != nil); + + ASSERT(pErrorStatus->canOverwrite); + ASSERT(pErrorStatus->canSkip); + ASSERT(pErrorStatus->canAbort); + ASSERT(!pErrorStatus->canRename); + + /* no firm policy, ask the user */ + ConfirmOverwriteDialog confOvwr; + PathName path(pErrorStatus->pathname); + + confOvwr.fExistingFile = pErrorStatus->pRecord->filename; + confOvwr.fExistingFileModWhen = + DateTimeToSeconds(&pErrorStatus->pRecord->recModWhen); + if (pErrorStatus->origPathname != nil) { + confOvwr.fNewFileSource = pErrorStatus->origPathname; + PathName checkPath(confOvwr.fNewFileSource); + confOvwr.fNewFileModWhen = checkPath.GetModWhen(); + } else { + confOvwr.fNewFileSource = "???"; + confOvwr.fNewFileModWhen = kDateNone; + } + + confOvwr.fAllowRename = false; + if (confOvwr.DoModal() == IDCANCEL) { + result = kNuAbort; + goto bail; + } + if (confOvwr.fResultRename) { + ASSERT(false); + result = kNuAbort; + goto bail; + } + if (confOvwr.fResultApplyToAll) { + if (confOvwr.fResultOverwrite) { + (void) NuSetValue(fpArchive, kNuValueHandleExisting, + kNuAlwaysOverwrite); + } else { + (void) NuSetValue(fpArchive, kNuValueHandleExisting, + kNuNeverOverwrite); + } + } + if (confOvwr.fResultOverwrite) + result = kNuOverwrite; + else + result = kNuSkip; + +bail: + return result; +} + +/* + * A file that used to be there isn't anymore. + * + * This should be exceedingly rare. + */ +NuResult +NufxArchive::HandleAddNotFound(const NuErrorStatus* pErrorStatus) +{ + CString errMsg; + + errMsg.Format("Failed while adding '%s': file no longer exists.", + pErrorStatus->pathname); + ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED); + + return kNuAbort; +} + + +/* + * =========================================================================== + * NufxArchive -- test files + * =========================================================================== + */ + +/* + * Test the records represented in the selection set. + */ +bool +NufxArchive::TestSelection(CWnd* pMsgWnd, SelectionSet* pSelSet) +{ + NuError nerr; + NufxEntry* pEntry; + CString errMsg; + bool retVal = false; + + ASSERT(fpArchive != nil); + + WMSG1("Testing %d entries\n", pSelSet->GetNumEntries()); + + SelectionEntry* pSelEntry = pSelSet->IterNext(); + while (pSelEntry != nil) { + pEntry = (NufxEntry*) pSelEntry->GetEntry(); + + WMSG2(" Testing %ld '%s'\n", pEntry->GetRecordIdx(), + pEntry->GetPathName()); + nerr = NuTestRecord(fpArchive, pEntry->GetRecordIdx()); + if (nerr != kNuErrNone) { + if (nerr == kNuErrAborted) { + CString title; + title.LoadString(IDS_MB_APP_NAME); + errMsg = "Cancelled."; + pMsgWnd->MessageBox(errMsg, title, MB_OK); + } else { + errMsg.Format("Failed while testing '%s': %s.", + pEntry->GetPathName(), NuStrError(nerr)); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + } + goto bail; + } + + pSelEntry = pSelSet->IterNext(); + } + + /* show success message */ + errMsg.Format("Tested %d file%s, no errors found.", + pSelSet->GetNumEntries(), + pSelSet->GetNumEntries() == 1 ? "" : "s"); + pMsgWnd->MessageBox(errMsg); + retVal = true; + +bail: + return retVal; +} + + +/* + * =========================================================================== + * NufxArchive -- delete files + * =========================================================================== + */ + +/* + * Delete the records represented in the selection set. + */ +bool +NufxArchive::DeleteSelection(CWnd* pMsgWnd, SelectionSet* pSelSet) +{ + NuError nerr; + NufxEntry* pEntry; + CString errMsg; + bool retVal = false; + + ASSERT(fpArchive != nil); + + WMSG1("Deleting %d entries\n", pSelSet->GetNumEntries()); + + /* mark entries for deletion */ + SelectionEntry* pSelEntry = pSelSet->IterNext(); + while (pSelEntry != nil) { + pEntry = (NufxEntry*) pSelEntry->GetEntry(); + + WMSG2(" Deleting %ld '%s'\n", pEntry->GetRecordIdx(), + pEntry->GetPathName()); + nerr = NuDeleteRecord(fpArchive, pEntry->GetRecordIdx()); + if (nerr != kNuErrNone) { + errMsg.Format("Unable to delete record %d: %s.", + pEntry->GetRecordIdx(), NuStrError(nerr)); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + goto bail; + } + + pSelEntry = pSelSet->IterNext(); + } + + /* actually do the delete */ + long statusFlags; + nerr = NuFlush(fpArchive, &statusFlags); + if (nerr != kNuErrNone) { + errMsg.Format("Unable to delete all files: %s.", NuStrError(nerr)); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + + /* see if it got converted to read-only status */ + if (statusFlags & kNuFlushReadOnly) + fIsReadOnly = true; + } + + if (InternalReload(fpMsgWnd) == kNuErrNone) + retVal = true; + +bail: + return retVal; +} + + +/* + * =========================================================================== + * NufxArchive -- rename files + * =========================================================================== + */ + +/* + * Rename the records represented in the selection set. + */ +bool +NufxArchive::RenameSelection(CWnd* pMsgWnd, SelectionSet* pSelSet) +{ + CString errMsg; + NuError nerr; + bool retVal = false; + + ASSERT(fpArchive != nil); + + WMSG1("Renaming %d entries\n", pSelSet->GetNumEntries()); + + /* + * Figure out if we're allowed to change the entire path. (This is + * doing it the hard way, but what the hell.) + */ + long cap = GetCapability(GenericArchive::kCapCanRenameFullPath); + bool renameFullPath = (cap != 0); + + WMSG1("Rename, fullpath=%d\n", renameFullPath); + + /* + * For each item in the selection set, bring up the "rename" dialog, + * and ask the GenericEntry to process it. + * + * If they hit "cancel" or there's an error, we still flush the + * previous changes. This is so that we don't have to create the + * same sort of deferred-write feature when renaming things in other + * sorts of archives (e.g. disk archives). + */ + SelectionEntry* pSelEntry = pSelSet->IterNext(); + while (pSelEntry != nil) { + NufxEntry* pEntry = (NufxEntry*) pSelEntry->GetEntry(); + WMSG1(" Renaming '%s'\n", pEntry->GetPathName()); + + RenameEntryDialog renameDlg(pMsgWnd); + renameDlg.SetCanRenameFullPath(renameFullPath); + renameDlg.SetCanChangeFssep(true); + renameDlg.fOldName = pEntry->GetPathName(); + renameDlg.fFssep = pEntry->GetFssep(); + renameDlg.fpArchive = this; + renameDlg.fpEntry = pEntry; + + int result = renameDlg.DoModal(); + if (result == IDOK) { + if (renameDlg.fFssep == '\0') + renameDlg.fFssep = kNufxNoFssep; + nerr = NuRename(fpArchive, pEntry->GetRecordIdx(), + renameDlg.fNewName, renameDlg.fFssep); + if (nerr != kNuErrNone) { + errMsg.Format("Unable to rename '%s': %s.", pEntry->GetPathName(), + NuStrError(nerr)); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + break; + } + WMSG2("Rename of '%s' to '%s' succeeded\n", + pEntry->GetDisplayName(), renameDlg.fNewName); + } else if (result == IDCANCEL) { + WMSG0("Canceling out of remaining renames\n"); + break; + } else { + /* 3rd possibility is IDIGNORE, i.e. skip this entry */ + WMSG1("Skipping rename of '%s'\n", pEntry->GetDisplayName()); + } + + pSelEntry = pSelSet->IterNext(); + } + + /* flush pending rename calls */ + { + CWaitCursor waitc; + + long statusFlags; + nerr = NuFlush(fpArchive, &statusFlags); + if (nerr != kNuErrNone) { + errMsg.Format("Unable to rename all files: %s.", + NuStrError(nerr)); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + + /* see if it got converted to read-only status */ + if (statusFlags & kNuFlushReadOnly) + fIsReadOnly = true; + } + } + + /* reload GenericArchive from NufxLib */ + if (InternalReload(fpMsgWnd) == kNuErrNone) + retVal = true; + + return retVal; +} + + +/* + * Verify that the a name is suitable. Called by RenameEntryDialog. + * + * Tests for context-specific syntax and checks for duplicates. + * + * Returns an empty string on success, or an error message on failure. + */ +CString +NufxArchive::TestPathName(const GenericEntry* pGenericEntry, + const CString& basePath, const CString& newName, char newFssep) const +{ + CString errMsg(""); + ASSERT(pGenericEntry != nil); + + ASSERT(basePath.IsEmpty()); + + /* can't start or end with fssep */ + if (newName.Left(1) == newFssep || newName.Right(1) == newFssep) { + errMsg.Format("Names in NuFX archives may not start or end with a " + "path separator character (%c).", + newFssep); + goto bail; + } + + /* if it's a disk image, don't allow complex paths */ + if (pGenericEntry->GetRecordKind() == GenericEntry::kRecordKindDisk) { + if (newName.Find(newFssep) != -1) { + errMsg.Format("Disk image names may not contain a path separator " + "character (%c).", + newFssep); + goto bail; + } + } + + /* + * Test for case-sensitive name collisions. Each individual path + * component must be compared + */ + GenericEntry* pEntry; + pEntry = GetEntries(); + while (pEntry != nil) { + if (pEntry != pGenericEntry && + ComparePaths(pEntry->GetPathName(), pEntry->GetFssep(), + newName, newFssep) == 0) + { + errMsg.Format("An entry with that name already exists."); + } + + pEntry = pEntry->GetNext(); + } + +bail: + return errMsg; +} + + +/* + * =========================================================================== + * NufxArchive -- recompress + * =========================================================================== + */ + +/* + * Recompress the files in the selection set. + * + * We have to uncompress the files into memory and then recompress them. + * We don't want to flush after every file (too slow), but we can't wait + * until they're expanded (unbounded memory requirements). So we have + * to keep expanding until we reach a certain limit, then call flush to + * push the changes out. + * + * Since we're essentially making the changes in place (it's actually + * all getting routed through the temp file), we need to delete the thread + * and re-add it. This isn't quite as thorough as "launder", which + * actually reconstructs the entire record. + */ +bool +NufxArchive::RecompressSelection(CWnd* pMsgWnd, SelectionSet* pSelSet, + const RecompressOptionsDialog* pRecompOpts) +{ + const int kMaxSizeInMemory = 2 * 1024 * 1024; // 2MB + CString errMsg; + NuError nerr; + bool retVal = false; + + /* set the compression type */ + nerr = NuSetValue(fpArchive, kNuValueDataCompression, + pRecompOpts->fCompressionType + kNuCompressNone); + if (nerr != kNuErrNone) { + WMSG1("GLITCH: unable to set compression type to %d\n", + pRecompOpts->fCompressionType); + /* keep going */ + } + + fProgressAsRecompress = true; + + /* + * Loop over all items in the selection set. Because the selection + * set has one entry for each interesting thread, we don't need to + * pry the NuRecord open and play with it. + * + * We should only be here for data forks, resource forks, and disk + * images. Comments and filenames are not compressed, and so cannot + * be recompressed. + */ + SelectionEntry* pSelEntry = pSelSet->IterNext(); + long sizeInMemory = 0; + bool result = true; + NufxEntry* pEntry = nil; + for ( ; pSelEntry != nil; pSelEntry = pSelSet->IterNext()) { + pEntry = (NufxEntry*) pSelEntry->GetEntry(); + + /* + * Compress each thread in turn. + */ + if (pEntry->GetHasDataFork()) { + result = RecompressThread(pEntry, GenericEntry::kDataThread, + pRecompOpts, &sizeInMemory, &errMsg); + if (!result) + break; + } + if (pEntry->GetHasRsrcFork()) { + result = RecompressThread(pEntry, GenericEntry::kRsrcThread, + pRecompOpts, &sizeInMemory, &errMsg); + if (!result) + break; + } + if (pEntry->GetHasDiskImage()) { + result = RecompressThread(pEntry, GenericEntry::kDiskImageThread, + pRecompOpts, &sizeInMemory, &errMsg); + if (!result) + break; + } + /* don't do anything with comments */ + + /* if we're sitting on too much, push it out */ + if (sizeInMemory > kMaxSizeInMemory) { + /* flush anything pending */ + long statusFlags; + nerr = NuFlush(fpArchive, &statusFlags); + if (nerr != kNuErrNone) { + if (nerr != kNuErrAborted) { + errMsg.Format("Unable to recompress all files: %s.", + NuStrError(nerr)); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + } else { + WMSG0("Cancelled out of sub-flush/compress\n"); + } + + /* see if it got converted to read-only status */ + if (statusFlags & kNuFlushReadOnly) + fIsReadOnly = true; + + goto bail; + } + + sizeInMemory = 0; + } + } + + /* handle errors that threw us out of the while loop */ + if (!result) { + ASSERT(pEntry != nil); + CString dispStr; + dispStr.Format("Failed while recompressing '%s': %s.", + pEntry->GetDisplayName(), errMsg); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + goto bail; + } + + + /* flush anything pending */ + long statusFlags; + nerr = NuFlush(fpArchive, &statusFlags); + if (nerr != kNuErrNone) { + if (nerr != kNuErrAborted) { + errMsg.Format("Unable to recompress all files: %s.", + NuStrError(nerr)); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + } else { + WMSG0("Cancelled out of flush/compress\n"); + } + + /* see if it got converted to read-only status */ + if (statusFlags & kNuFlushReadOnly) + fIsReadOnly = true; + } else { + retVal = true; + } + +bail: + /* abort anything that didn't get flushed */ + NuAbort(fpArchive); + /* reload to pick up changes */ + (void) InternalReload(pMsgWnd); + + fProgressAsRecompress = false; + return retVal; +} + +/* + * Recompress one thread. + * + * Returns "true" if things went okay, "false" on a fatal failure. + */ +bool +NufxArchive::RecompressThread(NufxEntry* pEntry, int threadKind, + const RecompressOptionsDialog* pRecompOpts, long* pSizeInMemory, + CString* pErrMsg) +{ + NuThread thread; + NuThreadID threadID; + NuError nerr; + NuDataSource* pSource = nil; + CString subErrMsg; + bool retVal = false; + char* buf = nil; + long len = 0; + + WMSG2(" Recompressing %ld '%s'\n", pEntry->GetRecordIdx(), + pEntry->GetDisplayName()); + + /* get a copy of the thread header */ + pEntry->FindThreadInfo(threadKind, &thread, pErrMsg); + if (!pErrMsg->IsEmpty()) { + pErrMsg->Format("Unable to locate thread for %s (type %d)", + pEntry->GetDisplayName(), threadKind); + goto bail; + } + threadID = NuGetThreadID(&thread); + + /* if it's already in the target format, skip it */ + if (thread.thThreadFormat == pRecompOpts->fCompressionType) { + WMSG2("Skipping (fmt=%d) '%s'\n", + pRecompOpts->fCompressionType, pEntry->GetDisplayName()); + return true; + } + + /* extract the thread */ + int result; + result = pEntry->ExtractThreadToBuffer(threadKind, &buf, &len, &subErrMsg); + if (result == IDCANCEL) { + WMSG0("Cancelled during extract!\n"); + ASSERT(buf == nil); + goto bail; /* abort anything that was pending */ + } else if (result != IDOK) { + pErrMsg->Format("Failed while extracting '%s': %s", + pEntry->GetDisplayName(), subErrMsg); + goto bail; + } + *pSizeInMemory += len; + + /* create a data source for it */ + nerr = NuCreateDataSourceForBuffer(kNuThreadFormatUncompressed, + 0, (const unsigned char*)buf, 0, len, ArrayDeleteHandler, + &pSource); + if (nerr != kNuErrNone) { + pErrMsg->Format("Unable to create NufxLib data source (len=%d).", + len); + goto bail; + } + buf = nil; // data source owns it now + + /* delete the existing thread */ + //WMSG1("+++ DELETE threadIdx=%d\n", thread.threadIdx); + nerr = NuDeleteThread(fpArchive, thread.threadIdx); + if (nerr != kNuErrNone) { + pErrMsg->Format("Unable to delete thread %d: %s", + pEntry->GetRecordIdx(), NuStrError(nerr)); + goto bail; + } + + /* mark the new thread for addition */ + //WMSG1("+++ ADD threadID=0x%08lx\n", threadID); + nerr = NuAddThread(fpArchive, pEntry->GetRecordIdx(), threadID, + pSource, nil); + if (nerr != kNuErrNone) { + pErrMsg->Format("Unable to add thread type %d: %s", + threadID, NuStrError(nerr)); + goto bail; + } + pSource = nil; // now owned by nufxlib + + /* at this point, we just wait for the flush in the outer loop */ + retVal = true; + +bail: + NuFreeDataSource(pSource); + return retVal; +} + + +/* + * =========================================================================== + * NufxArchive -- transfer files to another archive + * =========================================================================== + */ + +/* + * Transfer the selected files out of this archive and into another. + * + * We get one entry in the selection set per record. + * + * I think this now throws kXferCancelled whenever it's supposed to. Not + * 100% sure, but it looks good. + */ +GenericArchive::XferStatus +NufxArchive::XferSelection(CWnd* pMsgWnd, SelectionSet* pSelSet, + ActionProgressDialog* pActionProgress, const XferFileOptions* pXferOpts) +{ + WMSG0("NufxArchive XferSelection!\n"); + XferStatus retval = kXferFailed; + unsigned char* dataBuf = nil; + unsigned char* rsrcBuf = nil; + CString errMsg, dispMsg; + + pXferOpts->fTarget->XferPrepare(pXferOpts); + + SelectionEntry* pSelEntry = pSelSet->IterNext(); + for ( ; pSelEntry != nil; pSelEntry = pSelSet->IterNext()) { + long dataLen=-1, rsrcLen=-1; + NufxEntry* pEntry = (NufxEntry*) pSelEntry->GetEntry(); + FileDetails fileDetails; + CString errMsg; + + ASSERT(dataBuf == nil); + ASSERT(rsrcBuf == nil); + + /* in case we start handling CRC errors better */ + if (pEntry->GetDamaged()) { + WMSG1(" XFER skipping damaged entry '%s'\n", + pEntry->GetDisplayName()); + continue; + } + + WMSG1(" XFER converting '%s'\n", pEntry->GetDisplayName()); + + fileDetails.storageName = pEntry->GetDisplayName(); + fileDetails.fileType = pEntry->GetFileType(); + fileDetails.fileSysFmt = DiskImg::kFormatUnknown; + fileDetails.fileSysInfo = PathProposal::kDefaultStoredFssep; + fileDetails.access = pEntry->GetAccess(); + fileDetails.extraType = pEntry->GetAuxType(); + fileDetails.storageType = kNuStorageSeedling; + + time_t when; + when = time(nil); + UNIXTimeToDateTime(&when, &fileDetails.archiveWhen); + when = pEntry->GetModWhen(); + UNIXTimeToDateTime(&when, &fileDetails.modWhen); + when = pEntry->GetCreateWhen(); + UNIXTimeToDateTime(&when, &fileDetails.createWhen); + + pActionProgress->SetArcName(fileDetails.storageName); + if (pActionProgress->SetProgress(0) == IDCANCEL) { + retval = kXferCancelled; + goto bail; + } + + /* + * Handle all relevant threads in this record. We assume it's either + * a data/rsrc pair or a disk image. + */ + if (pEntry->GetHasDataFork()) { + /* + * Found a data thread. + */ + int result; + dataBuf = nil; + dataLen = 0; + result = pEntry->ExtractThreadToBuffer(GenericEntry::kDataThread, + (char**) &dataBuf, &dataLen, &errMsg); + if (result == IDCANCEL) { + WMSG0("Cancelled during data extract!\n"); + retval = kXferCancelled; + goto bail; /* abort anything that was pending */ + } else if (result != IDOK) { + dispMsg.Format("Failed while extracting '%s': %s.", + pEntry->GetDisplayName(), errMsg); + ShowFailureMsg(pMsgWnd, dispMsg, IDS_FAILED); + goto bail; + } + ASSERT(dataBuf != nil); + ASSERT(dataLen >= 0); + + } else if (pEntry->GetHasDiskImage()) { + /* + * No data thread found. Look for a disk image. + */ + int result; + dataBuf = nil; + dataLen = 0; + result = pEntry->ExtractThreadToBuffer(GenericEntry::kDiskImageThread, + (char**) &dataBuf, &dataLen, &errMsg); + if (result == IDCANCEL) { + WMSG0("Cancelled during data extract!\n"); + goto bail; /* abort anything that was pending */ + } else if (result != IDOK) { + dispMsg.Format("Failed while extracting '%s': %s.", + pEntry->GetDisplayName(), errMsg); + ShowFailureMsg(pMsgWnd, dispMsg, IDS_FAILED); + goto bail; + } + ASSERT(dataBuf != nil); + ASSERT(dataLen >= 0); + } + + /* + * See if there's a resource fork in here (either by itself or + * with a data fork). + */ + if (pEntry->GetHasRsrcFork()) { + int result; + rsrcBuf = nil; + rsrcLen = 0; + result = pEntry->ExtractThreadToBuffer(GenericEntry::kRsrcThread, + (char**) &rsrcBuf, &rsrcLen, &errMsg); + if (result == IDCANCEL) { + WMSG0("Cancelled during rsrc extract!\n"); + goto bail; /* abort anything that was pending */ + } else if (result != IDOK) { + dispMsg.Format("Failed while extracting '%s': %s.", + pEntry->GetDisplayName(), errMsg); + ShowFailureMsg(pMsgWnd, dispMsg, IDS_FAILED); + goto bail; + } + + fileDetails.storageType = kNuStorageExtended; + } else { + ASSERT(rsrcBuf == nil); + } + + if (dataLen < 0 && rsrcLen < 0) { + WMSG1(" XFER: WARNING: nothing worth transferring in '%s'\n", + pEntry->GetDisplayName()); + continue; + } + + errMsg = pXferOpts->fTarget->XferFile(&fileDetails, &dataBuf, dataLen, + &rsrcBuf, rsrcLen); + if (!errMsg.IsEmpty()) { + WMSG0("XferFile failed!\n"); + errMsg.Format("Failed while transferring '%s': %s.", + pEntry->GetDisplayName(), (const char*) errMsg); + ShowFailureMsg(pMsgWnd, errMsg, IDS_FAILED); + goto bail; + } + ASSERT(dataBuf == nil); + ASSERT(rsrcBuf == nil); + + if (pActionProgress->SetProgress(100) == IDCANCEL) { + retval = kXferCancelled; + goto bail; + } + } + + retval = kXferOK; + +bail: + if (retval != kXferOK) + pXferOpts->fTarget->XferAbort(pMsgWnd); + else + pXferOpts->fTarget->XferFinish(pMsgWnd); + delete[] dataBuf; + delete[] rsrcBuf; + return retval; +} + +/* + * Prepare to transfer files into a NuFX archive. + * + * We set the "allow duplicates" flag because DOS 3.3 volumes can have + * files with duplicate names. + */ +void +NufxArchive::XferPrepare(const XferFileOptions* pXferOpts) +{ + WMSG0(" NufxArchive::XferPrepare\n"); + (void) NuSetValue(fpArchive, kNuValueAllowDuplicates, true); +} + +/* + * Transfer the data and optional resource fork of a single file into the + * NuFX archive. + * + * "dataLen" and "rsrcLen" will be -1 if the corresponding fork doesn't + * exist. + * + * Returns 0 on success, -1 on failure. On success, "*pDataBuf" and + * "*pRsrcBuf" are set to nil (ownership transfers to NufxLib). + */ +CString +NufxArchive::XferFile(FileDetails* pDetails, unsigned char** pDataBuf, + long dataLen, unsigned char** pRsrcBuf, long rsrcLen) +{ + NuError nerr; + const int kFileTypeTXT = 0x04; + NuDataSource* pSource = nil; + CString errMsg; + + WMSG1(" NufxArchive::XferFile '%s'\n", pDetails->storageName); + WMSG4(" dataBuf=0x%08lx dataLen=%ld rsrcBuf=0x%08lx rsrcLen=%ld\n", + *pDataBuf, dataLen, *pRsrcBuf, rsrcLen); + ASSERT(pDataBuf != nil); + ASSERT(pRsrcBuf != nil); + + /* NuFX doesn't explicitly store directories */ + if (pDetails->entryKind == FileDetails::kFileKindDirectory) { + delete[] *pDataBuf; + delete[] *pRsrcBuf; + *pDataBuf = *pRsrcBuf = nil; + goto bail; + } + + ASSERT(dataLen >= 0 || rsrcLen >= 0); + ASSERT(*pDataBuf != nil || *pRsrcBuf != nil); + + /* add the record; we have "allow duplicates" enabled for clashes */ + NuRecordIdx recordIdx; + NuFileDetails nuFileDetails; + nuFileDetails = *pDetails; + + /* + * Odd bit of trivia: NufxLib refuses to accept an fssep of '\0'. It + * really wants to have one. Which is annoying, since files coming + * from DOS or Pascal don't have one. We therefore need to supply + * one, so we provide 0xff on the theory that nobody in their right + * mind would have it in an Apple II filename. + * + * Since we don't strip Pascal and ProDOS names down when we load + * the disks, it's possible the for 0xff to occur if the disk got + * damaged. For ProDOS we don't care, since it has an fssep, but + * Pascal could be at risk. DOS and RDOS are sanitized and so should + * be okay. + * + * One issue: we don't currently allow changing the fssep when renaming + * a file. We need to fix this, or else there's no way to rename a + * file into a subdirectory once it has been pasted in this way. + */ + if (NuGetSepFromSysInfo(nuFileDetails.fileSysInfo) == 0) { + nuFileDetails.fileSysInfo = + NuSetSepInSysInfo(nuFileDetails.fileSysInfo, kNufxNoFssep); + } + + nerr = NuAddRecord(fpArchive, &nuFileDetails, &recordIdx); + if (nerr != kNuErrNone) { + if (nerr != kNuErrAborted) { + errMsg.Format("Failed adding record: %s", NuStrError(nerr)); + //ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED); + } + // else the add was cancelled + goto bail; + } + + if (dataLen >= 0) { + ASSERT(*pDataBuf != nil); + + /* strip the high ASCII from DOS and RDOS text files */ + if (pDetails->entryKind != FileDetails::kFileKindDiskImage && + pDetails->fileType == kFileTypeTXT && + DiskImg::UsesDOSFileStructure(pDetails->fileSysFmt)) + { + WMSG1(" Stripping high ASCII from '%s'\n", pDetails->storageName); + unsigned char* ucp = *pDataBuf; + long len = dataLen; + + while (len--) + *ucp++ &= 0x7f; + } + + /* create a data source for the data fork; might be zero len */ + nerr = NuCreateDataSourceForBuffer(kNuThreadFormatUncompressed, 0, + *pDataBuf, 0, dataLen, ArrayDeleteHandler, &pSource); + if (nerr != kNuErrNone) { + errMsg = "Unable to create NufxLib data source."; + //ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED); + goto bail; + } + *pDataBuf = nil; /* owned by data source */ + + /* add the data fork, as a disk image if appropriate */ + NuThreadID targetID; + if (pDetails->entryKind == FileDetails::kFileKindDiskImage) + targetID = kNuThreadIDDiskImage; + else + targetID = kNuThreadIDDataFork; + + nerr = NuAddThread(fpArchive, recordIdx, targetID, pSource, nil); + if (nerr != kNuErrNone) { + errMsg.Format("Failed adding thread: %s.", NuStrError(nerr)); + //ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED); + goto bail; + } + pSource = nil; /* NufxLib owns it now */ + } + + /* add the resource fork, if one was provided */ + if (rsrcLen >= 0) { + ASSERT(*pRsrcBuf != nil); + + nerr = NuCreateDataSourceForBuffer(kNuThreadFormatUncompressed, 0, + *pRsrcBuf, 0, rsrcLen, ArrayDeleteHandler, &pSource); + if (nerr != kNuErrNone) { + errMsg = "Unable to create NufxLib data source."; + //ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED); + goto bail; + } + *pRsrcBuf = nil; /* owned by data source */ + + /* add the data fork */ + nerr = NuAddThread(fpArchive, recordIdx, kNuThreadIDRsrcFork, + pSource, nil); + if (nerr != kNuErrNone) { + errMsg.Format("Failed adding thread: %s.", NuStrError(nerr)); + //ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED); + goto bail; + } + pSource = nil; /* NufxLib owns it now */ + } + +bail: + NuFreeDataSource(pSource); + return errMsg; +} + +/* + * Abort the transfer. + * + * Since we don't do any interim flushes, we can just call NuAbort. If that + * weren't the case, we would need to delete all records and flush. + */ +void +NufxArchive::XferAbort(CWnd* pMsgWnd) +{ + NuError nerr; + CString errMsg; + + WMSG0(" NufxArchive::XferAbort\n"); + + nerr = NuAbort(fpArchive); + if (nerr != kNuErrNone) { + errMsg.Format("Failed while aborting procedure: %s.", NuStrError(nerr)); + ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED); + } +} + +/* + * Flush all changes to the archive. + */ +void +NufxArchive::XferFinish(CWnd* pMsgWnd) +{ + NuError nerr; + CString errMsg; + + WMSG0(" NufxArchive::XferFinish\n"); + + /* actually do the work */ + long statusFlags; + nerr = NuFlush(fpArchive, &statusFlags); + if (nerr != kNuErrNone) { + if (nerr != kNuErrAborted) { + errMsg.Format("Unable to add file: %s.", NuStrError(nerr)); + ShowFailureMsg(fpMsgWnd, errMsg, IDS_FAILED); + } + + /* see if it got converted to read-only status */ + if (statusFlags & kNuFlushReadOnly) + fIsReadOnly = true; + goto bail; + } + + (void) InternalReload(fpMsgWnd); + +bail: + return; +} + + +/* + * =========================================================================== + * NufxArchive -- add/update/delete comments + * =========================================================================== + */ + +/* + * Extract a comment from the archive, converting line terminators to CRLF. + * + * Returns "true" on success, "false" on failure. + */ +bool +NufxArchive::GetComment(CWnd* pMsgWnd, const GenericEntry* pGenericEntry, + CString* pStr) +{ + NufxEntry* pEntry = (NufxEntry*) pGenericEntry; + CString errMsg; + const char* kNewEOL = "\r\n"; + int result; + char* buf; + long len; + + ASSERT(pGenericEntry->GetHasComment()); + + /* use standard extract function to pull comment out */ + buf = nil; + len = 0; + result = pEntry->ExtractThreadToBuffer(GenericEntry::kCommentThread, + &buf, &len, &errMsg); + if (result != IDOK) { + WMSG1("Failed getting comment: %s\n", buf); + ASSERT(buf == nil); + return false; + } + + /* convert EOL and add '\0' */ + CString convStr; + const char* ccp; + + ccp = buf; + while (len-- && *ccp != '\0') { + if (len > 1 && *ccp == '\r' && *(ccp+1) == '\n') { + ccp++; + len--; + convStr += kNewEOL; + } else if (*ccp == '\r' || *ccp == '\n') { + convStr += kNewEOL; + } else { + convStr += *ccp; + } + ccp++; + } + + *pStr = convStr; + delete[] buf; + return true; +} + +/* + * Set the comment. This requires either adding a new comment or updating + * an existing one. The latter is constrained by the maximum size of the + * comment buffer. + * + * We want to update in place whenever possible because it's faster (don't + * have to rewrite the entire archive), but that really only holds for new + * archives or if we foolishly set the kNuValueModifyOrig flag. + * + * Cleanest approach is to delete the existing thread and add a new one. + * If somebody complains we can try to be smarter about it. + * + * Returns "true" on success, "false" on failure. + */ +bool +NufxArchive::SetComment(CWnd* pMsgWnd, GenericEntry* pGenericEntry, + const CString& str) +{ + NuDataSource* pSource = nil; + NufxEntry* pEntry = (NufxEntry*) pGenericEntry; + NuError nerr; + bool retVal = false; + + /* convert CRLF to CR */ + CString newStr(str); + char* srcp; + char* dstp; + srcp = dstp = newStr.GetBuffer(0); + while (*srcp != '\0') { + if (*srcp == '\r' && *(srcp+1) == '\n') { + srcp++; + *dstp = '\r'; + } else { + *dstp = *srcp; + } + srcp++; + dstp++; + } + *dstp = '\0'; + newStr.ReleaseBuffer(); + + /* get the thread info */ + CString errMsg; + NuThread thread; + NuThreadIdx threadIdx; + + pEntry->FindThreadInfo(GenericEntry::kCommentThread, &thread, &errMsg); + threadIdx = thread.threadIdx; + if (errMsg.IsEmpty()) { + /* delete existing thread */ + nerr = NuDeleteThread(fpArchive, threadIdx); + if (nerr != kNuErrNone) { + errMsg.Format("Unable to delete thread: %s.", NuStrError(nerr)); + goto bail; + } + } + + /* set a maximum pre-size value for the thread */ + long maxLen; + maxLen = ((newStr.GetLength() + 99) / 100) * 100; + if (maxLen < 200) + maxLen = 200; + + + /* create a data source to write from */ + nerr = NuCreateDataSourceForBuffer(kNuThreadFormatUncompressed, + maxLen, (const unsigned char*)(const char*)newStr, 0, + newStr.GetLength(), nil, &pSource); + if (nerr != kNuErrNone) { + errMsg.Format("Unable to create NufxLib data source (len=%d, maxLen=%d).", + newStr.GetLength(), maxLen); + goto bail; + } + + /* add the new thread */ + nerr = NuAddThread(fpArchive, pEntry->GetRecordIdx(), + kNuThreadIDComment, pSource, nil); + if (nerr != kNuErrNone) { + errMsg.Format("Unable to add comment thread: %s.", + NuStrError(nerr)); + goto bail; + } + pSource = nil; // nufxlib owns it now + + /* flush changes */ + long statusFlags; + nerr = NuFlush(fpArchive, &statusFlags); + if (nerr != kNuErrNone) { + errMsg.Format("Unable to flush comment changes: %s.", + NuStrError(nerr)); + goto bail; + } + + /* reload GenericArchive from NufxLib */ + if (InternalReload(fpMsgWnd) == kNuErrNone) + retVal = true; + +bail: + NuFreeDataSource(pSource); + if (!retVal) { + WMSG1("FAILED: %s\n", (LPCTSTR) errMsg); + NuAbort(fpArchive); + } + return retVal; +} + +/* + * Remove a comment. + * + * Returns "true" on success, "false" on failure. + */ +bool +NufxArchive::DeleteComment(CWnd* pMsgWnd, GenericEntry* pGenericEntry) +{ + CString errMsg; + NuError nerr; + NufxEntry* pEntry = (NufxEntry*) pGenericEntry; + NuThread thread; + NuThreadIdx threadIdx; + bool retVal = false; + + pEntry->FindThreadInfo(GenericEntry::kCommentThread, &thread, &errMsg); + if (!errMsg.IsEmpty()) + goto bail; + threadIdx = thread.threadIdx; + + nerr = NuDeleteThread(fpArchive, threadIdx); + if (nerr != kNuErrNone) { + errMsg.Format("Unable to delete thread: %s.", NuStrError(nerr)); + goto bail; + } + + /* flush changes */ + long statusFlags; + nerr = NuFlush(fpArchive, &statusFlags); + if (nerr != kNuErrNone) { + errMsg.Format("Unable to flush comment deletion: %s.", + NuStrError(nerr)); + goto bail; + } + + /* reload GenericArchive from NufxLib */ + if (InternalReload(pMsgWnd) == kNuErrNone) + retVal = true; + +bail: + if (retVal != 0) { + WMSG1("FAILED: %s\n", (LPCTSTR) errMsg); + NuAbort(fpArchive); + } + return retVal; +} + + +/* + * Set file properties via the NuSetRecordAttr call. + * + * Get the existing properties, copy the fields from FileProps over, and + * set them. + * + * [currently only supports file type, aux type, and access flags] + * + * Technically we should reload the GenericArchive from the NufxArchive, + * but the set of changes is pretty small, so we just make them here. + */ +bool +NufxArchive::SetProps(CWnd* pMsgWnd, GenericEntry* pEntry, + const FileProps* pProps) +{ + NuError nerr; + NufxEntry* pNufxEntry = (NufxEntry*) pEntry; + const NuRecord* pRecord; + NuRecordAttr recordAttr; + + WMSG3(" SET fileType=0x%02x auxType=0x%04x access=0x%02x\n", + pProps->fileType, pProps->auxType, pProps->access); + + nerr = NuGetRecord(fpArchive, pNufxEntry->GetRecordIdx(), &pRecord); + if (nerr != kNuErrNone) { + WMSG2("ERROR: couldn't find recordIdx %ld: %s\n", + pNufxEntry->GetRecordIdx(), NuStrError(nerr)); + return false; + } + + NuRecordCopyAttr(&recordAttr, pRecord); + recordAttr.fileType = pProps->fileType; + recordAttr.extraType = pProps->auxType; + recordAttr.access = pProps->access; + + nerr = NuSetRecordAttr(fpArchive, pNufxEntry->GetRecordIdx(), &recordAttr); + if (nerr != kNuErrNone) { + WMSG2("ERROR: couldn't set recordAttr %ld: %s\n", + pNufxEntry->GetRecordIdx(), NuStrError(nerr)); + return false; + } + + long statusFlags; + nerr = NuFlush(fpArchive, &statusFlags); + if (nerr != kNuErrNone) { + WMSG1("ERROR: NuFlush failed: %s\n", NuStrError(nerr)); + + /* see if it got converted to read-only status */ + if (statusFlags & kNuFlushReadOnly) + fIsReadOnly = true; + return false; + } + + WMSG0("Props set\n"); + + /* do this in lieu of reloading GenericArchive */ + pEntry->SetFileType(pProps->fileType); + pEntry->SetAuxType(pProps->auxType); + pEntry->SetAccess(pProps->access); + + return true; +} diff --git a/app/NufxArchive.h b/app/NufxArchive.h new file mode 100644 index 0000000..959d2f4 --- /dev/null +++ b/app/NufxArchive.h @@ -0,0 +1,181 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * NuFX archive support. + */ +#ifndef __NUFX_ARCHIVE__ +#define __NUFX_ARCHIVE__ + +#include "GenericArchive.h" +#include "../prebuilt/NufxLib.h" // ideally this wouldn't be here, only in .cpp + + +/* + * One file in an NuFX archive. + */ +class NufxEntry : public GenericEntry { +public: + NufxEntry(NuArchive* pArchive) : fpArchive(pArchive) + {} + virtual ~NufxEntry(void) {} + + NuRecordIdx GetRecordIdx(void) const { return fRecordIdx; } + void SetRecordIdx(NuRecordIdx idx) { fRecordIdx = idx; } + + // retrieve thread data + virtual int ExtractThreadToBuffer(int which, char** ppText, long* pLength, + CString* pErrMsg) const; + virtual int ExtractThreadToFile(int which, FILE* outfp, ConvertEOL conv, + ConvertHighASCII convHA, CString* pErrMsg) const; + virtual long GetSelectionSerial(void) const { return fRecordIdx; } + + virtual bool GetFeatureFlag(Feature feature) const { + if (feature == kFeaturePascalTypes || feature == kFeatureDOSTypes || + feature == kFeatureHasSimpleAccess) + return false; + else + return true; + } + + // This fills out several GenericEntry fields based on the contents + // of "*pRecord". + void AnalyzeRecord(const NuRecord* pRecord); + + friend class NufxArchive; + +private: + void FindThreadInfo(int which, NuThread* pThread, CString* pErrMsg) const; + + NuRecordIdx fRecordIdx; // unique record index + NuArchive* fpArchive; +}; + + +/* + * A generic archive plus NuFX-specific goodies. + */ +class NufxArchive : public GenericArchive { +public: + NufxArchive(void) : + fpArchive(nil), + fIsReadOnly(false), + fProgressAsRecompress(false), + fNumAdded(-1), + fpMsgWnd(nil), + fpAddOpts(nil) + {} + virtual ~NufxArchive(void) { (void) Close(); } + + // One-time initialization; returns an error string. + static CString AppInit(void); + + virtual OpenResult Open(const char* filename, bool readOnly, + CString* pErrMsg); + virtual CString New(const char* filename, const void* options); + virtual CString Flush(void) { return ""; } + virtual CString Reload(void); + virtual bool IsReadOnly(void) const { return fIsReadOnly; }; + virtual bool IsModified(void) const { return false; } + virtual void GetDescription(CString* pStr) const { *pStr = "NuFX"; } + virtual bool BulkAdd(ActionProgressDialog* pActionProgress, + const AddFilesDialog* pAddOpts); + virtual bool AddDisk(ActionProgressDialog* pActionProgress, + const AddFilesDialog* pAddOpts); + virtual bool CreateSubdir(CWnd* pMsgWnd, GenericEntry* pParentEntry, + const char* newName) + { ASSERT(false); return false; } + virtual bool TestSelection(CWnd* pMsgWnd, SelectionSet* pSelSet); + virtual bool DeleteSelection(CWnd* pMsgWnd, SelectionSet* pSelSet); + virtual bool RenameSelection(CWnd* pMsgWnd, SelectionSet* pSelSet); + virtual CString TestPathName(const GenericEntry* pGenericEntry, + const CString& basePath, const CString& newName, char newFssep) const; + virtual bool RenameVolume(CWnd* pMsgWnd, DiskFS* pDiskFS, + const char* newName) + { ASSERT(false); return false; } + virtual CString TestVolumeName(const DiskFS* pDiskFS, + const char* newName) const + { ASSERT(false); return "!"; } + virtual bool RecompressSelection(CWnd* pMsgWnd, SelectionSet* pSelSet, + const RecompressOptionsDialog* pRecompOpts); + virtual XferStatus XferSelection(CWnd* pMsgWnd, SelectionSet* pSelSet, + ActionProgressDialog* pActionProgress, const XferFileOptions* pXferOpts); + virtual bool GetComment(CWnd* pMsgWnd, const GenericEntry* pEntry, + CString* pStr); + virtual bool SetComment(CWnd* pMsgWnd, GenericEntry* pEntry, + const CString& str); + virtual bool DeleteComment(CWnd* pMsgWnd, GenericEntry* pEntry); + virtual bool SetProps(CWnd* pMsgWnd, GenericEntry* pEntry, + const FileProps* pProps); + virtual void PreferencesChanged(void); + virtual long GetCapability(Capability cap); + + // try not to use this + NuArchive* GetNuArchivePointer(void) const { return fpArchive; } + + // determine whether a particular type of compression is supported + static bool IsCompressionSupported(NuThreadFormat format); + + // convert from DateTime format to time_t + static time_t DateTimeToSeconds(const NuDateTime* pDateTime); + +private: + virtual CString Close(void) { + if (fpArchive != nil) { + WMSG0("Closing archive (aborting any un-flushed changes)\n"); + NuAbort(fpArchive); + NuClose(fpArchive); + fpArchive = nil; + } + return ""; + } + bool RecompressThread(NufxEntry* pEntry, int threadKind, + const RecompressOptionsDialog* pRecompOpts, long* pSizeInMemory, + CString* pErrMsg); + + virtual void XferPrepare(const XferFileOptions* pXferOpts); + virtual CString XferFile(FileDetails* pDetails, unsigned char** pDataBuf, + long dataLen, unsigned char** pRsrcBuf, long rsrcLen); + virtual void XferAbort(CWnd* pMsgWnd); + virtual void XferFinish(CWnd* pMsgWnd); + + virtual ArchiveKind GetArchiveKind(void) { return kArchiveNuFX; } + void AddPrep(CWnd* pWnd, const AddFilesDialog* pAddOpts); + void AddFinish(void); + virtual NuError DoAddFile(const AddFilesDialog* pAddOpts, + FileDetails* pDetails); + + static NuResult BulkAddErrorHandler(NuArchive* pArchive, void* vErrorStatus); + NuResult HandleReplaceExisting(const NuErrorStatus* pErrorStatus); + NuResult HandleAddNotFound(const NuErrorStatus* pErrorStatus); + + NuError LoadContents(void); + NuError InternalReload(CWnd* pMsgWnd); + static NuResult ContentFunc(NuArchive* pArchive, void* vpRecord); + + NuError SetCallbacks(void); + + // handle progress update messages + static NuResult ProgressUpdater(NuArchive* pArchive, void* vpProgress); + + // handle errors and debug messages from NufxLib. + static NuResult NufxErrorMsgHandler(NuArchive* pArchive, + void* vErrorMessage); + + // handle a DataSource resource release request + static NuResult ArrayDeleteHandler(NuArchive* pArchive, void* ptr); + + NuArchive* fpArchive; + bool fIsReadOnly; + + bool fProgressAsRecompress; // tweak progress updater + + /* state while adding files */ + int fNumAdded; + CWnd* fpMsgWnd; + const AddFilesDialog* fpAddOpts; +}; + +#endif /*__NUFX_ARCHIVE__*/ \ No newline at end of file diff --git a/app/OpenVolumeDialog.cpp b/app/OpenVolumeDialog.cpp new file mode 100644 index 0000000..24ee9e8 --- /dev/null +++ b/app/OpenVolumeDialog.cpp @@ -0,0 +1,736 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Allow the user to select a disk volume. + */ +#include "stdafx.h" +#include "OpenVolumeDialog.h" +#include "HelpTopics.h" +#include "Main.h" +#include "../diskimg/Win32Extra.h" // need disk geometry calls +#include "../diskimg/ASPI.h" +//#include "resource.h" + + +BEGIN_MESSAGE_MAP(OpenVolumeDialog, CDialog) + ON_COMMAND(IDHELP, OnHelp) + //ON_NOTIFY(NM_CLICK, IDC_VOLUME_LIST, OnListClick) + ON_NOTIFY(LVN_ITEMCHANGED, IDC_VOLUME_LIST, OnListChange) + ON_NOTIFY(NM_DBLCLK, IDC_VOLUME_LIST, OnListDblClick) + ON_CBN_SELCHANGE(IDC_VOLUME_FILTER, OnVolumeFilterSelChange) +END_MESSAGE_MAP() + + +/* + * Set up the list of drives. + */ +BOOL +OpenVolumeDialog::OnInitDialog(void) +{ + CDialog::OnInitDialog(); // do any DDX init stuff + const Preferences* pPreferences = GET_PREFERENCES(); + long defaultFilter; + + /* highlight/select entire line, not just filename */ + CListCtrl* pListView = (CListCtrl*) GetDlgItem(IDC_VOLUME_LIST); + ASSERT(pListView != nil); + + ListView_SetExtendedListViewStyleEx(pListView->m_hWnd, + LVS_EX_FULLROWSELECT, LVS_EX_FULLROWSELECT); + + /* disable the OK button until they click on something */ + CButton* pButton = (CButton*) GetDlgItem(IDOK); + ASSERT(pButton != nil); + + pButton->EnableWindow(FALSE); + + /* if the read-only state is fixed, don't let them change it */ + if (!fAllowROChange) { + CButton* pButton; + pButton = (CButton*) GetDlgItem(IDC_OPENVOL_READONLY); + ASSERT(pButton != nil); + pButton->EnableWindow(FALSE); + } + + /* prep the combo box */ + CComboBox* pCombo = (CComboBox*) GetDlgItem(IDC_VOLUME_FILTER); + ASSERT(pCombo != nil); + defaultFilter = pPreferences->GetPrefLong(kPrVolumeFilter); + if (defaultFilter >= kBoth && defaultFilter <= kPhysical) + pCombo->SetCurSel(defaultFilter); + else { + WMSG1("GLITCH: invalid defaultFilter in prefs (%d)\n", defaultFilter); + pCombo->SetCurSel(kLogical); + } + + /* two columns */ + CRect rect; + pListView->GetClientRect(&rect); + int width; + + width = pListView->GetStringWidth("XXVolume or Device NameXXmmmmmm"); + pListView->InsertColumn(0, "Volume or Device Name", LVCFMT_LEFT, width); + pListView->InsertColumn(1, "Remarks", LVCFMT_LEFT, + rect.Width() - width - ::GetSystemMetrics(SM_CXVSCROLL)); + + // Load the drive list. + LoadDriveList(); + + // Kluge the physical drive 0 stuff. + DiskImg::SetAllowWritePhys0(GET_PREFERENCES()->GetPrefBool(kPrOpenVolumePhys0)); + + return TRUE; +} + +/* + * Convert values. + */ +void +OpenVolumeDialog::DoDataExchange(CDataExchange* pDX) +{ + DDX_Check(pDX, IDC_OPENVOL_READONLY, fReadOnly); + WMSG1("DoDataExchange: fReadOnly==%d\n", fReadOnly); +} + +/* + * Load the set of logical and physical drives. + */ +void +OpenVolumeDialog::LoadDriveList(void) +{ + CWaitCursor waitc; + CComboBox* pCombo; + CListCtrl* pListView; + int itemIndex = 0; + int filterSelection; + + pListView = (CListCtrl*) GetDlgItem(IDC_VOLUME_LIST); + ASSERT(pListView != nil); + pCombo = (CComboBox*) GetDlgItem(IDC_VOLUME_FILTER); + ASSERT(pCombo != nil); + + pListView->DeleteAllItems(); + + /* + * Load the logical and physical drive sets as needed. Do the "physical" + * set first because it's usually what we want. + */ + filterSelection = pCombo->GetCurSel(); + if (filterSelection == kPhysical || filterSelection == kBoth) + LoadPhysicalDriveList(pListView, &itemIndex); + if (filterSelection == kLogical || filterSelection == kBoth) + LoadLogicalDriveList(pListView, &itemIndex); +} + +/* + * Determine the logical volumes available in the system and stuff them into + * the list. + */ +bool +OpenVolumeDialog::LoadLogicalDriveList(CListCtrl* pListView, int* pItemIndex) +{ + DWORD drivesAvailable; + bool isWin9x = IsWin9x(); + int itemIndex = *pItemIndex; + + ASSERT(pListView != nil); + + drivesAvailable = GetLogicalDrives(); + if (drivesAvailable == 0) { + WMSG1("GetLogicalDrives failed, err=0x%08lx\n", drivesAvailable); + return false; + } + WMSG1("GetLogicalDrives returned 0x%08lx\n", drivesAvailable); + + // SetErrorMode(SEM_FAILCRITICALERRORS) + + /* run through the list, from A-Z */ + int i; + for (i = 0; i < kMaxLogicalDrives; i++) { + fVolumeInfo[i].driveType = DRIVE_UNKNOWN; + + if ((drivesAvailable >> i) & 0x01) { + char driveName[] = "_:\\"; + driveName[0] = 'A' + i; + + unsigned int driveType; + const char* driveTypeComment = nil; + BOOL result; + + driveType = fVolumeInfo[i].driveType = GetDriveType(driveName); + switch (driveType) { + case DRIVE_UNKNOWN: + // The drive type cannot be determined. + break; + case DRIVE_NO_ROOT_DIR: + // The root path is invalid. For example, no volume is mounted at the path. + break; + case DRIVE_REMOVABLE: + // The disk can be removed from the drive. + driveTypeComment = "Removable"; + break; + case DRIVE_FIXED: + // The disk cannot be removed from the drive. + driveTypeComment = "Local Disk"; + break; + case DRIVE_REMOTE: + // The drive is a remote (network) drive. + driveTypeComment = "Network"; + break; + case DRIVE_CDROM: + // The drive is a CD-ROM drive. + driveTypeComment = "CD-ROM"; + break; + case DRIVE_RAMDISK: + // The drive is a RAM disk. + break; + default: + WMSG1("UNKNOWN DRIVE TYPE %d\n", driveType); + break; + } + + if (driveType == DRIVE_CDROM && !DiskImgLib::Global::GetHasSPTI()) { + /* use "physical" device via ASPI instead */ + WMSG1("Not including CD-ROM '%s' in logical drive list\n", + driveName); + continue; + } + + char volNameBuf[256]; + char fsNameBuf[64]; + const char* errorComment = nil; + //DWORD fsFlags; + CString entryName, entryRemarks; + + result = ::GetVolumeInformation(driveName, volNameBuf, + sizeof(volNameBuf), NULL, NULL, NULL /*&fsFlags*/, fsNameBuf, + sizeof(fsNameBuf)); + if (result == FALSE) { + DWORD err = GetLastError(); + if (err == ERROR_UNRECOGNIZED_VOLUME) { + // Win2K: media exists but format not recognized + errorComment = "Non-Windows format"; + } else if (err == ERROR_NOT_READY) { + // Win2K: device exists but no media loaded + if (isWin9x) { + WMSG1("Not showing drive '%s': not ready\n", + driveName); + continue; // safer not to show it + } else + errorComment = "Not ready"; + } else if (err == ERROR_PATH_NOT_FOUND /*Win2K*/ || + err == ERROR_INVALID_DATA /*Win98*/) + { + // Win2K/Win98: device letter not in use + WMSG1("GetVolumeInformation '%s': nothing there\n", + driveName); + continue; + } else if (err == ERROR_INVALID_PARAMETER) { + // Win2K: device is already open + //WMSG1("GetVolumeInformation '%s': currently open??\n", + // driveName); + errorComment = "(currently open?)"; + //continue; + } else if (err == ERROR_ACCESS_DENIED) { + // Win2K: disk is open no-read-sharing elsewhere + errorComment = "(already open read-write)"; + } else if (err == ERROR_GEN_FAILURE) { + // Win98: floppy format not recognzied + // --> we don't want to access ProDOS floppies via A: in + // Win98, so we skip it here + WMSG1("GetVolumeInformation '%s': general failure\n", + driveName); + continue; + } else if (err == ERROR_INVALID_FUNCTION) { + // Win2K: CD-ROM with HFS + if (driveType == DRIVE_CDROM) + errorComment = "Non-Windows format"; + else + errorComment = "(invalid disc?)"; + } else { + WMSG2("GetVolumeInformation '%s' failed: %ld\n", + driveName, GetLastError()); + continue; + } + ASSERT(errorComment != nil); + + entryName.Format("(%c:)", 'A' + i); + if (driveTypeComment != nil) + entryRemarks.Format("%s - %s", driveTypeComment, + errorComment); + else + entryRemarks.Format("%s", errorComment); + } else { + entryName.Format("%s (%c:)", volNameBuf, 'A' + i); + if (driveTypeComment != nil) + entryRemarks.Format("%s", driveTypeComment); + else + entryRemarks = ""; + } + + pListView->InsertItem(itemIndex, entryName); + pListView->SetItemText(itemIndex, 1, entryRemarks); + pListView->SetItemData(itemIndex, (DWORD) i + 'A'); +//WMSG1("%%%% added logical %d\n", itemIndex); + itemIndex++; + } else { + WMSG1(" (drive %c not available)\n", i + 'A'); + } + } + + *pItemIndex = itemIndex; + + return true; +} + +/* + * Add a list of physical drives to the list control. + * + * I don't see a clever way to do this in Win2K except to open the first 8 + * or so devices and see what happens. + * + * Win9x isn't much better, though you can be reasonably confident that there + * are at most 4 floppy drives and 4 hard drives. + */ +bool +OpenVolumeDialog::LoadPhysicalDriveList(CListCtrl* pListView, int* pItemIndex) +{ + bool isWin9x = IsWin9x(); + int itemIndex = *pItemIndex; + int i; + + if (isWin9x) { + // fairly arbitrary choices + const int kMaxFloppies = 4; + const int kMaxHardDrives = 8; + + for (i = 0; i < kMaxFloppies; i++) { + CString driveName, remark; + bool result; + + result = HasPhysicalDriveWin9x(i, &remark); + if (result) { + driveName.Format("Floppy disk %d", i); + pListView->InsertItem(itemIndex, driveName); + pListView->SetItemText(itemIndex, 1, remark); + pListView->SetItemData(itemIndex, (DWORD) i); +//WMSG1("%%%% added floppy %d\n", itemIndex); + itemIndex++; + } + } + for (i = 0; i < kMaxHardDrives; i++) { + CString driveName, remark; + bool result; + + result = HasPhysicalDriveWin9x(i + 128, &remark); + if (result) { + driveName.Format("Hard drive %d", i); + pListView->InsertItem(itemIndex, driveName); + pListView->SetItemText(itemIndex, 1, remark); + pListView->SetItemData(itemIndex, (DWORD) i + 128); +//WMSG1("%%%% added HD %d\n", itemIndex); + itemIndex++; + } + } + } else { + for (i = 0; i < kMaxPhysicalDrives; i++) { + CString driveName, remark; + bool result; + + result = HasPhysicalDriveWin2K(i + 128, &remark); + if (result) { + driveName.Format("Physical disk %d", i); + pListView->InsertItem(itemIndex, driveName); + pListView->SetItemText(itemIndex, 1, remark); + pListView->SetItemData(itemIndex, (DWORD) i + 128); // HD volume + itemIndex++; + } + } + } + + if (DiskImgLib::Global::GetHasASPI()) { + DIError dierr; + DiskImgLib::ASPI* pASPI = DiskImgLib::Global::GetASPI(); + ASPIDevice* deviceArray = nil; + int numDevices; + + dierr = pASPI->GetAccessibleDevices( + ASPI::kDevMaskCDROM | ASPI::kDevMaskHardDrive, + &deviceArray, &numDevices); + if (dierr == kDIErrNone) { + WMSG1("Adding %d ASPI CD-ROM devices\n", numDevices); + for (i = 0; i < numDevices; i++) { + CString driveName, remark; + CString addr, vendor, product; + DWORD aspiAddr; + + addr.Format("ASPI %d:%d:%d", + deviceArray[i].GetAdapter(), + deviceArray[i].GetTarget(), + deviceArray[i].GetLun()); + vendor = deviceArray[i].GetVendorID(); + vendor.TrimRight(); + product = deviceArray[i].GetProductID(); + product.TrimRight(); + + driveName.Format("%s %s", vendor, product); + if (deviceArray[i].GetDeviceType() == ASPIDevice::kTypeCDROM) + remark = "CD-ROM"; + else if (deviceArray[i].GetDeviceType() == ASPIDevice::kTypeDASD) + remark = "Direct-access device"; + if (!deviceArray[i].GetDeviceReady()) + remark += " - Not ready"; + + aspiAddr = (DWORD) 0xaa << 24 | + (DWORD) deviceArray[i].GetAdapter() << 16 | + (DWORD) deviceArray[i].GetTarget() << 8 | + (DWORD) deviceArray[i].GetLun(); + //WMSG2("ADDR for '%s' is 0x%08lx\n", + // (const char*) driveName, aspiAddr); + + pListView->InsertItem(itemIndex, driveName); + pListView->SetItemText(itemIndex, 1, remark); + pListView->SetItemData(itemIndex, aspiAddr); + itemIndex++; + } + } + + delete[] deviceArray; + } + + *pItemIndex = itemIndex; + return true; +} + +/* + * Determine whether physical device N exists. + * + * Pass in the Int13 unit number, i.e. 0x00 for the first floppy drive. Win9x + * makes direct access to the hard drive very difficult, so we don't even try. + */ +bool +OpenVolumeDialog::HasPhysicalDriveWin9x(int unit, CString* pRemark) +{ + HANDLE handle = nil; + const int VWIN32_DIOC_DOS_INT13 = 4; + const int CARRY_FLAG = 1; + BOOL result; + typedef struct _DIOC_REGISTERS { + DWORD reg_EBX; + DWORD reg_EDX; + DWORD reg_ECX; + DWORD reg_EAX; + DWORD reg_EDI; + DWORD reg_ESI; + DWORD reg_Flags; + } DIOC_REGISTERS, *PDIOC_REGISTERS; + DIOC_REGISTERS reg = {0}; + DWORD lastError, cb; + unsigned char buf[512]; + + if (unit > 4) + return false; // floppy drives only + + handle = CreateFile("\\\\.\\vwin32", 0, 0, NULL, + OPEN_EXISTING, FILE_FLAG_DELETE_ON_CLOSE, NULL); + if (handle == INVALID_HANDLE_VALUE) { + WMSG1(" Unable to open vwin32: %ld\n", ::GetLastError()); + return false; + } + +#if 0 // didn't do what I wanted + reg.reg_EAX = MAKEWORD(0, 0x00); // func 0x00 == reset controller + reg.reg_EDX = MAKEWORD(unit, 0); // specify driver + result = DeviceIoControl(handle, VWIN32_DIOC_DOS_INT13, ®, + sizeof(reg), ®, sizeof(reg), &cb, 0); + WMSG3(" DriveReset(drive=0x%02x) result=%d carry=%d\n", + unit, result, reg.reg_Flags & CARRY_FLAG); +#endif + + reg.reg_EAX = MAKEWORD(1, 0x02); // read 1 sector + reg.reg_EBX = (DWORD) buf; + reg.reg_ECX = MAKEWORD(1, 0); // sector 0 (+1), cylinder 0 + reg.reg_EDX = MAKEWORD(unit, 0); // head + + result = DeviceIoControl(handle, VWIN32_DIOC_DOS_INT13, ®, + sizeof(reg), ®, sizeof(reg), &cb, 0); + lastError = GetLastError(); + ::CloseHandle(handle); + + if (result == 0 || (reg.reg_Flags & CARRY_FLAG)) { + int ah = HIBYTE(reg.reg_EAX); + WMSG4(" DevIoCtrl(unit=%02xh) failed: result=%d lastErr=%d Flags=0x%08lx\n", + unit, result, lastError, reg.reg_Flags); + WMSG3(" AH=%d (EAX=0x%08lx) byte=0x%02x\n", ah, reg.reg_EAX, buf[0]); + if (ah != 1) { + // failure code 1 means "invalid parameter", drive doesn't exist + // mine returns 128, "timeout", when no disk is in the drive + *pRemark = "Not ready"; + return true; + } else + return false; + } + + *pRemark = "Removable"; + + return true; +} + +/* + * Determine whether physical device N exists. + * + * Pass in the Int13 unit number, i.e. 0x80 for the first hard drive. This + * should not be called with units for floppy drives (e.g. 0x00). + */ +bool +OpenVolumeDialog::HasPhysicalDriveWin2K(int unit, CString* pRemark) +{ + HANDLE hDevice; // handle to the drive to be examined + DISK_GEOMETRY dg; // disk drive geometry structure + DISK_GEOMETRY_EX dge; // extended geometry request buffer + BOOL result; // results flag + DWORD junk; // discard results + LONGLONG diskSize; // size of the drive, in bytes + CString fileName; + DWORD err; + + /* + * See if the drive is there. + */ + ASSERT(unit >= 128 && unit < 160); // arbitrary max + fileName.Format("\\\\.\\PhysicalDrive%d", unit - 128); + + hDevice = ::CreateFile((const char*) fileName, // drive to open + 0, // no access to the drive + FILE_SHARE_READ | FILE_SHARE_WRITE, // share mode + NULL, // default security attributes + OPEN_EXISTING, // disposition + 0, // file attributes + NULL); // do not copy file attributes + + if (hDevice == INVALID_HANDLE_VALUE) // cannot open the drive + return false; + + /* + * Try to get the drive geometry. First try with the fancy WinXP call, + * then fall back to the Win2K call if it doesn't exist. + */ + result = ::DeviceIoControl(hDevice, + IOCTL_DISK_GET_DRIVE_GEOMETRY_EX, + NULL, 0, // input buffer + &dge, sizeof(dge), // output buffer + &junk, // # bytes returned + (LPOVERLAPPED) NULL); // synchronous I/O + if (result) { + diskSize = dge.DiskSize.QuadPart; + WMSG1(" EX results for device %02xh\n", unit); + WMSG2(" Disk size = %I64d (bytes) = %I64d (MB)\n", + diskSize, diskSize / (1024*1024)); + if (diskSize > 1024*1024*1024) + pRemark->Format("Size is %.2fGB", + (double) diskSize / (1024.0 * 1024.0 * 1024.0)); + else + pRemark->Format("Size is %.2fMB", + (double) diskSize / (1024.0 * 1024.0)); + } else { + // Win2K shows ERROR_INVALID_FUNCTION or ERROR_NOT_SUPPORTED + WMSG1("IOCTL_DISK_GET_DRIVE_GEOMETRY_EX failed, error was %ld\n", + GetLastError()); + result = ::DeviceIoControl(hDevice, // device to be queried + IOCTL_DISK_GET_DRIVE_GEOMETRY, // operation to perform + NULL, 0, // no input buffer + &dg, sizeof(dg), // output buffer + &junk, // # bytes returned + (LPOVERLAPPED) NULL); // synchronous I/O + + if (result) { + WMSG1(" Results for device %02xh\n", unit); + WMSG1(" Cylinders = %I64d\n", dg.Cylinders); + WMSG1(" Tracks per cylinder = %ld\n", (ULONG) dg.TracksPerCylinder); + WMSG1(" Sectors per track = %ld\n", (ULONG) dg.SectorsPerTrack); + WMSG1(" Bytes per sector = %ld\n", (ULONG) dg.BytesPerSector); + + diskSize = dg.Cylinders.QuadPart * (ULONG)dg.TracksPerCylinder * + (ULONG)dg.SectorsPerTrack * (ULONG)dg.BytesPerSector; + WMSG2("Disk size = %I64d (bytes) = %I64d (MB)\n", diskSize, + diskSize / (1024 * 1024)); + if (diskSize > 1024*1024*1024) + pRemark->Format("Size is %.2fGB", + (double) diskSize / (1024.0 * 1024.0 * 1024.0)); + else + pRemark->Format("Size is %.2fMB", + (double) diskSize / (1024.0 * 1024.0)); + } else { + err = GetLastError(); + } + } + + ::CloseHandle(hDevice); + + if (!result) { + WMSG1("DeviceIoControl(IOCTL_DISK_GET_DRIVE_GEOMETRY) failed (err=%ld)\n", + err); + *pRemark = "Not ready"; + } + + return true; +} + + +/* + * Something changed in the list. Update the "OK" button. + */ +void +OpenVolumeDialog::OnListChange(NMHDR*, LRESULT* pResult) +{ + CListCtrl* pListView = (CListCtrl*) GetDlgItem(IDC_VOLUME_LIST); + CButton* pButton = (CButton*) GetDlgItem(IDOK); + pButton->EnableWindow(pListView->GetSelectedCount() != 0); + //WMSG1("ENABLE %d\n", pListView->GetSelectedCount() != 0); + + *pResult = 0; +} + +/* + * Double click. + */ +void +OpenVolumeDialog::OnListDblClick(NMHDR* pNotifyStruct, LRESULT* pResult) +{ + CListCtrl* pListView = (CListCtrl*) GetDlgItem(IDC_VOLUME_LIST); + CButton* pButton = (CButton*) GetDlgItem(IDOK); + + if (pListView->GetSelectedCount() != 0) { + pButton->EnableWindow(); + OnOK(); + } + + *pResult = 0; +} + +/* + * The volume filter drop-down box has changed. + */ +void +OpenVolumeDialog::OnVolumeFilterSelChange(void) +{ + CComboBox* pCombo = (CComboBox*) GetDlgItem(IDC_VOLUME_FILTER); + ASSERT(pCombo != nil); + WMSG1("+++ SELECTION IS NOW %d\n", pCombo->GetCurSel()); + LoadDriveList(); +} + +/* + * Verify their selection. + */ +void +OpenVolumeDialog::OnOK(void) +{ + /* + * Figure out the (zero-based) drive letter. + */ + CListCtrl* pListView = (CListCtrl*) GetDlgItem(IDC_VOLUME_LIST); + ASSERT(pListView != nil); + + if (pListView->GetSelectedCount() != 1) { + CString msg, failed; + failed.LoadString(IDS_FAILED); + msg.LoadString(IDS_VOLUME_SELECT_ONE); + MessageBox(msg, failed, MB_OK); + return; + } + + POSITION posn; + posn = pListView->GetFirstSelectedItemPosition(); + if (posn == nil) { + ASSERT(false); + return; + } + int num = pListView->GetNextSelectedItem(posn); + DWORD driveID = pListView->GetItemData(num); + UINT formatID = 0; + + if (HIBYTE(HIWORD(driveID)) == 0xaa) { + fChosenDrive.Format("%s%d:%d:%d\\", + DiskImgLib::kASPIDev, + LOBYTE(HIWORD(driveID)), + HIBYTE(LOWORD(driveID)), + LOBYTE(LOWORD(driveID))); + //ForceReadOnly(true); + } else if (driveID >= 'A' && driveID <= 'Z') { + /* + * Do we want to let them do this? We show some logical drives + * that we don't want them to actually use. + */ + switch (fVolumeInfo[driveID-'A'].driveType) { + case DRIVE_REMOVABLE: + case DRIVE_FIXED: + break; // allow + case DRIVE_CDROM: //formatID = IDS_VOLUME_NO_CDROM; + ForceReadOnly(true); + break; + case DRIVE_REMOTE: formatID = IDS_VOLUME_NO_REMOTE; + break; + case DRIVE_RAMDISK: formatID = IDS_VOLUME_NO_RAMDISK; + break; + case DRIVE_UNKNOWN: + case DRIVE_NO_ROOT_DIR: + default: formatID = IDS_VOLUME_NO_GENERIC; + break; + } + + fChosenDrive.Format("%c:\\", driveID); + } else if ((driveID >= 0 && driveID < 4) || + (driveID >= 0x80 && driveID < 0x88)) + { + fChosenDrive.Format("%02x:\\", driveID); + } else { + ASSERT(false); + return; + } + + if (formatID != 0) { + CString msg, notAllowed; + + notAllowed.LoadString(IDS_NOT_ALLOWED); + msg.LoadString(formatID); + MessageBox(msg, notAllowed, MB_OK); + } else { + Preferences* pPreferences = GET_PREFERENCES_WR(); + CComboBox* pCombo = (CComboBox*) GetDlgItem(IDC_VOLUME_FILTER); + pPreferences->SetPrefLong(kPrVolumeFilter, pCombo->GetCurSel()); + WMSG1("SETTING PREF TO %ld\n", pCombo->GetCurSel()); + + CDialog::OnOK(); + } +} + + +/* + * User pressed the "Help" button. + */ +void +OpenVolumeDialog::OnHelp(void) +{ + WinHelp(HELP_TOPIC_OPEN_VOLUME, HELP_CONTEXT); +} + + +/* + * Set the state of the "read only" checkbox in the dialog. + */ +void +OpenVolumeDialog::ForceReadOnly(bool readOnly) const +{ + CButton* pButton = (CButton*) GetDlgItem(IDC_OPENVOL_READONLY); + ASSERT(pButton != nil); + + if (readOnly) + pButton->SetCheck(BST_CHECKED); + else + pButton->SetCheck(BST_UNCHECKED); + WMSG1("FORCED READ ONLY %d\n", readOnly); +} diff --git a/app/OpenVolumeDialog.h b/app/OpenVolumeDialog.h new file mode 100644 index 0000000..169dcc8 --- /dev/null +++ b/app/OpenVolumeDialog.h @@ -0,0 +1,70 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Class definition for Open Volume dialog. + */ +#ifndef __OPEN_VOLUME_DIALOG__ +#define __OPEN_VOLUME_DIALOG__ + +#include +#include "resource.h" + +/* + * A dialog with a list control that we populate with the names of the + * volumes in the system. + */ +class OpenVolumeDialog : public CDialog { +public: + OpenVolumeDialog(CWnd* pParentWnd = NULL) : + CDialog(IDD_OPENVOLUMEDLG, pParentWnd), + fChosenDrive(""), + fAllowROChange(true) + { + Preferences* pPreferences = GET_PREFERENCES_WR(); + fReadOnly = pPreferences->GetPrefBool(kPrOpenVolumeRO); + } + + // Result: drive to open (e.g. "A:\" or "80:\") + CString fChosenDrive; + + // Did the user check "read only"? (sets default and holds return val) + BOOL fReadOnly; + // Set before calling DoModal to disable "read only" checkbox + bool fAllowROChange; + +protected: + virtual BOOL OnInitDialog(void); + virtual void DoDataExchange(CDataExchange* pDX); + virtual void OnOK(void); + + afx_msg void OnHelp(void); + afx_msg void OnListChange(NMHDR* pNotifyStruct, LRESULT* pResult); + afx_msg void OnListDblClick(NMHDR* pNotifyStruct, LRESULT* pResult); + afx_msg void OnVolumeFilterSelChange(void); + + // 0 is default; numbers must match up with pop-up menu order + // order also matters for range test in OnInitDialog + enum { kBoth=0, kLogical=1, kPhysical=2 }; + // common constants + enum { kMaxLogicalDrives = 26, kMaxPhysicalDrives = 8 }; + + void LoadDriveList(void); + bool LoadLogicalDriveList(CListCtrl* pListView, int* pItemIndex); + bool LoadPhysicalDriveList(CListCtrl* pListView, int* pItemIndex); + bool HasPhysicalDriveWin9x(int unit, CString* pRemark); + bool HasPhysicalDriveWin2K(int unit, CString* pRemark); + + void ForceReadOnly(bool readOnly) const; + + struct { + unsigned int driveType; + } fVolumeInfo[kMaxLogicalDrives]; + + + DECLARE_MESSAGE_MAP() +}; + +#endif /*__OPEN_VOLUME_DIALOG__*/ \ No newline at end of file diff --git a/app/PasteSpecialDialog.cpp b/app/PasteSpecialDialog.cpp new file mode 100644 index 0000000..a101ab1 --- /dev/null +++ b/app/PasteSpecialDialog.cpp @@ -0,0 +1,54 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Support for PasteSpecialDialog. + */ +#include "StdAfx.h" +#include "PasteSpecialDialog.h" + +#if 0 +BEGIN_MESSAGE_MAP(PasteSpecialDialog, CDialog) +END_MESSAGE_MAP() + +BOOL +PasteSpecialDialog::OnInitDialog(void) +{ + CString countStr; + CWnd* pWnd; + + countStr.Format(IDS_PASTE_SPECIAL_COUNT, 3); + pWnd = GetDlgItem(IDC_PASTE_SPECIAL_COUNT); + pWnd->SetWindowText(countStr); + + return CDialog::OnInitDialog(); +} +#endif + +/* + * Initialize radio control with value from fPasteHow. + */ +void +PasteSpecialDialog::DoDataExchange(CDataExchange* pDX) +{ + if (!pDX->m_bSaveAndValidate) { + UINT ctrlId; + + if (fPasteHow == kPastePaths) + ctrlId = IDC_PASTE_SPECIAL_PATHS; + else + ctrlId = IDC_PASTE_SPECIAL_NOPATHS; + + CButton* pButton = (CButton*) GetDlgItem(ctrlId); + pButton->SetCheck(BST_CHECKED); + } else { + CButton* pButton = (CButton*) GetDlgItem(IDC_PASTE_SPECIAL_PATHS); + + if (pButton->GetCheck() == BST_CHECKED) + fPasteHow = kPastePaths; + else + fPasteHow = kPasteNoPaths; + } +} diff --git a/app/PasteSpecialDialog.h b/app/PasteSpecialDialog.h new file mode 100644 index 0000000..08c9a12 --- /dev/null +++ b/app/PasteSpecialDialog.h @@ -0,0 +1,41 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Paste Special dialog. + */ +#ifndef __PASTESPECIALDIALOG__ +#define __PASTESPECIALDIALOG__ + +#include "resource.h" + +/* + * Simple dialog with a radio button. + */ +class PasteSpecialDialog : public CDialog { +public: + PasteSpecialDialog(CWnd* pParentWnd = nil) : + CDialog(IDD_PASTE_SPECIAL, pParentWnd), + fPasteHow(kPastePaths) + {} + virtual ~PasteSpecialDialog() {} + + /* right now this is boolean, but that may change */ + /* (e.g. "paste clipboard contents into new text file") */ + enum { + kPasteUnknown = 0, + kPastePaths, + kPasteNoPaths, + }; + int fPasteHow; + +protected: + //virtual BOOL OnInitDialog(void); + void DoDataExchange(CDataExchange* pDX); + + //DECLARE_MESSAGE_MAP() +}; + +#endif /*__PASTESPECIALDIALOG__*/ \ No newline at end of file diff --git a/app/Preferences.cpp b/app/Preferences.cpp new file mode 100644 index 0000000..42e958b --- /dev/null +++ b/app/Preferences.cpp @@ -0,0 +1,618 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Save and restore preferences from the config file. + */ +#include "stdafx.h" +#include "Preferences.h" +#include "NufxArchive.h" +#include "MyApp.h" +#include "../util/UtilLib.h" + +static const char* kDefaultTempPath = "."; + +/* registry section for columns */ +static const char* kColumnSect = _T("columns"); +/* registry section for file add options */ +static const char* kAddSect = _T("add"); +/* registry section for extraction options */ +static const char* kExtractSect = _T("extract"); +/* registry section for view options */ +static const char* kViewSect = _T("view"); +/* registry section for logical/physical volume operations */ +static const char* kVolumeSect = _T("volume"); +/* registry section for file-to-disk options */ +//static const char* kConvDiskSect = _T("conv-disk"); +/* registry section for disk-to-file options */ +static const char* kConvFileSect = _T("conv-file"); +/* registry section for folders */ +static const char* kFolderSect = _T("folders"); +/* registry section for preferences on property pages */ +static const char* kPrefsSect = _T("prefs"); +/* registry section for miscellaneous settings */ +static const char* kMiscSect = _T("misc"); + + +/* + * Map PrefNum to type and registry string. + * + * To make life easier, we require that the PrefNum (first entry) match the + * offset in the table. That way instead of searching for a match we can just + * index into the table. + */ +const Preferences::PrefMap Preferences::fPrefMaps[kPrefNumLastEntry] = { + /**/ { kPrefNumUnknown, kPTNone, nil, nil }, + + { kPrAddIncludeSubFolders, kBool, kAddSect, _T("include-sub-folders") }, + { kPrAddStripFolderNames, kBool, kAddSect, _T("strip-folder-names") }, + { kPrAddOverwriteExisting, kBool, kAddSect, _T("overwrite-existing") }, + { kPrAddTypePreservation, kLong, kAddSect, _T("type-preservation") }, + { kPrAddConvEOL, kLong, kAddSect, _T("conv-eol") }, + +// { kPrExtractPath, kString, kExtractSect, _T("path") }, + { kPrExtractConvEOL, kLong, kExtractSect, _T("conv-eol") }, + { kPrExtractConvHighASCII, kBool, kExtractSect, _T("conv-high-ascii") }, + { kPrExtractIncludeData, kBool, kExtractSect, _T("include-data") }, + { kPrExtractIncludeRsrc, kBool, kExtractSect, _T("include-rsrc") }, + { kPrExtractIncludeDisk, kBool, kExtractSect, _T("include-disk") }, + { kPrExtractEnableReformat, kBool, kExtractSect, _T("enable-reformat") }, + { kPrExtractDiskTo2MG, kBool, kExtractSect, _T("disk-to-2mg") }, + { kPrExtractAddTypePreservation, kBool, kExtractSect, _T("add-type-preservation") }, + { kPrExtractAddExtension, kBool, kExtractSect, _T("add-extension") }, + { kPrExtractStripFolderNames, kBool, kExtractSect, _T("strip-folder-names") }, + { kPrExtractOverwriteExisting, kBool, kExtractSect, _T("overwrite-existing") }, + +// { kPrViewIncludeDataForks, kBool, kViewSect, _T("include-data-forks") }, +// { kPrViewIncludeRsrcForks, kBool, kViewSect, _T("include-rsrc-forks") }, +// { kPrViewIncludeDiskImages, kBool, kViewSect, _T("include-disk-images") }, +// { kPrViewIncludeComments, kBool, kViewSect, _T("include-comments") }, + + { kPrConvFileEmptyFolders, kBool, kConvFileSect, _T("preserve-empty-folders") }, + + { kPrOpenArchiveFolder, kString, kFolderSect, _T("open-archive") }, + { kPrConvertArchiveFolder, kString, kFolderSect, _T("convert-archive") }, + { kPrAddFileFolder, kString, kFolderSect, _T("add-file") }, + { kPrExtractFileFolder, kString, kFolderSect, _T("extract-file") }, + + { kPrVolumeFilter, kLong, kVolumeSect, _T("open-filter") }, + //{ kPrVolumeReadOnly, kBool, kVolumeSect, _T("read-only") }, + + { kPrCassetteAlgorithm, kLong, kVolumeSect, _T("cassette-algorithm") }, + { kPrOpenWAVFolder, kString, kFolderSect, _T("open-wav") }, + + { kPrMimicShrinkIt, kBool, kPrefsSect, _T("mimic-shrinkit") }, + { kPrBadMacSHK, kBool, kPrefsSect, _T("bad-mac-shk") }, + { kPrReduceSHKErrorChecks, kBool, kPrefsSect, _T("reduce-shk-error-checks") }, + { kPrCoerceDOSFilenames, kBool, kPrefsSect, _T("coerce-dos-filenames") }, + { kPrSpacesToUnder, kBool, kPrefsSect, _T("spaces-to-under") }, + { kPrPasteJunkPaths, kBool, kPrefsSect, _T("paste-junk-paths") }, + { kPrBeepOnSuccess, kBool, kPrefsSect, _T("beep-on-success") }, + + { kPrQueryImageFormat, kBool, kPrefsSect, _T("query-image-format") }, + { kPrOpenVolumeRO, kBool, kPrefsSect, _T("open-volume-ro") }, + { kPrOpenVolumePhys0, kBool, kPrefsSect, _T("open-volume-phys0") }, + { kPrProDOSAllowLower, kBool, kPrefsSect, _T("prodos-allow-lower") }, + { kPrProDOSUseSparse, kBool, kPrefsSect, _T("prodos-use-sparse") }, + + { kPrCompressionType, kLong, kPrefsSect, _T("compression-type") }, + + { kPrMaxViewFileSize, kLong, kPrefsSect, _T("max-view-file-size") }, + { kPrNoWrapText, kBool, kPrefsSect, _T("no-wrap-text") }, + + { kPrHighlightHexDump, kBool, kPrefsSect, _T("highlight-hex-dump") }, + { kPrHighlightBASIC, kBool, kPrefsSect, _T("highlight-basic") }, + { kPrConvHiResBlackWhite, kBool, kPrefsSect, _T("conv-hi-res-black-white") }, + { kPrConvDHRAlgorithm, kLong, kPrefsSect, _T("dhr-algorithm") }, + { kPrRelaxGfxTypeCheck, kBool, kPrefsSect, _T("relax-gfx-type-check") }, + { kPrDisasmOneByteBrkCop, kBool, kPrefsSect, _T("disasm-onebytebrkcop") }, + //{ kPrEOLConvRaw, kBool, kPrefsSect, _T("eol-conv-raw") }, + { kPrConvTextEOL_HA, kBool, kPrefsSect, _T("conv-eol-ha") }, + { kPrConvPascalText, kBool, kPrefsSect, _T("conv-pascal-text") }, + { kPrConvPascalCode, kBool, kPrefsSect, _T("conv-pascal-code") }, + { kPrConvCPMText, kBool, kPrefsSect, _T("conv-cpm-text") }, + { kPrConvApplesoft, kBool, kPrefsSect, _T("conv-applesoft") }, + { kPrConvInteger, kBool, kPrefsSect, _T("conv-integer") }, + { kPrConvGWP, kBool, kPrefsSect, _T("conv-gwp") }, + { kPrConvText8, kBool, kPrefsSect, _T("conv-text8") }, + { kPrConvAWP, kBool, kPrefsSect, _T("conv-awp") }, + { kPrConvADB, kBool, kPrefsSect, _T("conv-adb") }, + { kPrConvASP, kBool, kPrefsSect, _T("conv-asp") }, + { kPrConvSCAssem, kBool, kPrefsSect, _T("conv-scassem") }, + { kPrConvDisasm, kBool, kPrefsSect, _T("conv-disasm") }, + { kPrConvHiRes, kBool, kPrefsSect, _T("conv-hi-res") }, + { kPrConvDHR, kBool, kPrefsSect, _T("conv-dhr") }, + { kPrConvSHR, kBool, kPrefsSect, _T("conv-shr") }, + { kPrConvPrintShop, kBool, kPrefsSect, _T("conv-print-shop") }, + { kPrConvMacPaint, kBool, kPrefsSect, _T("conv-mac-paint") }, + { kPrConvProDOSFolder, kBool, kPrefsSect, _T("conv-prodos-folder") }, + { kPrConvResources, kBool, kPrefsSect, _T("conv-resources") }, + + { kPrTempPath, kString, kPrefsSect, _T("temp-path") }, + { kPrExtViewerExts, kString, kPrefsSect, _T("extviewer-exts") }, + + { kPrLastOpenFilterIndex, kLong, kMiscSect, _T("open-filter-index") }, + + /**/ { kPrefNumLastRegistry, kPTNone, nil, nil }, + + { kPrViewTextTypeFace, kString, nil, nil }, + { kPrViewTextPointSize, kLong, nil, nil }, + { kPrFileViewerWidth, kLong, nil, nil }, + { kPrFileViewerHeight, kLong, nil, nil }, + { kPrDiskImageCreateFormat, kLong, nil, nil }, +}; + +/* + * Constructor. There should be only one Preferences object in the + * application, so this should only be run once. + */ +Preferences::Preferences(void) +{ + WMSG0("Initializing Preferences\n"); + + ScanPrefMaps(); // sanity-check the table + memset(fValues, 0, sizeof(fValues)); + + SetPrefBool(kPrAddIncludeSubFolders, true); + SetPrefBool(kPrAddStripFolderNames, false); + SetPrefBool(kPrAddOverwriteExisting, false); + SetPrefLong(kPrAddTypePreservation, 1); // kPreserveTypes + SetPrefLong(kPrAddConvEOL, 1); // kConvEOLType + + InitFolders(); // set default add/extract folders; overriden by reg + SetPrefLong(kPrExtractConvEOL, 0); // kConvEOLNone + SetPrefBool(kPrExtractConvHighASCII, true); + SetPrefBool(kPrExtractIncludeData, true); + SetPrefBool(kPrExtractIncludeRsrc, false); + SetPrefBool(kPrExtractIncludeDisk, true); + SetPrefBool(kPrExtractEnableReformat, false); + SetPrefBool(kPrExtractDiskTo2MG, false); + SetPrefBool(kPrExtractAddTypePreservation, true); + SetPrefBool(kPrExtractAddExtension, false); + SetPrefBool(kPrExtractStripFolderNames, false); + SetPrefBool(kPrExtractOverwriteExisting, false); + +// SetPrefBool(kPrViewIncludeDataForks, true); +// SetPrefBool(kPrViewIncludeRsrcForks, false); +// SetPrefBool(kPrViewIncludeDiskImages, false); +// SetPrefBool(kPrViewIncludeComments, false); + + SetPrefBool(kPrConvFileEmptyFolders, true); + + // string kPrOpenArchiveFolder + // string kPrAddFileFolder + // string kPrExtractFileFolder + + SetPrefLong(kPrVolumeFilter, 0); + //SetPrefBool(kPrVolumeReadOnly, true); + + SetPrefLong(kPrCassetteAlgorithm, 0); + // string kPrOpenWAVFolder + + SetPrefBool(kPrMimicShrinkIt, false); + SetPrefBool(kPrBadMacSHK, false); + SetPrefBool(kPrReduceSHKErrorChecks, false); + SetPrefBool(kPrCoerceDOSFilenames, false); + SetPrefBool(kPrSpacesToUnder, false); + SetPrefBool(kPrPasteJunkPaths, true); + SetPrefBool(kPrBeepOnSuccess, true); + + SetPrefBool(kPrQueryImageFormat, false); + SetPrefBool(kPrOpenVolumeRO, true); + SetPrefBool(kPrOpenVolumePhys0, false); + SetPrefBool(kPrProDOSAllowLower, false); + SetPrefBool(kPrProDOSUseSparse, true); + + SetPrefLong(kPrCompressionType, DefaultCompressionType()); + + SetPrefLong(kPrMaxViewFileSize, 1024*1024); // 1MB + SetPrefBool(kPrNoWrapText, false); + + SetPrefBool(kPrHighlightHexDump, false); + SetPrefBool(kPrHighlightBASIC, false); + SetPrefBool(kPrConvHiResBlackWhite, false); + SetPrefLong(kPrConvDHRAlgorithm, 1); // latched + SetPrefBool(kPrRelaxGfxTypeCheck, true); + SetPrefBool(kPrDisasmOneByteBrkCop, false); + //SetPrefBool(kPrEOLConvRaw, true); + SetPrefBool(kPrConvTextEOL_HA, true); + SetPrefBool(kPrConvPascalText, true); + SetPrefBool(kPrConvPascalCode, true); + SetPrefBool(kPrConvCPMText, true); + SetPrefBool(kPrConvApplesoft, true); + SetPrefBool(kPrConvInteger, true); + SetPrefBool(kPrConvGWP, true); + SetPrefBool(kPrConvText8, true); + SetPrefBool(kPrConvAWP, true); + SetPrefBool(kPrConvADB, true); + SetPrefBool(kPrConvASP, true); + SetPrefBool(kPrConvSCAssem, true); + SetPrefBool(kPrConvDisasm, true); + SetPrefBool(kPrConvHiRes, true); + SetPrefBool(kPrConvDHR, true); + SetPrefBool(kPrConvSHR, true); + SetPrefBool(kPrConvPrintShop, true); + SetPrefBool(kPrConvMacPaint, true); + SetPrefBool(kPrConvProDOSFolder, true); + SetPrefBool(kPrConvResources, true); + + InitTempPath(); // set default for kPrTempPath + SetPrefString(kPrExtViewerExts, "gif; jpg; jpeg"); + + SetPrefLong(kPrLastOpenFilterIndex, 0); + + SetPrefString(kPrViewTextTypeFace, "Courier New"); + SetPrefLong(kPrViewTextPointSize, 10); + long width = 680; /* exact width for 80-column text */ + long height = 510; /* exact height for file viewer to show IIgs graphic */ + if (GetSystemMetrics(SM_CXSCREEN) < width) + width = GetSystemMetrics(SM_CXSCREEN); + if (GetSystemMetrics(SM_CYSCREEN) < height) + height = GetSystemMetrics(SM_CYSCREEN); // may overlap system bar + //width = 640; height = 480; + SetPrefLong(kPrFileViewerWidth, width); + SetPrefLong(kPrFileViewerHeight, height); + SetPrefLong(kPrDiskImageCreateFormat, -1); +} + + +/* + * ========================================================================== + * ColumnLayout + * ========================================================================== + */ + +/* + * Restore column widths. + */ +void +ColumnLayout::LoadFromRegistry(const char* section) +{ + char numBuf[8]; + int i; + + for (i = 0; i < kNumVisibleColumns; i++) { + sprintf(numBuf, "%d", i); + + fColumnWidth[i] = gMyApp.GetProfileInt(section, numBuf, + fColumnWidth[i]); + fColumnWidth[i] = gMyApp.GetProfileInt(section, numBuf, + fColumnWidth[i]); + } + fSortColumn = gMyApp.GetProfileInt(section, _T("sort-column"), fSortColumn); + fAscending = (gMyApp.GetProfileInt(section, _T("ascending"), fAscending) != 0); +} + +/* + * Store column widths. + */ +void +ColumnLayout::SaveToRegistry(const char* section) +{ + char numBuf[8]; + int i; + + for (i = 0; i < kNumVisibleColumns; i++) { + sprintf(numBuf, "%d", i); + + gMyApp.WriteProfileInt(section, numBuf, fColumnWidth[i]); + } + gMyApp.WriteProfileInt(section, _T("sort-column"), fSortColumn); + gMyApp.WriteProfileInt(section, _T("ascending"), fAscending); +} + + +/* + * ========================================================================== + * Preferences + * ========================================================================== + */ + +/* + * Get a default value for the temp path. + */ +void +Preferences::InitTempPath(void) +{ + char buf[MAX_PATH]; + DWORD len; + CString tempPath; + + len = ::GetTempPath(sizeof(buf), buf); + if (len == 0) { + DWORD err = ::GetLastError(); + WMSG1("GetTempPath failed, err=%d\n", err); + tempPath = kDefaultTempPath; + } else if (len >= sizeof(buf)) { + /* sheesh! */ + WMSG1("GetTempPath wants a %d-byte buffer\n", len); + tempPath = kDefaultTempPath; + } else { + tempPath = buf; + } + + PathName path(tempPath); + WMSG1("Temp path is '%s'\n", tempPath); + path.SFNToLFN(); + tempPath = path.GetPathName(); + + WMSG1("Temp path (long form) is '%s'\n", tempPath); + + SetPrefString(kPrTempPath, tempPath); + +// ::GetFullPathName(fTempPath, sizeof(buf), buf, &foo); +// ::SetCurrentDirectory(buf); +// ::GetCurrentDirectory(sizeof(buf2), buf2); +} + +/* + * Set default values for the various folders. + */ +void +Preferences::InitFolders(void) +{ + CString path; + + if (GetMyDocuments(&path)) { + SetPrefString(kPrOpenArchiveFolder, path); + SetPrefString(kPrConvertArchiveFolder, path); + SetPrefString(kPrAddFileFolder, path); + SetPrefString(kPrExtractFileFolder, path); + SetPrefString(kPrOpenWAVFolder, path); + } else { + char buf[MAX_PATH]; + ::GetCurrentDirectory(sizeof(buf), buf); + SetPrefString(kPrOpenArchiveFolder, buf); + SetPrefString(kPrConvertArchiveFolder, buf); + SetPrefString(kPrAddFileFolder, buf); + SetPrefString(kPrExtractFileFolder, buf); + SetPrefString(kPrOpenWAVFolder, buf); + } + + WMSG1("Default folder is '%s'\n", GetPrefString(kPrExtractFileFolder)); +} + +/* + * Get the path to the "My Documents" folder. + */ +bool +Preferences::GetMyDocuments(CString* pPath) +{ + LPITEMIDLIST pidl = nil; + LPMALLOC lpMalloc = nil; + HRESULT hr; + bool result = false; + + hr = ::SHGetMalloc(&lpMalloc); + if (FAILED(hr)) + return nil; + + hr = SHGetSpecialFolderLocation(nil, CSIDL_PERSONAL, &pidl); + if (FAILED(hr)) { + WMSG0("WARNING: unable to get CSIDL_PERSONAL\n"); + goto bail; + } + + result = (Pidl::GetPath(pidl, pPath) != FALSE); + if (!result) { + WMSG0("WARNING: unable to convert CSIDL_PERSONAL to path\n"); + /* fall through with "result" */ + } + +bail: + lpMalloc->Free(pidl); + lpMalloc->Release(); + return result; +} + +/* + * Determine the type of compression to use as a default, based on what this + * version of NufxLib supports. + * + * Note this happens *before* the AppInit call, so we should restrict this to + * things that are version-safe for all of NufxLib v2.x. + */ +int +Preferences::DefaultCompressionType(void) +{ + if (NufxArchive::IsCompressionSupported(kNuThreadFormatLZW2)) + return kNuThreadFormatLZW2; + else + return kNuThreadFormatUncompressed; +} + +/* + * Preference getters and setters. + */ +bool +Preferences::GetPrefBool(PrefNum num) const +{ + if (!ValidateEntry(num, kBool)) + return false; + //return (bool) (fValues[num]); + return (bool) ((long) (fValues[num]) != 0); +} +void +Preferences::SetPrefBool(PrefNum num, bool val) +{ + if (!ValidateEntry(num, kBool)) + return; + fValues[num] = (void*) val; +} +long +Preferences::GetPrefLong(PrefNum num) const +{ + if (!ValidateEntry(num, kLong)) + return -1; + return (long) fValues[num]; +} +void +Preferences::SetPrefLong(PrefNum num, long val) +{ + if (!ValidateEntry(num, kLong)) + return; + fValues[num] = (void*) val; +} +const char* +Preferences::GetPrefString(PrefNum num) const +{ + if (!ValidateEntry(num, kString)) + return nil; + return (const char*) fValues[num]; +} +void +Preferences::SetPrefString(PrefNum num, const char* str) +{ + if (!ValidateEntry(num, kString)) + return; + free(fValues[num]); + if (str == nil) + fValues[num] = nil; + else { + fValues[num] = new char[strlen(str) +1]; + if (fValues[num] != nil) + strcpy((char*)fValues[num], str); + } +} + +/* + * Free storage for any string entries. + */ +void +Preferences::FreeStringValues(void) +{ + int i; + + for (i = 0; i < kPrefNumLastEntry; i++) { + if (fPrefMaps[i].type == kString) { + delete[] fValues[i]; + } + } +} + + +/* + * Do a quick scan of the PrefMaps to identify duplicate, misplaced, and + * missing entries. + */ +void +Preferences::ScanPrefMaps(void) +{ + int i, j; + + /* scan PrefNum */ + for (i = 0; i < kPrefNumLastEntry; i++) { + if (fPrefMaps[i].num != i) { + WMSG2("HEY: PrefMaps[%d] has num=%d\n", i, fPrefMaps[i].num); + ASSERT(false); + break; + } + } + + /* look for duplicate strings */ + for (i = 0; i < kPrefNumLastEntry; i++) { + for (j = i+1; j < kPrefNumLastEntry; j++) { + if (fPrefMaps[i].registryKey == nil || + fPrefMaps[j].registryKey == nil) + { + continue; + } + if (strcasecmp(fPrefMaps[i].registryKey, + fPrefMaps[j].registryKey) == 0 && + strcasecmp(fPrefMaps[i].registrySection, + fPrefMaps[j].registrySection) == 0) + { + WMSG4("HEY: PrefMaps[%d] and [%d] both have '%s'/'%s'\n", + i, j, fPrefMaps[i].registrySection, + fPrefMaps[i].registryKey); + ASSERT(false); + break; + } + } + } +} + +/* + * Load preferences from the registry. + */ +int +Preferences::LoadFromRegistry(void) +{ + CString sval; + bool bval; + long lval; + + WMSG0("Loading preferences from registry\n"); + + fColumnLayout.LoadFromRegistry(kColumnSect); + + int i; + for (i = 0; i < kPrefNumLastRegistry; i++) { + if (fPrefMaps[i].registryKey == nil) + continue; + + switch (fPrefMaps[i].type) { + case kBool: + bval = GetPrefBool(fPrefMaps[i].num); + SetPrefBool(fPrefMaps[i].num, + GetBool(fPrefMaps[i].registrySection, fPrefMaps[i].registryKey, bval)); + break; + case kLong: + lval = GetPrefLong(fPrefMaps[i].num); + SetPrefLong(fPrefMaps[i].num, + GetInt(fPrefMaps[i].registrySection, fPrefMaps[i].registryKey, lval)); + break; + case kString: + sval = GetPrefString(fPrefMaps[i].num); + SetPrefString(fPrefMaps[i].num, + GetString(fPrefMaps[i].registrySection, fPrefMaps[i].registryKey, sval)); + break; + default: + WMSG2("Invalid type %d on num=%d\n", fPrefMaps[i].type, i); + ASSERT(false); + break; + } + } + + return 0; +} + +/* + * Save preferences to the registry. + */ +int +Preferences::SaveToRegistry(void) +{ + WMSG0("Saving preferences to registry\n"); + + fColumnLayout.SaveToRegistry(kColumnSect); + + int i; + for (i = 0; i < kPrefNumLastRegistry; i++) { + if (fPrefMaps[i].registryKey == nil) + continue; + + switch (fPrefMaps[i].type) { + case kBool: + WriteBool(fPrefMaps[i].registrySection, fPrefMaps[i].registryKey, + GetPrefBool(fPrefMaps[i].num)); + break; + case kLong: + WriteInt(fPrefMaps[i].registrySection, fPrefMaps[i].registryKey, + GetPrefLong(fPrefMaps[i].num)); + break; + case kString: + WriteString(fPrefMaps[i].registrySection, fPrefMaps[i].registryKey, + GetPrefString(fPrefMaps[i].num)); + break; + default: + WMSG2("Invalid type %d on num=%d\n", fPrefMaps[i].type, i); + ASSERT(false); + break; + } + } + + return 0; +} diff --git a/app/Preferences.h b/app/Preferences.h new file mode 100644 index 0000000..490ef55 --- /dev/null +++ b/app/Preferences.h @@ -0,0 +1,298 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Keep track of user preferences. + * + * How to add a new preference item: + * - Add an entry to the PrefNum enum, below. + * - Add a corresponding entry to Preferences::fPrefMaps, adding a new + * section to the registry if appropriate. + * - Add a default value to Preferences::Preferences. If not specified, + * strings will be nil and numeric values will be zero. + */ +#ifndef __PREFERENCES__ +#define __PREFERENCES__ + +#include "MyApp.h" + +class ContentList; + +/* + * Number of visible columns. (We no longer have "invisible" columns, so the + * name is somewhat misleading.) + * + * This is used widely. Update with care. + */ +const int kNumVisibleColumns = 9; + +/* + * Used to save & restore column layout and sorting preferences for + * the ContentList class. + */ +class ColumnLayout { +public: + ColumnLayout(void) { + for (int i = 0; i < kNumVisibleColumns; i++) + fColumnWidth[i] = kWidthDefaulted; + fSortColumn = kNumVisibleColumns; // means "use original order" + fAscending = true; + } + ~ColumnLayout(void) {} + + void LoadFromRegistry(const char* section); + void SaveToRegistry(const char* section); + + int GetColumnWidth(int col) const { + ASSERT(col >= 0 && col < kNumVisibleColumns); + return fColumnWidth[col]; + } + void SetColumnWidth(int col, int width) { + ASSERT(col >= 0 && col < kNumVisibleColumns); + ASSERT(width >= 0 || width == kWidthDefaulted); + fColumnWidth[col] = width; + } + + int GetSortColumn(void) const { return fSortColumn; } + void SetSortColumn(int col) { + ASSERT(col >= 0 && col <= kNumVisibleColumns); + fSortColumn = col; + } + bool GetAscending(void) const { return fAscending; } + void SetAscending(bool val) { fAscending = val; } + + /* column width value used to flag "defaulted" status */ + enum { kWidthDefaulted = -1 }; + /* minimium width of column 0 (pathname) */ + enum { kMinCol0Width = 50 }; + +private: + // Create a dummy list control to get default column widths. + void DetermineDefaultWidths(ContentList* pList); + + int fColumnWidth[kNumVisibleColumns]; + int fSortColumn; + bool fAscending; +}; + + +/* + * Preferences type enumeration. + * + * This is logically part of the Preferences object, but it's annoying to + * have to specify the scope resolution operator everywhere. + */ +typedef enum { + /**/ kPrefNumUnknown = 0, + +/* these are saved in the registry */ + + // sticky settings for add file options + kPrAddIncludeSubFolders, // bool + kPrAddStripFolderNames, // bool + kPrAddOverwriteExisting, // bool + kPrAddTypePreservation, // long + kPrAddConvEOL, // long + + // sticky settings for file extraction + //kPrExtractPath, // string + kPrExtractConvEOL, // long + kPrExtractConvHighASCII, // bool + kPrExtractIncludeData, // bool + kPrExtractIncludeRsrc, // bool + kPrExtractIncludeDisk, // bool + kPrExtractEnableReformat, // bool + kPrExtractDiskTo2MG, // bool + kPrExtractAddTypePreservation, // bool + kPrExtractAddExtension, // bool + kPrExtractStripFolderNames, // bool + kPrExtractOverwriteExisting, // bool + +// // view file options +// kPrViewIncludeDataForks, // bool +// kPrViewIncludeRsrcForks, // bool +// kPrViewIncludeDiskImages, // bool +// kPrViewIncludeComments, // bool + + // convert disk image to file archive + //kPrConvFileConvDOSText, // bool + //kPrConvFileConvPascalText, // bool + kPrConvFileEmptyFolders, // bool + + // folders for CFileDialog initialization + kPrOpenArchiveFolder, // string + kPrConvertArchiveFolder, // string + kPrAddFileFolder, // string + kPrExtractFileFolder, // string + + // logical/physical volume prefs + kPrVolumeFilter, // long + //kPrVolumeReadOnly, // bool + + // cassette import/export prefs + kPrCassetteAlgorithm, // long + kPrOpenWAVFolder, // string + + // items from the Preferences propertypages (must be saved/restored) + kPrMimicShrinkIt, // bool + kPrBadMacSHK, // bool + kPrReduceSHKErrorChecks, // bool + kPrCoerceDOSFilenames, // bool + kPrSpacesToUnder, // bool + kPrPasteJunkPaths, // bool + kPrBeepOnSuccess, // bool + + kPrQueryImageFormat, // bool + kPrOpenVolumeRO, // bool + kPrOpenVolumePhys0, // bool + kPrProDOSAllowLower, // bool + kPrProDOSUseSparse, // bool + + kPrCompressionType, // long + + kPrMaxViewFileSize, // long + kPrNoWrapText, // bool + + kPrHighlightHexDump, // bool + kPrHighlightBASIC, // bool + kPrConvHiResBlackWhite, // bool + kPrConvDHRAlgorithm, // long + kPrRelaxGfxTypeCheck, // bool + kPrDisasmOneByteBrkCop, // bool + //kPrEOLConvRaw, // bool + kPrConvTextEOL_HA, // bool + kPrConvPascalText, // bool + kPrConvPascalCode, // bool + kPrConvCPMText, // bool + kPrConvApplesoft, // bool + kPrConvInteger, // bool + kPrConvGWP, // bool + kPrConvText8, // bool + kPrConvAWP, // bool + kPrConvADB, // bool + kPrConvASP, // bool + kPrConvSCAssem, // bool + kPrConvDisasm, // bool + kPrConvHiRes, // bool + kPrConvDHR, // bool + kPrConvSHR, // bool + kPrConvPrintShop, // bool + kPrConvMacPaint, // bool + kPrConvProDOSFolder, // bool + kPrConvResources, // bool + + kPrTempPath, // string + kPrExtViewerExts, // string + + // open file dialog + kPrLastOpenFilterIndex, // long + + /**/ kPrefNumLastRegistry, +/* these are temporary settings, not saved in the registry */ + + // sticky settings for internal file viewer (ViewFilesDialog) + kPrViewTextTypeFace, // string + kPrViewTextPointSize, // long + kPrFileViewerWidth, // long + kPrFileViewerHeight, // long + + // sticky setting for disk image creator + kPrDiskImageCreateFormat, // long + + /**/ kPrefNumLastEntry +} PrefNum; + + +/* + * Container for preferences. + */ +class Preferences { +public: + Preferences(void); + ~Preferences(void) { + FreeStringValues(); + } + + // Load/save preferences from/to registry. + int LoadFromRegistry(void); + int SaveToRegistry(void); + + ColumnLayout* GetColumnLayout(void) { return &fColumnLayout; } + //bool GetShowToolbarText(void) const { return fShowToolbarText; } + //void SetShowToolbarText(bool val) { fShowToolbarText = val; } + + bool GetPrefBool(PrefNum num) const; + void SetPrefBool(PrefNum num, bool val); + long GetPrefLong(PrefNum num) const; + void SetPrefLong(PrefNum num, long val); + const char* GetPrefString(PrefNum num) const; + void SetPrefString(PrefNum num, const char* str); + + +private: + void InitTempPath(void); + void InitFolders(void); + bool GetMyDocuments(CString* pPath); + int DefaultCompressionType(void); + void FreeStringValues(void); + + /* + * Internal data structure used to manage preferences. + */ + typedef enum { kPTNone, kBool, kLong, kString } PrefType; + typedef struct PrefMap { + PrefNum num; + PrefType type; + const char* registrySection; + const char* registryKey; + } PrefMap; + static const PrefMap fPrefMaps[kPrefNumLastEntry]; + void ScanPrefMaps(void); + + // this holds the actual values + void* fValues[kPrefNumLastEntry]; + + // verify that the entry exists and has the expected type + bool ValidateEntry(PrefNum num, PrefType type) const { + if (num <= kPrefNumUnknown || num >= kPrefNumLastEntry) { + ASSERT(false); + return false; + } + if (fPrefMaps[num].type != type) { + ASSERT(false); + return false; + } + return true; + } + + // column widths for ContentList + ColumnLayout fColumnLayout; + + /* + * Registry helpers. + */ + UINT GetInt(const char* section, const char* key, int dflt) { + return gMyApp.GetProfileInt(section, key, dflt); + } + bool GetBool(const char* section, const char* key, bool dflt) { + return (gMyApp.GetProfileInt(section, key, dflt) != 0); + } + CString GetString(const char* section, const char* key, + const char* dflt) + { + return gMyApp.GetProfileString(section, key, dflt); + } + BOOL WriteInt(const char* section, const char* key, int value) { + return gMyApp.WriteProfileInt(section, key, value); + } + BOOL WriteBool(const char* section, const char* key, bool value) { + return gMyApp.WriteProfileInt(section, key, value); + } + BOOL WriteString(const char* section, const char* key, const char* value) { + return gMyApp.WriteProfileString(section, key, value); + } +}; + +#endif /*__PREFERENCES__*/ \ No newline at end of file diff --git a/app/PrefsDialog.cpp b/app/PrefsDialog.cpp new file mode 100644 index 0000000..19271f7 --- /dev/null +++ b/app/PrefsDialog.cpp @@ -0,0 +1,713 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Implementation classes for the preferences property sheet + */ +#include "stdafx.h" +#include "PrefsDialog.h" +#include "ChooseDirDialog.h" +#include "EditAssocDialog.h" +#include "Main.h" +#include "NufxArchive.h" +#include "HelpTopics.h" +#include "resource.h" +#include // need WM_COMMANDHELP + + +/* + * =========================================================================== + * PrefsGeneralPage + * =========================================================================== + */ + +BEGIN_MESSAGE_MAP(PrefsGeneralPage, CPropertyPage) + ON_CONTROL_RANGE(BN_CLICKED, IDC_COL_PATHNAME, IDC_COL_ACCESS, OnChangeRange) + ON_BN_CLICKED(IDC_PREF_SHRINKIT_COMPAT, OnChange) + ON_BN_CLICKED(IDC_PREF_REDUCE_SHK_ERROR_CHECKS, OnChange) + ON_BN_CLICKED(IDC_PREF_SHK_BAD_MAC, OnChange) + ON_BN_CLICKED(IDC_PREF_COERCE_DOS, OnChange) + ON_BN_CLICKED(IDC_PREF_SPACES_TO_UNDER, OnChange) + ON_BN_CLICKED(IDC_PREF_PASTE_JUNKPATHS, OnChange) + ON_BN_CLICKED(IDC_PREF_SUCCESS_BEEP, OnChange) + ON_BN_CLICKED(IDC_COL_DEFAULTS, OnDefaults) + ON_BN_CLICKED(IDC_PREF_ASSOCIATIONS, OnAssociations) + ON_MESSAGE(WM_HELP, OnHelp) + ON_MESSAGE(WM_COMMANDHELP, OnCommandHelp) +END_MESSAGE_MAP() + + +/* + * If they clicked on a checkbox, just mark the page as dirty so the "apply" + * button will be enabled. + */ +void +PrefsGeneralPage::OnChange(void) +{ + SetModified(TRUE); +} +void +PrefsGeneralPage::OnChangeRange(UINT nID) +{ + SetModified(TRUE); +} + +/* + * Handle a click of the "defaults" button. + * + * Since we don't actually set column widths here, we need to tell the main + * window that the defaults button was pushed. It needs to reset all column + * widths to defaults, and then take into account any checking and un-checking + * that was done after "defaults" was pushed. + */ +void +PrefsGeneralPage::OnDefaults(void) +{ + WMSG0("DEFAULTS!\n"); + + CButton* pButton; + + fDefaultsPushed = true; + + ASSERT(IDC_COL_ACCESS == IDC_COL_PATHNAME + (kNumVisibleColumns-1)); + + /* assumes that the controls are numbered sequentially */ + for (int i = 0; i < kNumVisibleColumns; i++) { + pButton = (CButton*) GetDlgItem(IDC_COL_PATHNAME+i); + ASSERT(pButton != nil); + pButton->SetCheck(1); // 0=unchecked, 1=checked, 2=indeterminate + } + + SetModified(TRUE); +} + +/* + * They clicked on "assocations". Bring up the association edit dialog. + * If they click "OK", make a copy of the changes and mark us as modified so + * the Apply/Cancel buttons behave as expected. + */ +void +PrefsGeneralPage::OnAssociations(void) +{ + EditAssocDialog assocDlg; + + assocDlg.fOurAssociations = fOurAssociations; + fOurAssociations = nil; + + if (assocDlg.DoModal() == IDOK) { + // steal the modified associations + delete[] fOurAssociations; + fOurAssociations = assocDlg.fOurAssociations; + assocDlg.fOurAssociations = nil; + SetModified(TRUE); + } +} + +/* + * Convert values. + * + * The various column checkboxes are independent. We still do the xfer + * for "pathname" even though it's disabled. + */ +void +PrefsGeneralPage::DoDataExchange(CDataExchange* pDX) +{ + fReady = true; + + ASSERT(NELEM(fColumn) == 9); + DDX_Check(pDX, IDC_COL_PATHNAME, fColumn[0]); + DDX_Check(pDX, IDC_COL_TYPE, fColumn[1]); + DDX_Check(pDX, IDC_COL_AUXTYPE, fColumn[2]); + DDX_Check(pDX, IDC_COL_MODDATE, fColumn[3]); + DDX_Check(pDX, IDC_COL_FORMAT, fColumn[4]); + DDX_Check(pDX, IDC_COL_SIZE, fColumn[5]); + DDX_Check(pDX, IDC_COL_RATIO, fColumn[6]); + DDX_Check(pDX, IDC_COL_PACKED, fColumn[7]); + DDX_Check(pDX, IDC_COL_ACCESS, fColumn[8]); + + DDX_Check(pDX, IDC_PREF_SHRINKIT_COMPAT, fMimicShrinkIt); + DDX_Check(pDX, IDC_PREF_SHK_BAD_MAC, fBadMacSHK); + DDX_Check(pDX, IDC_PREF_REDUCE_SHK_ERROR_CHECKS, fReduceSHKErrorChecks); + DDX_Check(pDX, IDC_PREF_COERCE_DOS, fCoerceDOSFilenames); + DDX_Check(pDX, IDC_PREF_SPACES_TO_UNDER, fSpacesToUnder); + DDX_Check(pDX, IDC_PREF_PASTE_JUNKPATHS, fPasteJunkPaths); + DDX_Check(pDX, IDC_PREF_SUCCESS_BEEP, fBeepOnSuccess); +} + +/* + * Context help request (question mark button). + */ +LONG +PrefsGeneralPage::OnHelp(UINT wParam, LONG lParam) +{ + WinHelp((DWORD) ((HELPINFO*) lParam)->iCtrlId, HELP_CONTEXTPOPUP); + return TRUE; // yes, we handled it +} +/* + * User pressed the PropertySheet "Help" button. + */ +LONG +PrefsGeneralPage::OnCommandHelp(UINT, LONG) +{ + WinHelp(HELP_TOPIC_PREFS_GENERAL, HELP_CONTEXT); + return 0; // doesn't matter +} + + +/* + * =========================================================================== + * PrefsDiskImagePage + * =========================================================================== + */ + +BEGIN_MESSAGE_MAP(PrefsDiskImagePage, CPropertyPage) + ON_BN_CLICKED(IDC_PDISK_CONFIRM_FORMAT, OnChange) + ON_BN_CLICKED(IDC_PDISK_OPENVOL_RO, OnChange) + ON_BN_CLICKED(IDC_PDISK_OPENVOL_PHYS0, OnChange) + ON_BN_CLICKED(IDC_PDISK_PRODOS_ALLOWLOWER, OnChange) + ON_BN_CLICKED(IDC_PDISK_PRODOS_USESPARSE, OnChange) + ON_MESSAGE(WM_HELP, OnHelp) + ON_MESSAGE(WM_COMMANDHELP, OnCommandHelp) +END_MESSAGE_MAP() + +/* + * Set up our spin button. + */ +BOOL +PrefsDiskImagePage::OnInitDialog(void) +{ + //WMSG0("OnInit!\n"); + return CPropertyPage::OnInitDialog(); +} + +/* + * Enable the "apply" button. + */ +void +PrefsDiskImagePage::OnChange(void) +{ + WMSG0("OnChange\n"); + SetModified(TRUE); +} +//void +//PrefsDiskImagePage::OnChangeRange(UINT nID) +//{ +// WMSG1("OnChangeRange id=%d\n", nID); +// SetModified(TRUE); +//} + + +/* + * Convert values. + */ +void +PrefsDiskImagePage::DoDataExchange(CDataExchange* pDX) +{ + fReady = true; + DDX_Check(pDX, IDC_PDISK_CONFIRM_FORMAT, fQueryImageFormat); + DDX_Check(pDX, IDC_PDISK_OPENVOL_RO, fOpenVolumeRO); + DDX_Check(pDX, IDC_PDISK_OPENVOL_PHYS0, fOpenVolumePhys0); + DDX_Check(pDX, IDC_PDISK_PRODOS_ALLOWLOWER, fProDOSAllowLower); + DDX_Check(pDX, IDC_PDISK_PRODOS_USESPARSE, fProDOSUseSparse); +} + +/* + * Context help request (question mark button). + */ +LONG +PrefsDiskImagePage::OnHelp(UINT wParam, LONG lParam) +{ + WinHelp((DWORD) ((HELPINFO*) lParam)->iCtrlId, HELP_CONTEXTPOPUP); + return TRUE; // yes, we handled it +} +/* + * User pressed the PropertySheet "Help" button. + */ +LONG +PrefsDiskImagePage::OnCommandHelp(UINT, LONG) +{ + WinHelp(HELP_TOPIC_PREFS_DISK_IMAGE, HELP_CONTEXT); + return 0; // doesn't matter +} + + +/* + * =========================================================================== + * PrefsCompressionPage + * =========================================================================== + */ + +BEGIN_MESSAGE_MAP(PrefsCompressionPage, CPropertyPage) + ON_CONTROL_RANGE(BN_CLICKED, IDC_DEFC_UNCOMPRESSED, IDC_DEFC_BZIP2, OnChangeRange) + ON_MESSAGE(WM_HELP, OnHelp) + ON_MESSAGE(WM_COMMANDHELP, OnCommandHelp) +END_MESSAGE_MAP() + + +/* + * Disable compression types not supported by the NufxLib DLL. + */ +BOOL +PrefsCompressionPage::OnInitDialog(void) +{ + if (!NufxArchive::IsCompressionSupported(kNuThreadFormatHuffmanSQ)) { + DisableWnd(IDC_DEFC_SQUEEZE); + if (fCompressType == kNuThreadFormatHuffmanSQ) + fCompressType = kNuThreadFormatUncompressed; + } + if (!NufxArchive::IsCompressionSupported(kNuThreadFormatLZW1)) { + DisableWnd(IDC_DEFC_LZW1); + if (fCompressType == kNuThreadFormatLZW1) + fCompressType = kNuThreadFormatUncompressed; + } + if (!NufxArchive::IsCompressionSupported(kNuThreadFormatLZW2)) { + DisableWnd(IDC_DEFC_LZW2); + if (fCompressType == kNuThreadFormatLZW2) { + fCompressType = kNuThreadFormatUncompressed; + } + } + if (!NufxArchive::IsCompressionSupported(kNuThreadFormatLZC12)) { + DisableWnd(IDC_DEFC_LZC12); + if (fCompressType == kNuThreadFormatLZC12) + fCompressType = kNuThreadFormatUncompressed; + } + if (!NufxArchive::IsCompressionSupported(kNuThreadFormatLZC16)) { + DisableWnd(IDC_DEFC_LZC16); + if (fCompressType == kNuThreadFormatLZC16) + fCompressType = kNuThreadFormatUncompressed; + } + if (!NufxArchive::IsCompressionSupported(kNuThreadFormatDeflate)) { + DisableWnd(IDC_DEFC_DEFLATE); + if (fCompressType == kNuThreadFormatDeflate) + fCompressType = kNuThreadFormatUncompressed; + } + if (!NufxArchive::IsCompressionSupported(kNuThreadFormatBzip2)) { + DisableWnd(IDC_DEFC_BZIP2); + if (fCompressType == kNuThreadFormatBzip2) + fCompressType = kNuThreadFormatUncompressed; + } + + /* now invoke DoDataExchange with our modified fCompressType */ + return CPropertyPage::OnInitDialog(); +} + +/* + * Disable a window in our dialog. + */ +void +PrefsCompressionPage::DisableWnd(int id) +{ + CWnd* pWnd; + pWnd = GetDlgItem(id); + if (pWnd == nil) { + ASSERT(false); + return; + } + pWnd->EnableWindow(FALSE); +} + +/* + * Enable the "apply" button. + */ +void +PrefsCompressionPage::OnChangeRange(UINT nID) +{ + SetModified(TRUE); +} + +/* + * Convert values. + * + * Compression types match the NuThreadFormat enum in NufxLib.h, starting + * with IDC_DEFC_UNCOMPRESSED. + */ +void +PrefsCompressionPage::DoDataExchange(CDataExchange* pDX) +{ + //WMSG0("OnInit comp!\n"); + fReady = true; + DDX_Radio(pDX, IDC_DEFC_UNCOMPRESSED, fCompressType); +} + +/* + * Context help request (question mark button). + */ +LONG +PrefsCompressionPage::OnHelp(UINT wParam, LONG lParam) +{ + WinHelp((DWORD) ((HELPINFO*) lParam)->iCtrlId, HELP_CONTEXTPOPUP); + return TRUE; // yes, we handled it +} +/* + * User pressed the PropertySheet "Help" button. + */ +LONG +PrefsCompressionPage::OnCommandHelp(UINT, LONG) +{ + WinHelp(HELP_TOPIC_PREFS_COMPRESSION, HELP_CONTEXT); + return 0; // doesn't matter +} + + +/* + * =========================================================================== + * PrefsFviewPage + * =========================================================================== + */ + +BEGIN_MESSAGE_MAP(PrefsFviewPage, CPropertyPage) + ON_CONTROL_RANGE(BN_CLICKED, IDC_PVIEW_NOWRAP_TEXT, IDC_PVIEW_HIRES_BW, OnChangeRange) + ON_CONTROL_RANGE(BN_CLICKED, IDC_PVIEW_HITEXT, IDC_PVIEW_TEXT8, OnChangeRange) + ON_EN_CHANGE(IDC_PVIEW_SIZE_EDIT, OnChange) + ON_CBN_SELCHANGE(IDC_PVIEW_DHR_CONV_COMBO, OnChange) + ON_MESSAGE(WM_HELP, OnHelp) + ON_MESSAGE(WM_COMMANDHELP, OnCommandHelp) +END_MESSAGE_MAP() + +/* + * Set up our spin button. + */ +BOOL +PrefsFviewPage::OnInitDialog(void) +{ + //WMSG0("OnInit!\n"); + CSpinButtonCtrl* pSpin; + + //WMSG0("Configuring spin\n"); + + pSpin = (CSpinButtonCtrl*) GetDlgItem(IDC_PVIEW_SIZE_SPIN); + ASSERT(pSpin != nil); + + UDACCEL uda; + uda.nSec = 0; + uda.nInc = 64; + pSpin->SetRange(1, 32767); + pSpin->SetAccel(1, &uda); + WMSG0("OnInit done!\n"); + + return CPropertyPage::OnInitDialog(); +} + +/* + * Enable the "apply" button. + */ +void +PrefsFviewPage::OnChange(void) +{ + WMSG0("OnChange\n"); + SetModified(TRUE); +} +void +PrefsFviewPage::OnChangeRange(UINT nID) +{ + WMSG1("OnChangeRange id=%d\n", nID); + SetModified(TRUE); +} + + +/* + * Convert values. + */ +void +PrefsFviewPage::DoDataExchange(CDataExchange* pDX) +{ + fReady = true; + //DDX_Check(pDX, IDC_PVIEW_EOL_RAW, fEOLConvRaw); + DDX_Check(pDX, IDC_PVIEW_NOWRAP_TEXT, fNoWrapText); + DDX_Check(pDX, IDC_PVIEW_BOLD_HEXDUMP, fHighlightHexDump); + DDX_Check(pDX, IDC_PVIEW_BOLD_BASIC, fHighlightBASIC); + DDX_Check(pDX, IDC_PVIEW_DISASM_ONEBYTEBRKCOP, fConvDisasmOneByteBrkCop); + DDX_Check(pDX, IDC_PVIEW_HIRES_BW, fConvHiResBlackWhite); + DDX_CBIndex(pDX, IDC_PVIEW_DHR_CONV_COMBO, fConvDHRAlgorithm); + + DDX_Check(pDX, IDC_PVIEW_HITEXT, fConvTextEOL_HA); + DDX_Check(pDX, IDC_PVIEW_CPMTEXT, fConvCPMText); + DDX_Check(pDX, IDC_PVIEW_PASCALTEXT, fConvPascalText); + DDX_Check(pDX, IDC_PVIEW_PASCALCODE, fConvPascalCode); + DDX_Check(pDX, IDC_PVIEW_APPLESOFT, fConvApplesoft); + DDX_Check(pDX, IDC_PVIEW_INTEGER, fConvInteger); + DDX_Check(pDX, IDC_PVIEW_GWP, fConvGWP); + DDX_Check(pDX, IDC_PVIEW_TEXT8, fConvText8); + DDX_Check(pDX, IDC_PVIEW_AWP, fConvAWP); + DDX_Check(pDX, IDC_PVIEW_ADB, fConvADB); + DDX_Check(pDX, IDC_PVIEW_ASP, fConvASP); + DDX_Check(pDX, IDC_PVIEW_SCASSEM, fConvSCAssem); + DDX_Check(pDX, IDC_PVIEW_DISASM, fConvDisasm); + + DDX_Check(pDX, IDC_PVIEW_HIRES, fConvHiRes); + DDX_Check(pDX, IDC_PVIEW_DHR, fConvDHR); + DDX_Check(pDX, IDC_PVIEW_SHR, fConvSHR); + DDX_Check(pDX, IDC_PVIEW_PRINTSHOP, fConvPrintShop); + DDX_Check(pDX, IDC_PVIEW_MACPAINT, fConvMacPaint); + DDX_Check(pDX, IDC_PVIEW_PRODOSFOLDER, fConvProDOSFolder); + DDX_Check(pDX, IDC_PVIEW_RESOURCES, fConvResources); + DDX_Check(pDX, IDC_PVIEW_RELAX_GFX, fRelaxGfxTypeCheck); + + DDX_Text(pDX, IDC_PVIEW_SIZE_EDIT, fMaxViewFileSizeKB); + DDV_MinMaxUInt(pDX, fMaxViewFileSizeKB, 1, 32767); +} + +/* + * Context help request (question mark button). + */ +LONG +PrefsFviewPage::OnHelp(UINT wParam, LONG lParam) +{ + WinHelp((DWORD) ((HELPINFO*) lParam)->iCtrlId, HELP_CONTEXTPOPUP); + return TRUE; // yes, we handled it +} +/* + * User pressed the PropertySheet "Help" button. + */ +LONG +PrefsFviewPage::OnCommandHelp(UINT, LONG) +{ + WinHelp(HELP_TOPIC_PREFS_FVIEW, HELP_CONTEXT); + return 0; // doesn't matter +} + + +/* + * =========================================================================== + * PrefsFilesPage + * =========================================================================== + */ + +BEGIN_MESSAGE_MAP(PrefsFilesPage, CPropertyPage) + ON_EN_CHANGE(IDC_PREF_TEMP_FOLDER, OnChange) + ON_EN_CHANGE(IDC_PREF_EXTVIEWER_EXTS, OnChange) + ON_BN_CLICKED(IDC_PREF_CHOOSE_TEMP_FOLDER, OnChooseFolder) + ON_MESSAGE(WM_HELP, OnHelp) + ON_MESSAGE(WM_COMMANDHELP, OnCommandHelp) +END_MESSAGE_MAP() + + +/* + * Set up the "choose folder" button. + */ +BOOL +PrefsFilesPage::OnInitDialog(void) +{ + fChooseFolderButton.ReplaceDlgCtrl(this, IDC_PREF_CHOOSE_TEMP_FOLDER); + fChooseFolderButton.SetBitmapID(IDB_CHOOSE_FOLDER); + + return CPropertyPage::OnInitDialog(); +} + +/* + * Enable the "apply" button. + */ +void +PrefsFilesPage::OnChange(void) +{ + SetModified(TRUE); +} + +/* + * Convert values. + */ +void +PrefsFilesPage::DoDataExchange(CDataExchange* pDX) +{ + fReady = true; + DDX_Text(pDX, IDC_PREF_TEMP_FOLDER, fTempPath); + DDX_Text(pDX, IDC_PREF_EXTVIEWER_EXTS, fExtViewerExts); + + /* validate the path field */ + if (pDX->m_bSaveAndValidate) { + if (fTempPath.IsEmpty()) { + CString appName; + appName.LoadString(IDS_MB_APP_NAME); + MessageBox("You must specify a path for temp files", + appName, MB_OK); + pDX->Fail(); + } + + // we *could* try to validate the path here... + } +} + +/* + * They want to choose the folder from a menu hierarchy. Show them a list. + */ +void +PrefsFilesPage::OnChooseFolder(void) +{ + ChooseDirDialog chooseDir(this); + CWnd* pEditWnd; + CString editPath; + + /* get the currently-showing text from the edit field */ + pEditWnd = GetDlgItem(IDC_PREF_TEMP_FOLDER); + ASSERT(pEditWnd != nil); + pEditWnd->GetWindowText(editPath); + + chooseDir.SetPathName(editPath); + if (chooseDir.DoModal() == IDOK) { + const char* ccp = chooseDir.GetPathName(); + WMSG1("New temp path chosen = '%s'\n", ccp); + + pEditWnd->SetWindowText(ccp); + + // activate the "apply" button + OnChange(); + } +} + +/* + * Context help request (question mark button). + */ +LONG +PrefsFilesPage::OnHelp(UINT wParam, LONG lParam) +{ + WinHelp((DWORD) ((HELPINFO*) lParam)->iCtrlId, HELP_CONTEXTPOPUP); + return TRUE; // yes, we handled it +} +/* + * User pressed the PropertySheet "Help" button. + */ +LONG +PrefsFilesPage::OnCommandHelp(UINT, LONG) +{ + WinHelp(HELP_TOPIC_PREFS_FILES, HELP_CONTEXT); + return 0; // doesn't matter +} + + +/* + * =========================================================================== + * PrefsSheet + * =========================================================================== + */ + +BEGIN_MESSAGE_MAP(PrefsSheet, CPropertySheet) + ON_WM_NCCREATE() + ON_BN_CLICKED(ID_APPLY_NOW, OnApplyNow) + ON_COMMAND(ID_HELP, OnIDHelp) + ON_MESSAGE(WM_HELP, OnHelp) +END_MESSAGE_MAP() + +/* + * Construct the preferences dialog from the individual pages. + */ +PrefsSheet::PrefsSheet(CWnd* pParentWnd) : + CPropertySheet("Preferences", pParentWnd) +{ + AddPage(&fGeneralPage); + AddPage(&fDiskImagePage); + AddPage(&fFviewPage); + AddPage(&fCompressionPage); + AddPage(&fFilesPage); + + /* this happens automatically with appropriate ID_HELP handlers */ + //m_psh.dwFlags |= PSH_HASHELP; +} + +/* + * Enable the context help button. + * + * We don't seem to get a PreCreateWindow or OnInitDialog, but we can + * intercept the WM_NCCREATE message and override the default behavior. + */ +BOOL +PrefsSheet::OnNcCreate(LPCREATESTRUCT cs) +{ + //WMSG0("PrefsSheet OnNcCreate\n"); + BOOL val = CPropertySheet::OnNcCreate(cs); + ModifyStyleEx(0, WS_EX_CONTEXTHELP); + return val; +} + +/* + * Handle the "apply" button. We only want to process updates for property + * pages that have been constructed, and they only get constructed when + * the user clicks on them. + * + * We also have to watch out for DDV tests that should prevent the "apply" + * from succeeding, e.g. the file viewer size limit. + */ +void +PrefsSheet::OnApplyNow(void) +{ + BOOL result; + + if (fGeneralPage.fReady) { + //WMSG0("Apply to general?\n"); + result = fGeneralPage.UpdateData(TRUE); + if (!result) + return; + } + if (fDiskImagePage.fReady) { + //WMSG0("Apply to disk images?\n"); + result = fDiskImagePage.UpdateData(TRUE); + if (!result) + return; + } + if (fCompressionPage.fReady) { + //WMSG0("Apply to compression?\n"); + result = fCompressionPage.UpdateData(TRUE); + if (!result) + return; + } + if (fFviewPage.fReady) { + //WMSG0("Apply to fview?\n"); + result = fFviewPage.UpdateData(TRUE); + if (!result) + return; + } + + if (fFilesPage.fReady) { + //WMSG0("Apply to fview?\n"); + result = fFilesPage.UpdateData(TRUE); + if (!result) + return; + } + + /* reset all to "unmodified" state */ + WMSG0("All 'applies' were successful\n"); + ((MainWindow*) AfxGetMainWnd())->ApplyNow(this); + fGeneralPage.SetModified(FALSE); + fGeneralPage.fDefaultsPushed = false; + fDiskImagePage.SetModified(FALSE); + fCompressionPage.SetModified(FALSE); + fFviewPage.SetModified(FALSE); + fFilesPage.SetModified(FALSE); +} + +/* + * Handle a press of the "Help" button by redirecting it back to ourselves + * as a WM_COMMANDHELP message. If we don't do this, the main window ends + * up getting our WM_COMMAND(ID_HELP) message. + * + * We still need to define an ID_HELP WM_COMMAND handler in the main window, + * or the CPropertySheet code refuses to believe that help is enabled for + * the application as a whole. + * + * The PropertySheet object handles the WM_COMMANDHELP message and redirects + * it to the active PropertyPage. Each page must handle WM_COMMANDHELP by + * opening an appropriate chapter in the help file. + */ +void +PrefsSheet::OnIDHelp(void) +{ + WMSG0("PrefsSheet OnIDHelp\n"); + SendMessage(WM_COMMANDHELP); +} + +/* + * Context help request (question mark button) on something outside of the + * property page, most likely the Apply or Cancel button. + */ +LONG +PrefsSheet::OnHelp(UINT wParam, LONG lParam) +{ + HELPINFO* lpHelpInfo = (HELPINFO*) lParam; + + WMSG0("PrefsSheet OnHelp\n"); + DWORD context = lpHelpInfo->iCtrlId; + WinHelp(context, HELP_CONTEXTPOPUP); + + return TRUE; // yes, we handled it +} diff --git a/app/PrefsDialog.h b/app/PrefsDialog.h new file mode 100644 index 0000000..2b3cc8e --- /dev/null +++ b/app/PrefsDialog.h @@ -0,0 +1,241 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Classes to support the Preferences property pages. + */ +#ifndef __PREFSDIALOG__ +#define __PREFSDIALOG__ + +#include "Preferences.h" +#include "../util/UtilLib.h" +#include "resource.h" + +/* + * The "general" page, which controls how we display information to the user. + */ +class PrefsGeneralPage : public CPropertyPage +{ +public: + PrefsGeneralPage(void) : + CPropertyPage(IDD_PREF_GENERAL), + fReady(false), + fMimicShrinkIt(FALSE), + fBadMacSHK(FALSE), + fReduceSHKErrorChecks(FALSE), + fCoerceDOSFilenames(FALSE), + fSpacesToUnder(FALSE), + fDefaultsPushed(FALSE), + fOurAssociations(nil) + {} + virtual ~PrefsGeneralPage(void) { + delete[] fOurAssociations; + } + + bool fReady; + + // fields on this page + BOOL fColumn[kNumVisibleColumns]; + BOOL fMimicShrinkIt; + BOOL fBadMacSHK; + BOOL fReduceSHKErrorChecks; + BOOL fCoerceDOSFilenames; + BOOL fSpacesToUnder; + BOOL fPasteJunkPaths; + BOOL fBeepOnSuccess; + BOOL fDefaultsPushed; + + // initialized if we opened the file associations edit page + bool* fOurAssociations; + +protected: + virtual void DoDataExchange(CDataExchange* pDX); + + afx_msg void OnChange(void); + afx_msg void OnChangeRange(UINT); + afx_msg void OnDefaults(void); + afx_msg void OnAssociations(void); + afx_msg LONG OnHelp(UINT wParam, LONG lParam); + afx_msg LONG OnCommandHelp(UINT wParam, LONG lParam); + + DECLARE_MESSAGE_MAP() +}; + +/* + * The "disk image" page, for selecting disk image preferences. + */ +class PrefsDiskImagePage : public CPropertyPage +{ +public: + PrefsDiskImagePage(void) : + CPropertyPage(IDD_PREF_DISKIMAGE), + fReady(false), + fQueryImageFormat(FALSE), + fOpenVolumeRO(FALSE), + fProDOSAllowLower(FALSE), + fProDOSUseSparse(FALSE) + {} + + bool fReady; + + BOOL fQueryImageFormat; + BOOL fOpenVolumeRO; + BOOL fOpenVolumePhys0; + BOOL fProDOSAllowLower; + BOOL fProDOSUseSparse; + +protected: + virtual BOOL OnInitDialog(void); + virtual void DoDataExchange(CDataExchange* pDX); + + afx_msg void OnChange(void); + //afx_msg void OnChangeRange(UINT); + afx_msg LONG OnHelp(UINT wParam, LONG lParam); + afx_msg LONG OnCommandHelp(UINT wParam, LONG lParam); + + DECLARE_MESSAGE_MAP() +}; + +/* + * The "compression" page, which lets the user choose a default compression + * method. + */ +class PrefsCompressionPage : public CPropertyPage +{ +public: + PrefsCompressionPage(void) : + CPropertyPage(IDD_PREF_COMPRESSION), fReady(false) + {} + + bool fReady; + + int fCompressType; // radio button index + +protected: + virtual BOOL OnInitDialog(void); + virtual void DoDataExchange(CDataExchange* pDX); + + afx_msg void OnChangeRange(UINT); + afx_msg LONG OnHelp(UINT wParam, LONG lParam); + afx_msg LONG OnCommandHelp(UINT wParam, LONG lParam); + +private: + void DisableWnd(int id); + + DECLARE_MESSAGE_MAP() +}; + +/* + * The "fview" page, for selecting preferences for the internal file viewer. + */ +class PrefsFviewPage : public CPropertyPage +{ +public: + PrefsFviewPage(void) : + CPropertyPage(IDD_PREF_FVIEW), fReady(false) + {} + bool fReady; + + BOOL fEOLConvRaw; + BOOL fNoWrapText; + BOOL fHighlightHexDump; + BOOL fHighlightBASIC; + BOOL fConvDisasmOneByteBrkCop; + BOOL fConvHiResBlackWhite; + int fConvDHRAlgorithm; // drop list + + BOOL fConvTextEOL_HA; + BOOL fConvCPMText; + BOOL fConvPascalText; + BOOL fConvPascalCode; + BOOL fConvApplesoft; + BOOL fConvInteger; + BOOL fConvGWP; + BOOL fConvText8; + BOOL fConvAWP; + BOOL fConvADB; + BOOL fConvASP; + BOOL fConvSCAssem; + BOOL fConvDisasm; + + BOOL fConvHiRes; + BOOL fConvDHR; + BOOL fConvSHR; + BOOL fConvPrintShop; + BOOL fConvMacPaint; + BOOL fConvProDOSFolder; + BOOL fConvResources; + BOOL fRelaxGfxTypeCheck; + + UINT fMaxViewFileSizeKB; + +protected: + virtual BOOL OnInitDialog(void); + virtual void DoDataExchange(CDataExchange* pDX); + + afx_msg void OnChange(void); + afx_msg void OnChangeRange(UINT); + afx_msg LONG OnHelp(UINT wParam, LONG lParam); + afx_msg LONG OnCommandHelp(UINT wParam, LONG lParam); + + DECLARE_MESSAGE_MAP() +}; + +/* + * The "compression" page, which lets the user choose a default compression + * method for NuFX archives. + */ +class PrefsFilesPage : public CPropertyPage +{ +public: + PrefsFilesPage(void) : + CPropertyPage(IDD_PREF_FILES), fReady(false) + {} + + bool fReady; + + CString fTempPath; + CString fExtViewerExts; + +protected: + virtual BOOL OnInitDialog(void); + virtual void DoDataExchange(CDataExchange* pDX); + + afx_msg void OnChange(void); + afx_msg void OnChooseFolder(void); + afx_msg LONG OnHelp(UINT wParam, LONG lParam); + afx_msg LONG OnCommandHelp(UINT wParam, LONG lParam); + + MyBitmapButton fChooseFolderButton; + + DECLARE_MESSAGE_MAP() +}; + + +/* + * Property sheet that wraps around the preferences pages. + */ +class PrefsSheet : public CPropertySheet +{ +public: + PrefsSheet(CWnd* pParentWnd = NULL); + + PrefsGeneralPage fGeneralPage; + PrefsDiskImagePage fDiskImagePage; + PrefsCompressionPage fCompressionPage; + PrefsFviewPage fFviewPage; + PrefsFilesPage fFilesPage; + +protected: + BOOL OnNcCreate(LPCREATESTRUCT cs); + + afx_msg void OnApplyNow(); + LONG OnHelp(UINT wParam, LONG lParam); + void OnIDHelp(void); + + DECLARE_MESSAGE_MAP() +}; + +#endif /*__PREFSDIALOG__*/ \ No newline at end of file diff --git a/app/Print.cpp b/app/Print.cpp new file mode 100644 index 0000000..0ab6470 --- /dev/null +++ b/app/Print.cpp @@ -0,0 +1,821 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Support for printing. + */ +#include "stdafx.h" +#include "Print.h" +#include "Main.h" +#include "Preferences.h" + + +/* + * ========================================================================== + * PrintStuff + * ========================================================================== + */ + +/*static*/ const char* PrintStuff::kCourierNew = _T("Courier New"); +/*static*/ const char* PrintStuff::kTimesNewRoman = _T("Times New Roman"); + +/* + * Set up various values. + */ +void +PrintStuff::InitBasics(CDC* pDC) +{ + ASSERT(pDC != nil); + ASSERT(fpDC == nil); + + fpDC = pDC; + + /* make sure we're in MM_TEXT mode */ + pDC->SetMapMode(MM_TEXT); + + /* get device capabilities; logPixels is e.g. 300 */ + fVertRes = pDC->GetDeviceCaps(VERTRES); + fHorzRes = pDC->GetDeviceCaps(HORZRES); + fLogPixelsX = pDC->GetDeviceCaps(LOGPIXELSX); + fLogPixelsY = pDC->GetDeviceCaps(LOGPIXELSY); + WMSG4("+++ logPixelsX=%d logPixelsY=%d fHorzRes=%d fVertRes=%d\n", + fLogPixelsX, fLogPixelsY, fHorzRes, fVertRes); +} + +/* + * Create a new font, based on the number of lines per page we need. + */ +void +PrintStuff::CreateFontByNumLines(CFont* pFont, int numLines) +{ + ASSERT(pFont != nil); + ASSERT(numLines > 0); + ASSERT(fpDC != nil); + + /* required height */ + int reqCharHeight; + reqCharHeight = (fVertRes + numLines/2) / numLines; + + /* magic fudge factor */ + int fudge = reqCharHeight / 24; + WMSG2(" Reducing reqCharHeight from %d to %d\n", + reqCharHeight, reqCharHeight - fudge); + reqCharHeight -= fudge; + + pFont->CreateFont(reqCharHeight, 0, 0, 0, FW_NORMAL, 0, 0, 0, + DEFAULT_CHARSET, OUT_CHARACTER_PRECIS, CLIP_CHARACTER_PRECIS, + DEFAULT_QUALITY, DEFAULT_PITCH | FF_DONTCARE, kTimesNewRoman); + /*fpOldFont =*/ fpDC->SelectObject(pFont); +} + + +/* + * Returns the width of the string. + */ +int +PrintStuff::StringWidth(const CString& str) +{ + CSize size; + size = fpDC->GetTextExtent(str); + return size.cx; +} + +/* + * Trim a string to the specified number of pixels. If it's too large, + * ellipsis will be added on the left or right. + */ +int +PrintStuff::TrimString(CString* pStr, int width, bool addOnLeft) +{ + static const char* kEllipsis = "..."; + CString newStr; + int strWidth; + CSize size; + + size = fpDC->GetTextExtent(kEllipsis); + + if (width < size.cx) { + ASSERT(false); + return width; + } + + newStr = *pStr; + + /* + * Do a linear search. This would probably be better served with a + * binary search or at least a good textmetric-based guess. + */ + strWidth = StringWidth(newStr); + while (strWidth > width) { + if (pStr->IsEmpty()) { + ASSERT(false); + return width; + } + if (addOnLeft) { + *pStr = pStr->Right(pStr->GetLength() -1); + newStr = kEllipsis + *pStr; + } else { + *pStr = pStr->Left(pStr->GetLength() -1); + newStr = *pStr + kEllipsis; + } + + if (!addOnLeft) { + WMSG1("Now trying '%s'\n", (LPCTSTR) newStr); + } + strWidth = StringWidth(newStr); + } + + *pStr = newStr; + return strWidth; +} + + +/* + * ========================================================================== + * PrintContentList + * ========================================================================== + */ + +/* + * Calculate some constant values. + */ +void +PrintContentList::Setup(CDC* pDC, CWnd* pParent) +{ + /* init base class */ + InitBasics(pDC); + + /* init our stuff */ + CreateFontByNumLines(&fPrintFont, kTargetLinesPerPage); + + fpParentWnd = pParent; + + /* compute text metrics */ + TEXTMETRIC metrics; + pDC->GetTextMetrics(&metrics); + fCharHeight = metrics.tmHeight + metrics.tmExternalLeading; + fLinesPerPage = fVertRes / fCharHeight; + + WMSG2("fVertRes=%d, fCharHeight=%d\n", fVertRes, fCharHeight); + + /* set up our slightly reduced lines per page */ + ASSERT(fLinesPerPage > kHeaderLines+1); + fCLLinesPerPage = fLinesPerPage - kHeaderLines; +} + +/* + * Compute the number of pages in fpContentList. + */ +void +PrintContentList::CalcNumPages(void) +{ + /* set up our local goodies */ + ASSERT(fpContentList != nil); + int numLines = fpContentList->GetItemCount(); + ASSERT(numLines > 0); + + fNumPages = (numLines + fCLLinesPerPage -1) / fCLLinesPerPage; + ASSERT(fNumPages > 0); + + WMSG3("Using numLines=%d, fNumPages=%d, fCLLinesPerPage=%d\n", + numLines, fNumPages, fCLLinesPerPage); +} + +/* + * Initiate printing of the specified list to the specified DC. + * + * Returns 0 if all went well, nonzero on cancellation or failure. + */ +int +PrintContentList::Print(const ContentList* pContentList) +{ + fpContentList = pContentList; + CalcNumPages(); + + fFromPage = 1; + fToPage = fNumPages; + return StartPrint(); +} +int +PrintContentList::Print(const ContentList* pContentList, int fromPage, int toPage) +{ + fpContentList = pContentList; + CalcNumPages(); + + fFromPage = fromPage; + fToPage = toPage; + return StartPrint(); +} + +/* + * Kick off the print job. + */ +int +PrintContentList::StartPrint(void) +{ + MainWindow* pMain = (MainWindow*)::AfxGetMainWnd(); + BOOL bres; + int jobID; + int result = -1; + + ASSERT(fFromPage >= 1); + ASSERT(fToPage <= fNumPages); + + // clear the abort flag + pMain->SetAbortPrinting(false); + + // obstruct input to the main window + fpParentWnd->EnableWindow(FALSE); + + // create a print-cancel dialog +// PrintCancelDialog* pPCD = new PrintCancelDialog; + CancelDialog* pPCD = new CancelDialog; + bres = pPCD->Create(&pMain->fAbortPrinting, IDD_PRINT_CANCEL, fpParentWnd); + if (bres == FALSE) { + WMSG0("WARNING: PrintCancelDialog init failed\n"); + } else { + fpDC->SetAbortProc(pMain->PrintAbortProc); + } + + fDocTitle = pMain->GetPrintTitle(); + + // set up the print job + CString printTitle; + printTitle.LoadString(IDS_PRINT_CL_JOB_TITLE); + DOCINFO di; + ::ZeroMemory(&di, sizeof(DOCINFO)); + di.cbSize = sizeof(DOCINFO); + di.lpszDocName = printTitle; + + jobID = fpDC->StartDoc(&di); + if (jobID <= 0) { + WMSG0("Got invalid jobID from StartDoc\n"); + goto bail; + } + WMSG1("Got jobID=%d\n", jobID); + + // do the printing + if (DoPrint() != 0) { + WMSG0("Printing was aborted\n"); + fpDC->AbortDoc(); + } else { + WMSG0("Printing was successful\n"); + fpDC->EndDoc(); + result = 0; + } + +bail: + // destroy print-cancel dialog and restore main window + fpParentWnd->EnableWindow(TRUE); + //fpParentWnd->SetActiveWindow(); + if (pPCD != nil) + pPCD->DestroyWindow(); + + return result; +} + + +/* + * Print all pages. + * + * Returns 0 on success, nonzero on failure. + */ +int +PrintContentList::DoPrint(void) +{ + WMSG2("Printing from page=%d to page=%d\n", fFromPage, fToPage); + + for (int page = fFromPage; page <= fToPage; page++) { + if (fpDC->StartPage() <= 0) { + WMSG0("StartPage returned <= 0, returning -1\n"); + return -1; + } + + DoPrintPage(page); + + // delay so we can test "cancel" button +// { +// MainWindow* pMain = (MainWindow*)::AfxGetMainWnd(); +// pMain->EventPause(1000); +// } + + + if (fpDC->EndPage() <= 0) { + WMSG0("EndPage returned <= 0, returning -1\n"); + return -1; + } + } + + return 0; +} + +/* + * Print page N of the content list, where N is a 1-based count. + */ +void +PrintContentList::DoPrintPage(int page) +{ + /* + * Column widths, on an arbitrary scale. These will be + * scaled appropriately for the page resolution. + */ + static const struct { + const char* name; + int width; + bool rightJust; + } kColumnWidths[kNumVisibleColumns] = { + { "Pathname", 250, false }, // 200 + { "Type", 40, false }, // 44 + { "Auxtype", 47, false }, // 42 + { "Mod Date", 96, false }, // 99 + { "Format", 52, false }, // 54 + { "Size", 55, true }, // 60 + { "Ratio", 40, true }, // 41 + { "Packed", 55, true }, // 60 + { "Access", 39, false }, // 53 upper, 45 lower + }; + const int kBorderWidth = 3; + + /* normalize */ + float widthMult; + int totalWidth; + + totalWidth = 0; + for (int i = 0; i < NELEM(kColumnWidths); i++) + totalWidth += kColumnWidths[i].width; + + widthMult = (float) fHorzRes / totalWidth; + WMSG3("totalWidth=%d, fHorzRes=%d, mult=%.3f\n", + totalWidth, fHorzRes, widthMult); + + /* + * Calculate some goodies. + */ + int start, end; + start = (page-1) * fCLLinesPerPage; + end = start + fCLLinesPerPage; + if (end >= fpContentList->GetItemCount()) + end = fpContentList->GetItemCount()-1; + + int offset, nextOffset, cellWidth, border; + border = (int) (kBorderWidth * widthMult); + + /* + * Print page header. + */ + fpDC->TextOut(0, 1 * fCharHeight, fDocTitle); + CString pageNum; + pageNum.Format("Page %d/%d", page, fNumPages); + int pageNumWidth = StringWidth(pageNum); + fpDC->TextOut(fHorzRes - pageNumWidth, 1 * fCharHeight, pageNum); + + /* + * Print data. + */ + for (int row = -1; row < fCLLinesPerPage && start + row <= end; row++) { + CString text; + offset = 0; + + for (int col = 0; col < kNumVisibleColumns; col++) { + cellWidth = (int) ((float)kColumnWidths[col].width * widthMult); + nextOffset = offset + cellWidth; + if (col != kNumVisibleColumns-1) + cellWidth -= border; + if (col == 0) + cellWidth -= (border*2); // extra border on pathname + + int yOffset; + if (row == -1) { + text = kColumnWidths[col].name; + yOffset = (row+kHeaderLines-1) * fCharHeight; + } else { + text = fpContentList->GetItemText(start + row, col); + yOffset = (row+kHeaderLines) * fCharHeight; + } + + int strWidth; + strWidth = TrimString(&text, cellWidth, col == 0); + + if (kColumnWidths[col].rightJust) + fpDC->TextOut((offset + cellWidth) - strWidth, yOffset, text); + else + fpDC->TextOut(offset, yOffset, text); + + offset = nextOffset; + } + } + + /* + * Add some fancy lines. + */ + CPen penBlack(PS_SOLID, 1, RGB(0, 0, 0)); + CPen* pOldPen = fpDC->SelectObject(&penBlack); + fpDC->MoveTo(0, (int) (fCharHeight * (kHeaderLines - 0.5))); + fpDC->LineTo(fHorzRes, (int) (fCharHeight * (kHeaderLines - 0.5))); + + //fpDC->MoveTo(0, 0); + //fpDC->LineTo(fHorzRes, fVertRes); + //fpDC->MoveTo(fHorzRes-1, 0); + //fpDC->LineTo(0, fVertRes); + + fpDC->SelectObject(pOldPen); +} + + +/* + * ========================================================================== + * PrintRichEdit + * ========================================================================== + */ + +/* + * Calculate some constant values. + */ +void +PrintRichEdit::Setup(CDC* pDC, CWnd* pParent) +{ + /* preflighting can cause this to be initialized twice */ + fpDC = nil; + + /* init base class */ + InitBasics(pDC); + + if (!fInitialized) { + /* + * Find a nice font for the title area. + */ + const int kPointSize = 10; + int fontHeight; + BOOL result; + + fontHeight = -MulDiv(kPointSize, fLogPixelsY, 72); + + result = fTitleFont.CreateFont(fontHeight, 0, 0, 0, FW_NORMAL, 0, 0, 0, + DEFAULT_CHARSET, OUT_CHARACTER_PRECIS, CLIP_CHARACTER_PRECIS, + DEFAULT_QUALITY, DEFAULT_PITCH | FF_DONTCARE, kTimesNewRoman); + ASSERT(result); // everybody has Times New Roman + } + + fpParentWnd = pParent; + fInitialized = true; +} + +/* + * Pre-flight the print process to get the number of pages. + */ +int +PrintRichEdit::PrintPreflight(CRichEditCtrl* pREC, int* pNumPages) +{ + fStartChar = 0; + fEndChar = -1; + fStartPage = 0; + fEndPage = -1; + return StartPrint(pREC, "(test)", pNumPages, false); +} + +/* + * Print all pages. + */ +int +PrintRichEdit::PrintAll(CRichEditCtrl* pREC, const char* title) +{ + fStartChar = 0; + fEndChar = -1; + fStartPage = 0; + fEndPage = -1; + return StartPrint(pREC, title, nil, true); +} + +/* + * Print a range of pages. + */ +int +PrintRichEdit::PrintPages(CRichEditCtrl* pREC, const char* title, + int startPage, int endPage) +{ + fStartChar = 0; + fEndChar = -1; + fStartPage = startPage; + fEndPage = endPage; + return StartPrint(pREC, title, nil, true); +} + +/* + * Print the selected area. + */ +int +PrintRichEdit::PrintSelection(CRichEditCtrl* pREC, const char* title, + long startChar, long endChar) +{ + fStartChar = startChar; + fEndChar = endChar; + fStartPage = 0; + fEndPage = -1; + return StartPrint(pREC, title, nil, true); +} + +/* + * Start the printing process by posting a print-cancel dialog. + */ +int +PrintRichEdit::StartPrint(CRichEditCtrl* pREC, const char* title, + int* pNumPages, bool doPrint) +{ + CancelDialog* pPCD = nil; + MainWindow* pMain = (MainWindow*)::AfxGetMainWnd(); + int result; + + /* set up the print cancel dialog */ + if (doPrint) { + BOOL bres; + + /* disable main UI */ + fpParentWnd->EnableWindow(FALSE); + + pPCD = new CancelDialog; + bres = pPCD->Create(&pMain->fAbortPrinting, IDD_PRINT_CANCEL, + fpParentWnd); + + /* set up the DC's print abort callback */ + if (bres != FALSE) + fpDC->SetAbortProc(pMain->PrintAbortProc); + } + + result = DoPrint(pREC, title, pNumPages, doPrint); + + if (doPrint) { + fpParentWnd->EnableWindow(TRUE); + if (pPCD != nil) + pPCD->DestroyWindow(); + } + + return result; +} + +/* + * Do some prep work before printing. + */ +void +PrintRichEdit::PrintPrep(FORMATRANGE* pFR) +{ + CFont* pOldFont; + + /* make sure we're in MM_TEXT mode */ + fpDC->SetMapMode(MM_TEXT); + + TEXTMETRIC metrics; + pOldFont = fpDC->SelectObject(&fTitleFont); + fpDC->GetTextMetrics(&metrics); + fCharHeight = metrics.tmHeight + metrics.tmExternalLeading; + fpDC->SelectObject(pOldFont); + //WMSG1("CHAR HEIGHT is %d\n", fCharHeight); + + /* compute fLeftMargin and fRightMargin */ + ComputeMargins(); + + /* + * Set up the FORMATRANGE values. The Rich Edit stuff likes to have + * measurements in TWIPS, whereas the printer is using DC pixel + * values. fLogPixels_ tells us how many pixels per inch. + */ + memset(pFR, 0, sizeof(FORMATRANGE)); + pFR->hdc = pFR->hdcTarget = fpDC->m_hDC; + + /* + * Set frame for printable area, in TWIPS. The printer DC will set + * its own "reasonable" margins, so the area here is not the entire + * sheet of paper. + */ + pFR->rcPage.left = pFR->rcPage.top = 0; + pFR->rcPage.right = (fHorzRes * kTwipsPerInch) / fLogPixelsX; + pFR->rcPage.bottom = (fVertRes * kTwipsPerInch) / fLogPixelsY; + + long topOffset = (long) ((fCharHeight * 1.5 * kTwipsPerInch) / fLogPixelsY); + pFR->rc.top = pFR->rcPage.top + topOffset; + pFR->rc.bottom = pFR->rcPage.bottom; + pFR->rc.left = pFR->rcPage.left + (fLeftMargin * kTwipsPerInch) / fLogPixelsX; + pFR->rc.right = pFR->rcPage.right - (fRightMargin * kTwipsPerInch) / fLogPixelsX; + + WMSG2("PRINTABLE AREA is %d wide x %d high (twips)\n", + pFR->rc.right - pFR->rc.left, pFR->rc.bottom - pFR->rc.top); + WMSG2("FRAME is %d wide x %d high (twips)\n", + pFR->rcPage.right - pFR->rcPage.left, pFR->rcPage.bottom - pFR->rcPage.top); + + pFR->chrg.cpMin = fStartChar; + pFR->chrg.cpMax = fEndChar; +} + +/* + * Compute the size of the left and right margins, based on the width of 80 + * characters of 10-point Courier New on the current printer. + * + * Sets fLeftMargin and fRightMargin, in printer DC pixels. + */ +void +PrintRichEdit::ComputeMargins(void) +{ + CFont tmpFont; + CFont* pOldFont; + int char80width, fontHeight, totalMargin; + BOOL result; + + fontHeight = -MulDiv(10, fLogPixelsY, 72); + + result = tmpFont.CreateFont(fontHeight, 0, 0, 0, FW_NORMAL, 0, 0, 0, + DEFAULT_CHARSET, OUT_CHARACTER_PRECIS, CLIP_CHARACTER_PRECIS, + DEFAULT_QUALITY, DEFAULT_PITCH | FF_DONTCARE, kCourierNew); + ASSERT(result); + + pOldFont = fpDC->SelectObject(&tmpFont); + // in theory we could compute one 'X' * 80; this seems more reliable + char str[81]; + for (int i = 0; i < 80; i++) + str[i] = 'X'; + str[i] = '\0'; + char80width = StringWidth(str); + fpDC->SelectObject(pOldFont); + + //WMSG1("char80 string width=%d\n", char80width); + + /* + * If there's not enough room on the page, set the margins to zero. + * If the margins required exceed two inches, just set the margin + * to one inch on either side. + */ + totalMargin = fHorzRes - char80width; + if (totalMargin < 0) { + WMSG0(" Page not wide enough, setting margins to zero\n"); + fLeftMargin = fRightMargin = 0; + } else if (totalMargin > fLogPixelsX * 2) { + WMSG0(" Page too wide, setting margins to 1 inch\n"); + fLeftMargin = fRightMargin = fLogPixelsX; + } else { + // try to get leftMargin equal to 1/2" + fLeftMargin = totalMargin / 2; + if (fLeftMargin > fLogPixelsX / 2) + fLeftMargin = fLogPixelsX / 2; + fRightMargin = totalMargin - fLeftMargin -1; + WMSG3(" +++ Margins (in %d pixels/inch) are left=%ld right=%ld\n", + fLogPixelsX, fLeftMargin, fRightMargin); + } +} + +/* + * Send the contents of the rich edit control to the printer DC. + * + * This was derived from Microsft KB article 129860. + */ +int +PrintRichEdit::DoPrint(CRichEditCtrl* pREC, const char* title, + int* pNumPages, bool doPrint) +{ + FORMATRANGE fr; + DOCINFO di; + long textLength, textPrinted, lastTextPrinted; + int pageNum; + + WMSG2("DoPrint: title='%s' doPrint=%d\n", title, doPrint); + WMSG4(" startChar=%d endChar=%d startPage=%d endPage=%d\n", + fStartChar, fEndChar, fStartPage, fEndPage); + + /* + * Get the title font and fill out the FORMATRANGE structure. + */ + PrintPrep(&fr); + + /* fill out a DOCINFO */ + memset(&di, 0, sizeof(di)); + di.cbSize = sizeof(DOCINFO); + di.lpszDocName = title; + + if (doPrint) + fpDC->StartDoc(&di); + + /* + * Here's the really strange part of the affair. The GetTextLength call + * shown in the MSFT KB article doesn't return the correct number of + * characters. The CRichEditView code in MFC uses a GetTextLengthEx + * call, which is documented as being part of the CRichEditCtrl but + * doesn't appear in the header files. Call it the hard way. + * + * If you print a "raw" file with carriage returns, and you use version + * 5.30.23.1200 of "riched20.dll", you get "9609" from GetTextLength and + * "9528" from GetTextLengthEx when the document's length is 9528 and + * there are 81 carriage returns. The value you want to use is 9528, + * because the print code doesn't double-up the count on CRs. + * + * If instead you use version 5.30.23.1215, you get the same answer + * from both calls, and printing works fine. + * + * GetTextLengthEx is part of "riched20.dll". Win9x uses "riched32.dll", + * which doesn't support the call. + */ +#ifdef _UNICODE +# error "should be code page 1200, not CP_ACP" +#endif + GETTEXTLENGTHEX exLenReq; + long basicTextLength, extdTextLength; + basicTextLength = pREC->GetTextLength(); + exLenReq.flags = GTL_PRECISE | GTL_NUMCHARS; + exLenReq.codepage = CP_ACP; + extdTextLength = (long)::SendMessage(pREC->m_hWnd, EM_GETTEXTLENGTHEX, + (WPARAM) &exLenReq, (LPARAM) NULL); + WMSG2("RichEdit text length: std=%ld extd=%ld\n", + basicTextLength, extdTextLength); + + if (fEndChar == -1) { + if (extdTextLength > 0) + textLength = extdTextLength; + else + textLength = basicTextLength; + } else + textLength = fEndChar - fStartChar; + + WMSG1(" +++ starting while loop, textLength=%ld\n", textLength); + pageNum = 0; + lastTextPrinted = -1; + do { + bool skipPage = false; + pageNum++; + WMSG1(" +++ while loop: pageNum is %d\n", pageNum); + + if (fEndPage > 0) { + if (pageNum < fStartPage) + skipPage = true; + if (pageNum > fEndPage) + break; // out of while, ending print + } + + if (doPrint && !skipPage) { + fpDC->StartPage(); + + CFont* pOldFont = fpDC->SelectObject(&fTitleFont); + fpDC->TextOut(0, 0 * fCharHeight, title); + CString pageNumStr; + pageNumStr.Format("Page %d", pageNum); + int pageNumWidth = StringWidth(pageNumStr); + fpDC->TextOut(fHorzRes - pageNumWidth, 0 * fCharHeight, pageNumStr); + fpDC->SelectObject(pOldFont); + + CPen penBlack(PS_SOLID, 1, RGB(0, 0, 0)); + CPen* pOldPen = fpDC->SelectObject(&penBlack); + int ycoord = (int) (fCharHeight * 1.25); + fpDC->MoveTo(0, ycoord-1); + fpDC->LineTo(fHorzRes, ycoord-1); + fpDC->MoveTo(0, ycoord); + fpDC->LineTo(fHorzRes, ycoord); + + fpDC->SelectObject(pOldPen); + } + + //WMSG1(" +++ calling FormatRange(%d)\n", doPrint && !skipPage); + //LogHexDump(&fr, sizeof(fr)); + + /* print a page full of RichEdit stuff */ + textPrinted = pREC->FormatRange(&fr, doPrint && !skipPage); + WMSG1(" +++ returned from FormatRange (textPrinted=%d)\n", + textPrinted); + if (textPrinted <= lastTextPrinted) { + /* the earlier StartPage can't be undone, so we'll get an + extra blank page at the very end */ + WMSG3("GLITCH: no new text printed (printed=%ld, last=%ld, len=%ld)\n", + textPrinted, lastTextPrinted, textLength); + pageNum--; // fix page count estimator + break; + } + lastTextPrinted = textPrinted; + + // delay so we can test "cancel" button + //((MainWindow*)::AfxGetMainWnd())->EventPause(1000); + + if (doPrint && !skipPage) { + if (fpDC->EndPage() <= 0) { + /* the "cancel" button was hit */ + WMSG0("EndPage returned <= 0 (cancelled)\n"); + fpDC->AbortDoc(); + return -1; + } + } + + if (textPrinted < textLength) { + fr.chrg.cpMin = textPrinted; + fr.chrg.cpMax = fEndChar; // -1 if nothing selected + } + } while (textPrinted < textLength); + + //WMSG0(" +++ calling FormatRange(nil, FALSE)\n"); + pREC->FormatRange(nil, FALSE); + //WMSG0(" +++ returned from final FormatRange\n"); + + if (doPrint) + fpDC->EndDoc(); + + if (pNumPages != nil) + *pNumPages = pageNum; + + WMSG1("Printing completed (textPrinted=%ld)\n", textPrinted); + + return 0; +} diff --git a/app/Print.h b/app/Print.h new file mode 100644 index 0000000..abd2a49 --- /dev/null +++ b/app/Print.h @@ -0,0 +1,143 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Goodies needed for printing. + */ +#ifndef __PRINT__ +#define __PRINT__ + +#include "ContentList.h" +#include "resource.h" + + +/* + * Printing base class. + */ +class PrintStuff { +protected: + PrintStuff(void) : fpDC(nil) {} + virtual ~PrintStuff(void) {} + + /* get basic goodies, based on the DC */ + virtual void InitBasics(CDC* pDC); + + // Trim a string until it's <= width; returns final width. + int TrimString(CString* pStr, int width, bool addOnLeft = false); + + // Fills in blank "pFont" object with font that tries to get us N + // lines per page. + void CreateFontByNumLines(CFont* pFont, int numLines); + + int StringWidth(const CString& str); + + static const char* kCourierNew; + static const char* kTimesNewRoman; + + enum { + kTwipsPerInch = 1440 + }; + + /* the printer DC */ + CDC* fpDC; + + //CFont* fpOldFont; + + /* some stuff gleaned from fpDC */ + int fVertRes; + int fHorzRes; + int fLogPixelsX; + int fLogPixelsY; +}; + + +/* + * Print a content list. + */ +class PrintContentList : public PrintStuff { +public: + PrintContentList(void) : fpContentList(nil), fCLLinesPerPage(0) {} + virtual ~PrintContentList(void) {} + + /* set the DC and the parent window (for the cancel box) */ + virtual void Setup(CDC* pDC, CWnd* pParent); + + int Print(const ContentList* pContentList); + int Print(const ContentList* pContentList, int fromPage, int toPage); + + /* this is used to set up the page range selection in print dialog */ + static int GetLinesPerPage(void) { + return kTargetLinesPerPage - kHeaderLines; + } + +private: + void CalcNumPages(void); + int StartPrint(void); + int DoPrint(void); + void DoPrintPage(int page); + + enum { + kHeaderLines = 4, // lines of header stuff on each page + kTargetLinesPerPage = 64, // use fLinesPerPage for actual value + }; + + CWnd* fpParentWnd; + CFont fPrintFont; + int fLinesPerPage; + int fCharHeight; + int fEllipsisWidth; + + const ContentList* fpContentList; + CString fDocTitle; + int fCLLinesPerPage; // fLinesPerPage - kHeaderLines + int fNumPages; + int fFromPage; + int fToPage; +}; + + +/* + * Print the contents of a RichEdit control. + */ +class PrintRichEdit : public PrintStuff { +public: + PrintRichEdit(void) : fInitialized(false), fpParentWnd(nil) {} + virtual ~PrintRichEdit(void) {} + + /* set the DC and the parent window (for the cancel box) */ + virtual void Setup(CDC* pDC, CWnd* pParent); + + /* + * Commence printing. + */ + int PrintPreflight(CRichEditCtrl* pREC, int* pNumPages); + int PrintAll(CRichEditCtrl* pREC, const char* title); + int PrintPages(CRichEditCtrl* pREC, const char* title, int startPage, + int endPage); + int PrintSelection(CRichEditCtrl* pREC, const char* title, long startChar, + long endChar); + +private: + int StartPrint(CRichEditCtrl* pREC, const char* title, + int* pNumPages, bool doPrint); + void PrintPrep(FORMATRANGE* pFR); + void ComputeMargins(void); + int DoPrint(CRichEditCtrl* pREC, const char* title, int* pNumPages, + bool doPrint); + + bool fInitialized; + CFont fTitleFont; + int fCharHeight; + int fLeftMargin; + int fRightMargin; + + CWnd* fpParentWnd; + int fStartChar; + int fEndChar; + int fStartPage; + int fEndPage; +}; + +#endif /*__PRINT__*/ \ No newline at end of file diff --git a/app/ProgressCounterDialog.h b/app/ProgressCounterDialog.h new file mode 100644 index 0000000..438005f --- /dev/null +++ b/app/ProgressCounterDialog.h @@ -0,0 +1,70 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Show the progress of something that has no definite bound. Because we + * don't know when we need to stop, we just count upward. + */ +#ifndef __PROGRESSCOUNTERDIALOG__ +#define __PROGRESSCOUNTERDIALOG__ + +#include "resource.h" + +/* + * Modeless dialog; must be allocated on the heap. + */ +class ProgressCounterDialog : public CancelDialog { +public: + BOOL Create(const CString& descr, CWnd* pParentWnd = NULL) { + fpParentWnd = pParentWnd; + fDescr = descr; + fCountFormat = "%d"; + fCancel = false; + + /* disable the parent window before we're created */ + if (pParentWnd != NULL) + pParentWnd->EnableWindow(FALSE); + return CancelDialog::Create(&fCancel, IDD_PROGRESS_COUNTER, + pParentWnd); + } + /* enable the parent window before we're destroyed */ + virtual BOOL DestroyWindow(void) { + if (fpParentWnd != nil) + fpParentWnd->EnableWindow(TRUE); + return ModelessDialog::DestroyWindow(); + } + + /* set a format string, e.g. "Processing file %d" */ + void SetCounterFormat(const CString& fmt) { fCountFormat = fmt; } + + /* set the current count */ + void SetCount(int count) { + CString msg; + msg.Format(fCountFormat, count); + GetDlgItem(IDC_PROGRESS_COUNTER_COUNT)->SetWindowText(msg); + } + + /* get the status of the "cancelled" flag */ + bool GetCancel(void) const { return fCancel; } + +private: + BOOL OnInitDialog(void) { + CancelDialog::OnInitDialog(); + + CWnd* pWnd = GetDlgItem(IDC_PROGRESS_COUNTER_DESC); + pWnd->SetWindowText(fDescr); + pWnd = GetDlgItem(IDC_PROGRESS_COUNTER_COUNT); + pWnd->SetWindowText(""); + pWnd->SetFocus(); // get focus off of the Cancel button + return FALSE; // accept our focus + } + + CWnd* fpParentWnd; + CString fDescr; + CString fCountFormat; + bool fCancel; +}; + +#endif /*__PROGRESSCOUNTERDIALOG__*/ diff --git a/app/RecompressOptionsDialog.cpp b/app/RecompressOptionsDialog.cpp new file mode 100644 index 0000000..7bb6696 --- /dev/null +++ b/app/RecompressOptionsDialog.cpp @@ -0,0 +1,95 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Support for RecompressOptionsDialog. + */ +#include "stdafx.h" +#include "RecompressOptionsDialog.h" +#include "NufxArchive.h" +#include "HelpTopics.h" + +//BEGIN_MESSAGE_MAP(UseSelectionDialog, CDialog) +// ON_WM_HELPINFO() +// //ON_COMMAND(IDHELP, OnHelp) +//END_MESSAGE_MAP() + + +/* + * Set up our modified version of the "use selection" dialog. + */ +BOOL +RecompressOptionsDialog::OnInitDialog(void) +{ + fCompressionIdx = LoadComboBox((NuThreadFormat) fCompressionType); + + return UseSelectionDialog::OnInitDialog(); +} + +/* + * Load strings into the combo box. Only load formats supported by the + * NufxLib DLL. + * + * Returns the combo box index for the format matching "fmt". + */ +int +RecompressOptionsDialog::LoadComboBox(NuThreadFormat fmt) +{ + static const struct { + NuThreadFormat format; + const char* name; + } kComboStrings[] = { + { kNuThreadFormatUncompressed, "No compression" }, + { kNuThreadFormatHuffmanSQ, "Squeeze" }, + { kNuThreadFormatLZW1, "Dynamic LZW/1" }, + { kNuThreadFormatLZW2, "Dynamic LZW/2" }, + { kNuThreadFormatLZC12, "12-bit LZC" }, + { kNuThreadFormatLZC16, "16-bit LZC" }, + { kNuThreadFormatDeflate, "Deflate" }, + { kNuThreadFormatBzip2, "Bzip2" }, + }; + + CComboBox* pCombo; + int idx, comboIdx; + int retIdx = 0; + + pCombo = (CComboBox*) GetDlgItem(IDC_RECOMP_COMP); + ASSERT(pCombo != nil); + + for (idx = comboIdx = 0; idx < NELEM(kComboStrings); idx++) { + if (NufxArchive::IsCompressionSupported(kComboStrings[idx].format)) { + pCombo->AddString(kComboStrings[idx].name); + pCombo->SetItemData(comboIdx, kComboStrings[idx].format); + + if (kComboStrings[idx].format == fmt) + retIdx = comboIdx; + + comboIdx++; + } + } + + return retIdx; +} + +/* + * Convert values. + */ +void +RecompressOptionsDialog::DoDataExchange(CDataExchange* pDX) +{ + DDX_CBIndex(pDX, IDC_RECOMP_COMP, fCompressionIdx); + + if (pDX->m_bSaveAndValidate) { + CComboBox* pCombo; + pCombo = (CComboBox*) GetDlgItem(IDC_RECOMP_COMP); + ASSERT(pCombo != nil); + + fCompressionType = pCombo->GetItemData(fCompressionIdx); + WMSG2("DDX got type=%d from combo index %d\n", + fCompressionType, fCompressionIdx); + } + + UseSelectionDialog::DoDataExchange(pDX); +} diff --git a/app/RecompressOptionsDialog.h b/app/RecompressOptionsDialog.h new file mode 100644 index 0000000..6682cb9 --- /dev/null +++ b/app/RecompressOptionsDialog.h @@ -0,0 +1,43 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Options for recompressing files. This is derived from the "use selection" + * dialog. + */ +#ifndef __RECOMPRESS_OPTIONS_DIALOG__ +#define __RECOMPRESS_OPTIONS_DIALOG__ + +#include "UseSelectionDialog.h" +#include "../prebuilt/NufxLib.h" +#include "resource.h" + +/* + * Straightforward confirmation plus a drop-list. + */ +class RecompressOptionsDialog : public UseSelectionDialog { +public: + RecompressOptionsDialog(int selCount, CWnd* pParentWnd = NULL) : + UseSelectionDialog(selCount, pParentWnd, IDD_RECOMPRESS_OPTS) + { + fCompressionType = 0; + } + virtual ~RecompressOptionsDialog(void) {} + + // maps directly to NuThreadFormat enum + int fCompressionType; + +private: + virtual BOOL OnInitDialog(void); + virtual void DoDataExchange(CDataExchange* pDX); + + int LoadComboBox(NuThreadFormat fmt); + + int fCompressionIdx; // drop list index + + //DECLARE_MESSAGE_MAP() +}; + +#endif /*__RECOMPRESS_OPTIONS_DIALOG__*/ \ No newline at end of file diff --git a/app/Registry.cpp b/app/Registry.cpp new file mode 100644 index 0000000..a38b567 --- /dev/null +++ b/app/Registry.cpp @@ -0,0 +1,723 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Windows Registry operations. + */ +#include "stdafx.h" +#include "Registry.h" +#include "Main.h" +#include "MyApp.h" + +#define kRegAppName "CiderPress" +#define kRegExeName "CiderPress.exe" +#define kCompanyName "faddenSoft" + +static const char* kRegKeyCPKVersions = _T("vrs"); +static const char* kRegKeyCPKExpire = _T("epr"); + +/* + * Application path. Add two keys: + * + * (default) = FullPathName + * Full pathname of the executable file. + * Path = Path + * The $PATH that will be in effect when the program starts (but only if + * launched from the Windows explorer). + */ +static const char* kAppKeyBase = + _T("HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\" kRegExeName); + +/* + * Local settings. App stuff goes in the per-user key, registration info is + * in the per-machine key. + */ +static const char* kMachineSettingsBaseKey = + _T("HKEY_LOCAL_MACHINE\\SOFTWARE\\" kCompanyName "\\" kRegAppName); +static const char* kUserSettingsBaseKey = + _T("HKEY_CURRENT_USER\\Software\\" kCompanyName "\\" kRegAppName); + +/* + * Set this key + ".XXX" to (Default)=AppID. This associates the file + * type with kRegAppID. + */ +//static const char* kFileExtensionBase = _T("HKEY_CLASSES_ROOT"); + +/* + * Description of data files. Set this key + AppID to 40-char string, e.g. + * (Default)=CompanyName AppName Version DataType + * + * Can also set DefaultIcon = Pathname [,Index] + */ +//static const char* kAppIDBase = _T("HKEY_CLASSES_ROOT"); + +/* + * Put one of these under the AppID to specify the icon for a file type. + */ +static const char* kDefaultIcon = _T("DefaultIcon"); + +static const char* kRegKeyCPKStr = "CPK"; + +/* + * Table of file type associations. They will appear in the UI in the same + * order that they appear here, so try to maintain alphabetical order. + */ +#define kAppIDNuFX _T("CiderPress.NuFX") +#define kAppIDDiskImage _T("CiderPress.DiskImage") +#define kAppIDBinaryII _T("CiderPress.BinaryII") +#define kNoAssociation _T("(no association)") +const MyRegistry::FileTypeAssoc MyRegistry::kFileTypeAssoc[] = { + { _T(".2MG"), kAppIDDiskImage }, + { _T(".APP"), kAppIDDiskImage }, + { _T(".BNY"), kAppIDBinaryII }, + { _T(".BQY"), kAppIDBinaryII }, + { _T(".BSE"), kAppIDNuFX }, + { _T(".BXY"), kAppIDNuFX }, + { _T(".D13"), kAppIDDiskImage }, + { _T(".DDD"), kAppIDDiskImage }, + { _T(".DO"), kAppIDDiskImage }, + { _T(".DSK"), kAppIDDiskImage }, + { _T(".FDI"), kAppIDDiskImage }, + { _T(".HDV"), kAppIDDiskImage }, + { _T(".IMG"), kAppIDDiskImage }, + { _T(".NIB"), kAppIDDiskImage }, + { _T(".PO"), kAppIDDiskImage }, + { _T(".SDK"), kAppIDDiskImage }, + { _T(".SEA"), kAppIDNuFX }, + { _T(".SHK"), kAppIDNuFX }, +// { _T(".DC"), kAppIDDiskImage }, +// { _T(".DC6"), kAppIDDiskImage }, +// { _T(".GZ"), kAppIDDiskImage }, +// { _T(".NB2"), kAppIDDiskImage }, +// { _T(".RAW"), kAppIDDiskImage }, +// { _T(".ZIP"), kAppIDDiskImage }, +}; + +static const struct { + const char* user; + const char* reg; +} gBadKeys[] = { + { "Nimrod Bonehead", "CP1-68C069-62CC9444" }, + { "Connie Tan", "CP1-877B2C-A428FFD6" }, +}; + + +/* + * ========================================================================== + * One-time install/uninstall + * ========================================================================== + */ + +/* + * This is called immediately after installation finishes. + * + * We want to snatch up any unused file type associations. We define them + * as "unused" if the entry does not exist in the registry at all. A more + * thorough installer would also verify that the appID actually existed + * and "steal" any apparent orphans, but we can let the user do that manually. + */ +void +MyRegistry::OneTimeInstall(void) const +{ + /* start by stomping on our appIDs */ + WMSG0(" Removing appIDs\n"); + RegDeleteKeyNT(HKEY_CLASSES_ROOT, kAppIDNuFX); + RegDeleteKeyNT(HKEY_CLASSES_ROOT, kAppIDDiskImage); + RegDeleteKeyNT(HKEY_CLASSES_ROOT, kAppIDBinaryII); + + /* configure the appIDs */ + FixBasicSettings(); + + /* configure extensions */ + int i, res; + for (i = 0; i < NELEM(kFileTypeAssoc); i++) { + HKEY hExtKey; + res = RegOpenKeyEx(HKEY_CLASSES_ROOT, kFileTypeAssoc[i].ext, 0, + KEY_READ, &hExtKey); + if (res == ERROR_SUCCESS) { + WMSG1(" Found existing HKCR\\'%s', leaving alone\n", + kFileTypeAssoc[i].ext); + RegCloseKey(hExtKey); + } else if (res == ERROR_FILE_NOT_FOUND) { + OwnExtension(kFileTypeAssoc[i].ext, kFileTypeAssoc[i].appID); + } else { + WMSG2(" Got error %ld opening HKCR\\'%s', leaving alone\n", + res, kFileTypeAssoc[i].ext); + } + } +} + +/* + * Remove things that the standard uninstall script won't. + * + * We want to un-set any of our file associations. We don't really need to + * clean up the ".xxx" entries, because removing their appID entries is enough + * to fry their little brains, but it's probably the right thing to do. + * + * We definitely want to strip out our appIDs. + */ +void +MyRegistry::OneTimeUninstall(void) const +{ + /* drop any associations we hold */ + int i; + for (i = 0; i < NELEM(kFileTypeAssoc); i++) { + CString ext, handler; + bool ours; + + GetFileAssoc(i, &ext, &handler, &ours); + if (ours) { + DisownExtension(ext); + } + } + + /* remove our appIDs */ + WMSG0(" Removing appIDs\n"); + RegDeleteKeyNT(HKEY_CLASSES_ROOT, kAppIDNuFX); + RegDeleteKeyNT(HKEY_CLASSES_ROOT, kAppIDDiskImage); + RegDeleteKeyNT(HKEY_CLASSES_ROOT, kAppIDBinaryII); +} + + +/* + * ========================================================================== + * Shareware registration logic + * ========================================================================== + */ + +/* [removed] */ + +/* + * ========================================================================== + * Windows shell game + * ========================================================================== + */ + +/* + * Return the application's registry key. This is used as the argument to + * CWinApp::SetRegistryKey(). The GetProfile{Int,String} calls combine this + * (in m_pszRegistryKey) with the app name (in m_pszProfileName) and prepend + * "HKEY_CURRENT_USER\Software\". + */ +const char* +MyRegistry::GetAppRegistryKey(void) const +{ + return kCompanyName; +} + +/* + * See if an AppID is one we recognize. + */ +bool +MyRegistry::IsOurAppID(const char* id) const +{ + return (strcasecmp(id, kAppIDNuFX) == 0 || + strcasecmp(id, kAppIDDiskImage) == 0 || + strcasecmp(id, kAppIDBinaryII) == 0); +} + +/* + * Fix the basic registry settings, e.g. our AppID classes. + * + * We don't overwrite values that already exist. We want to hold on to the + * installer's settings, which should get whacked if the program is + * uninstalled or reinstalled. This is here for "installer-less" environments + * and to cope with registry damage. + */ +void +MyRegistry::FixBasicSettings(void) const +{ + const char* exeName = gMyApp.GetExeFileName(); + ASSERT(exeName != nil && strlen(exeName) > 0); + + WMSG0("Fixing any missing file type AppID entries in registry\n"); + + ConfigureAppID(kAppIDNuFX, "NuFX Archive (CiderPress)", exeName, 1); + ConfigureAppID(kAppIDBinaryII, "Binary II (CiderPress)", exeName, 2); + ConfigureAppID(kAppIDDiskImage, "Disk Image (CiderPress)", exeName, 3); +} + +/* + * Set up the registry goodies for one appID. + */ +void +MyRegistry::ConfigureAppID(const char* appID, const char* descr, + const char* exeName, int iconIdx) const +{ + WMSG2(" Configuring '%s' for '%s'\n", appID, exeName); + + HKEY hAppKey = nil; + HKEY hIconKey = nil; + + DWORD dw; + if (RegCreateKeyEx(HKEY_CLASSES_ROOT, appID, 0, REG_NONE, + REG_OPTION_NON_VOLATILE, KEY_WRITE|KEY_READ, NULL, + &hAppKey, &dw) == ERROR_SUCCESS) + { + ConfigureAppIDSubFields(hAppKey, descr, exeName); + + if (RegCreateKeyEx(hAppKey, kDefaultIcon, 0, REG_NONE, + REG_OPTION_NON_VOLATILE, KEY_WRITE|KEY_READ, NULL, + &hIconKey, &dw) == ERROR_SUCCESS) + { + DWORD type, size; + unsigned char buf[256]; + long res; + + size = sizeof(buf); + res = RegQueryValueEx(hIconKey, "", nil, &type, buf, &size); + if (res == ERROR_SUCCESS && size > 1) { + WMSG1(" Icon for '%s' already exists, not altering\n", appID); + } else { + CString iconStr; + iconStr.Format("%s,%d", exeName, iconIdx); + + if (RegSetValueEx(hIconKey, "", 0, REG_SZ, (const unsigned char*) + (const char*) iconStr, strlen(iconStr)) == ERROR_SUCCESS) + { + WMSG2(" Set icon for '%s' to '%s'\n", appID, (LPCTSTR) iconStr); + } else { + WMSG2(" WARNING: unable to set DefaultIcon for '%s' to '%s'\n", + appID, (LPCTSTR) iconStr); + } + } + } else { + WMSG1("WARNING: couldn't set up DefaultIcon for '%s'\n", appID); + } + } else { + WMSG1("WARNING: couldn't create AppID='%s'\n", appID); + } + + RegCloseKey(hIconKey); + RegCloseKey(hAppKey); +} + +/* + * Set up the current key's default (which is used as the explorer + * description) and put the "Open" command in "...\shell\open\command". + */ +void +MyRegistry::ConfigureAppIDSubFields(HKEY hAppKey, const char* descr, + const char* exeName) const +{ + HKEY hShellKey, hOpenKey, hCommandKey; + DWORD dw; + + ASSERT(hAppKey != nil); + ASSERT(descr != nil); + ASSERT(exeName != nil); + hShellKey = hOpenKey = hCommandKey = nil; + + if (RegSetValueEx(hAppKey, "", 0, REG_SZ, (const unsigned char*) descr, + strlen(descr)) != ERROR_SUCCESS) + { + WMSG1(" WARNING: unable to set description to '%s'\n", descr); + } + + if (RegCreateKeyEx(hAppKey, _T("shell"), 0, REG_NONE, + REG_OPTION_NON_VOLATILE, KEY_WRITE|KEY_READ, NULL, + &hShellKey, &dw) == ERROR_SUCCESS) + { + if (RegCreateKeyEx(hShellKey, _T("open"), 0, REG_NONE, + REG_OPTION_NON_VOLATILE, KEY_WRITE|KEY_READ, NULL, + &hOpenKey, &dw) == ERROR_SUCCESS) + { + if (RegCreateKeyEx(hOpenKey, _T("command"), 0, REG_NONE, + REG_OPTION_NON_VOLATILE, KEY_WRITE|KEY_READ, NULL, + &hCommandKey, &dw) == ERROR_SUCCESS) + { + DWORD type, size; + unsigned char buf[MAX_PATH+8]; + long res; + + size = sizeof(buf); + res = RegQueryValueEx(hCommandKey, "", nil, &type, buf, &size); + if (res == ERROR_SUCCESS && size > 1) { + WMSG1(" Command already exists, not altering ('%s')\n", buf); + } else { + CString openCmd; + + openCmd.Format("\"%s\" \"%%1\"", exeName); + if (RegSetValueEx(hCommandKey, "", 0, REG_SZ, + (const unsigned char*) (const char*) openCmd, + strlen(openCmd)) == ERROR_SUCCESS) + { + WMSG1(" Set command to '%s'\n", openCmd); + } else { + WMSG1(" WARNING: unable to set open cmd '%s'\n", openCmd); + } + } + } + } + } + + RegCloseKey(hCommandKey); + RegCloseKey(hOpenKey); + RegCloseKey(hShellKey); +} + + +/* + * Return the number of file type associations. + */ +int +MyRegistry::GetNumFileAssocs(void) const +{ + return NELEM(kFileTypeAssoc); +} + +#if 0 +/* + * Return information on a file association. + * + * Check to see if we're the application that will be launched. + * + * Problem: the file must *actually exist* for this to work. + */ +void +MyRegistry::GetFileAssoc(int idx, CString* pExt, CString* pHandler, + bool* pOurs) const +{ + char buf[MAX_PATH]; + + *pExt = kFileTypeAssoc[idx].ext; + *pHandler = ""; + *pOurs = false; + + HINSTANCE res = FindExecutable(*pExt, "\\", buf); + if ((long) res > 32) { + WMSG1("Executable is '%s'\n", buf); + *pHandler = buf; + } else { + WMSG1("FindExecutable failed (err=%d)\n", res); + *pHandler = kNoAssociation; + } +} +#endif + + +/* + * Return information on a file association. + * + * We check to see if the file extension is associated with one of our + * application ID strings. We don't bother to check whether the appID + * strings are still associated with CiderPress, since nobody should be + * messing with those. + * + * BUG: we should be checking to see what the shell actually does to + * take into account the overrides that users can set. + */ +void +MyRegistry::GetFileAssoc(int idx, CString* pExt, CString* pHandler, + bool* pOurs) const +{ + ASSERT(idx >= 0 && idx < NELEM(kFileTypeAssoc)); + long res; + + *pExt = kFileTypeAssoc[idx].ext; + *pHandler = ""; + + CString appID; + HKEY hExtKey = nil; + + res = RegOpenKeyEx(HKEY_CLASSES_ROOT, *pExt, 0, KEY_READ, &hExtKey); + if (res == ERROR_SUCCESS) { + unsigned char buf[260]; + DWORD type, size; + + size = sizeof(buf); + res = RegQueryValueEx(hExtKey, "", nil, &type, buf, &size); + if (res == ERROR_SUCCESS) { + WMSG1(" Got '%s'\n", buf); + appID = buf; + + if (GetAssocAppName(appID, pHandler) != 0) + *pHandler = appID; + } else { + WMSG1("RegQueryValueEx failed on '%s'\n", (LPCTSTR) *pExt); + } + } else { + WMSG1(" RegOpenKeyEx failed on '%s'\n", *pExt); + } + + *pOurs = false; + if (pHandler->IsEmpty()) { + *pHandler = kNoAssociation; + } else { + *pOurs = IsOurAppID(appID); + } + + RegCloseKey(hExtKey); +} + +/* + * Given an application ID, determine the application's name. + * + * This requires burrowing down into HKEY_CLASSES_ROOT\\shell\open\. + */ +int +MyRegistry::GetAssocAppName(const CString& appID, CString* pCmd) const +{ + CString keyName; + unsigned char buf[260]; + DWORD type, size = sizeof(buf); + HKEY hAppKey = nil; + long res; + int result = -1; + + keyName = appID + "\\shell\\open\\command"; + + res = RegOpenKeyEx(HKEY_CLASSES_ROOT, keyName, 0, KEY_READ, &hAppKey); + if (res == ERROR_SUCCESS) { + res = RegQueryValueEx(hAppKey, "", nil, &type, buf, &size); + if (res == ERROR_SUCCESS) { + CString cmd(buf); + int pos; + + /* cut it down to just the EXE name */ + ReduceToToken(&cmd); + + pos = cmd.ReverseFind('\\'); + if (pos != -1 && pos != cmd.GetLength()-1) { + cmd = cmd.Right(cmd.GetLength() - pos -1); + } + + *pCmd = cmd; + result = 0; + } else { + WMSG1("Unable to open shell\\open\\command for '%s'\n", appID); + } + } else { + CString errBuf; + GetWin32ErrorString(res, &errBuf); + + WMSG2("Unable to open AppID key '%s' (%s)\n", + keyName, (LPCTSTR) errBuf); + } + + RegCloseKey(hAppKey); + return result; +} + +/* + * Reduce a compound string to just its first token. + */ +void +MyRegistry::ReduceToToken(CString* pStr) const +{ + char* argv[1]; + int argc = 1; + char* mangle = strdup(*pStr); + + VectorizeString(mangle, argv, &argc); + + if (argc == 1) + *pStr = argv[0]; + + free(mangle); +} + +/* + * Set the state of a file association. There are four possibilities: + * + * - We own it, we want to keep owning it: do nothing. + * - We don't own it, we want to keep not owning it: do nothing. + * - We own it, we don't want it anymore: remove ".xxx" entry. + * - We don't own it, we want to own it: remove ".xxx" entry and replace it. + * + * Returns 0 on success, nonzero on failure. + */ +int +MyRegistry::SetFileAssoc(int idx, bool wantIt) const +{ + const char* ext; + bool weOwnIt; + int result = 0; + + ASSERT(idx >= 0 && idx < NELEM(kFileTypeAssoc)); + + ext = kFileTypeAssoc[idx].ext; + weOwnIt = GetAssocState(ext); + WMSG3("SetFileAssoc: ext='%s' own=%d want=%d\n", ext, weOwnIt, wantIt); + + if (weOwnIt && !wantIt) { + /* reset it */ + WMSG1(" SetFileAssoc: clearing '%s'\n", ext); + result = DisownExtension(ext); + } else if (!weOwnIt && wantIt) { + /* take it */ + WMSG1(" SetFileAssoc: taking '%s'\n", ext); + result = OwnExtension(ext, kFileTypeAssoc[idx].appID); + } else { + WMSG1(" SetFileAssoc: do nothing with '%s'\n", ext); + /* do nothing */ + } + + return 0; +} + +/* + * Determine whether or not the filetype described by "ext" is one that we + * currently manage. + * + * Returns "true" if so, "false" if not. Returns "false" on any errors + * encountered. + */ +bool +MyRegistry::GetAssocState(const char* ext) const +{ + unsigned char buf[260]; + HKEY hExtKey = nil; + DWORD type, size; + int res; + bool result = false; + + res = RegOpenKeyEx(HKEY_CLASSES_ROOT, ext, 0, KEY_READ, &hExtKey); + if (res == ERROR_SUCCESS) { + size = sizeof(buf); + res = RegQueryValueEx(hExtKey, "", nil, &type, buf, &size); + if (res == ERROR_SUCCESS && type == REG_SZ) { + /* compare it to known appID values */ + WMSG2(" Found '%s', testing '%s'\n", ext, buf); + if (IsOurAppID((char*)buf)) + result = true; + } + } + + RegCloseKey(hExtKey); + return result; +} + +/* + * Drop ownership of a file extension. + * + * We assume we own it. + * + * Returns 0 on success, -1 on error. + */ +int +MyRegistry::DisownExtension(const char* ext) const +{ + ASSERT(ext != nil); + ASSERT(ext[0] == '.'); + if (ext == nil || strlen(ext) < 2) + return -1; + + if (RegDeleteKeyNT(HKEY_CLASSES_ROOT, ext) == ERROR_SUCCESS) { + WMSG1(" HKCR\\%s subtree deleted\n", ext); + } else { + WMSG1(" Failed deleting HKCR\\'%s'\n", ext); + return -1; + } + + return 0; +} + +/* + * Take ownership of a file extension. + * + * Returns 0 on success, -1 on error. + */ +int +MyRegistry::OwnExtension(const char* ext, const char* appID) const +{ + ASSERT(ext != nil); + ASSERT(ext[0] == '.'); + if (ext == nil || strlen(ext) < 2) + return -1; + + HKEY hExtKey = nil; + DWORD dw; + int res, result = -1; + + /* delete the old key (which might be a hierarchy) */ + res = RegDeleteKeyNT(HKEY_CLASSES_ROOT, ext); + if (res == ERROR_SUCCESS) { + WMSG1(" HKCR\\%s subtree deleted\n", ext); + } else if (res == ERROR_FILE_NOT_FOUND) { + WMSG1(" No HKCR\\%s subtree to delete\n", ext); + } else { + WMSG1(" Failed deleting HKCR\\'%s'\n", ext); + goto bail; + } + + /* set the new key */ + if (RegCreateKeyEx(HKEY_CLASSES_ROOT, ext, 0, REG_NONE, + REG_OPTION_NON_VOLATILE, KEY_WRITE|KEY_READ, NULL, + &hExtKey, &dw) == ERROR_SUCCESS) + { + res = RegSetValueEx(hExtKey, "", 0, REG_SZ, + (const unsigned char*) appID, strlen(appID)); + if (res == ERROR_SUCCESS) { + WMSG2(" Set '%s' to '%s'\n", ext, appID); + result = 0; + } else { + WMSG3("Failed setting '%s' to '%s' (res=%d)\n", ext, appID, res); + goto bail; + } + } + +bail: + RegCloseKey(hExtKey); + return result; +} + + +// (This comes from the MSDN sample sources.) +// +// The sample code makes no attempt to check or recover from partial +// deletions. +// +// A registry key that is opened by an application can be deleted +// without error by another application in both Windows 95 and +// Windows NT. This is by design. +// +#define MAX_KEY_LENGTH 256 // not in any header I can find ++ATM +DWORD +MyRegistry::RegDeleteKeyNT(HKEY hStartKey, LPCTSTR pKeyName) const +{ + DWORD dwRtn, dwSubKeyLength; + LPTSTR pSubKey = NULL; + TCHAR szSubKey[MAX_KEY_LENGTH]; // (256) this should be dynamic. + HKEY hKey; + + // Do not allow NULL or empty key name + if ( pKeyName && lstrlen(pKeyName)) + { + if( (dwRtn=RegOpenKeyEx(hStartKey,pKeyName, + 0, KEY_ENUMERATE_SUB_KEYS | DELETE, &hKey )) == ERROR_SUCCESS) + { + while (dwRtn == ERROR_SUCCESS ) + { + dwSubKeyLength = MAX_KEY_LENGTH; + dwRtn=RegEnumKeyEx( + hKey, + 0, // always index zero, because we're deleting it + szSubKey, + &dwSubKeyLength, + NULL, + NULL, + NULL, + NULL + ); + + if(dwRtn == ERROR_NO_MORE_ITEMS) + { + dwRtn = RegDeleteKey(hStartKey, pKeyName); + break; + } + else if(dwRtn == ERROR_SUCCESS) + dwRtn=RegDeleteKeyNT(hKey, szSubKey); + } + RegCloseKey(hKey); + // Do not save return code because error + // has already occurred + } + } + else + dwRtn = ERROR_BADKEY; + + return dwRtn; +} diff --git a/app/Registry.h b/app/Registry.h new file mode 100644 index 0000000..42540a4 --- /dev/null +++ b/app/Registry.h @@ -0,0 +1,85 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * A class representing the system registry. + */ +#ifndef __REGISTRY__ +#define __REGISTRY__ + + +/* + * All access to the registry (except for GetProfileInt/GetProfileString) + * should go through this. + */ +class MyRegistry { +public: + MyRegistry(void) {} + ~MyRegistry(void) {} + + typedef enum RegStatus { + kRegUnknown = 0, + kRegNotSet, // unregistered + kRegExpired, // unregistered, expired + kRegValid, // registration present and valid + kRegInvalid, // registration present, but invalid (!) + kRegFailed, // error occurred during registration + } RegStatus; + + void OneTimeInstall(void) const; + void OneTimeUninstall(void) const; + + /* + int GetRegistration(CString* pUser, CString* pCompany, + CString* pReg, CString* pVersions, CString* pExpire); + int SetRegistration(const CString& user, const CString& company, + const CString& reg, const CString& versions, const CString& expire); + RegStatus CheckRegistration(CString* pResult); + bool IsValidRegistrationKey(const CString& user, + const CString& company, const CString& reg); + */ + + // Get the registry key to be used for our application. + const char* GetAppRegistryKey(void) const; + + // Fix basic settings, e.g. HKCR AppID classes. + void FixBasicSettings(void) const; + + int GetNumFileAssocs(void) const; + void GetFileAssoc(int idx, CString* pExt, CString* pHandler, + bool* pOurs) const; + int SetFileAssoc(int idx, bool wantIt) const; + + static unsigned short ComputeStringCRC(const char* str); + +private: + typedef struct FileTypeAssoc { + const char* ext; // e.g. ".SHK" + const char* appID; // e.g. "CiderPress.NuFX" + } FileTypeAssoc; + + static const FileTypeAssoc kFileTypeAssoc[]; + + bool IsOurAppID(const char* id) const; + void ConfigureAppID(const char* appID, const char* descr, + const char* exeName, int iconIdx) const; + void ConfigureAppIDSubFields(HKEY hAppKey, const char* descr, + const char* exeName) const; + int GetAssocAppName(const CString& appID, CString* pCmd) const; + void ReduceToToken(CString* pStr) const; + bool GetAssocState(const char* ext) const; + int DisownExtension(const char* ext) const; + int OwnExtension(const char* ext, const char* appID) const; + DWORD RegDeleteKeyNT(HKEY hStartKey, LPCTSTR pKeyName) const; + + /* key validation */ + static unsigned short CalcCRC16(unsigned short seed, + const unsigned char* ptr, int count); + static char* StripStrings(const char* str1, const char* str2); + void ComputeKey(const char* chBuf, int salt, long* pKeyLo, long* pKeyHi); + int VerifyKey(const char* user, const char* company, const char* key); +}; + +#endif /*__REGISTRY__*/ \ No newline at end of file diff --git a/app/RenameEntryDialog.cpp b/app/RenameEntryDialog.cpp new file mode 100644 index 0000000..297a690 --- /dev/null +++ b/app/RenameEntryDialog.cpp @@ -0,0 +1,135 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Support for RenameEntryDialog. + */ +#include "stdafx.h" +#include "RenameEntryDialog.h" +#include "HelpTopics.h" + +BEGIN_MESSAGE_MAP(RenameEntryDialog, CDialog) + ON_WM_HELPINFO() + ON_COMMAND(IDHELP, OnHelp) + ON_BN_CLICKED(IDC_RENAME_SKIP, OnSkip) +END_MESSAGE_MAP() + + +/* + * Set up the control. + */ +BOOL +RenameEntryDialog::OnInitDialog(void) +{ + ASSERT(fBasePath.IsEmpty()); + fOldFile = fOldName; + fFssepStr = fFssep; + + CEdit* pEdit = (CEdit*) GetDlgItem(IDC_RENAME_PATHSEP); + pEdit->SetReadOnly(!fCanChangeFssep); + pEdit->LimitText(1); + + /* if they can't rename the full path, only give them the file name */ + if (fCanRenameFullPath || fFssep == '\0') { + fNewName = fOldName; + // fBasePath is empty + } else { + int offset; + + offset = fOldName.ReverseFind(fFssep); + if (offset < fOldName.GetLength()) { + fBasePath = fOldName.Left(offset); + fNewName = fOldName.Right(fOldName.GetLength() - (offset+1)); + } else { + /* weird -- filename ended with an fssep? */ + ASSERT(false); // debugbreak + // fBasePath is empty + fNewName = fOldName; + } + } + + /* do the DoDataExchange stuff */ + CDialog::OnInitDialog(); + + /* select the editable text and set the focus */ + pEdit = (CEdit*) GetDlgItem(IDC_RENAME_NEW); + ASSERT(pEdit != nil); + pEdit->SetSel(0, -1); + pEdit->SetFocus(); + + return FALSE; // we set the focus +} + +/* + * Convert values. + */ +void +RenameEntryDialog::DoDataExchange(CDataExchange* pDX) +{ + CString msg, failed; + + msg = ""; + failed.LoadString(IDS_MB_APP_NAME); + + /* fNewName must come last, or the focus will be set on the wrong field + when we return after failure */ + DDX_Text(pDX, IDC_RENAME_OLD, fOldFile); + DDX_Text(pDX, IDC_RENAME_PATHSEP, fFssepStr); + DDX_Text(pDX, IDC_RENAME_NEW, fNewName); + + /* validate the path field */ + if (pDX->m_bSaveAndValidate) { + if (fNewName.IsEmpty()) { + msg = "You must specify a new name."; + goto fail; + } + + msg = fpArchive->TestPathName(fpEntry, fBasePath, fNewName, fFssep); + if (!msg.IsEmpty()) + goto fail; + + if (fFssepStr.IsEmpty()) + fFssep = '\0'; + else + fFssep = fFssepStr.GetAt(0); // could be '\0', that's okay + } + + return; + +fail: + ASSERT(!msg.IsEmpty()); + MessageBox(msg, failed, MB_OK); + pDX->Fail(); + return; +} + +/* + * User pressed the "skip" button, which causes us to bail with a result that + * skips the rename but continues with the series. + */ +void +RenameEntryDialog::OnSkip(void) +{ + EndDialog(IDIGNORE); +} + +/* + * Context help request (question mark button). + */ +BOOL +RenameEntryDialog::OnHelpInfo(HELPINFO* lpHelpInfo) +{ + WinHelp((DWORD) lpHelpInfo->iCtrlId, HELP_CONTEXTPOPUP); + return TRUE; // yes, we handled it +} + +/* + * User pressed the "Help" button. + */ +void +RenameEntryDialog::OnHelp(void) +{ + WinHelp(HELP_TOPIC_RENAME_ENTRY, HELP_CONTEXT); +} diff --git a/app/RenameEntryDialog.h b/app/RenameEntryDialog.h new file mode 100644 index 0000000..d2d221d --- /dev/null +++ b/app/RenameEntryDialog.h @@ -0,0 +1,70 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Rename an archive entry. + */ +#ifndef __RENAMEENTRYDIALOG__ +#define __RENAMEENTRYDIALOG__ + +#include "GenericArchive.h" +#include "resource.h" + +/* + * Rename an entry in an archive (as opposed to renaming a file on the local + * hard drive). + * + * We use the GenericArchive to verify that the name is valid before we + * shut the dialog. + * + * We should probably dim the "Skip" button when there aren't any more entries + * to rename. This requires that the caller tell us when we're on the last + * one. + */ +class RenameEntryDialog : public CDialog { +public: + RenameEntryDialog(CWnd* pParentWnd = NULL) : + CDialog(IDD_RENAME_ENTRY, pParentWnd) + { + fFssep = '='; + //fNewNameLimit = 0; + fpArchive = nil; + fpEntry = nil; + fCanRenameFullPath = false; + fCanChangeFssep = false; + } + virtual ~RenameEntryDialog(void) {} + + void SetCanRenameFullPath(bool val) { fCanRenameFullPath = val; } + void SetCanChangeFssep(bool val) { fCanChangeFssep = val; } + + CString fOldName; + char fFssep; + CString fNewName; + //int fNewNameLimit; // max #of chars accepted, or 0 + const GenericArchive* fpArchive; + const GenericEntry* fpEntry; + +protected: + // overrides + virtual BOOL OnInitDialog(void); + virtual void DoDataExchange(CDataExchange* pDX); + + afx_msg void OnSkip(void); + afx_msg BOOL OnHelpInfo(HELPINFO* lpHelpInfo); + afx_msg void OnHelp(void); + +private: + //CString fOldPath; // pathname component, or empty if canRenFull + CString fOldFile; // filename component, or full name if ^^^ + CString fBasePath; + CString fFssepStr; + bool fCanRenameFullPath; + bool fCanChangeFssep; + + DECLARE_MESSAGE_MAP() +}; + +#endif /*__RENAMEENTRYDIALOG__*/ diff --git a/app/RenameVolumeDialog.cpp b/app/RenameVolumeDialog.cpp new file mode 100644 index 0000000..eddd436 --- /dev/null +++ b/app/RenameVolumeDialog.cpp @@ -0,0 +1,180 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Implementation of RenameVolumeDialog. + * + * Show a tree with possible volumes and sub-volumes, and ask the user to + * enter the desired name (or volume number). + * + * We need to have the tree, rather than just clicking on an entry in the file + * list, because we want to be able to change names and volume numbers on + * disks with no files. + */ +#include "stdafx.h" +#include "RenameVolumeDialog.h" +#include "DiskFSTree.h" +#include "DiskArchive.h" +#include "HelpTopics.h" + +BEGIN_MESSAGE_MAP(RenameVolumeDialog, CDialog) + ON_NOTIFY(TVN_SELCHANGED, IDC_RENAMEVOL_TREE, OnSelChanged) + ON_BN_CLICKED(IDHELP, OnHelp) + ON_WM_HELPINFO() +END_MESSAGE_MAP() + +/* + * Set up the control. + */ +BOOL +RenameVolumeDialog::OnInitDialog(void) +{ + /* do the DoDataExchange stuff */ + CDialog::OnInitDialog(); + + CTreeCtrl* pTree = (CTreeCtrl*) GetDlgItem(IDC_RENAMEVOL_TREE); + DiskImgLib::DiskFS* pDiskFS = fpArchive->GetDiskFS(); + + ASSERT(pTree != nil); + + fDiskFSTree.fIncludeSubdirs = false; + fDiskFSTree.fExpandDepth = -1; + if (!fDiskFSTree.BuildTree(pDiskFS, pTree)) { + WMSG0("Tree load failed!\n"); + OnCancel(); + } + + int count = pTree->GetCount(); + WMSG1("ChooseAddTargetDialog tree has %d items\n", count); + + /* select the default text and set the focus */ + CEdit* pEdit = (CEdit*) GetDlgItem(IDC_RENAMEVOL_NEW); + ASSERT(pEdit != nil); + pEdit->SetSel(0, -1); + pEdit->SetFocus(); + + return FALSE; // we set the focus +} + +/* + * Convert values. + */ +void +RenameVolumeDialog::DoDataExchange(CDataExchange* pDX) +{ + CString msg, failed; + //DiskImgLib::DiskFS* pDiskFS = fpArchive->GetDiskFS(); + + msg = ""; + failed.LoadString(IDS_MB_APP_NAME); + + /* put fNewName last so it gets the focus after failure */ + DDX_Text(pDX, IDC_RENAMEVOL_NEW, fNewName); + + /* validate the path field */ + if (pDX->m_bSaveAndValidate) { + /* + * Make sure they chose a volume that can be modified. + */ + CTreeCtrl* pTree = (CTreeCtrl*) GetDlgItem(IDC_RENAMEVOL_TREE); + CString errMsg, appName; + appName.LoadString(IDS_MB_APP_NAME); + + HTREEITEM selected; + selected = pTree->GetSelectedItem(); + if (selected == nil) { + errMsg = "Please select a disk to rename."; + MessageBox(errMsg, appName, MB_OK); + pDX->Fail(); + return; + } + + DiskFSTree::TargetData* pTargetData; + pTargetData = (DiskFSTree::TargetData*) pTree->GetItemData(selected); + if (!pTargetData->selectable) { + errMsg = "You can't rename that volume."; + MessageBox(errMsg, appName, MB_OK); + pDX->Fail(); + return; + } + ASSERT(pTargetData->kind == DiskFSTree::kTargetDiskFS); + + /* + * Verify that the new name is okay. (Do this *after* checking the + * volume above to avoid spurious complaints about unsupported + * filesystems.) + */ + if (fNewName.IsEmpty()) { + msg = "You must specify a new name."; + goto fail; + } + msg = fpArchive->TestVolumeName(pTargetData->pDiskFS, fNewName); + if (!msg.IsEmpty()) + goto fail; + + + /* + * Looks good. Fill in the answer. + */ + fpChosenDiskFS = pTargetData->pDiskFS; + } + + return; + +fail: + ASSERT(!msg.IsEmpty()); + MessageBox(msg, failed, MB_OK); + pDX->Fail(); + return; +} + +/* + * Get a notification whenever the selection changes. Use it to stuff a + * default value into the edit box. + */ +void +RenameVolumeDialog::OnSelChanged(NMHDR* pnmh, LRESULT* pResult) +{ + CTreeCtrl* pTree = (CTreeCtrl*) GetDlgItem(IDC_RENAMEVOL_TREE); + HTREEITEM selected; + CString newText; + + selected = pTree->GetSelectedItem(); + if (selected != nil) { + DiskFSTree::TargetData* pTargetData; + pTargetData = (DiskFSTree::TargetData*) pTree->GetItemData(selected); + if (pTargetData->selectable) { + newText = pTargetData->pDiskFS->GetBareVolumeName(); + } else { + newText = ""; + } + } + + CEdit* pEdit = (CEdit*) GetDlgItem(IDC_RENAMEVOL_NEW); + ASSERT(pEdit != nil); + pEdit->SetWindowText(newText); + pEdit->SetSel(0, -1); + + *pResult = 0; +} + +/* + * Context help request (question mark button). + */ +BOOL +RenameVolumeDialog::OnHelpInfo(HELPINFO* lpHelpInfo) +{ + WinHelp((DWORD) lpHelpInfo->iCtrlId, HELP_CONTEXTPOPUP); + return TRUE; // yes, we handled it +} + +/* + * User pressed Ye Olde Helppe Button. + */ +void +RenameVolumeDialog::OnHelp(void) +{ + WinHelp(HELP_TOPIC_RENAME_VOLUME, HELP_CONTEXT); +} diff --git a/app/RenameVolumeDialog.h b/app/RenameVolumeDialog.h new file mode 100644 index 0000000..520e7a6 --- /dev/null +++ b/app/RenameVolumeDialog.h @@ -0,0 +1,50 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Declarations for "rename volume" dialog. + */ +#ifndef __RENAMEVOLUME__ +#define __RENAMEVOLUME__ + +#include "DiskFSTree.h" +#include "resource.h" + +class DiskArchive; + +/* + * Get a pointer to the DiskFS that we're altering, and a valid string for + * the new volume name. + */ +class RenameVolumeDialog : public CDialog { +public: + RenameVolumeDialog(CWnd* pParentWnd = NULL) : + CDialog(IDD_RENAME_VOLUME, pParentWnd) + { + fpArchive = nil; + } + virtual ~RenameVolumeDialog(void) {} + + const DiskArchive* fpArchive; + CString fNewName; + DiskImgLib::DiskFS* fpChosenDiskFS; + +protected: + // overrides + virtual BOOL OnInitDialog(void); + virtual void DoDataExchange(CDataExchange* pDX); + + afx_msg void OnSelChanged(NMHDR* pnmh, LRESULT* pResult); + afx_msg BOOL OnHelpInfo(HELPINFO* lpHelpInfo); + afx_msg void OnHelp(void); + + DiskFSTree fDiskFSTree; + +private: + + DECLARE_MESSAGE_MAP() +}; + +#endif /*__RENAMEVOLUME__*/ diff --git a/app/Squeeze.cpp b/app/Squeeze.cpp new file mode 100644 index 0000000..ca5a6a9 --- /dev/null +++ b/app/Squeeze.cpp @@ -0,0 +1,411 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Implementation of SQueeze (RLE+Huffman) compression. + * + * This was ripped fairly directly from Squeeze.c in NufxLib. Because + * there's relatively little code, and providing direct access to the + * compression functions already in NuLib is a little unwieldy, I've just + * cut & pasted the necessary pieces here. + */ +#include "stdafx.h" +#include "Squeeze.h" +#include "NufxArchive.h" + +#define kSqBufferSize 8192 /* must hold full SQ header, and % 128 */ + +#define kNuSQMagic 0xff76 /* magic value for file header */ +#define kNuSQRLEDelim 0x90 /* RLE delimiter */ +#define kNuSQEOFToken 256 /* distinguished stop symbol */ +#define kNuSQNumVals 257 /* 256 symbols + stop */ + + +/* + * =========================================================================== + * Unsqueeze + * =========================================================================== + */ + +/* + * State during uncompression. + */ +typedef struct USQState { + unsigned long dataInBuffer; + unsigned char* dataPtr; + int bitPosn; + int bits; + + /* + * Decoding tree; first "nodeCount" values are populated. Positive + * values are indices to another node in the tree, negative values + * are literals (+1 because "negative zero" doesn't work well). + */ + int nodeCount; + struct { + short child[2]; /* left/right kids, must be signed 16-bit */ + } decTree[kNuSQNumVals-1]; +} USQState; + + +/* + * Decode the next symbol from the Huffman stream. + */ +static NuError +USQDecodeHuffSymbol(USQState* pUsqState, int* pVal) +{ + short val = 0; + int bits, bitPosn; + + bits = pUsqState->bits; /* local copy */ + bitPosn = pUsqState->bitPosn; + + do { + if (++bitPosn > 7) { + /* grab the next byte and use that */ + bits = *pUsqState->dataPtr++; + bitPosn = 0; + if (!pUsqState->dataInBuffer--) + return kNuErrBufferUnderrun; + + val = pUsqState->decTree[val].child[1 & bits]; + } else { + /* still got bits; shift right and use it */ + val = pUsqState->decTree[val].child[1 & (bits >>= 1)]; + } + } while (val >= 0); + + /* val is negative literal; add one to make it zero-based then negate it */ + *pVal = -(val + 1); + + pUsqState->bits = bits; + pUsqState->bitPosn = bitPosn; + + return kNuErrNone; +} + + +/* + * Read two bytes of signed data out of the buffer. + */ +static inline NuError +USQReadShort(USQState* pUsqState, short* pShort) +{ + if (pUsqState->dataInBuffer < 2) + return kNuErrBufferUnderrun; + + *pShort = *pUsqState->dataPtr++; + *pShort |= (*pUsqState->dataPtr++) << 8; + pUsqState->dataInBuffer -= 2; + + return kNuErrNone; +} + +/* + * Wrapper for fread(). Note the arguments resemble read(2) rather + * than fread(3S). + */ +static NuError +SQRead(FILE* fp, void* buf, size_t nbyte) +{ + size_t result; + + ASSERT(buf != nil); + ASSERT(nbyte > 0); + ASSERT(fp != nil); + + errno = 0; + result = fread(buf, 1, nbyte, fp); + if (result != nbyte) + return errno ? (NuError)errno : kNuErrFileRead; + return kNuErrNone; +} + + +/* + * Expand "SQ" format. Archive file should already be seeked. + * + * Because we have a stop symbol, knowing the uncompressed length of + * the file is not essential. + * + * If "outExp" is nil, no output is produced (useful for "test" mode). + */ +NuError +UnSqueeze(FILE* fp, unsigned long realEOF, ExpandBuffer* outExp, + bool fullSqHeader, int blockSize) +{ + NuError err = kNuErrNone; + USQState usqState; + unsigned long compRemaining, getSize; + unsigned short magic, fileChecksum, checksum; // fullSqHeader only + short nodeCount; + int i, inrep; + unsigned char* tmpBuf = nil; + unsigned char lastc = 0; + + tmpBuf = (unsigned char*) malloc(kSqBufferSize); + if (tmpBuf == nil) { + err = kNuErrMalloc; + goto bail; + } + + usqState.dataInBuffer = 0; + usqState.dataPtr = tmpBuf; + + compRemaining = realEOF; + if ((fullSqHeader && compRemaining < 8) || + (!fullSqHeader && compRemaining < 3)) + { + err = kNuErrBadData; + WMSG0("too short to be valid SQ data\n"); + goto bail; + } + + /* + * Round up to the nearest 128-byte boundary. We need to read + * everything out of the file in case this is a streaming archive. + * Because the compressed data has an embedded stop symbol, it's okay + * to "overrun" the expansion code. + */ + if (blockSize != 0) { + compRemaining = + ((compRemaining + blockSize-1) / blockSize) * blockSize; + } + + /* want to grab up to kSqBufferSize bytes */ + if (compRemaining > kSqBufferSize) + getSize = kSqBufferSize; + else + getSize = compRemaining; + + /* + * Grab a big chunk. "compRemaining" is the amount of compressed + * data left in the file, usqState.dataInBuffer is the amount of + * compressed data left in the buffer. + * + * For BNY, we want to read 128-byte blocks. + */ + if (getSize) { + ASSERT(getSize <= kSqBufferSize); + err = SQRead(fp, usqState.dataPtr, getSize); + if (err != kNuErrNone) { + WMSG1("failed reading compressed data (%ld bytes)\n", getSize); + goto bail; + } + usqState.dataInBuffer += getSize; + if (getSize > compRemaining) + compRemaining = 0; + else + compRemaining -= getSize; + } + + /* reset dataPtr */ + usqState.dataPtr = tmpBuf; + + /* + * Read the header. We assume that the header will fit in the + * compression buffer ( sq allowed 300+ for the filename, plus + * 257*2 for the tree, plus misc). + */ + ASSERT(kSqBufferSize > 1200); + if (fullSqHeader) { + err = USQReadShort(&usqState, (short*)&magic); + if (err != kNuErrNone) + goto bail; + if (magic != kNuSQMagic) { + err = kNuErrBadData; + WMSG0("bad magic number in SQ block\n"); + goto bail; + } + + err = USQReadShort(&usqState, (short*)&fileChecksum); + if (err != kNuErrNone) + goto bail; + + checksum = 0; + + /* skip over the filename */ + while (*usqState.dataPtr++ != '\0') + usqState.dataInBuffer--; + usqState.dataInBuffer--; + } + + err = USQReadShort(&usqState, &nodeCount); + if (err != kNuErrNone) + goto bail; + if (nodeCount < 0 || nodeCount >= kNuSQNumVals) { + err = kNuErrBadData; + WMSG1("invalid decode tree in SQ (%d nodes)\n", nodeCount); + goto bail; + } + usqState.nodeCount = nodeCount; + + /* initialize for possibly empty tree (only happens on an empty file) */ + usqState.decTree[0].child[0] = -(kNuSQEOFToken+1); + usqState.decTree[0].child[1] = -(kNuSQEOFToken+1); + + /* read the nodes, ignoring "read errors" until we're done */ + for (i = 0; i < nodeCount; i++) { + err = USQReadShort(&usqState, &usqState.decTree[i].child[0]); + err = USQReadShort(&usqState, &usqState.decTree[i].child[1]); + } + if (err != kNuErrNone) { + err = kNuErrBadData; + WMSG0("SQ data looks truncated at tree\n"); + goto bail; + } + + usqState.bitPosn = 99; /* force an immediate read */ + + /* + * Start pulling data out of the file. We have to Huffman-decode + * the input, and then feed that into an RLE expander. + * + * A completely lopsided (and broken) Huffman tree could require + * 256 tree descents, so we want to try to ensure we have at least 256 + * bits in the buffer. Otherwise, we could get a false buffer underrun + * indication back from DecodeHuffSymbol. + * + * The SQ sources actually guarantee that a code will fit entirely + * in 16 bits, but there's no reason not to use the larger value. + */ + inrep = false; + while (1) { + int val; + + if (usqState.dataInBuffer < 65 && compRemaining) { + /* + * Less than 256 bits, but there's more in the file. + * + * First thing we do is slide the old data to the start of + * the buffer. + */ + if (usqState.dataInBuffer) { + ASSERT(tmpBuf != usqState.dataPtr); + memmove(tmpBuf, usqState.dataPtr, usqState.dataInBuffer); + } + usqState.dataPtr = tmpBuf; + + /* + * Next we read as much as we can. + */ + if (kSqBufferSize - usqState.dataInBuffer < compRemaining) + getSize = kSqBufferSize - usqState.dataInBuffer; + else + getSize = compRemaining; + + ASSERT(getSize <= kSqBufferSize); + //WMSG2("Reading from offset=%ld (compRem=%ld)\n", + // ftell(fp), compRemaining); + err = SQRead(fp, usqState.dataPtr + usqState.dataInBuffer, + getSize); + if (err != kNuErrNone) { + WMSG2("failed reading compressed data (%ld bytes, err=%d)\n", + getSize, err); + goto bail; + } + usqState.dataInBuffer += getSize; + if (getSize > compRemaining) + compRemaining = 0; + else + compRemaining -= getSize; + + ASSERT(compRemaining < 32767*65536); + ASSERT(usqState.dataInBuffer <= kSqBufferSize); + } + + err = USQDecodeHuffSymbol(&usqState, &val); + if (err != kNuErrNone) { + WMSG0("failed decoding huff symbol\n"); + goto bail; + } + + if (val == kNuSQEOFToken) + break; + + /* + * Feed the symbol into the RLE decoder. + */ + if (inrep) { + /* + * Last char was RLE delim, handle this specially. We use + * --val instead of val-- because we already emitted the + * first occurrence of the char (right before the RLE delim). + */ + if (val == 0) { + /* special case -- just an escaped RLE delim */ + lastc = kNuSQRLEDelim; + val = 2; + } + while (--val) { + /*if (pCrc != nil) + *pCrc = Nu_CalcCRC16(*pCrc, &lastc, 1);*/ + if (outExp != nil) + outExp->Putc(lastc); + if (fullSqHeader) { + checksum += lastc; + } + } + inrep = false; + } else { + /* last char was ordinary */ + if (val == kNuSQRLEDelim) { + /* set a flag and catch the count the next time around */ + inrep = true; + } else { + lastc = val; + /*if (pCrc != nil) + *pCrc = Nu_CalcCRC16(*pCrc, &lastc, 1);*/ + if (outExp != nil) + outExp->Putc(lastc); + if (fullSqHeader) { + checksum += lastc; + } + } + } + + } + + if (inrep) { + err = kNuErrBadData; + WMSG0("got stop symbol when run length expected\n"); + goto bail; + } + + if (fullSqHeader) { + /* verify the checksum stored in the SQ file */ + if (checksum != fileChecksum) { + err = kNuErrBadDataCRC; + WMSG2("expected 0x%04x, got 0x%04x (SQ)\n", fileChecksum, checksum); + goto bail; + } else { + WMSG1("--- SQ checksums match (0x%04x)\n", checksum); + } + } + + /* + * Gobble up any unused bytes in the last 128-byte block. There + * shouldn't be more than that left over. + */ + if (compRemaining > kSqBufferSize) { + err = kNuErrBadData; + WMSG1("wow: found %ld bytes left over\n", compRemaining); + goto bail; + } + if (compRemaining) { + WMSG1("+++ slurping up last %ld bytes\n", compRemaining); + err = SQRead(fp, tmpBuf, compRemaining); + if (err != kNuErrNone) { + WMSG0("failed reading leftovers\n"); + goto bail; + } + } + +bail: + //if (outfp != nil) + // fflush(outfp); + free(tmpBuf); + return err; +} diff --git a/app/Squeeze.h b/app/Squeeze.h new file mode 100644 index 0000000..827bfb0 --- /dev/null +++ b/app/Squeeze.h @@ -0,0 +1,15 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Implementation of SQueeze compression. + */ +#ifndef __SQUEEZE__ +#define __SQUEEZE__ + +NuError UnSqueeze(FILE* fp, unsigned long realEOF, ExpandBuffer* outExp, + bool fullSqHeader, int blockSize); + +#endif /*__SQUEEZE__*/ diff --git a/app/StdAfx.cpp b/app/StdAfx.cpp new file mode 100644 index 0000000..ebc3fd8 --- /dev/null +++ b/app/StdAfx.cpp @@ -0,0 +1,13 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +// stdafx.cpp : source file that includes just the standard includes +// diskimg.pch will be the pre-compiled header +// stdafx.obj will contain the pre-compiled type information + +#include "stdafx.h" + +// TODO: reference any additional headers you need in STDAFX.H +// and not in this file diff --git a/app/StdAfx.h b/app/StdAfx.h new file mode 100644 index 0000000..ddd5c67 --- /dev/null +++ b/app/StdAfx.h @@ -0,0 +1,45 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +// stdafx.h : include file for standard system include files, +// or project specific include files that are used frequently, but +// are changed infrequently +// + +#if !defined(AFX_STDAFX_H__1CB7B33E_42BF_4A98_B814_4198EA8ACC57__INCLUDED_) +#define AFX_STDAFX_H__1CB7B33E_42BF_4A98_B814_4198EA8ACC57__INCLUDED_ + +#if _MSC_VER > 1000 +#pragma once +#endif // _MSC_VER > 1000 + +// Insert your headers here +#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers +#define VC_EXTRALEAN + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "../diskimg/DiskImg.h" +#include "../util/UtilLib.h" +#include "Main.h" + +// TODO: reference additional headers your program requires here + +//{{AFX_INSERT_LOCATION}} +// Microsoft Visual C++ will insert additional declarations immediately before the previous line. + +#endif // !defined(AFX_STDAFX_H__1CB7B33E_42BF_4A98_B814_4198EA8ACC57__INCLUDED_) diff --git a/app/SubVolumeDialog.cpp b/app/SubVolumeDialog.cpp new file mode 100644 index 0000000..0302602 --- /dev/null +++ b/app/SubVolumeDialog.cpp @@ -0,0 +1,64 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Support for the sub-volume selection dialog. + * + * This just picks a sub-volume. Image format overrides and blocks vs. + * sectors should be chosen elsewhere. + */ +#include "stdafx.h" +#include "SubVolumeDialog.h" +#include "resource.h" + + +BEGIN_MESSAGE_MAP(SubVolumeDialog, CDialog) + ON_LBN_DBLCLK(IDC_SUBV_LIST, OnItemDoubleClicked) +END_MESSAGE_MAP() + + +/* + * Set up the control. + */ +BOOL +SubVolumeDialog::OnInitDialog(void) +{ + ASSERT(fpDiskFS != nil); + + CListBox* pListBox = (CListBox*) GetDlgItem(IDC_SUBV_LIST); + ASSERT(pListBox != nil); + +// if (pListBox->SetTabStops(12) != TRUE) { +// ASSERT(false); +// } + + DiskFS::SubVolume* pSubVol = fpDiskFS->GetNextSubVolume(nil); + ASSERT(pSubVol != nil); // shouldn't be here otherwise + while (pSubVol != nil) { + pListBox->AddString(pSubVol->GetDiskFS()->GetVolumeID()); + + pSubVol = fpDiskFS->GetNextSubVolume(pSubVol); + } + + return CDialog::OnInitDialog(); +} + +/* + * Do the DDX thang. + */ +void +SubVolumeDialog::DoDataExchange(CDataExchange* pDX) +{ + DDX_LBIndex(pDX, IDC_SUBV_LIST, fListBoxIndex); +} + +/* + * Accept a double-click as an "OK". + */ +void +SubVolumeDialog::OnItemDoubleClicked(void) +{ + OnOK(); +} diff --git a/app/SubVolumeDialog.h b/app/SubVolumeDialog.h new file mode 100644 index 0000000..c25cab6 --- /dev/null +++ b/app/SubVolumeDialog.h @@ -0,0 +1,47 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Sub-volume selection dialog. + */ +#ifndef __SUBVOLUMEDIALOG__ +#define __SUBVOLUMEDIALOG__ + +#include "resource.h" +#include "../diskimg/DiskImg.h" +using namespace DiskImgLib; + +/* + * Display the sub-volume selection dialog, which is primarily a list box + * with the sub-volumes listed in it. + */ +class SubVolumeDialog : public CDialog { +public: + SubVolumeDialog(CWnd* pParentWnd = NULL) : + CDialog(IDD_SUBV, pParentWnd) + { + fListBoxIndex = 0; + } + virtual ~SubVolumeDialog(void) {} + + void Setup(DiskFS* pDiskFS) { fpDiskFS = pDiskFS; } + + /* so long as we don't sort the list, this number is enough */ + int fListBoxIndex; + +protected: + // overrides + virtual BOOL OnInitDialog(void); + virtual void DoDataExchange(CDataExchange* pDX); + + afx_msg void OnItemDoubleClicked(void); + +private: + DiskFS* fpDiskFS; + + DECLARE_MESSAGE_MAP() +}; + +#endif /*__SUBVOLUMEDIALOG__*/ \ No newline at end of file diff --git a/app/Tools.cpp b/app/Tools.cpp new file mode 100644 index 0000000..f5b7aa2 --- /dev/null +++ b/app/Tools.cpp @@ -0,0 +1,2495 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Some items from the "tools" menu. + * + * [ There's a lot of cutting and pasting going on in here. Some of this + * stuff needs to get refactored. ++ATM 20040729 ] + */ +#include "StdAfx.h" +#include "Main.h" +#include "DiskEditDialog.h" +#include "ImageFormatDialog.h" +#include "DiskConvertDialog.h" +#include "ChooseDirDialog.h" +#include "DoneOpenDialog.h" +#include "OpenVolumeDialog.h" +#include "DiskEditOpenDialog.h" +#include "VolumeCopyDialog.h" +#include "CreateImageDialog.h" +#include "DiskArchive.h" +#include "EOLScanDialog.h" +#include "TwoImgPropsDialog.h" +#include // need chsize() for TwoImgProps + + +/* + * Put up the ImageFormatDialog and apply changes to "pImg". + * + * "*pDisplayFormat" gets the result of user changes to the display format. + * If "pDisplayFormat" is nil, the "query image format" feature will be + * disabled. + * + * Returns IDCANCEL if the user cancelled out of the dialog, IDOK otherwise. + * On error, "*pErrMsg" will be non-empty. + */ +int +MainWindow::TryDiskImgOverride(DiskImg* pImg, const char* fileSource, + DiskImg::FSFormat defaultFormat, int* pDisplayFormat, bool allowUnknown, + CString* pErrMsg) +{ + ImageFormatDialog imf; + + *pErrMsg = ""; + imf.InitializeValues(pImg); + imf.fFileSource = fileSource; + imf.fAllowUnknown = allowUnknown; + if (pDisplayFormat == nil) + imf.SetQueryDisplayFormat(false); + + /* don't show "unknown format" if we have a default value */ + if (defaultFormat != DiskImg::kFormatUnknown && + imf.fFSFormat == DiskImg::kFormatUnknown) + { + imf.fFSFormat = defaultFormat; + } + + WMSG2(" On entry, sectord=%d format=%d\n", + imf.fSectorOrder, imf.fFSFormat); + + if (imf.DoModal() != IDOK) { + WMSG0(" User bailed on IMF dialog\n"); + return IDCANCEL; + } + + WMSG2(" On exit, sectord=%d format=%d\n", + imf.fSectorOrder, imf.fFSFormat); + + if (pDisplayFormat != nil) + *pDisplayFormat = imf.fDisplayFormat; + if (imf.fSectorOrder != pImg->GetSectorOrder() || + imf.fFSFormat != pImg->GetFSFormat()) + { + WMSG0("Initial values overridden, forcing img format\n"); + DIError dierr; + dierr = pImg->OverrideFormat(pImg->GetPhysicalFormat(), imf.fFSFormat, + imf.fSectorOrder); + if (dierr != kDIErrNone) { + pErrMsg->Format("Unable to access disk image using selected" + " parameters. Error: %s.", + DiskImgLib::DIStrError(dierr)); + // fall through to "return IDOK" + } + } + + return IDOK; +} + + +/* + * ========================================================================== + * Disk Editor + * ========================================================================== + */ + +/* + * User wants to edit a disk. + */ +void +MainWindow::OnToolsDiskEdit(void) +{ + DIError dierr; + DiskImg img; + CString loadName, saveFolder; + CString failed, errMsg; + DiskEditOpenDialog diskEditOpen(this); + /* create three, show one */ + BlockEditDialog blockEdit(this); + SectorEditDialog sectorEdit(this); + NibbleEditDialog nibbleEdit(this); + DiskEditDialog* pEditDialog; + int displayFormat; + bool readOnly = true; + + /* flush current archive in case that's what we're planning to edit */ + OnFileSave(); + + failed.LoadString(IDS_FAILED); + + diskEditOpen.fArchiveOpen = false; + if (fpOpenArchive != nil && + fpOpenArchive->GetArchiveKind() == GenericArchive::kArchiveDiskImage) + { + diskEditOpen.fArchiveOpen = true; + } + + if (diskEditOpen.DoModal() != IDOK) + goto bail; + + /* + * Choose something to open, based on "fOpenWhat". + */ + if (diskEditOpen.fOpenWhat == DiskEditOpenDialog::kOpenFile) { + CString openFilters, saveFolder; + + openFilters = kOpenDiskImage; + openFilters += kOpenAll; + openFilters += kOpenEnd; + CFileDialog dlg(TRUE, "dsk", NULL, OFN_FILEMUSTEXIST, openFilters, this); + + /* for now, everything is read-only */ + dlg.m_ofn.Flags |= OFN_HIDEREADONLY; + dlg.m_ofn.lpstrInitialDir = fPreferences.GetPrefString(kPrOpenArchiveFolder); + + if (dlg.DoModal() != IDOK) + goto bail; + + loadName = dlg.GetPathName(); + readOnly = true; // add to file dialog + + saveFolder = dlg.m_ofn.lpstrFile; + saveFolder = saveFolder.Left(dlg.m_ofn.nFileOffset); + fPreferences.SetPrefString(kPrOpenArchiveFolder, saveFolder); + } else if (diskEditOpen.fOpenWhat == DiskEditOpenDialog::kOpenVolume) { + OpenVolumeDialog dlg(this); + int result; + + result = dlg.DoModal(); + if (result != IDOK) + goto bail; + + loadName = dlg.fChosenDrive; + readOnly = (dlg.fReadOnly != 0); + } else if (diskEditOpen.fOpenWhat == DiskEditOpenDialog::kOpenCurrent) { + // get values from currently open archive + loadName = fpOpenArchive->GetPathName(); + readOnly = fpOpenArchive->IsReadOnly(); + } else { + WMSG1("GLITCH: unexpected fOpenWhat %d\n", diskEditOpen.fOpenWhat); + ASSERT(false); + goto bail; + } + + WMSG3("Disk editor what=%d name='%s' ro=%d\n", + diskEditOpen.fOpenWhat, loadName, readOnly); + + +#if 1 + { + CWaitCursor waitc; + /* open the image file and analyze it */ + dierr = img.OpenImage(loadName, PathProposal::kLocalFssep, true); + } +#else + /* quick test of memory-buffer-based interface */ + FILE* tmpfp; + char* phatbuf; + long length; + + tmpfp = fopen(loadName, "rb"); + ASSERT(tmpfp != nil); + fseek(tmpfp, 0, SEEK_END); + length = ftell(tmpfp); + rewind(tmpfp); + WMSG1(" PHATBUF %d\n", length); + phatbuf = new char[length]; + if (fread(phatbuf, length, 1, tmpfp) != 1) + WMSG1("FREAD FAILED %d\n", errno); + fclose(tmpfp); + dierr = img.OpenImage(phatbuf, length, true); +#endif + + if (dierr != kDIErrNone) { + errMsg.Format("Unable to open disk image: %s.", + DiskImgLib::DIStrError(dierr)); + MessageBox(errMsg, failed, MB_OK|MB_ICONSTOP); + goto bail; + } + +#if 0 + { + /* + * TEST - set custom entry to match Sheila NIB image. We have to + * do this here so that the disk routines can analyze the disk + * correctly. We need a way to enter these parameters in the + * disk editor and then re-analyze the image. (Not to mention a way + * to flip in and out of block/sector/nibble mode.) + */ + DiskImg::NibbleDescr sheilaDescr = + { + "H.A.L. Labs (Sheila)", + 16, + { 0xd5, 0xaa, 0xad }, { 0xde, 0xaa, 0xeb }, + 0x00, // checksum seed + true, // verify checksum + true, // verify track + 2, // epilog verify count + { 0xd5, 0xaa, 0xda }, { 0xde, 0xaa, 0xeb }, + 0x00, // checksum seed + true, // verify checksum + 2, // epilog verify count + DiskImg::kNibbleEnc62, + DiskImg::kNibbleSpecialNone, + }; + /* same thing, but for original 13-sector Zork */ + DiskImg::NibbleDescr zork13Descr = + { + "Zork 13-sector", + 13, + { 0xd5, 0xaa, 0xb5 }, { 0xde, 0xaa, 0xeb }, + 0x00, + false, + false, + 0, + { 0xd5, 0xaa, 0xad }, { 0xde, 0xaa, 0xeb }, + 0x1f, + true, + 0, + DiskImg::kNibbleEnc53, + DiskImg::kNibbleSpecialNone, + }; + + img.SetCustomNibbleDescr(&zork13Descr); + } +#endif + + + if (img.AnalyzeImage() != kDIErrNone) { + errMsg.Format("The file '%s' doesn't seem to hold a valid disk image.", + loadName); + MessageBox(errMsg, failed, MB_OK|MB_ICONSTOP); + goto bail; + } + + if (img.ShowAsBlocks()) + displayFormat = ImageFormatDialog::kShowAsBlocks; + else + displayFormat = ImageFormatDialog::kShowAsSectors; + + /* if they can't do anything but view nibbles, don't demand an fs format */ + bool allowUnknown; + allowUnknown = false; + if (!img.GetHasSectors() && !img.GetHasBlocks() && img.GetHasNibbles()) + allowUnknown = true; + + /* + * If requested (or necessary), verify the format. + */ + if (img.GetFSFormat() == DiskImg::kFormatUnknown || + img.GetSectorOrder() == DiskImg::kSectorOrderUnknown || + fPreferences.GetPrefBool(kPrQueryImageFormat)) + { + if (TryDiskImgOverride(&img, loadName, DiskImg::kFormatUnknown, + &displayFormat, allowUnknown, &errMsg) != IDOK) + { + goto bail; + } + if (!errMsg.IsEmpty()) { + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + } + + + /* select edit dialog type, based on blocks vs. sectors */ + if (displayFormat == ImageFormatDialog::kShowAsSectors) + pEditDialog = §orEdit; + else if (displayFormat == ImageFormatDialog::kShowAsBlocks) + pEditDialog = &blockEdit; + else + pEditDialog = &nibbleEdit; + + /* + * Create an appropriate DiskFS object and hand it to the edit dialog. + */ + DiskFS* pDiskFS; + pDiskFS = img.OpenAppropriateDiskFS(true); + if (pDiskFS == nil) { + WMSG0("HEY: OpenAppropriateDiskFS failed!\n"); + goto bail; + } + + pDiskFS->SetScanForSubVolumes(DiskFS::kScanSubEnabled); + + { + CWaitCursor wait; // big ProDOS volumes can be slow + dierr = pDiskFS->Initialize(&img, DiskFS::kInitFull); + } + if (dierr != kDIErrNone) { + errMsg.Format("Warning: error during disk scan: %s.", + DiskImgLib::DIStrError(dierr)); + MessageBox(errMsg, failed, MB_OK | MB_ICONEXCLAMATION); + /* keep going */ + } + pEditDialog->Setup(pDiskFS, loadName); + (void) pEditDialog->DoModal(); + + delete pDiskFS; + + /* + * FUTURE: if we edited the file we have open in the contentlist, + * we need to post a warning and/or close it in the contentlist. + * Or maybe just re-open it? Allow that as an option. + */ + +bail: +#if 0 + delete phatbuf; +#endif + return; +} + + +/* + * ========================================================================== + * Disk Converter + * ========================================================================== + */ + +/* + * Convert a disk image from one format to another. + */ +void +MainWindow::OnToolsDiskConv(void) +{ + DIError dierr; + CString openFilters, errMsg; + CString loadName, saveName, saveFolder; + DiskImg srcImg, dstImg; + DiskConvertDialog convDlg(this); + CString storageName; + + /* flush current archive in case that's what we're planning to convert */ + OnFileSave(); + + dstImg.SetNuFXCompressionType(fPreferences.GetPrefLong(kPrCompressionType)); + + /* + * Select the image to convert. + */ + openFilters = kOpenDiskImage; + openFilters += kOpenAll; + openFilters += kOpenEnd; + CFileDialog dlg(TRUE, "dsk", NULL, OFN_FILEMUSTEXIST, openFilters, this); + + /* for now, everything is read-only */ + dlg.m_ofn.Flags |= OFN_HIDEREADONLY; + dlg.m_ofn.lpstrTitle = "Select image to convert"; + dlg.m_ofn.lpstrInitialDir = fPreferences.GetPrefString(kPrOpenArchiveFolder); + + if (dlg.DoModal() != IDOK) + goto bail; + loadName = dlg.GetPathName(); + + saveFolder = dlg.m_ofn.lpstrFile; + saveFolder = saveFolder.Left(dlg.m_ofn.nFileOffset); + fPreferences.SetPrefString(kPrOpenArchiveFolder, saveFolder); + + /* open the image file and analyze it */ + dierr = srcImg.OpenImage(loadName, PathProposal::kLocalFssep, true); + if (dierr != kDIErrNone) { + errMsg.Format("Unable to open disk image: %s.", + DiskImgLib::DIStrError(dierr)); + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + + if (srcImg.AnalyzeImage() != kDIErrNone) { + errMsg.Format("The file '%s' doesn't seem to hold a valid disk image.", + loadName); + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + + /* + * If confirm image format is set, or we can't figure out the sector + * ordering, prompt the user. + */ + if (srcImg.GetSectorOrder() == DiskImg::kSectorOrderUnknown || + fPreferences.GetPrefBool(kPrQueryImageFormat)) + { + if (TryDiskImgOverride(&srcImg, loadName, DiskImg::kFormatGenericProDOSOrd, + nil, false, &errMsg) != IDOK) + { + goto bail; + } + if (!errMsg.IsEmpty()) { + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + } + + /* + * If this is a ProDOS volume, use the disk volume name as the default + * value for "storageName" (which is used for NuFX archives and DC42). + */ + if (srcImg.GetFSFormat() == DiskImg::kFormatProDOS) { + CWaitCursor waitc; + DiskFS* pDiskFS = srcImg.OpenAppropriateDiskFS(); + // use "headerOnly", which gets the volume name + dierr = pDiskFS->Initialize(&srcImg, DiskFS::kInitHeaderOnly); + if (dierr == kDIErrNone) { + storageName = pDiskFS->GetVolumeName(); + } + delete pDiskFS; + } else { + /* use filename as storageName (exception for DiskCopy42 later) */ + storageName = FilenameOnly(loadName, '\\'); + } + WMSG1(" Using '%s' as storageName\n", storageName); + + /* transfer the DOS volume num, if one was set */ + dstImg.SetDOSVolumeNum(srcImg.GetDOSVolumeNum()); + WMSG1("DOS volume number set to %d\n", dstImg.GetDOSVolumeNum()); + + DiskImg::FSFormat origFSFormat; + origFSFormat = srcImg.GetFSFormat(); + + /* + * The converter always tries to read and write images as if they were + * ProDOS blocks. This way the only sector ordering changes are caused by + * differences in the sector ordering, rather than differences in the + * assumed filesystem types (which may not be knowable). + */ + dierr = srcImg.OverrideFormat(srcImg.GetPhysicalFormat(), + DiskImg::kFormatGenericProDOSOrd, srcImg.GetSectorOrder()); + if (dierr != kDIErrNone) { + errMsg.Format("Internal error: couldn't switch to generic ProDOS: %s.", + DiskImgLib::DIStrError(dierr)); + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + + /* + * Put up a dialog to figure out what we want to do with this image. + * + * We have a fair amount of faith that this will not pick impossible + * combinations. If we do, we will fail later on in CreateImage. + */ + convDlg.Init(&srcImg); + if (convDlg.DoModal() != IDOK) { + WMSG0(" User bailed out of convert dialog\n"); + goto bail; + } + + /* + * Examine their choices. + */ + DiskImg::OuterFormat outerFormat; + DiskImg::FileFormat fileFormat; + DiskImg::PhysicalFormat physicalFormat; + DiskImg::SectorOrder sectorOrder; + + if (DetermineImageSettings(convDlg.fConvertIdx, (convDlg.fAddGzip != 0), + &outerFormat, &fileFormat, &physicalFormat, §orOrder) != 0) + { + goto bail; + } + + const DiskImg::NibbleDescr* pNibbleDescr; + pNibbleDescr = srcImg.GetNibbleDescr(); + if (pNibbleDescr == nil && DiskImg::IsNibbleFormat(physicalFormat)) { + /* + * We're writing to a nibble format, so we have to decide how the + * disk should be formatted. The source doesn't specify it, so we + * use generic 13- or 16-sector, defaulting to the latter when in + * doubt. + */ + if (srcImg.GetHasSectors() && srcImg.GetNumSectPerTrack() == 13) { + pNibbleDescr = DiskImg::GetStdNibbleDescr( + DiskImg::kNibbleDescrDOS32Std); + } else { + pNibbleDescr = DiskImg::GetStdNibbleDescr( + DiskImg::kNibbleDescrDOS33Std); + } + } + WMSG2(" NibbleDescr is 0x%08lx (%s)\n", (long) pNibbleDescr, + pNibbleDescr != nil ? pNibbleDescr->description : "---"); + + if (srcImg.GetFileFormat() == DiskImg::kFileFormatTrackStar && + fileFormat != DiskImg::kFileFormatTrackStar) + { + /* converting from TrackStar to anything else */ + CString msg, appName; + msg.LoadString(IDS_TRACKSTAR_TO_OTHER_WARNING); + appName.LoadString(IDS_MB_APP_NAME); + if (MessageBox(msg, appName, MB_OKCANCEL | MB_ICONWARNING) != IDOK) { + WMSG0(" User bailed after trackstar-to-other warning\n"); + goto bail; + } + } else if (srcImg.GetFileFormat() == DiskImg::kFileFormatFDI && + fileFormat != DiskImg::kFileFormatTrackStar && + srcImg.GetNumBlocks() != 1600) + { + /* converting from 5.25" FDI to anything but TrackStar */ + CString msg, appName; + msg.LoadString(IDS_FDI_TO_OTHER_WARNING); + appName.LoadString(IDS_MB_APP_NAME); + if (MessageBox(msg, appName, MB_OKCANCEL | MB_ICONWARNING) != IDOK) { + WMSG0(" User bailed after fdi-to-other warning\n"); + goto bail; + } + } else if (srcImg.GetHasNibbles() && DiskImg::IsSectorFormat(physicalFormat)) + { + /* converting from nibble to non-nibble format */ + CString msg, appName; + msg.LoadString(IDS_NIBBLE_TO_SECTOR_WARNING); + appName.LoadString(IDS_MB_APP_NAME); + if (MessageBox(msg, appName, MB_OKCANCEL | MB_ICONWARNING) != IDOK) { + WMSG0(" User bailed after nibble-to-sector warning\n"); + goto bail; + } + } else if (srcImg.GetHasNibbles() && + DiskImg::IsNibbleFormat(physicalFormat) && + srcImg.GetPhysicalFormat() != physicalFormat) + { + /* converting between differing nibble formats */ + CString msg, appName; + msg.LoadString(IDS_DIFFERENT_NIBBLE_WARNING); + appName.LoadString(IDS_MB_APP_NAME); + if (MessageBox(msg, appName, MB_OKCANCEL | MB_ICONWARNING) != IDOK) { + WMSG0(" User bailed after differing-nibbles warning\n"); + goto bail; + } + } + + /* + * If the source is a UNIDOS volume and the target format is DiskCopy 4.2, + * use DOS sector ordering instead of ProDOS block ordering. For some + * reason the disks come out that way. + */ + if (origFSFormat == DiskImg::kFormatUNIDOS && + fileFormat == DiskImg::kFileFormatDiskCopy42) + { + WMSG0(" Switching to DOS sector ordering for UNIDOS/DiskCopy42"); + sectorOrder = DiskImg::kSectorOrderDOS; + } + if (origFSFormat != DiskImg::kFormatProDOS && + fileFormat == DiskImg::kFileFormatDiskCopy42) + { + WMSG0(" Nuking storage name for non-ProDOS DiskCopy42 image"); + storageName = ""; // want to use "-not a mac disk" for non-ProDOS + } + + /* + * Pick file to save into. + */ + { + CFileDialog saveDlg(FALSE, convDlg.fExtension, NULL, + OFN_OVERWRITEPROMPT|OFN_NOREADONLYRETURN|OFN_HIDEREADONLY, + "All Files (*.*)|*.*||", this); + + CString saveFolder; + CString title = "New disk image (."; + title += convDlg.fExtension; + title += ")"; + + saveDlg.m_ofn.lpstrTitle = title; + saveDlg.m_ofn.lpstrInitialDir = + fPreferences.GetPrefString(kPrConvertArchiveFolder); + + if (saveDlg.DoModal() != IDOK) { + WMSG0(" User bailed out of image save dialog\n"); + goto bail; + } + + saveFolder = saveDlg.m_ofn.lpstrFile; + saveFolder = saveFolder.Left(saveDlg.m_ofn.nFileOffset); + fPreferences.SetPrefString(kPrConvertArchiveFolder, saveFolder); + + saveName = saveDlg.GetPathName(); + } + WMSG1("File will be saved to '%s'\n", saveName); + + /* DiskImgLib does not like it if file already exists */ + errMsg = RemoveFile(saveName); + if (!errMsg.IsEmpty()) { + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + + /* + * Create the image file. Adjust the number of tracks if we're + * copying to or from TrackStar or FDI images. + */ + int dstNumTracks; + int dstNumBlocks; + bool isPartial; + dstNumTracks = srcImg.GetNumTracks(); + dstNumBlocks = srcImg.GetNumBlocks(); + isPartial = false; + + if (srcImg.GetFileFormat() == DiskImg::kFileFormatTrackStar && + fileFormat != DiskImg::kFileFormatTrackStar && + srcImg.GetNumTracks() == 40) + { + /* from TrackStar to other */ + dstNumTracks = 35; + dstNumBlocks = 280; + isPartial = true; + } + if (srcImg.GetFileFormat() == DiskImg::kFileFormatFDI && + fileFormat != DiskImg::kFileFormatFDI && + srcImg.GetNumTracks() != 35 && srcImg.GetNumBlocks() != 1600) + { + /* from 5.25" FDI to other */ + dstNumTracks = 35; + dstNumBlocks = 280; + isPartial = true; + } + + if (srcImg.GetFileFormat() != DiskImg::kFileFormatTrackStar && + fileFormat == DiskImg::kFileFormatTrackStar && + dstNumTracks == 35) + { + /* from other to TrackStar */ + isPartial = true; + } + + if (srcImg.GetHasNibbles() && + DiskImg::IsNibbleFormat(physicalFormat) && + physicalFormat == srcImg.GetPhysicalFormat()) + { + /* + * For nibble-to-nibble with the same track format, copy it as + * a collection of tracks. + */ + dierr = dstImg.CreateImage(saveName, storageName, + outerFormat, + fileFormat, + physicalFormat, + pNibbleDescr, + sectorOrder, + DiskImg::kFormatGenericProDOSOrd, + dstNumTracks, srcImg.GetNumSectPerTrack(), + false /* must format */); + } else if (srcImg.GetHasBlocks()) { + /* + * For general case, copy as a block image, converting in and out of + * nibbles as needed. + */ + dierr = dstImg.CreateImage(saveName, storageName, + outerFormat, + fileFormat, + physicalFormat, + pNibbleDescr, + sectorOrder, + DiskImg::kFormatGenericProDOSOrd, + dstNumBlocks, + false /* only needed for nibble?? */); + } else if (srcImg.GetHasSectors()) { + /* + * We should only get here when converting to/from D13. We have to + * special-case this because this was originally written to support + * block copying as the lowest common denominator. D13 screwed + * everything up. :-) + */ + dierr = dstImg.CreateImage(saveName, storageName, + outerFormat, + fileFormat, + physicalFormat, + pNibbleDescr, + sectorOrder, + DiskImg::kFormatGenericProDOSOrd, // needs to match above + dstNumTracks, srcImg.GetNumSectPerTrack(), + false /* only need for dest=nibble? */); + } else { + /* + * Generally speaking, we don't allow the user to make choices that + * would get us here. In particular, the UI should not allow the + * user to convert directly between nibble formats when the source + * image doesn't have a recognizeable block format. + */ + ASSERT(false); + dierr = kDIErrInternal; + } + if (dierr != kDIErrNone) { + errMsg.Format("Couldn't create disk image: %s.", + DiskImgLib::DIStrError(dierr)); + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + + /* + * Do the actual copy, either as blocks or tracks. + */ + dierr = CopyDiskImage(&dstImg, &srcImg, false, isPartial, nil); + if (dierr != kDIErrNone) { + errMsg.Format("Copy failed: %s.", DiskImgLib::DIStrError(dierr)); + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + + dierr = srcImg.CloseImage(); + if (dierr != kDIErrNone) { + errMsg.Format("ERROR: srcImg close failed (err=%d)\n", dierr); + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + + dierr = dstImg.CloseImage(); + if (dierr != kDIErrNone) { + errMsg.Format("ERROR: dstImg close failed (err=%d)\n", dierr); + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + + SuccessBeep(); + + /* + * We're done. Give them the opportunity to open the disk image they + * just created. + */ + { + DoneOpenDialog doneOpen(this); + + if (doneOpen.DoModal() == IDOK) { + WMSG1(" At user request, opening '%s'\n", saveName); + + DoOpenArchive(saveName, convDlg.fExtension, + kFilterIndexDiskImage, false); + } + } + +bail: + return; +} + +/* + * Determine the settings we need to pass into DiskImgLib to create the + * desired disk image format. + * + * Returns 0 on success, -1 on failure. + */ +int +MainWindow::DetermineImageSettings(int convertIdx, bool addGzip, + DiskImg::OuterFormat* pOuterFormat, DiskImg::FileFormat* pFileFormat, + DiskImg::PhysicalFormat* pPhysicalFormat, + DiskImg::SectorOrder* pSectorOrder) +{ + if (addGzip) + *pOuterFormat = DiskImg::kOuterFormatGzip; + else + *pOuterFormat = DiskImg::kOuterFormatNone; + + switch (convertIdx) { + case DiskConvertDialog::kConvDOSRaw: + *pFileFormat = DiskImg::kFileFormatUnadorned; + *pPhysicalFormat = DiskImg::kPhysicalFormatSectors; + *pSectorOrder = DiskImg::kSectorOrderDOS; + break; + case DiskConvertDialog::kConvDOS2MG: + *pFileFormat = DiskImg::kFileFormat2MG; + *pPhysicalFormat = DiskImg::kPhysicalFormatSectors; + *pSectorOrder = DiskImg::kSectorOrderDOS; + break; + case DiskConvertDialog::kConvProDOSRaw: + *pFileFormat = DiskImg::kFileFormatUnadorned; + *pPhysicalFormat = DiskImg::kPhysicalFormatSectors; + *pSectorOrder = DiskImg::kSectorOrderProDOS; + break; + case DiskConvertDialog::kConvProDOS2MG: + *pFileFormat = DiskImg::kFileFormat2MG; + *pPhysicalFormat = DiskImg::kPhysicalFormatSectors; + *pSectorOrder = DiskImg::kSectorOrderProDOS; + break; + case DiskConvertDialog::kConvNibbleRaw: + *pFileFormat = DiskImg::kFileFormatUnadorned; + *pPhysicalFormat = DiskImg::kPhysicalFormatNib525_6656; + *pSectorOrder = DiskImg::kSectorOrderPhysical; + break; + case DiskConvertDialog::kConvNibble2MG: + *pFileFormat = DiskImg::kFileFormat2MG; + *pPhysicalFormat = DiskImg::kPhysicalFormatNib525_6656; + *pSectorOrder = DiskImg::kSectorOrderPhysical; + break; + case DiskConvertDialog::kConvD13: + *pFileFormat = DiskImg::kFileFormatUnadorned; + *pPhysicalFormat = DiskImg::kPhysicalFormatSectors; + *pSectorOrder = DiskImg::kSectorOrderDOS; + break; + case DiskConvertDialog::kConvDiskCopy42: + *pFileFormat = DiskImg::kFileFormatDiskCopy42; + *pPhysicalFormat = DiskImg::kPhysicalFormatSectors; + *pSectorOrder = DiskImg::kSectorOrderProDOS; + break; + case DiskConvertDialog::kConvTrackStar: + *pFileFormat = DiskImg::kFileFormatTrackStar; + *pPhysicalFormat = DiskImg::kPhysicalFormatNib525_Var; + *pSectorOrder = DiskImg::kSectorOrderPhysical; + break; + case DiskConvertDialog::kConvNuFX: + *pFileFormat = DiskImg::kFileFormatNuFX; + *pPhysicalFormat = DiskImg::kPhysicalFormatSectors; + *pSectorOrder = DiskImg::kSectorOrderProDOS; + break; + case DiskConvertDialog::kConvSim2eHDV: + *pFileFormat = DiskImg::kFileFormatSim2eHDV; + *pPhysicalFormat = DiskImg::kPhysicalFormatSectors; + *pSectorOrder = DiskImg::kSectorOrderProDOS; + break; + case DiskConvertDialog::kConvDDD: + *pFileFormat = DiskImg::kFileFormatDDD; + *pPhysicalFormat = DiskImg::kPhysicalFormatSectors; + *pSectorOrder = DiskImg::kSectorOrderDOS; + break; + default: + ASSERT(false); + WMSG1(" WHOA: invalid conv type %d\n", convertIdx); + return -1; + } + + return 0; +} + +static inline int MIN(int val1, int val2) +{ + return (val1 < val2) ? val1 : val2; +} + +/* + * Do a block copy or track copy from one disk image to another. + * + * If "bulk" is set, warning dialogs are suppressed. If "partial" is set, + * copies between volumes of different sizes are allowed. + * + * This originally just did a block copy. Nibble track copies were added + * later, and sector copies were added even later. + */ +DIError +MainWindow::CopyDiskImage(DiskImg* pDstImg, DiskImg* pSrcImg, bool bulk, + bool partial, ProgressCancelDialog* pPCDialog) +{ + DIError dierr = kDIErrNone; + CString errMsg; + unsigned char* dataBuf = nil; + + if (pSrcImg->GetHasNibbles() && pDstImg->GetHasNibbles() && + pSrcImg->GetPhysicalFormat() == pDstImg->GetPhysicalFormat()) + { + /* + * Copy as a series of nibble tracks. + * + * NOTE: we could do better here for 6384 to ".app", but in + * practice nobody cares anyway. + */ + if (!partial) { + ASSERT(pSrcImg->GetNumTracks() == pDstImg->GetNumTracks()); + } + + //unsigned char trackBuf[kTrackAllocSize]; + long trackLen; + int numTracks; + + dataBuf = new unsigned char[kTrackAllocSize]; + if (dataBuf == nil) { + dierr = kDIErrMalloc; + goto bail; + } + + numTracks = MIN(pSrcImg->GetNumTracks(), pDstImg->GetNumTracks()); + WMSG1("Nibble track copy (%d tracks)\n", numTracks); + for (int track = 0; track < numTracks; track++) { + dierr = pSrcImg->ReadNibbleTrack(track, dataBuf, &trackLen); + if (dierr != kDIErrNone) { + errMsg.Format("ERROR: read on track %d failed (err=%d)\n", + track, dierr); + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + dierr = pDstImg->WriteNibbleTrack(track, dataBuf, trackLen); + if (dierr != kDIErrNone) { + errMsg.Format("ERROR: write on track %d failed (err=%d)\n", + track, dierr); + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + + /* these aren't slow enough that we need progress updating */ + } + } else if (!pSrcImg->GetHasBlocks() || !pDstImg->GetHasBlocks()) { + /* + * Do a sector copy, for D13 images (which can't be accessed as blocks). + */ + if (!partial) { + ASSERT(pSrcImg->GetNumTracks() == pDstImg->GetNumTracks()); + ASSERT(pSrcImg->GetNumSectPerTrack() == pDstImg->GetNumSectPerTrack()); + } + + long numTracks, numSectPerTrack; + int numBadSectors = 0; + + dataBuf = new unsigned char[256]; // one sector + if (dataBuf == nil) { + dierr = kDIErrMalloc; + goto bail; + } + + numTracks = MIN(pSrcImg->GetNumTracks(), pDstImg->GetNumTracks()); + numSectPerTrack = MIN(pSrcImg->GetNumSectPerTrack(), + pDstImg->GetNumSectPerTrack()); + WMSG2("Sector copy (%d tracks / %d sectors)\n", + numTracks, numSectPerTrack); + for (int track = 0; track < numTracks; track++) { + for (int sector = 0; sector < numSectPerTrack; sector++) { + dierr = pSrcImg->ReadTrackSector(track, sector, dataBuf); + if (dierr != kDIErrNone) { + WMSG2("Bad sector T=%d S=%d\n", track, sector); + numBadSectors++; + dierr = kDIErrNone; + memset(dataBuf, 0, 256); + } + dierr = pDstImg->WriteTrackSector(track, sector, dataBuf); + if (dierr != kDIErrNone) { + errMsg.Format("ERROR: write of T=%d S=%d failed (err=%d)\n", + track, sector, dierr); + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + } + + /* these aren't slow enough that we need progress updating */ + } + + if (!bulk && numBadSectors != 0) { + CString appName; + appName.LoadString(IDS_MB_APP_NAME); + errMsg.Format("Skipped %ld unreadable sector%s.", numBadSectors, + numBadSectors == 1 ? "" : "s"); + MessageBox(errMsg, appName, MB_OK | MB_ICONWARNING); + } + } else { + /* + * Do a block copy, copying multiple blocks at a time for performance. + */ + if (!partial) { + ASSERT(pSrcImg->GetNumBlocks() == pDstImg->GetNumBlocks()); + } + + //unsigned char blkBuf[512]; + long numBadBlocks = 0; + long numBlocks; + int blocksPerRead; + + numBlocks = MIN(pSrcImg->GetNumBlocks(), pDstImg->GetNumBlocks()); + if (numBlocks <= 2880) + blocksPerRead = 9; // better granularity (one floppy track) + else + blocksPerRead = 64; // 32K per read; max seems to be 64K? + + dataBuf = new unsigned char[blocksPerRead * 512]; + if (dataBuf == nil) { + dierr = kDIErrMalloc; + goto bail; + } + + WMSG2("--- BLOCK COPY (%ld blocks, %d per)\n", + numBlocks, blocksPerRead); + for (long block = 0; block < numBlocks; ) { + long blocksThisTime = blocksPerRead; + if (block + blocksThisTime > numBlocks) + blocksThisTime = numBlocks - block; + + dierr = pSrcImg->ReadBlocks(block, blocksThisTime, dataBuf); + if (dierr != kDIErrNone) { + if (blocksThisTime != 1) { + /* + * Media with errors. Drop to one block per read. + */ + WMSG2(" Bad sector encountered at %ld(%ld), slowing\n", + block, blocksThisTime); + blocksThisTime = blocksPerRead = 1; + continue; // retry this block + } + numBadBlocks++; + dierr = kDIErrNone; + memset(dataBuf, 0, 512); + } + dierr = pDstImg->WriteBlocks(block, blocksThisTime, dataBuf); + if (dierr != kDIErrNone) { + if (dierr != kDIErrWriteProtected) { + errMsg.Format("ERROR: write of block %ld failed (%s)\n", + block, DiskImgLib::DIStrError(dierr)); + ShowFailureMsg(this, errMsg, IDS_FAILED); + } + goto bail; + } + + /* if we have a cancel dialog, keep it lively */ + if (pPCDialog != nil && (block % 18) == 0) { + int status; + PeekAndPump(); + LONGLONG bigBlock = block; + bigBlock = bigBlock * ProgressCancelDialog::kProgressResolution; + status = pPCDialog->SetProgress((int)(bigBlock / numBlocks)); + if (status == IDCANCEL) { + dierr = kDIErrCancelled; // pretend it came from DiskImg + goto bail; + } + } else if (bulk && (block % 512) == 0) { + PeekAndPump(); + } + + block += blocksThisTime; + } + + if (!bulk && numBadBlocks != 0) { + CString appName; + appName.LoadString(IDS_MB_APP_NAME); + errMsg.Format("Skipped %ld unreadable block%s.", numBadBlocks, + numBadBlocks == 1 ? "" : "s"); + MessageBox(errMsg, appName, MB_OK | MB_ICONWARNING); + } + } + +bail: + delete[] dataBuf; + return dierr; +} + + +/* + * ========================================================================== + * Bulk disk convert + * ========================================================================== + */ + +/* + * Sub-class the generic libutil CancelDialog class. + */ +class BulkConvCancelDialog : public CancelDialog { +public: + BOOL Create(CWnd* pParentWnd = NULL) { + fAbortOperation = false; + return CancelDialog::Create(&fAbortOperation, + IDD_BULKCONV, pParentWnd); + } + + void SetCurrentFile(const char* fileName) { + CWnd* pWnd = GetDlgItem(IDC_BULKCONV_PATHNAME); + ASSERT(pWnd != nil); + pWnd->SetWindowText(fileName); + } + + bool fAbortOperation; + +private: + void OnOK(void) { + WMSG0("Ignoring BulkConvCancelDialog OnOK\n"); + } + + MainWindow* GetMainWindow(void) const { + return (MainWindow*)::AfxGetMainWnd(); + } +}; + +/* + * Handle a request for a bulk disk conversion. + */ +void +MainWindow::OnToolsBulkDiskConv(void) +{ + const int kFileNameBufSize = 32768; + DiskConvertDialog convDlg(this); + ChooseDirDialog chooseDirDlg(this); + BulkConvCancelDialog* pCancelDialog = new BulkConvCancelDialog; // on heap + CString openFilters, errMsg; + CString saveFolder, targetDir; + int nameCount; + + /* flush current archive in case that's what we're planning to convert */ + OnFileSave(); + + /* + * Select the set of images to convert. + */ + openFilters = kOpenDiskImage; + openFilters += kOpenAll; + openFilters += kOpenEnd; + CFileDialog dlg(TRUE, "dsk", NULL, OFN_FILEMUSTEXIST, openFilters, this); + + dlg.m_ofn.lpstrFile = new char[kFileNameBufSize]; + dlg.m_ofn.lpstrFile[0] = dlg.m_ofn.lpstrFile[1] = '\0'; + dlg.m_ofn.nMaxFile = kFileNameBufSize; + dlg.m_ofn.Flags |= OFN_HIDEREADONLY; // open all images as read-only + dlg.m_ofn.Flags |= OFN_ALLOWMULTISELECT; + dlg.m_ofn.lpstrTitle = "Select images to convert"; + dlg.m_ofn.lpstrInitialDir = fPreferences.GetPrefString(kPrOpenArchiveFolder); + + if (dlg.DoModal() != IDOK) + goto bail; + + saveFolder = dlg.m_ofn.lpstrFile; + saveFolder = saveFolder.Left(dlg.m_ofn.nFileOffset); + fPreferences.SetPrefString(kPrOpenArchiveFolder, saveFolder); + + /* count up the number of entries */ + POSITION posn; + posn = dlg.GetStartPosition(); + nameCount = 0; + while (posn != nil) { + CString pathName; + pathName = dlg.GetNextPathName(posn); + nameCount++; + } + WMSG1("BulkConv got nameCount=%d\n", nameCount); + + /* + * Choose the target directory. + * + * We use the "convert archive" folder by default. + */ + chooseDirDlg.SetPathName(fPreferences.GetPrefString(kPrConvertArchiveFolder)); + if (chooseDirDlg.DoModal() != IDOK) + goto bail; + + targetDir = chooseDirDlg.GetPathName(); + fPreferences.SetPrefString(kPrConvertArchiveFolder, targetDir); + + /* + * Put up a dialog to select the target conversion format. + * + * It is up to the user to select a format that matches the selected + * files. If it doesn't (e.g. converting an 800K floppy to DDD format), + * the process will fail later on. + */ + convDlg.Init(nameCount); + if (convDlg.DoModal() != IDOK) { + WMSG0(" User bailed out of convert dialog\n"); + goto bail; + } + + /* initialize cancel dialog, and disable main window */ + EnableWindow(FALSE); + if (pCancelDialog->Create(this) == FALSE) { + WMSG0("Cancel dialog init failed?!\n"); + ASSERT(false); + goto bail; + } + + /* + * Loop through all selected files and convert them one at a time. + */ + posn = dlg.GetStartPosition(); + while (posn != nil) { + CString pathName; + pathName = dlg.GetNextPathName(posn); + WMSG1(" BulkConv: source path='%s'\n", pathName); + + pCancelDialog->SetCurrentFile(FilenameOnly(pathName, '\\')); + PeekAndPump(); + if (pCancelDialog->fAbortOperation) + break; + BulkConvertImage(pathName, targetDir, convDlg, &errMsg); + + if (!errMsg.IsEmpty()) { + /* show error message, do OK/Cancel */ + /* do we need to delete the output file on failure? In general + we can't, because we could have failed because the file + already existed. */ + CString failed; + int res; + + failed.LoadString(IDS_FAILED); + errMsg += "\n\nSource file: "; + errMsg += pathName; + errMsg += "\n\nClick OK to skip this and continue, or Cancel to " + "stop now."; + res = pCancelDialog->MessageBox(errMsg, + failed, MB_OKCANCEL | MB_ICONERROR); + if (res != IDOK) + goto bail; + } + } + + if (!pCancelDialog->fAbortOperation) + SuccessBeep(); + +bail: + // restore the main window to prominence + EnableWindow(TRUE); + //SetActiveWindow(); + if (pCancelDialog != nil) + pCancelDialog->DestroyWindow(); + + delete[] dlg.m_ofn.lpstrFile; + return; +} + +/* + * Convert one image during a bulk conversion. + * + * [Much of this is copy & pasted from OnToolsDiskConv(). This needs to get + * refactored.] + * + * On failure, the reason for failure is stuffed into "*pErrMsg". + */ +void +MainWindow::BulkConvertImage(const char* pathName, const char* targetDir, + const DiskConvertDialog& convDlg, CString* pErrMsg) +{ + DIError dierr; + CString saveName; + DiskImg srcImg, dstImg; + CString storageName; + PathName srcPath(pathName); + CString fileName, ext; + + *pErrMsg = ""; + + dstImg.SetNuFXCompressionType( + fPreferences.GetPrefLong(kPrCompressionType)); + + /* open the image file and analyze it */ + dierr = srcImg.OpenImage(pathName, PathProposal::kLocalFssep, true); + if (dierr != kDIErrNone) { + pErrMsg->Format("Unable to open disk image: %s.", + DiskImgLib::DIStrError(dierr)); + goto bail; + } + + if (srcImg.AnalyzeImage() != kDIErrNone) { + pErrMsg->Format("The file doesn't seem to hold a valid disk image."); + goto bail; + } + +#if 0 // don't feel like posting this UI + /* + * If we can't figure out the sector ordering, prompt the user. Don't + * go into it if they have "confirm format" selected, since that would be + * annoying. If they need to confirm it, they can use the one-at-a-time + * interface. + */ + if (srcImg.GetSectorOrder() == DiskImg::kSectorOrderUnknown) { + if (TryDiskImgOverride(&srcImg, pathName, DiskImg::kFormatGenericProDOSOrd, + nil, pErrMsg) != IDOK) + { + *pErrMsg = "Image conversion cancelled."; + } + if (!pErrMsg->IsEmpty()) + goto bail; + } +#else + if (srcImg.GetSectorOrder() == DiskImg::kSectorOrderUnknown) { + *pErrMsg = "Could not determine the disk image sector ordering. You " + "may need to change the file extension."; + goto bail; + } +#endif + + /* transfer the DOS volume num, if one was set */ + dstImg.SetDOSVolumeNum(srcImg.GetDOSVolumeNum()); + WMSG1("DOS volume number set to %d\n", dstImg.GetDOSVolumeNum()); + + DiskImg::FSFormat origFSFormat; + origFSFormat = srcImg.GetFSFormat(); + + /* + * The converter always tries to read and write images as if they were + * ProDOS blocks. This way the only sector ordering changes are caused by + * differences in the sector ordering, rather than differences in the + * assumed filesystem types (which may not be knowable). + */ + dierr = srcImg.OverrideFormat(srcImg.GetPhysicalFormat(), + DiskImg::kFormatGenericProDOSOrd, srcImg.GetSectorOrder()); + if (dierr != kDIErrNone) { + pErrMsg->Format("Internal error: couldn't switch to generic ProDOS: %s.", + DiskImgLib::DIStrError(dierr)); + goto bail; + } + + /* + * Examine their choices. + */ + DiskImg::OuterFormat outerFormat; + DiskImg::FileFormat fileFormat; + DiskImg::PhysicalFormat physicalFormat; + DiskImg::SectorOrder sectorOrder; + + if (DetermineImageSettings(convDlg.fConvertIdx, (convDlg.fAddGzip != 0), + &outerFormat, &fileFormat, &physicalFormat, §orOrder) != 0) + { + *pErrMsg = "Odd: couldn't configure image settings"; + goto bail; + } + + const DiskImg::NibbleDescr* pNibbleDescr; + pNibbleDescr = srcImg.GetNibbleDescr(); + if (pNibbleDescr == nil && DiskImg::IsNibbleFormat(physicalFormat)) { + /* + * We're writing to a nibble format, so we have to decide how the + * disk should be formatted. The source doesn't specify it, so we + * use generic 13- or 16-sector, defaulting to the latter when in + * doubt. + */ + if (srcImg.GetHasSectors() && srcImg.GetNumSectPerTrack() == 13) { + pNibbleDescr = DiskImg::GetStdNibbleDescr( + DiskImg::kNibbleDescrDOS32Std); + } else { + pNibbleDescr = DiskImg::GetStdNibbleDescr( + DiskImg::kNibbleDescrDOS33Std); + } + } + WMSG2(" NibbleDescr is 0x%08lx (%s)\n", (long) pNibbleDescr, + pNibbleDescr != nil ? pNibbleDescr->description : "---"); + + /* + * Create the new filename based on the old filename. + */ + saveName = targetDir; + if (saveName.Right(1) != '\\') + saveName += '\\'; + fileName = srcPath.GetFileName(); + ext = srcPath.GetExtension(); // extension, including '.' + if (ext.CompareNoCase(".gz") == 0) { + /* got a .gz, see if there's anything else in front of it */ + CString tmpName, ext2; + tmpName = srcPath.GetPathName(); + tmpName = tmpName.Left(tmpName.GetLength() - ext.GetLength()); + PathName tmpPath(tmpName); + ext2 = tmpPath.GetExtension(); + if (ext2.GetLength() >= 2 && ext2.GetLength() <= 4) + ext = ext2 + ext; + + saveName += fileName.Left(fileName.GetLength() - ext.GetLength()); + } else { + if (ext.GetLength() < 2 || ext.GetLength() > 4) { + /* no meaningful extension */ + saveName += fileName; + } else { + saveName += fileName.Left(fileName.GetLength() - ext.GetLength()); + } + } + storageName = FilenameOnly(saveName, '\\'); // grab this for SHK name + saveName += '.'; + saveName += convDlg.fExtension; + WMSG2(" Bulk converting '%s' to '%s'\n", pathName, saveName); + + /* + * If this is a ProDOS volume, use the disk volume name as the default + * value for "storageName" (which is only used for NuFX archives). + */ + if (srcImg.GetFSFormat() == DiskImg::kFormatProDOS) { + CWaitCursor waitc; + DiskFS* pDiskFS = srcImg.OpenAppropriateDiskFS(); + // set "headerOnly" since we only need the volume name + dierr = pDiskFS->Initialize(&srcImg, DiskFS::kInitHeaderOnly); + if (dierr == kDIErrNone) { + storageName = pDiskFS->GetVolumeName(); + } + delete pDiskFS; + } else { + /* just use storageName as set earlier, unless target is DiskCopy42 */ + if (fileFormat == DiskImg::kFileFormatDiskCopy42) + storageName = ""; // want to use "not a mac disk" for non-ProDOS + } + WMSG1(" Using '%s' as storageName\n", storageName); + + /* + * If the source is a UNIDOS volume and the target format is DiskCopy 4.2, + * use DOS sector ordering instead of ProDOS block ordering. For some + * reason the disks come out that way. + */ + if (origFSFormat == DiskImg::kFormatUNIDOS && + fileFormat == DiskImg::kFileFormatDiskCopy42) + { + WMSG0(" Switching to DOS sector ordering for UNIDOS/DiskCopy42"); + sectorOrder = DiskImg::kSectorOrderDOS; + } + + /* + * Create the image file. Adjust the number of tracks if we're + * copying to or from a TrackStar image. + */ + int dstNumTracks; + int dstNumBlocks; + bool isPartial; + dstNumTracks = srcImg.GetNumTracks(); + dstNumBlocks = srcImg.GetNumBlocks(); + isPartial = false; + + if (srcImg.GetFileFormat() == DiskImg::kFileFormatTrackStar && + fileFormat != DiskImg::kFileFormatTrackStar && + srcImg.GetNumTracks() == 40) + { + /* from TrackStar to other */ + dstNumTracks = 35; + dstNumBlocks = 280; + isPartial = true; + } + if (srcImg.GetFileFormat() == DiskImg::kFileFormatFDI && + fileFormat != DiskImg::kFileFormatTrackStar && + srcImg.GetNumTracks() != 35 && srcImg.GetNumBlocks() != 1600) + { + /* from 5.25" FDI to other */ + dstNumTracks = 35; + dstNumBlocks = 280; + isPartial = true; + } + + if (srcImg.GetFileFormat() != DiskImg::kFileFormatTrackStar && + fileFormat == DiskImg::kFileFormatTrackStar && + dstNumTracks == 35) + { + /* other to TrackStar */ + isPartial = true; + } + + if (srcImg.GetHasNibbles() && + DiskImg::IsNibbleFormat(physicalFormat) && + physicalFormat == srcImg.GetPhysicalFormat()) + { + /* for nibble-to-nibble with the same track format, copy it + as collection of tracks */ + dierr = dstImg.CreateImage(saveName, storageName, + outerFormat, + fileFormat, + physicalFormat, + pNibbleDescr, + sectorOrder, + DiskImg::kFormatGenericProDOSOrd, + srcImg.GetNumTracks(), srcImg.GetNumSectPerTrack(), + false /* must format */); + } else if (srcImg.GetHasBlocks()) { + /* for general case, create as a block image */ + ASSERT(srcImg.GetHasBlocks()); + dierr = dstImg.CreateImage(saveName, storageName, + outerFormat, + fileFormat, + physicalFormat, + pNibbleDescr, + sectorOrder, + DiskImg::kFormatGenericProDOSOrd, + srcImg.GetNumBlocks(), + false /* only need for nibble? */); + } else if (srcImg.GetHasSectors()) { + /* + * We should only get here when converting to/from D13. We have to + * special-case this because this was originally written to support + * block copying as the lowest common denominator. D13 screwed + * everything up. :-) + */ + dierr = dstImg.CreateImage(saveName, storageName, + outerFormat, + fileFormat, + physicalFormat, + pNibbleDescr, + sectorOrder, + DiskImg::kFormatGenericProDOSOrd, // needs to match above + dstNumTracks, srcImg.GetNumSectPerTrack(), + false /* only need for dest=nibble? */); + } else { + /* e.g. unrecognizeable nibble to blocks */ + *pErrMsg = "Could not convert to requested format."; + goto bail; + } + if (dierr != kDIErrNone) { + if (dierr == kDIErrInvalidCreateReq) + *pErrMsg = "Could not convert to requested format."; + else + pErrMsg->Format("Couldn't construct disk image: %s.", + DiskImgLib::DIStrError(dierr)); + goto bail; + } + + /* + * Do the actual copy, either as blocks or tracks. + */ + dierr = CopyDiskImage(&dstImg, &srcImg, true, isPartial, nil); + if (dierr != kDIErrNone) + goto bail; + + dierr = dstImg.CloseImage(); + if (dierr != kDIErrNone) { + pErrMsg->Format("ERROR: dstImg close failed (err=%d)\n", dierr); + goto bail; + } + + dierr = srcImg.CloseImage(); + if (dierr != kDIErrNone) { + pErrMsg->Format("ERROR: srcImg close failed (err=%d)\n", dierr); + goto bail; + } + +bail: + return; +} + + +/* + * ========================================================================== + * SST Merge + * ========================================================================== + */ + +const int kSSTNumTracks = 35; +const int kSSTNumSectPerTrack = 16; +const int kSSTTrackLen = 6656; + +/* + * Merge two SST images into a single NIB image. + */ +void +MainWindow::OnToolsSSTMerge(void) +{ + const int kBadCountThreshold = 3072; + DiskImg srcImg0, srcImg1; + CString appName, saveName, saveFolder, errMsg; + unsigned char* trackBuf = nil; + long badCount; + + // no need to flush -- can't really open raw SST images + + CFileDialog saveDlg(FALSE, _T("nib"), NULL, + OFN_OVERWRITEPROMPT|OFN_NOREADONLYRETURN|OFN_HIDEREADONLY, + "All Files (*.*)|*.*||", this); + + appName.LoadString(IDS_MB_APP_NAME); + + trackBuf = new unsigned char[kSSTNumTracks * kSSTTrackLen]; + if (trackBuf == nil) + goto bail; + + /* + * Open the two images and verify that they are what they seem. + */ + badCount = 0; + if (SSTOpenImage(0, &srcImg0) != 0) + goto bail; + if (SSTLoadData(0, &srcImg0, trackBuf, &badCount) != 0) + goto bail; + WMSG1("FOUND %ld bad bytes in part 0\n", badCount); + if (badCount > kBadCountThreshold) { + errMsg.LoadString(IDS_BAD_SST_IMAGE); + if (MessageBox(errMsg, appName, MB_OKCANCEL | MB_ICONWARNING) != IDOK) + goto bail; + } + + badCount = 0; + if (SSTOpenImage(1, &srcImg1) != 0) + goto bail; + if (SSTLoadData(1, &srcImg1, trackBuf, &badCount) != 0) + goto bail; + WMSG1("FOUND %ld bad bytes in part 1\n", badCount); + if (badCount > kBadCountThreshold) { + errMsg.LoadString(IDS_BAD_SST_IMAGE); + if (MessageBox(errMsg, appName, MB_OKCANCEL | MB_ICONWARNING) != IDOK) + goto bail; + } + + /* + * Realign the tracks and OR 0x80 to everything. + */ + SSTProcessTrackData(trackBuf); + + /* + * Pick the output file and write the buffer to it. + */ + saveDlg.m_ofn.lpstrTitle = _T("Save .NIB disk image as..."); + saveDlg.m_ofn.lpstrInitialDir = fPreferences.GetPrefString(kPrOpenArchiveFolder); + if (saveDlg.DoModal() != IDOK) { + WMSG0(" User bailed out of image save dialog\n"); + goto bail; + } + saveFolder = saveDlg.m_ofn.lpstrFile; + saveFolder = saveFolder.Left(saveDlg.m_ofn.nFileOffset); + fPreferences.SetPrefString(kPrOpenArchiveFolder, saveFolder); + + saveName = saveDlg.GetPathName(); + WMSG1("File will be saved to '%s'\n", saveName); + + /* remove the file if it exists */ + errMsg = RemoveFile(saveName); + if (!errMsg.IsEmpty()) { + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + + FILE* fp; + fp = fopen(saveName, "wb"); + if (fp == nil) { + errMsg.Format("Unable to create '%s': %s.", + saveName, strerror(errno)); + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + + if (fwrite(trackBuf, kSSTNumTracks * kSSTTrackLen, 1, fp) != 1) { + errMsg.Format("Failed while writing to new image file: %s.", + strerror(errno)); + ShowFailureMsg(this, errMsg, IDS_FAILED); + fclose(fp); + goto bail; + } + + fclose(fp); + + SuccessBeep(); + + /* + * We're done. Give them the opportunity to open the disk image they + * just created. + */ + { + DoneOpenDialog doneOpen(this); + + if (doneOpen.DoModal() == IDOK) { + WMSG1(" At user request, opening '%s'\n", saveName); + + DoOpenArchive(saveName, "nib", kFilterIndexDiskImage, false); + } + } + +bail: + delete[] trackBuf; + return; +} + +/* + * Open one of the SST images. + * + * Configures "pDiskImg" appropriately. + * + * Returns 0 on success, nonzero on failure. + */ +int +MainWindow::SSTOpenImage(int seqNum, DiskImg* pDiskImg) +{ + DIError dierr; + int result = -1; + CString openFilters, errMsg; + CString loadName, saveFolder; + + /* + * Select the image to convert. + */ + openFilters = kOpenDiskImage; + openFilters += kOpenAll; + openFilters += kOpenEnd; + CFileDialog dlg(TRUE, "dsk", NULL, OFN_FILEMUSTEXIST, openFilters, this); + + dlg.m_ofn.Flags |= OFN_HIDEREADONLY; + if (seqNum == 0) + dlg.m_ofn.lpstrTitle = "Select first SST image"; + else + dlg.m_ofn.lpstrTitle = "Select second SST image"; + dlg.m_ofn.lpstrInitialDir = fPreferences.GetPrefString(kPrOpenArchiveFolder); + + if (dlg.DoModal() != IDOK) + goto bail; + loadName = dlg.GetPathName(); + + saveFolder = dlg.m_ofn.lpstrFile; + saveFolder = saveFolder.Left(dlg.m_ofn.nFileOffset); + fPreferences.SetPrefString(kPrOpenArchiveFolder, saveFolder); + + /* open the image file and analyze it */ + dierr = pDiskImg->OpenImage(loadName, PathProposal::kLocalFssep, true); + if (dierr != kDIErrNone) { + errMsg.Format("Unable to open disk image: %s.", + DiskImgLib::DIStrError(dierr)); + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + + if (pDiskImg->AnalyzeImage() != kDIErrNone) { + errMsg.Format("The file '%s' doesn't seem to hold a valid disk image.", + loadName); + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + + /* + * If confirm image format is set, or we can't figure out the sector + * ordering, prompt the user. + */ + if (pDiskImg->GetSectorOrder() == DiskImg::kSectorOrderUnknown || + fPreferences.GetPrefBool(kPrQueryImageFormat)) + { + if (TryDiskImgOverride(pDiskImg, loadName, + DiskImg::kFormatGenericDOSOrd, nil, false, &errMsg) != IDOK) + { + goto bail; + } + if (!errMsg.IsEmpty()) { + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + } + + if (pDiskImg->GetFSFormat() != DiskImg::kFormatUnknown && + !DiskImg::IsGenericFormat(pDiskImg->GetFSFormat())) + { + errMsg = "This disk image appears to have a valid filesystem. SST" + " images are just raw track dumps."; + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + if (pDiskImg->GetNumTracks() != kSSTNumTracks || + pDiskImg->GetNumSectPerTrack() != kSSTNumSectPerTrack) + { + errMsg = "ERROR: only 5.25\" floppy disk images can be SST inputs."; + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + + /* use DOS filesystem sector ordering */ + dierr = pDiskImg->OverrideFormat(pDiskImg->GetPhysicalFormat(), + DiskImg::kFormatGenericDOSOrd, pDiskImg->GetSectorOrder()); + if (dierr != kDIErrNone) { + errMsg = "ERROR: internal failure: format override failed."; + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + + result = 0; + +bail: + return result; +} + +/* + * Copy 17.5 tracks of data from the SST image to a .NIB image. + * + * Data is stored in all 16 sectors of track 0, followed by the first + * 12 sectors of track 1, then on to track 2. Total of $1a00 bytes. + * + * Returns 0 on success, -1 on failure. + */ +int +MainWindow::SSTLoadData(int seqNum, DiskImg* pDiskImg, unsigned char* trackBuf, + long* pBadCount) +{ + DIError dierr; + unsigned char sctBuf[256]; + int track, sector; + long bufOffset; + + for (track = 0; track < kSSTNumTracks; track++) { + int virtualTrack = track + (seqNum * kSSTNumTracks); + bufOffset = SSTGetBufOffset(virtualTrack); + //WMSG3("USING offset=%ld (track=%d / %d)\n", + // bufOffset, track, virtualTrack); + + if (virtualTrack & 0x01) { + /* odd-numbered track, sectors 15-4 */ + for (sector = 15; sector >= 4; sector--) { + dierr = pDiskImg->ReadTrackSector(track, sector, sctBuf); + if (dierr != kDIErrNone) { + WMSG2("ERROR: on track=%d sector=%d\n", + track, sector); + return -1; + } + + *pBadCount += SSTCountBadBytes(sctBuf, 256); + + memcpy(trackBuf + bufOffset, sctBuf, 256); + bufOffset += 256; + } + } else { + for (sector = 13; sector >= 0; sector--) { + dierr = pDiskImg->ReadTrackSector(track, sector, sctBuf); + if (dierr != kDIErrNone) { + WMSG2("ERROR: on track=%d sector=%d\n", + track, sector); + return -1; + } + + *pBadCount += SSTCountBadBytes(sctBuf, 256); + + memcpy(trackBuf + bufOffset, sctBuf, 256); + bufOffset += 256; + } + } + } + + return 0; +} + +/* + * Compute the destination file offset for a particular source track. The + * track number ranges from 0 to 69 inclusive. Sectors from two adjacent + * "cooked" tracks are combined into a single "raw nibbilized" track. + * + * The data is ordered like this: + * track 1 sector 15 --> track 1 sector 4 (12 sectors) + * track 0 sector 13 --> track 0 sector 0 (14 sectors) + * + * Total of 26 sectors, or $1a00 bytes. + */ +long +MainWindow::SSTGetBufOffset(int track) +{ + assert(track >= 0 && track < kSSTNumTracks*2); + + long offset; + + if (track & 0x01) { + /* odd, use start of data */ + offset = (track / 2) * kSSTTrackLen; + } else { + /* even, start of data plus 12 sectors */ + offset = (track / 2) * kSSTTrackLen + 12 * 256; + } + + assert(offset >= 0 && offset < kSSTTrackLen * kSSTNumTracks); + + return offset; +} + +/* + * Count the number of "bad" bytes in the sector. + * + * Strictly speaking, a "bad" byte is anything that doesn't appear in the + * 6&2 decoding table, 5&3 decoding table, special list (D5, AA), and + * can't be used as a 4+4 encoding value. + * + * We just use $80 - $92, which qualify for all of the above. + */ +long +MainWindow::SSTCountBadBytes(const unsigned char* sctBuf, int count) +{ + long badCount = 0; + unsigned char uch; + + while (count--) { + uch = (*sctBuf) | 0x80; + if (uch >= 0x80 && uch <= 0x92) + badCount++; + sctBuf++; + } + + return badCount; +} + +/* + * Run through the data, adding 0x80 everywhere and re-aligning the + * tracks so that the big clump of sync bytes is at the end. + */ +void +MainWindow::SSTProcessTrackData(unsigned char* trackBuf) +{ + unsigned char* trackPtr; + int track; + + for (track = 0, trackPtr = trackBuf; track < kSSTNumTracks; + track++, trackPtr += kSSTTrackLen) + { + bool inRun; + int start, longestStart; + int count7f, longest = -1; + int i; + + inRun = false; + for (i = 0; i < kSSTTrackLen; i++) { + if (trackPtr[i] == 0x7f) { + if (inRun) { + count7f++; + } else { + count7f = 1; + start = i; + inRun = true; + } + } else { + if (inRun) { + if (count7f > longest) { + longest = count7f; + longestStart = start; + } + inRun = false; + } else { + /* do nothing */ + } + } + + trackPtr[i] |= 0x80; + } + + + if (longest == -1) { + WMSG1("HEY: couldn't find any 0x7f in track %d\n", + track); + } else { + WMSG3("Found run of %d at %d in track %d\n", + longest, longestStart, track); + + int bkpt = longestStart + longest; + assert(bkpt < kSSTTrackLen); + + char oneTrack[kSSTTrackLen]; + memcpy(oneTrack, trackPtr, kSSTTrackLen); + + /* copy it back so sync bytes are at end of track */ + memcpy(trackPtr, oneTrack + bkpt, kSSTTrackLen - bkpt); + memcpy(trackPtr + (kSSTTrackLen - bkpt), oneTrack, bkpt); + } + } +} + + +/* + * ========================================================================== + * Volume Copier + * ========================================================================== + */ + +void +MainWindow::OnToolsVolumeCopierVolume(void) +{ + VolumeCopier(false); +} +void +MainWindow::OnToolsVolumeCopierFile(void) +{ + VolumeCopier(true); +} + +/* + * Select a volume and then invoke the volcopy dialog. + */ +void +MainWindow::VolumeCopier(bool openFile) +{ + VolumeCopyDialog copyDlg(this); + DiskImg srcImg; + //DiskFS* pDiskFS = nil; + DIError dierr; + CString failed, errMsg, msg; + CString deviceName; + bool readOnly = false; + int result; + + /* flush current archive in case that's what we're planning to edit */ + OnFileSave(); + + failed.LoadString(IDS_FAILED); + + if (!openFile) { + /* + * Select the volume to manipulate. + */ + OpenVolumeDialog openVolDlg(this); + //openVolDlg.fReadOnly = false; + //openVolDlg.fAllowROChange = true; + result = openVolDlg.DoModal(); + if (result != IDOK) + goto bail; + deviceName = openVolDlg.fChosenDrive; + readOnly = (openVolDlg.fReadOnly != 0); + } else { + /* + * Open a disk image file instead. + */ + CString openFilters; + openFilters = kOpenDiskImage; + openFilters += kOpenAll; + openFilters += kOpenEnd; + CFileDialog fileDlg(TRUE, "dsk", NULL, OFN_FILEMUSTEXIST, openFilters, this); + + //dlg.m_ofn.Flags |= OFN_HIDEREADONLY; + fileDlg.m_ofn.Flags &= ~(OFN_READONLY); + fileDlg.m_ofn.lpstrTitle = "Select disk image file"; + fileDlg.m_ofn.lpstrInitialDir = fPreferences.GetPrefString(kPrOpenArchiveFolder); + + if (fileDlg.DoModal() != IDOK) + goto bail; + deviceName = fileDlg.GetPathName(); + readOnly = (fileDlg.GetReadOnlyPref() != 0); + } + + /* + * Open the disk image and figure out what it is. + */ + { + CWaitCursor waitc; + + DiskImg::SetAllowWritePhys0(false); + + dierr = srcImg.OpenImage(deviceName, '\0', readOnly); + if (dierr == kDIErrAccessDenied) { + if (openFile) { + errMsg.Format("Unable to open '%s': %s (try opening the file" + " with 'Read Only' checked).", deviceName, + DiskImgLib::DIStrError(dierr)); + } else if (!IsWin9x() && !openFile) { + errMsg.Format("Unable to open '%s': %s (make sure you have" + " administrator privileges).", deviceName, + DiskImgLib::DIStrError(dierr)); + } else { + errMsg.Format("Unable to open '%s': %s.", deviceName, + DiskImgLib::DIStrError(dierr)); + } + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } else if (dierr != kDIErrNone) { + errMsg.Format("Unable to open '%s': %s.", deviceName, + DiskImgLib::DIStrError(dierr)); + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + + /* analyze it to get #of blocks and determine the FS */ + if (srcImg.AnalyzeImage() != kDIErrNone) { + errMsg.Format("There isn't a valid disk image here?!?"); + MessageBox(errMsg, failed, MB_OK|MB_ICONSTOP); + goto bail; + } + } + + /* + * If requested (or necessary), verify the format. + */ + if (srcImg.GetFSFormat() == DiskImg::kFormatUnknown || + srcImg.GetSectorOrder() == DiskImg::kSectorOrderUnknown || + fPreferences.GetPrefBool(kPrQueryImageFormat)) + { + if (TryDiskImgOverride(&srcImg, deviceName, DiskImg::kFormatUnknown, + nil, true, &errMsg) != IDOK) + { + goto bail; + } + if (!errMsg.IsEmpty()) { + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + } + + /* + * Hand the DiskImg object off to the volume copier dialog. + */ + copyDlg.fpDiskImg = &srcImg; + copyDlg.fPathName = deviceName; + (void) copyDlg.DoModal(); + + /* + * The volume copier could have modified our open file. If it has, + * we need to close and reopen the archive. + */ + srcImg.CloseImage(); // could interfere with volume reopen + if (fNeedReopen) { + PeekAndPump(); // clear out dialog + ReopenArchive(); + } + +bail: + return; +} + + +/* + * ========================================================================== + * Disk image creator + * ========================================================================== + */ + +/* + * Create a new disk image. + */ +void +MainWindow::OnToolsDiskImageCreator(void) +{ + CreateImageDialog createDlg(this); + DiskArchive* pNewArchive = nil; + + createDlg.fDiskFormatIdx = + fPreferences.GetPrefLong(kPrDiskImageCreateFormat); + if (createDlg.fDiskFormatIdx < 0) + createDlg.fDiskFormatIdx = CreateImageDialog::kFmtProDOS; + + /* + * Ask the user what sort of disk they'd like to create. + */ + if (createDlg.DoModal() != IDOK) + return; + + fPreferences.SetPrefLong(kPrDiskImageCreateFormat, createDlg.fDiskFormatIdx); + + /* + * Set up the options struct. We set base.sectorOrder later. + */ + assert(createDlg.fNumBlocks > 0); + + DiskArchive::NewOptions options; + memset(&options, 0, sizeof(options)); + switch (createDlg.fDiskFormatIdx) { + case CreateImageDialog::kFmtBlank: + options.base.format = DiskImg::kFormatUnknown; + options.blank.numBlocks = createDlg.fNumBlocks; + break; + case CreateImageDialog::kFmtProDOS: + options.base.format = DiskImg::kFormatProDOS; + options.prodos.numBlocks = createDlg.fNumBlocks; + options.prodos.volName = createDlg.fVolName_ProDOS; + break; + case CreateImageDialog::kFmtPascal: + options.base.format = DiskImg::kFormatPascal; + options.pascalfs.numBlocks = createDlg.fNumBlocks; + options.pascalfs.volName = createDlg.fVolName_Pascal; + break; + case CreateImageDialog::kFmtHFS: + options.base.format = DiskImg::kFormatMacHFS; + options.hfs.numBlocks = createDlg.fNumBlocks; + options.hfs.volName = createDlg.fVolName_HFS; + break; + case CreateImageDialog::kFmtDOS32: + options.base.format = DiskImg::kFormatDOS32; + options.dos.volumeNum = createDlg.fDOSVolumeNum; + options.dos.allocDOSTracks = (createDlg.fAllocTracks_DOS != 0); + options.dos.numTracks = 35; + options.dos.numSectors = 13; + break; + case CreateImageDialog::kFmtDOS33: + options.base.format = DiskImg::kFormatDOS33; + options.dos.volumeNum = createDlg.fDOSVolumeNum; + options.dos.allocDOSTracks = (createDlg.fAllocTracks_DOS != 0); + if (createDlg.fNumBlocks <= 400) { + ASSERT(createDlg.fNumBlocks % 8 == 0); + options.dos.numTracks = createDlg.fNumBlocks / 8; + options.dos.numSectors = 16; + } else if (createDlg.fNumBlocks <= 800) { + ASSERT(createDlg.fNumBlocks % 16 == 0); + options.dos.numTracks = createDlg.fNumBlocks / 16; + options.dos.numSectors = 32; + options.dos.allocDOSTracks = false; + } else { + ASSERT(false); + return; + } + break; + default: + WMSG1("Invalid fDiskFormatIdx %d from CreateImageDialog\n", + createDlg.fDiskFormatIdx); + ASSERT(false); + return; + } + + /* + * Select the file to store it in. + */ + CString filename, saveFolder, errStr; + int filterIndex = 1; + CString formats; + + if (createDlg.fDiskFormatIdx == CreateImageDialog::kFmtDOS32) { + formats = "13-sector disk (*.d13)|*.d13|"; + } else { + formats = "ProDOS-ordered image (*.po)|*.po|"; + if (createDlg.fNumBlocks == 280) { + formats += "DOS-ordered image (*.do)|*.do|"; + filterIndex = 2; + } + } + formats += "|"; + + CFileDialog saveDlg(FALSE, _T("po"), NULL, + OFN_OVERWRITEPROMPT|OFN_NOREADONLYRETURN|OFN_HIDEREADONLY, + formats, this); + saveDlg.m_ofn.lpstrTitle = "New Disk Image"; + saveDlg.m_ofn.lpstrInitialDir = fPreferences.GetPrefString(kPrOpenArchiveFolder); + saveDlg.m_ofn.nFilterIndex = filterIndex; + + if (saveDlg.DoModal() != IDOK) { + WMSG0(" User cancelled xfer from image create dialog\n"); + return; + } + + saveFolder = saveDlg.m_ofn.lpstrFile; + saveFolder = saveFolder.Left(saveDlg.m_ofn.nFileOffset); + fPreferences.SetPrefString(kPrOpenArchiveFolder, saveFolder); + + filename = saveDlg.GetPathName(); + WMSG2(" Will xfer to file '%s' (filterIndex=%d)\n", + filename, saveDlg.m_ofn.nFilterIndex); + + if (createDlg.fDiskFormatIdx == CreateImageDialog::kFmtDOS32) { + options.base.sectorOrder = DiskImg::kSectorOrderDOS; + } else { + if (saveDlg.m_ofn.nFilterIndex == 2) + options.base.sectorOrder = DiskImg::kSectorOrderDOS; + else + options.base.sectorOrder = DiskImg::kSectorOrderProDOS; + } + + /* remove file if it already exists */ + CString errMsg; + errMsg = RemoveFile(filename); + if (!errMsg.IsEmpty()) { + ShowFailureMsg(this, errMsg, IDS_FAILED); + return; + } + + pNewArchive = new DiskArchive; + + /* create the new archive, showing a "busy" message */ + { + ExclusiveModelessDialog* pWaitDlg = new ExclusiveModelessDialog; + pWaitDlg->Create(IDD_FORMATTING, this); + pWaitDlg->CenterWindow(); + PeekAndPump(); // redraw + CWaitCursor waitc; + + errStr = pNewArchive->New(filename, &options); + + pWaitDlg->DestroyWindow(); + //PeekAndPump(); // redraw + } + + delete pNewArchive; // close it, either way + if (!errStr.IsEmpty()) { + ShowFailureMsg(this, errStr, IDS_FAILED); + (void) unlink(filename); + } else { + WMSG0("Disk image created successfully\n"); +#if 0 + SuccessBeep(); + + /* give them the opportunity to open the new disk image */ + DoneOpenDialog doneOpen(this); + + if (doneOpen.DoModal() == IDOK) { + WMSG1(" At user request, opening '%s'\n", filename); + + DoOpenArchive(filename, "dsk", kFilterIndexDiskImage, false); + } +#else + if (createDlg.fDiskFormatIdx != CreateImageDialog::kFmtBlank) + DoOpenArchive(filename, "dsk", kFilterIndexDiskImage, false); +#endif + } +} + + +/* + * ========================================================================== + * EOL scanner + * ========================================================================== + */ + +/* + * Scan and report on the end-of-line markers found in a file. + * + * Useful for identifying files that have been mangled by ASCII conversions. + */ +void +MainWindow::OnToolsEOLScanner(void) +{ + CString fileName, saveFolder, errMsg; + + CString openFilters; + openFilters = kOpenAll; + openFilters += kOpenEnd; + CFileDialog fileDlg(TRUE, "dsk", NULL, OFN_FILEMUSTEXIST, openFilters, this); + + fileDlg.m_ofn.Flags |= OFN_HIDEREADONLY; + //fileDlg.m_ofn.Flags &= ~(OFN_READONLY); + fileDlg.m_ofn.lpstrTitle = "Select file to scan"; + fileDlg.m_ofn.lpstrInitialDir = fPreferences.GetPrefString(kPrOpenArchiveFolder); + + if (fileDlg.DoModal() != IDOK) + return; + fileName = fileDlg.GetPathName(); + + saveFolder = fileDlg.m_ofn.lpstrFile; + saveFolder = saveFolder.Left(fileDlg.m_ofn.nFileOffset); + fPreferences.SetPrefString(kPrOpenArchiveFolder, saveFolder); + + WMSG1("Scanning '%s'\n", (const char*) fileName); + + FILE* fp; + fp = fopen(fileName, "rb"); + if (fp == nil) { + errMsg.Format("Unable to open '%s': %s.", fileName, strerror(errno)); + ShowFailureMsg(this, errMsg, IDS_FAILED); + return; + } + + long numCR, numLF, numCRLF, numHAChars, numChars; + bool lastCR; + int ic; + + /* + * Plow through the file, counting up characters. + */ + numCR = numLF = numCRLF = numChars = numHAChars = 0; + lastCR = false; + while (true) { + ic = getc(fp); + if (ic == EOF) + break; + + if ((ic & 0x80) != 0) + numHAChars++; + + if (ic == '\r') { + lastCR = true; + numCR++; + } else if (ic == '\n') { + if (lastCR) { + numCR--; + numCRLF++; + lastCR = false; + } else { + numLF++; + } + } else { + lastCR = false; + } + numChars++; + } + fclose(fp); + + WMSG4("Got CR=%ld LF=%ld CRLF=%ld (numChars=%ld)\n", + numCR, numLF, numCRLF, numChars); + + EOLScanDialog output; + output.fCountCR = numCR; + output.fCountLF = numLF; + output.fCountCRLF = numCRLF; + output.fCountChars = numChars; + output.fCountHighASCII = numHAChars; + (void) output.DoModal(); +} + + +/* + * ========================================================================== + * 2MG disk image properties editor + * ========================================================================== + */ + +/* + * Edit the properties (but not the disk image inside) a .2MG disk image. + */ +void +MainWindow::OnToolsTwoImgProps(void) +{ + CString fileName, saveFolder, errMsg; + CString openFilters; + + /* flush current archive in case that's what we're planning to edit */ + OnFileSave(); + + /* + * Select the file to open. + */ + openFilters = "2MG Disk Images (.2mg .2img)|*.2mg;*.2img|"; + openFilters += kOpenAll; + openFilters += kOpenEnd; + CFileDialog fileDlg(TRUE, "2mg", NULL, OFN_FILEMUSTEXIST, openFilters, this); + + fileDlg.m_ofn.Flags |= OFN_HIDEREADONLY; + //fileDlg.m_ofn.Flags &= ~(OFN_READONLY); + fileDlg.m_ofn.lpstrTitle = "Select file to edit"; + fileDlg.m_ofn.lpstrInitialDir = fPreferences.GetPrefString(kPrOpenArchiveFolder); + + if (fileDlg.DoModal() != IDOK) + return; + fileName = fileDlg.GetPathName(); + + saveFolder = fileDlg.m_ofn.lpstrFile; + saveFolder = saveFolder.Left(fileDlg.m_ofn.nFileOffset); + fPreferences.SetPrefString(kPrOpenArchiveFolder, saveFolder); + + /* + * Open it up. + */ + bool changed; + changed = EditTwoImgProps(fileName); + + if (changed && IsOpenPathName(fileName)) { + PeekAndPump(); // clear out dialog + ReopenArchive(); + } +} + +/* + * Edit the properties of a 2MG file. + * + * Returns "true" if the file was modified, "false" if not. + */ +bool +MainWindow::EditTwoImgProps(const char* fileName) +{ + TwoImgPropsDialog dialog; + TwoImgHeader header; + FILE* fp = nil; + bool dirty = false; + CString errMsg; + long totalLength; + bool readOnly = false; + + WMSG1("EditTwoImgProps '%s'\n", fileName); + fp = fopen(fileName, "r+b"); + if (fp == nil) { + int firstError = errno; + fp = fopen(fileName, "rb"); + if (fp == nil) { + errMsg.Format("Unable to open '%s': %s.", + fileName, strerror(firstError)); + goto bail; + } else + readOnly = true; + } + + fseek(fp, 0, SEEK_END); + totalLength = ftell(fp); + rewind(fp); + + if (header.ReadHeader(fp, totalLength) != 0) { + errMsg.Format("Unable to process 2MG header in '%s'" + " (are you sure this is in 2MG format?).", + fileName); + goto bail; + } + + dialog.Setup(&header, readOnly); + if (dialog.DoModal() == IDOK) { + long result; + //header.SetCreatorChunk("fubar", 5); + header.DumpHeader(); + + rewind(fp); + if (header.WriteHeader(fp) != 0) { + errMsg = "Unable to write 2MG header"; + goto bail; + } + + /* + * Clip off the footer. They might have had one before but don't + * have one now. If they do have one now we'll add it back in a + * second. + */ + result = fseek(fp, header.fDataOffset + header.fDataLen, SEEK_SET); + if (result < 0) { + errMsg = "Unable to seek to end of 2MG file"; + goto bail; + } + dirty = true; + + if (::chsize(fileno(fp), ftell(fp)) != 0) { + errMsg = "Unable to truncate 2MG file before writing footer"; + goto bail; + } + + if (header.fCmtLen || header.fCreatorLen) { + if (header.WriteFooter(fp) != 0) { + errMsg = "Unable to write 2MG footer"; + goto bail; + } + } + + WMSG0("2MG success!\n"); + } + +bail: + if (fp != nil) + fclose(fp); + if (!errMsg.IsEmpty()) { + ShowFailureMsg(this, errMsg, IDS_FAILED); + } + + return dirty; +} diff --git a/app/TwoImgPropsDialog.cpp b/app/TwoImgPropsDialog.cpp new file mode 100644 index 0000000..b5080a6 --- /dev/null +++ b/app/TwoImgPropsDialog.cpp @@ -0,0 +1,178 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * TwoImg properties editor. + */ +#include "StdAfx.h" +#include "TwoImgPropsDialog.h" + +BEGIN_MESSAGE_MAP(TwoImgPropsDialog, CDialog) + ON_BN_CLICKED(IDC_TWOIMG_LOCKED, OnChange) + ON_BN_CLICKED(IDC_TWOIMG_DOSVOLSET, OnChange) + ON_EN_CHANGE(IDC_TWOIMG_DOSVOLNUM, OnChange) + ON_EN_CHANGE(IDC_TWOIMG_COMMENT, OnChange) + ON_WM_HELPINFO() +END_MESSAGE_MAP() + + +/* + * Initialize the dialog from fpHeader. + */ +BOOL +TwoImgPropsDialog::OnInitDialog(void) +{ + CWnd* pWnd; + CEdit* pEdit; + CString tmpStr; + + ASSERT(fpHeader != nil); + + /* + * Set up the static fields. + */ + pWnd = GetDlgItem(IDC_TWOIMG_CREATOR); + tmpStr.Format("'%s'", fpHeader->GetCreatorStr()); + pWnd->SetWindowText(tmpStr); + + pWnd = GetDlgItem(IDC_TWOIMG_VERSION); + tmpStr.Format("%d", fpHeader->fVersion); + pWnd->SetWindowText(tmpStr); + + pWnd = GetDlgItem(IDC_TWOIMG_FORMAT); + switch (fpHeader->fImageFormat) { + case TwoImgHeader::kImageFormatDOS: tmpStr = "DOS order sectors"; break; + case TwoImgHeader::kImageFormatProDOS: tmpStr = "ProDOS order sectors"; break; + case TwoImgHeader::kImageFormatNibble: tmpStr = "Raw nibbles"; break; + default: tmpStr = "Unknown"; break; + } + pWnd->SetWindowText(tmpStr); + + pWnd = GetDlgItem(IDC_TWOIMG_BLOCKS); + tmpStr.Format("%d", fpHeader->fNumBlocks); + pWnd->SetWindowText(tmpStr); + + /* + * Restrict the edit field. + */ + pEdit = (CEdit*) GetDlgItem(IDC_TWOIMG_DOSVOLNUM); + pEdit->LimitText(3); // 1-254 + + /* + * Disable the "Save" button. + */ + pWnd = GetDlgItem(IDOK); + pWnd->EnableWindow(FALSE); + + /* for read-only mode, all buttons are disabled */ + if (fReadOnly) { + GetDlgItem(IDC_TWOIMG_LOCKED)->EnableWindow(FALSE); + GetDlgItem(IDC_TWOIMG_DOSVOLSET)->EnableWindow(FALSE); + GetDlgItem(IDC_TWOIMG_COMMENT)->EnableWindow(FALSE); + GetDlgItem(IDC_TWOIMG_DOSVOLNUM)->EnableWindow(FALSE); + + GetWindowText(tmpStr); + tmpStr += " (read-only)"; + SetWindowText(tmpStr); + } + + return CDialog::OnInitDialog(); +} + +/* + * Do the data exchange, and set values in the header. + */ +void +TwoImgPropsDialog::DoDataExchange(CDataExchange* pDX) +{ + BOOL locked, dosVolSet; + CString comment; + int dosVolNum; + + if (pDX->m_bSaveAndValidate) { + DDX_Check(pDX, IDC_TWOIMG_LOCKED, locked); + DDX_Check(pDX, IDC_TWOIMG_DOSVOLSET, dosVolSet); + DDX_Text(pDX, IDC_TWOIMG_COMMENT, comment); + DDX_Text(pDX, IDC_TWOIMG_DOSVOLNUM, dosVolNum); + + WMSG1("GOT dosVolNum = %d\n", dosVolNum); + + fpHeader->fFlags &= ~(TwoImgHeader::kFlagLocked); + if (locked) + fpHeader->fFlags |= TwoImgHeader::kFlagLocked; + + fpHeader->fFlags &= ~(TwoImgHeader::kDOSVolumeMask); + fpHeader->fFlags &= ~(TwoImgHeader::kDOSVolumeSet); + if (dosVolSet) { + fpHeader->fFlags |= TwoImgHeader::kDOSVolumeSet; + fpHeader->fFlags |= (dosVolNum & TwoImgHeader::kDOSVolumeMask); + + CString appStr, errMsg; + if (dosVolNum < 1 || dosVolNum > 254) { + appStr.LoadString(IDS_MB_APP_NAME); + errMsg.LoadString(IDS_VALID_VOLNAME_DOS); + MessageBox(errMsg, appStr, MB_OK); + pDX->Fail(); + } else { + fpHeader->SetDOSVolumeNum(dosVolNum); + } + } + + + if (!comment.IsEmpty()) + fpHeader->SetComment(comment); + else + fpHeader->SetComment(nil); + } else { + CWnd* pWnd; + + locked = (fpHeader->fFlags & TwoImgHeader::kFlagLocked) != 0; + dosVolSet = (fpHeader->fFlags & TwoImgHeader::kDOSVolumeSet) != 0; + comment = fpHeader->GetComment(); + if (dosVolSet) + dosVolNum = fpHeader->GetDOSVolumeNum(); + else + dosVolNum = TwoImgHeader::kDefaultVolumeNum; + + DDX_Check(pDX, IDC_TWOIMG_LOCKED, locked); + DDX_Check(pDX, IDC_TWOIMG_DOSVOLSET, dosVolSet); + DDX_Text(pDX, IDC_TWOIMG_COMMENT, comment); + DDX_Text(pDX, IDC_TWOIMG_DOSVOLNUM, dosVolNum); + + /* set initial state of dos volume number edit field */ + if (!fReadOnly) { + pWnd = GetDlgItem(IDC_TWOIMG_DOSVOLNUM); + pWnd->EnableWindow(dosVolSet); + } + } +} + +/* + * If they changed anything, enable the "save" button. + */ +void +TwoImgPropsDialog::OnChange(void) +{ + CButton* pButton; + UINT checked; + + ASSERT(!fReadOnly); + + GetDlgItem(IDOK)->EnableWindow(TRUE); + + pButton = (CButton*) GetDlgItem(IDC_TWOIMG_DOSVOLSET); + checked = pButton->GetCheck(); + GetDlgItem(IDC_TWOIMG_DOSVOLNUM)->EnableWindow(checked == BST_CHECKED); +} + +/* + * Context help request (question mark button). + */ +BOOL +TwoImgPropsDialog::OnHelpInfo(HELPINFO* lpHelpInfo) +{ + WinHelp((DWORD) lpHelpInfo->iCtrlId, HELP_CONTEXTPOPUP); + return TRUE; // yes, we handled it +} diff --git a/app/TwoImgPropsDialog.h b/app/TwoImgPropsDialog.h new file mode 100644 index 0000000..de82919 --- /dev/null +++ b/app/TwoImgPropsDialog.h @@ -0,0 +1,49 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Class definition for TwoImg properties edit dialog. + */ +#ifndef __TWOIMG_PROPS_DIALOG__ +#define __TWOIMG_PROPS_DIALOG__ + +#include "resource.h" +#include "../diskimg/TwoImg.h" + + +/* + * Dialog with a bunch of controls that map to fields in the TwoImg file + * header. We want to keep the "Save" button ("OK") dimmed until we have + * something to write. + */ +class TwoImgPropsDialog : public CDialog { +public: + TwoImgPropsDialog(CWnd* pParentWnd = NULL) : + CDialog(IDD_TWOIMG_PROPS, pParentWnd), + fpHeader(NULL), fReadOnly(false) + {} + + + void Setup(TwoImgHeader* pHeader, bool readOnly) { + fpHeader = pHeader; + fReadOnly = readOnly; + } + +protected: + // overrides + virtual BOOL OnInitDialog(void); + virtual void DoDataExchange(CDataExchange* pDX); + + afx_msg BOOL OnHelpInfo(HELPINFO* lpHelpInfo); + afx_msg void OnChange(void); + + TwoImgHeader* fpHeader; + bool fReadOnly; + //bool fModified; + + DECLARE_MESSAGE_MAP() +}; + +#endif /*__TWOIMG_PROPS_DIALOG__*/ \ No newline at end of file diff --git a/app/UseSelectionDialog.cpp b/app/UseSelectionDialog.cpp new file mode 100644 index 0000000..1e0af26 --- /dev/null +++ b/app/UseSelectionDialog.cpp @@ -0,0 +1,85 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Support for UseSelectionDialog. + */ +#include "stdafx.h" +#include "UseSelectionDialog.h" +#include "HelpTopics.h" + +BEGIN_MESSAGE_MAP(UseSelectionDialog, CDialog) + ON_WM_HELPINFO() + //ON_COMMAND(IDHELP, OnHelp) +END_MESSAGE_MAP() + + +/* + * Set up the dialog that lets the user choose file deletion options. + * + * All we really need to do is update the string that indicates how many + * files have been selected. + */ +BOOL +UseSelectionDialog::OnInitDialog(void) +{ + CString str; + CString selStr; + CWnd* pWnd; + + CDialog::OnInitDialog(); + + /* grab the radio button with the selection count */ + pWnd = GetDlgItem(IDC_USE_SELECTED); + ASSERT(pWnd != nil); + + /* set the string using a string table entry */ + if (fSelectedCount == 1) { + str.LoadString(fSelCountID); + pWnd->SetWindowText(str); + } else { + str.LoadString(fSelCountsID); + selStr.Format((LPCTSTR) str, fSelectedCount); + pWnd->SetWindowText(selStr); + + if (fSelectedCount == 0) + pWnd->EnableWindow(FALSE); + } + + /* set the other strings */ + str.LoadString(fTitleID); + SetWindowText(str); + + pWnd = GetDlgItem(IDC_USE_ALL); + ASSERT(pWnd != nil); + str.LoadString(fAllID); + pWnd->SetWindowText(str); + + pWnd = GetDlgItem(IDOK); + ASSERT(pWnd != nil); + str.LoadString(fOkLabelID); + pWnd->SetWindowText(str); + + return TRUE; +} + +/* + * Convert values. + */ +void +UseSelectionDialog::DoDataExchange(CDataExchange* pDX) +{ + DDX_Radio(pDX, IDC_USE_SELECTED, fFilesToAction); +} + +/* + * Context help request (question mark button). + */ +BOOL +UseSelectionDialog::OnHelpInfo(HELPINFO* lpHelpInfo) +{ + WinHelp((DWORD) lpHelpInfo->iCtrlId, HELP_CONTEXTPOPUP); + return TRUE; // yes, we handled it +} diff --git a/app/UseSelectionDialog.h b/app/UseSelectionDialog.h new file mode 100644 index 0000000..b50bf66 --- /dev/null +++ b/app/UseSelectionDialog.h @@ -0,0 +1,62 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Acknowledge and clarify a request to delete files. + */ +#ifndef __USE_SELECTION_DIALOG__ +#define __USE_SELECTION_DIALOG__ + +//#include "../util/UtilLib.h" +#include "resource.h" + +/* + * Straightforward confirmation. + */ +class UseSelectionDialog : public CDialog { +public: + UseSelectionDialog(int selCount, CWnd* pParentWnd = NULL, int rsrcID = IDD_USE_SELECTION) : + CDialog(rsrcID, pParentWnd), fSelectedCount(selCount) + { + // init values; these should be overridden before DoModal + fFilesToAction = 0; + } + virtual ~UseSelectionDialog(void) {} + + // set up dialog parameters; must be called before DoModal + void Setup(int titleID, int okLabelID, int countID, int countsID, + int allID) + { + fTitleID = titleID; + fOkLabelID = okLabelID; + fSelCountID = countID; + fSelCountsID = countsID; + fAllID = allID; + } + + enum { kActionSelection = 0, kActionAll = 1 }; + int fFilesToAction; + +protected: + virtual BOOL OnInitDialog(void); + virtual void DoDataExchange(CDataExchange* pDX); + + afx_msg BOOL OnHelpInfo(HELPINFO* lpHelpInfo); + //afx_msg void OnHelp(void); + +private: + int fSelectedCount; + + /* dialog parameters */ + int fTitleID; + int fOkLabelID; + int fSelCountID; + int fSelCountsID; + int fAllID; + + DECLARE_MESSAGE_MAP() +}; + +#endif /*__USE_SELECTION_DIALOG__*/ \ No newline at end of file diff --git a/app/ViewFilesDialog.cpp b/app/ViewFilesDialog.cpp new file mode 100644 index 0000000..c062d34 --- /dev/null +++ b/app/ViewFilesDialog.cpp @@ -0,0 +1,1413 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Support for the "view files" dialog box. + */ +#include "stdafx.h" +#include "ViewFilesDialog.h" +#include "Main.h" +#include "Print.h" +#include "HelpTopics.h" +#include "../util/UtilLib.h" + + +/* + * =========================================================================== + * ViewFilesDialog + * =========================================================================== + */ + +static const UINT gFindReplaceID = RegisterWindowMessage(FINDMSGSTRING); + +BEGIN_MESSAGE_MAP(ViewFilesDialog, CDialog) + ON_WM_CREATE() + ON_WM_DESTROY() + ON_WM_SIZE() + ON_WM_GETMINMAXINFO() + ON_REGISTERED_MESSAGE(gFindReplaceID, OnFindDialogMessage) + ON_COMMAND(IDC_FVIEW_NEXT, OnFviewNext) + ON_COMMAND(IDC_FVIEW_PREV, OnFviewPrev) + ON_COMMAND(IDC_FVIEW_FONT, OnFviewFont) + ON_COMMAND(IDC_FVIEW_PRINT, OnFviewPrint) + ON_COMMAND(IDC_FVIEW_FIND, OnFviewFind) + ON_BN_CLICKED(IDC_FVIEW_DATA, OnFviewData) + ON_BN_CLICKED(IDC_FVIEW_RSRC, OnFviewRsrc) + ON_BN_CLICKED(IDC_FVIEW_CMMT, OnFviewCmmt) + ON_COMMAND(IDC_FVIEW_FMT_BEST, OnFviewFmtBest) + ON_COMMAND(IDC_FVIEW_FMT_HEX, OnFviewFmtHex) + ON_COMMAND(IDC_FVIEW_FMT_RAW, OnFviewFmtRaw) + ON_CBN_SELCHANGE(IDC_FVIEW_FORMATSEL, OnFormatSelChange) + ON_COMMAND(IDHELP, OnHelp) +END_MESSAGE_MAP() + +/* + * Window creation. Stuff the desired text into the RichEdit box. + */ +BOOL +ViewFilesDialog::OnInitDialog(void) +{ + WMSG0("Now in VFD OnInitDialog!\n"); + + ASSERT(fpSelSet != nil); + + /* delete dummy control and insert our own with modded styles */ + CRichEditCtrl* pEdit = (CRichEditCtrl*)GetDlgItem(IDC_FVIEW_EDITBOX); + ASSERT(pEdit != nil); + CRect rect; + pEdit->GetWindowRect(&rect); + pEdit->DestroyWindow(); + ScreenToClient(&rect); + + DWORD styles = ES_MULTILINE | ES_AUTOVSCROLL | ES_READONLY | + WS_BORDER | WS_VSCROLL | WS_VISIBLE | WS_TABSTOP | + ES_NOHIDESEL; + if (fNoWrapText) + styles |= ES_AUTOHSCROLL | WS_HSCROLL; + fEditCtrl.Create(styles, rect, this, IDC_FVIEW_EDITBOX); + fEditCtrl.SetFocus(); + /* + * HEY: I think we can do this with pEdit->ShowScrollBar(SB_BOTH) !! + * Could also use GetWindowLong/SetWindowLong to change the window style; + * probably need a SetWindowPos to cause changes to flush. + */ + + + /* + * We want to adjust the size of the window to match the last size used. + * However, if we do this after creating the edit dialog but before it + * actually has data to display, then when we stuff data into it the + * scroll bar goodies are all out of whack. + */ + fFirstResize = true; +#if 0 + const Preferences* pPreferences = GET_PREFERENCES(); + long width = pPreferences->GetFileViewerWidth(); + long height = pPreferences->GetFileViewerHeight(); + CRect fullRect; + GetWindowRect(&fullRect); + WMSG2(" VFD pre-size %dx%d\n", fullRect.Width(), fullRect.Height()); + fullRect.right = fullRect.left + width; + fullRect.bottom = fullRect.top + height; + MoveWindow(fullRect, FALSE); +#endif + + // This invokes UpdateData, which calls DoDataExchange, which leads to + // the StreamIn call. So don't do this until everything else is ready. + CDialog::OnInitDialog(); + + WMSG0("VFD OnInitDialog done\n"); + return FALSE; // don't let Windows set the focus +} + + +/* + * Window creation stuff. Set the icon and the "gripper". + */ +int +ViewFilesDialog::OnCreate(LPCREATESTRUCT lpcs) +{ + WMSG0("VFD OnCreate\n"); + + HICON hIcon; + hIcon = ::AfxGetApp()->LoadIcon(IDI_FILE_VIEWER); + SetIcon(hIcon, TRUE); + + GetClientRect(&fLastWinSize); + + CRect initRect(fLastWinSize); + initRect.left = initRect.right - ::GetSystemMetrics(SM_CXVSCROLL); + initRect.top = initRect.bottom - ::GetSystemMetrics(SM_CYHSCROLL); + fGripper.Create(WS_CHILD | WS_VISIBLE | + SBS_SIZEBOX | SBS_SIZEBOXBOTTOMRIGHTALIGN | SBS_SIZEGRIP, + initRect, this, AFX_IDW_SIZE_BOX); + + WMSG0("VFD OnCreate done\n"); + return 0; +} + +/* + * Window is going away. Save the current size. + */ +void +ViewFilesDialog::OnDestroy(void) +{ + Preferences* pPreferences = GET_PREFERENCES_WR(); + CRect rect; + GetWindowRect(&rect); + + pPreferences->SetPrefLong(kPrFileViewerWidth, rect.Width()); + pPreferences->SetPrefLong(kPrFileViewerHeight, rect.Height()); + + CDialog::OnDestroy(); +} + +/* + * Override OnOK/OnCancel so we don't bail out while we're in the middle of + * loading something. It would actually be kind of nice to be able to do + * so, so someday we should make the "cancel" button work, or perhaps allow + * prev/next to skip over the thing being loaded. "TO DO" + */ +void +ViewFilesDialog::OnOK(void) +{ + if (fBusy) + MessageBeep(-1); + else { + CRect rect; + + GetWindowRect(&rect); + WMSG2(" VFD size now %dx%d\n", rect.Width(), rect.Height()); + + CDialog::OnOK(); + } +} +void +ViewFilesDialog::OnCancel(void) +{ + if (fBusy) + MessageBeep(-1); + else + CDialog::OnCancel(); +} + + +/* + * Restrict the minimum window size to something reasonable. + */ +void +ViewFilesDialog::OnGetMinMaxInfo(MINMAXINFO* pMMI) +{ + pMMI->ptMinTrackSize.x = 664; + pMMI->ptMinTrackSize.y = 200; +} + +/* + * When the window resizes, we have to tell the edit box to expand, and + * rearrange the controls inside it. + */ +void +ViewFilesDialog::OnSize(UINT nType, int cx, int cy) +{ + CDialog::OnSize(nType, cx, cy); + + //WMSG2("Dialog: old size %d,%d\n", + // fLastWinSize.Width(), fLastWinSize.Height()); + WMSG2("Dialog: new size %d,%d\n", cx, cy); + + if (fLastWinSize.Width() == cx && fLastWinSize.Height() == cy) { + WMSG0("VFD OnSize: no change\n"); + return; + } + + int deltaX, deltaY; + deltaX = cx - fLastWinSize.Width(); + deltaY = cy - fLastWinSize.Height(); + //WMSG2("Delta is %d,%d\n", deltaX, deltaY); + + ShiftControls(deltaX, deltaY); + + GetClientRect(&fLastWinSize); +} + +/* + * Adjust the positions and sizes of the controls. + * + * This relies on MinMaxInfo to guarantee that nothing falls off an edge. + */ +void +ViewFilesDialog::ShiftControls(int deltaX, int deltaY) +{ + HDWP hdwp; + + /* + * Use deferred reposn so that they don't end up drawing on top of each + * other and getting all weird. + * + * IMPORTANT: the DeferWindowPos stuff changes the tab order of the + * items in the window. The controls must be added in the reverse + * order in which they appear in the window. + */ + hdwp = BeginDeferWindowPos(15); + hdwp = MoveControl(hdwp, this, AFX_IDW_SIZE_BOX, deltaX, deltaY); + hdwp = MoveControl(hdwp, this, IDHELP, deltaX, deltaY); + hdwp = MoveControl(hdwp, this, IDC_FVIEW_FONT, deltaX, deltaY); + hdwp = MoveControl(hdwp, this, IDC_FVIEW_PRINT, deltaX, deltaY); + hdwp = MoveControl(hdwp, this, IDC_FVIEW_FIND, deltaX, deltaY); + hdwp = MoveControl(hdwp, this, IDC_FVIEW_FMT_RAW, 0, deltaY); + hdwp = MoveControl(hdwp, this, IDC_FVIEW_FMT_HEX, 0, deltaY); + hdwp = MoveControl(hdwp, this, IDC_FVIEW_FMT_BEST, 0, deltaY); + hdwp = MoveControl(hdwp, this, IDC_FVIEW_PREV, 0, deltaY); + hdwp = MoveControl(hdwp, this, IDC_FVIEW_NEXT, 0, deltaY); + hdwp = MoveControl(hdwp, this, IDC_FVIEW_CMMT, 0, deltaY); + hdwp = MoveControl(hdwp, this, IDC_FVIEW_RSRC, 0, deltaY); + hdwp = MoveControl(hdwp, this, IDC_FVIEW_DATA, 0, deltaY); + hdwp = MoveStretchControl(hdwp, this, IDC_FVIEW_FORMATSEL, 0, deltaY, deltaX, 0); + hdwp = StretchControl(hdwp, this, IDC_FVIEW_EDITBOX, deltaX, deltaY); + hdwp = MoveControl(hdwp, this, IDOK, deltaX, deltaY); + if (!EndDeferWindowPos(hdwp)) { + WMSG0("EndDeferWindowPos failed\n"); + } + + /* + * Work around buggy CRichEdit controls. The inner edit area is only + * resized when the box is shrunk, not when it's expanded, and the + * results are inconsistent between Win98 and Win2K. + * + * Set the internal size equal to the size of the entire edit box. + * This should be large enough to make everything work right, but small + * enough to avoid funky scrolling behavior. (If you want to set this + * more precisely, don't forget that scroll bars are not part of the + * edit control client area, and their sizes must be factored in.) + */ + CRect rect; + CRichEditCtrl* pEdit = (CRichEditCtrl*) GetDlgItem(IDC_FVIEW_EDITBOX); + ASSERT(pEdit != nil); + //pEdit->GetClientRect(&rect); + pEdit->GetWindowRect(&rect); + //GetClientRect(&rect); + rect.left = 2; + rect.top = 2; + pEdit->SetRect(&rect); +} + + +/* + * Shuffle data in and out. + */ +void +ViewFilesDialog::DoDataExchange(CDataExchange* pDX) +{ + CDialog::DoDataExchange(pDX); + + if (pDX->m_bSaveAndValidate) { + WMSG0("COPY OUT\n"); + } else { + WMSG0("COPY IN\n"); + OnFviewNext(); + } +} + +static void +DumpBitmapInfo(HBITMAP hBitmap) +{ + BITMAP info; + CBitmap* pBitmap = CBitmap::FromHandle(hBitmap); + int gotten; + + gotten = pBitmap->GetObject(sizeof(info), &info); + + WMSG2("DumpBitmapInfo: gotten=%d of %d\n", gotten, sizeof(info)); + WMSG1(" bmType = %d\n", info.bmType); + WMSG2(" bmWidth=%d, bmHeight=%d\n", info.bmWidth, info.bmHeight); + WMSG1(" bmWidthBytes=%d\n", info.bmWidthBytes); + WMSG1(" bmPlanes=%d\n", info.bmPlanes); + WMSG1(" bmBitsPixel=%d\n", info.bmBitsPixel); + WMSG1(" bmPits = 0x%08lx\n", info.bmBits); + +} + +/* + * Display a buffer of text in the RichEdit control. + * + * The RichEdit dialog will hold its own copy of the data, so "pHolder" can + * be safely destroyed after this returns. + */ +void +ViewFilesDialog::DisplayText(const char* fileName) +{ + CWaitCursor wait; // streaming of big files can take a little while + bool errFlg; + bool emptyFlg = false; + bool editHadFocus = false; + + ASSERT(fpOutput != nil); + ASSERT(fileName != nil); + + errFlg = fpOutput->GetOutputKind() == ReformatOutput::kOutputErrorMsg; + + ASSERT(fpOutput->GetOutputKind() != ReformatOutput::kOutputUnknown); + + CRichEditCtrl* pEdit = (CRichEditCtrl*) GetDlgItem(IDC_FVIEW_EDITBOX); + ASSERT(pEdit != nil); + + /* retain the selection even if we lose focus [can't do this in OnInit] */ + pEdit->SetOptions(ECOOP_OR, ECO_SAVESEL); + +#if 0 + /* + * Start by trashing anything that's there. Not strictly necessary, + * but it prevents the control from trying to maintain the old stuff + * in an undo buffer. (Not entirely sure if a stream-in operation is + * undoable, but it costs very little to be certain.) + * + * UPDATE: I turned this off because it was dinging the speaker (?!). + * Might be doing that because it's in read-only mode. + */ + pEdit->SetSel(0, -1); + pEdit->Clear(); + pEdit->EmptyUndoBuffer(); +#endif + + /* + * There's a redraw flash that goes away if you change the input + * focus to something other than the edit ctrl. (Move between large + * files; it looks like you can see the text being selected and + * hightlighted. The control doesn't have an "always highlight" flag + * set, so if the focus is on a different control it doesn't light up.) + * + * Since we're currently forcing the focus to be on the edit ctrl later + * on, we just jam it to something else here. If nothing has the focus, + * as can happen if we click on "resource fork" and then Alt-N to a + * file without a resource fork, we force the focus to return to the + * edit window. + * + * NOTE: this would look a little better if we used the Prev/Next + * buttons to hold the temporary focus, but we need to know which key + * the user hit. We could also create a bogus control, move it into + * negative space where it will be invisible, and use that as a "focus + * holder". + */ + CWnd* pFocusWnd = GetFocus(); + if (pFocusWnd == nil || pFocusWnd->m_hWnd == pEdit->m_hWnd) { + editHadFocus = true; + GetDlgItem(IDOK)->SetFocus(); + } + + /* + * The text color isn't getting reset when we reload the control. I + * can't find a "set default text color" call, so I'm reformatting + * part of the buffer. + * + * Here's the weird part: it doesn't seem to matter what color I + * set it to under Win2K. It reverts to black so long as I do anything + * here. Under Win98, it uses the new color. + */ + //if (0) + { + CHARFORMAT cf; + cf.cbSize = sizeof(CHARFORMAT); + cf.dwMask = CFM_COLOR; + cf.crTextColor = RGB(0, 0, 0); + pEdit->SetSel(0, 1); // must select at least one char + pEdit->SetSelectionCharFormat(cf); + } + + /* + * Add the appropriate data. If the "bitmap" flag is set, use the + * MyDIBitmap pointer instead. + */ + if (fpOutput->GetOutputKind() == ReformatOutput::kOutputBitmap) { + CClientDC dcScreen(this); + HBITMAP hBitmap; + + if (fpRichEditOle == nil) { + /* can't do this in OnInitDialog -- m_pWnd isn't initialized */ + fpRichEditOle = pEdit->GetIRichEditOle(); + ASSERT(fpRichEditOle != nil); + } + + //FILE* fp = fopen("C:/test/output.bmp", "wb"); + //if (fp != nil) { + // pDib->WriteToFile(fp); + // fclose(fp); + //} + + hBitmap = fpOutput->GetDIB()->ConvertToDDB(dcScreen.m_hDC); + if (hBitmap == nil) { + WMSG0("ConvertToDDB failed!\n"); + pEdit->SetWindowText("Internal error."); + errFlg = true; + } else { + //DumpBitmapInfo(hBitmap); + //DumpBitmapInfo(pDib->GetHandle()); + + WMSG0("Inserting bitmap\n"); + pEdit->SetWindowText(""); + CImageDataObject::InsertBitmap(fpRichEditOle, hBitmap); + + /* RichEditCtrl has it now */ + ::DeleteObject(hBitmap); + } + } else { + /* + * Stream the data in, using the appropriate format. Since we don't + * have the "replace selection" flag set, this replaces everything + * that's currently in there. + * + * We can't use SetWindowText() unless we're willing to forgo viewing + * of binary files in "raw" form. There doesn't seem to be any other + * difference between the two approaches. + */ + const char* textBuf; + long textLen; + int streamFormat; + + textBuf = fpOutput->GetTextBuf(); + textLen = fpOutput->GetTextLen(); + streamFormat = SF_TEXT; + if (fpOutput->GetOutputKind() == ReformatOutput::kOutputRTF) + streamFormat = SF_RTF; + if (fpOutput->GetTextLen() == 0) { + textBuf = _T("(file is empty)"); + textLen = strlen(textBuf); + emptyFlg = true; + EnableFormatSelection(FALSE); + } + if (fpOutput->GetOutputKind() == ReformatOutput::kOutputErrorMsg) + EnableFormatSelection(FALSE); + + /* make sure the control will hold everything we throw at it */ + pEdit->LimitText(textLen+1); + WMSG2("Streaming %ld bytes (kind=%d)\n", + textLen, fpOutput->GetOutputKind()); + + /* clear this early to avoid loading onto yellow */ + if (errFlg) + pEdit->SetBackgroundColor(FALSE, RGB(255, 255, 0)); + else if (emptyFlg) + pEdit->SetBackgroundColor(FALSE, RGB(192, 192, 192)); + else + pEdit->SetBackgroundColor(TRUE, 0); + + RichEditXfer xfer(textBuf, textLen); + EDITSTREAM es; + es.dwCookie = (DWORD) &xfer; + es.dwError = 0; + es.pfnCallback = RichEditXfer::EditStreamCallback; + long count; + count = pEdit->StreamIn(streamFormat, es); + WMSG2("StreamIn returned count=%ld dwError=%d\n", count, es.dwError); + + if (es.dwError != 0) { + /* a -16 error can happen if the type is RTF but contents are not */ + char errorText[256]; + + sprintf(errorText, + "ERROR: failed while loading data (err=0x%08lx)\n" + "(File contents might be too big for Windows to display)\n", + es.dwError); + RichEditXfer errXfer(errorText, strlen(errorText)); + es.dwCookie = (DWORD) &errXfer; + es.dwError = 0; + + count = pEdit->StreamIn(SF_TEXT, es); + WMSG2("Error StreamIn returned count=%ld dwError=%d\n", count, es.dwError); + + errFlg = true; + } + + //pEdit->SetSel(0, 0); + } + + /* move us back to the top */ + pEdit->LineScroll(-pEdit->GetFirstVisibleLine()); + + /* just in case it's trying to hold on to something */ + pEdit->EmptyUndoBuffer(); + + /* work around bug that creates unnecessary scroll bars */ + pEdit->SetScrollRange(SB_VERT, 0, 0, TRUE); + pEdit->SetScrollRange(SB_HORZ, 0, 0, TRUE); + + /* display the entire message in the user's selected font */ + if (!fpOutput->GetMultipleFontsFlag()) { + // adjust the font, stripping default boldness from SF_TEXT + NewFontSelected(fpOutput->GetOutputKind() != ReformatOutput::kOutputRTF); + } + + /* enable/disable the scroll bars */ + //pEdit->EnableScrollBar(SB_BOTH, ESB_DISABLE_BOTH); + + if (errFlg) + pEdit->SetBackgroundColor(FALSE, RGB(255, 255, 0)); + else if (emptyFlg) + pEdit->SetBackgroundColor(FALSE, RGB(192, 192, 192)); + else + pEdit->SetBackgroundColor(TRUE, 0); + + /* + * Work around a Windows bug that prevents the scroll bars from + * being displayed immediately. This makes them appear, but the + * vertical scroll bar comes out funky on short files (fixed with + * the SetScrollRange call above). + * + * Best guess: when the edit box is resized, it chooses the scroll bar + * configuration based on the currently-loaded data. If you resize it + * and *then* add data, you're stuck with the previous scroll bar + * values. This doesn't quite make sense though... + * + * This works: + * - Set up dialog. + * - Load data. + * - Do minor twiddling. + * - Resize box significantly. + * + * This works: + * - (box already has data in it) + * - Load new data. + * - Do minor twiddling. + * + * This doesn't: + * - Set up dialog + * - Resize box significantly. + * - Load data. + * - Do minor twiddling. + * + * There might be some first-time setup issues in here. Hard to say. + * Anything related to RichEdit controls is extremely fragile, and must + * be tested with a variety of inputs, preference settings, and under + * at least Win98 and Win2K (which are *very* different). + */ + if (fFirstResize) { + /* adjust the size of the window to match the last size used */ + const Preferences* pPreferences = GET_PREFERENCES(); + long width = pPreferences->GetPrefLong(kPrFileViewerWidth); + long height = pPreferences->GetPrefLong(kPrFileViewerHeight); + CRect fullRect; + GetWindowRect(&fullRect); + //WMSG2(" VFD pre-size %dx%d\n", fullRect.Width(), fullRect.Height()); + fullRect.right = fullRect.left + width; + fullRect.bottom = fullRect.top + height; + MoveWindow(fullRect, TRUE); + + editHadFocus = true; // force focus on edit box + + fFirstResize = false; + } else { + /* this should be enough */ + ShiftControls(0, 0); + } + + if (fpOutput->GetOutputKind() == ReformatOutput::kOutputBitmap) { + /* get the cursor off of the image */ + pEdit->SetSel(-1, -1); + } + + /* + * We want the focus to be on the text window so keyboard selection + * commands work. However, it's also nice to be able to arrow through + * the format selection box. + */ + if (editHadFocus) + pEdit->SetFocus(); + + fTitle = fileName; + //if (fpOutput->GetOutputKind() == ReformatOutput::kOutputText || + // fpOutput->GetOutputKind() == ReformatOutput::kOutputRTF || + // fpOutput->GetOutputKind() == ReformatOutput::kOutputCSV || + // fpOutput->GetOutputKind() == ReformatOutput::kOutputBitmap || + // fpOutput->GetOutputKind() == ReformatOutput::kOutputRaw) + //{ + // not for error messages + fTitle += _T(" ["); + fTitle += fpOutput->GetFormatDescr(); + fTitle += _T("]"); + //} else if (fpOutput->GetOutputKind() == ReformatOutput::kOutputRaw) { + // fTitle += _T(" [Raw]"); + //} + + CString winTitle = _T("File Viewer - "); + winTitle += fTitle; + SetWindowText(winTitle); + + /* + * Enable or disable the next/prev buttons. + */ + CButton* pButton; + pButton = (CButton*) GetDlgItem(IDC_FVIEW_PREV); + pButton->EnableWindow(fpSelSet->IterHasPrev()); + pButton = (CButton*) GetDlgItem(IDC_FVIEW_NEXT); + pButton->EnableWindow(fpSelSet->IterHasNext()); +} + +/* + * Handle the "next" button. + * + * (This is also called from DoDataExchange.) + */ +void +ViewFilesDialog::OnFviewNext(void) +{ + ReformatHolder::ReformatPart part; + ReformatHolder::ReformatID id; + int result; + + if (fBusy) { + WMSG0("BUSY!\n"); + return; + } + + fBusy = true; + + if (!fpSelSet->IterHasNext()) { + ASSERT(false); + return; + } + + /* + * Get the pieces of the file. + */ + SelectionEntry* pSelEntry = fpSelSet->IterNext(); + GenericEntry* pEntry = pSelEntry->GetEntry(); + result = ReformatPrep(pEntry); +#if 0 + { + // for debugging -- simulate failure + result = -1; + delete fpHolder; + fpHolder = nil; + delete fpOutput; + fpOutput = nil; + } +#endif + + fBusy = false; + if (result != 0) { + ASSERT(fpHolder == nil); + ASSERT(fpOutput == nil); + return; + } + + /* + * Format a piece. + */ + ConfigurePartButtons(pSelEntry->GetEntry()); + part = GetSelectedPart(); + id = ConfigureFormatSel(part); + Reformat(pSelEntry->GetEntry(), part, id); + + DisplayText(pSelEntry->GetEntry()->GetDisplayName()); +} + +/* + * Handle the "prev" button. + */ +void +ViewFilesDialog::OnFviewPrev(void) +{ + ReformatHolder::ReformatPart part; + ReformatHolder::ReformatID id; + int result; + + if (fBusy) { + WMSG0("BUSY!\n"); + return; + } + + fBusy = true; + + if (!fpSelSet->IterHasPrev()) { + ASSERT(false); + return; + } + + /* + * Get the pieces of the file. + */ + SelectionEntry* pSelEntry = fpSelSet->IterPrev(); + GenericEntry* pEntry = pSelEntry->GetEntry(); + result = ReformatPrep(pEntry); + + fBusy = false; + if (result != 0) { + ASSERT(fpHolder == nil); + ASSERT(fpOutput == nil); + return; + } + + /* + * Format a piece. + */ + ConfigurePartButtons(pEntry); + part = GetSelectedPart(); + id = ConfigureFormatSel(part); + Reformat(pEntry, part, id); + + DisplayText(pEntry->GetDisplayName()); +} + +/* + * Configure the radio buttons that determine which part to view, enabling + * only those that make sense. + * + * Try to keep the previously-set button set. + * + * If "pEntry" is nil, all buttons are disabled (useful for first-time + * initialization). + */ +void +ViewFilesDialog::ConfigurePartButtons(const GenericEntry* pEntry) +{ + int id = 0; + + CButton* pDataWnd = (CButton*) GetDlgItem(IDC_FVIEW_DATA); + CButton* pRsrcWnd = (CButton*) GetDlgItem(IDC_FVIEW_RSRC); + CButton* pCmmtWnd = (CButton*) GetDlgItem(IDC_FVIEW_CMMT); + ASSERT(pDataWnd != nil && pRsrcWnd != nil && pCmmtWnd != nil); + + /* figure out what was checked before, ignoring if it's not available */ + if (pDataWnd->GetCheck() == BST_CHECKED && pEntry->GetHasDataFork()) + id = IDC_FVIEW_DATA; + else if (pRsrcWnd->GetCheck() == BST_CHECKED && pEntry->GetHasRsrcFork()) + id = IDC_FVIEW_RSRC; + else if (pCmmtWnd->GetCheck() == BST_CHECKED && pEntry->GetHasComment()) + id = IDC_FVIEW_CMMT; + + /* couldn't keep previous check, find a new one */ + if (id == 0) { + if (pEntry->GetHasDataFork()) + id = IDC_FVIEW_DATA; + else if (pEntry->GetHasRsrcFork()) + id = IDC_FVIEW_RSRC; + else if (pEntry->GetHasComment()) + id = IDC_FVIEW_CMMT; + // else leave it set to 0 + } + + /* set up the dialog */ + pDataWnd->SetCheck(BST_UNCHECKED); + pRsrcWnd->SetCheck(BST_UNCHECKED); + pCmmtWnd->SetCheck(BST_UNCHECKED); + + if (pEntry == nil) { + pDataWnd->EnableWindow(FALSE); + pRsrcWnd->EnableWindow(FALSE); + pCmmtWnd->EnableWindow(FALSE); + } else { + pDataWnd->EnableWindow(pEntry->GetHasDataFork()); + pRsrcWnd->EnableWindow(pEntry->GetHasRsrcFork()); + pCmmtWnd->EnableWindow(pEntry->GetHasComment()); + } + + if (id == IDC_FVIEW_RSRC) + pRsrcWnd->SetCheck(BST_CHECKED); + else if (id == IDC_FVIEW_CMMT) + pCmmtWnd->SetCheck(BST_CHECKED); + else + pDataWnd->SetCheck(BST_CHECKED); +} + +/* + * Figure out which part of the file is selected (data/rsrc/comment). + * + * If no part is selected, throws up its hands and returns kPartData. + */ +ReformatHolder::ReformatPart +ViewFilesDialog::GetSelectedPart(void) +{ + CButton* pDataWnd = (CButton*) GetDlgItem(IDC_FVIEW_DATA); + CButton* pRsrcWnd = (CButton*) GetDlgItem(IDC_FVIEW_RSRC); + CButton* pCmmtWnd = (CButton*) GetDlgItem(IDC_FVIEW_CMMT); + ASSERT(pDataWnd != nil && pRsrcWnd != nil && pCmmtWnd != nil); + + if (pDataWnd->GetCheck() == BST_CHECKED) + return ReformatHolder::kPartData; + else if (pRsrcWnd->GetCheck() == BST_CHECKED) + return ReformatHolder::kPartRsrc; + else if (pCmmtWnd->GetCheck() == BST_CHECKED) + return ReformatHolder::kPartCmmt; + else { + assert(false); + return ReformatHolder::kPartData; + } +} + +/* + * Set up the fpHolder. Does not reformat the data, just loads the source + * material and runs the applicability tests. + * + * Returns 0 on success, -1 on failure. + */ +int +ViewFilesDialog::ReformatPrep(GenericEntry* pEntry) +{ + CWaitCursor waitc; // can be slow reading data from floppy + MainWindow* pMainWindow = GET_MAIN_WINDOW(); + int result; + + delete fpHolder; + fpHolder = nil; + + result = pMainWindow->GetFileParts(pEntry, &fpHolder); + if (result != 0) { + WMSG0("GetFileParts(prev) failed!\n"); + ASSERT(fpHolder == nil); + return -1; + } + + /* set up the ReformatHolder */ + MainWindow::ConfigureReformatFromPreferences(fpHolder); + + /* prep for applicability test */ + fpHolder->SetSourceAttributes( + pEntry->GetFileType(), + pEntry->GetAuxType(), + MainWindow::ReformatterSourceFormat(pEntry->GetSourceFS()), + pEntry->GetFileNameExtension()); + + /* figure out which reformatters apply to this file */ + WMSG0("Testing reformatters\n"); + fpHolder->TestApplicability(); + + return 0; +} + +/* + * Reformat a file. + * + * Returns 0 if the file was reformatted, -1 if not + */ +int +ViewFilesDialog::Reformat(const GenericEntry* pEntry, + ReformatHolder::ReformatPart part, ReformatHolder::ReformatID id) +{ + CWaitCursor waitc; + + delete fpOutput; + fpOutput = nil; + + /* run the best one */ + fpOutput = fpHolder->Apply(part, id); + +//bail: + if (fpOutput != nil) { + // success -- do some sanity checks + switch (fpOutput->GetOutputKind()) { + case ReformatOutput::kOutputText: + case ReformatOutput::kOutputRTF: + case ReformatOutput::kOutputCSV: + case ReformatOutput::kOutputErrorMsg: + ASSERT(fpOutput->GetTextBuf() != nil); + ASSERT(fpOutput->GetDIB() == nil); + break; + case ReformatOutput::kOutputBitmap: + ASSERT(fpOutput->GetDIB() != nil); + ASSERT(fpOutput->GetTextBuf() == nil); + break; + case ReformatOutput::kOutputRaw: + // text buf might be nil + ASSERT(fpOutput->GetDIB() == nil); + break; + } + return 0; + } else { + /* shouldn't get here; handle it if we do */ + static const char* kFailMsg = _T("Internal error\r\n"); + fpOutput = new ReformatOutput; + fpOutput->SetTextBuf((char*) kFailMsg, strlen(kFailMsg), false); + fpOutput->SetOutputKind(ReformatOutput::kOutputErrorMsg); + return -1; + } +} + +/* + * Set up the entries in the drop box based on the "applicable" array in + * fpHolder. The set of values is different for each part of the file. + * + * Returns the default reformatter ID. This is always entry #0. + * + * I tried making it "sticky", so that if the user chose to go into hex + * dump mode it would stay there. We returned either entry #0 in the + * combo box, or the previously-selected reformatter ID if it also + * applies to this file. This was a little whacked, e.g. Intbasic vs. + * S-C assembler got ugly, so I tried restricting it to "always" classes. + * But then, the first time you hit a binary file with no reformatter, + * you're stuck there. Eventually I decided to discard the idea. + */ +ReformatHolder::ReformatID +ViewFilesDialog::ConfigureFormatSel(ReformatHolder::ReformatPart part) +{ + //ReformatHolder::ReformatID prevID = ReformatHolder::kReformatUnknown; + ReformatHolder::ReformatID returnID = ReformatHolder::kReformatRaw; + CComboBox* pCombo = (CComboBox*) GetDlgItem(IDC_FVIEW_FORMATSEL); + + WMSG0("--- ConfigureFormatSel\n"); + + //int sel; + //sel = pCombo->GetCurSel(); + //if (sel != CB_ERR) + // prevID = (ReformatHolder::ReformatID) pCombo->GetItemData(sel); + //WMSG1(" prevID = %d\n", prevID); + + EnableFormatSelection(TRUE); + pCombo->ResetContent(); + + /* + * Fill out the combo box with the reformatter entries. + * + * There's probably a way to do this that doesn't involve abusing + * enums, but this'll do for now. + */ + int applyIdx, idIdx; + bool preferred = true; + int comboIdx; + for (applyIdx = ReformatHolder::kApplicMAX; + applyIdx > ReformatHolder::kApplicNot; /*no incr*/) + { + if (applyIdx == ReformatHolder::kApplicMAX) + goto skip; + if (applyIdx == ReformatHolder::kApplicUnknown) + goto skip; + + int testApplies; + + testApplies = applyIdx; + if (preferred) + testApplies |= ReformatHolder::kApplicPreferred; + + for (idIdx = 0; idIdx < ReformatHolder::kReformatMAX; idIdx++) { + if (idIdx == ReformatHolder::kReformatUnknown) + continue; + + ReformatHolder::ReformatApplies applies; + + applies = fpHolder->GetApplic(part, + (ReformatHolder::ReformatID) idIdx); + if ((int) applies == testApplies) { + /* match! */ + CString str; + //WMSG2("MATCH at %d (0x%02x)\n", idIdx, testApplies); + str.Format("%s", + ReformatHolder::GetReformatName((ReformatHolder::ReformatID) idIdx)); + comboIdx = pCombo->AddString(str); + pCombo->SetItemData(comboIdx, idIdx); + + /* define initial selection as best item */ + if (comboIdx == 0) + pCombo->SetCurSel(comboIdx); + + //if (idIdx == (int) prevID && + // applyIdx == ReformatHolder::kApplicAlways) + //{ + // WMSG0(" Found 'always' prevID, selecting\n"); + // pCombo->SetCurSel(comboIdx); + //} + } + } + +skip: + if (!preferred) + applyIdx--; + preferred = !preferred; + } + + /* return whatever we now have selected */ + int sel = pCombo->GetCurSel(); + WMSG1(" At end, sel is %d\n", sel); + if (sel != CB_ERR) + returnID = (ReformatHolder::ReformatID) pCombo->GetItemData(sel); + + return returnID; +} + +/* + * The user has changed entries in the format selection drop box. + * + * Also called from the "quick change" buttons. + */ +void +ViewFilesDialog::OnFormatSelChange(void) +{ + CComboBox* pCombo = (CComboBox*) GetDlgItem(IDC_FVIEW_FORMATSEL); + ASSERT(pCombo != nil); + WMSG1("+++ SELECTION IS NOW %d\n", pCombo->GetCurSel()); + + SelectionEntry* pSelEntry = fpSelSet->IterCurrent(); + GenericEntry* pEntry = pSelEntry->GetEntry(); + ReformatHolder::ReformatPart part; + ReformatHolder::ReformatID id; + + part = GetSelectedPart(); + id = (ReformatHolder::ReformatID) pCombo->GetItemData(pCombo->GetCurSel()); + Reformat(pEntry, part, id); + + DisplayText(pEntry->GetDisplayName()); +} + +/* + * Change the fork we're looking at. + */ +void +ViewFilesDialog::OnFviewData(void) +{ + ForkSelectCommon(ReformatHolder::kPartData); +} +void +ViewFilesDialog::OnFviewRsrc(void) +{ + ForkSelectCommon(ReformatHolder::kPartRsrc); +} +void +ViewFilesDialog::OnFviewCmmt(void) +{ + ForkSelectCommon(ReformatHolder::kPartCmmt); +} +void +ViewFilesDialog::ForkSelectCommon(ReformatHolder::ReformatPart part) +{ + GenericEntry* pEntry; + ReformatHolder::ReformatID id; + + WMSG1("Switching to file part=%d\n", part); + ASSERT(fpHolder != nil); + ASSERT(fpSelSet != nil); + ASSERT(fpSelSet->IterCurrent() != nil); + pEntry = fpSelSet->IterCurrent()->GetEntry(); + ASSERT(pEntry != nil); + + id = ConfigureFormatSel(part); + + Reformat(pEntry, part, id); + DisplayText(pEntry->GetDisplayName()); +} + +/* + * Switch to hex dump mode. + */ +void +ViewFilesDialog::OnFviewFmtBest(void) +{ + CComboBox* pCombo = (CComboBox*) GetDlgItem(IDC_FVIEW_FORMATSEL); + pCombo->SetCurSel(0); // best is always at the top + OnFormatSelChange(); + pCombo->SetFocus(); +} + +/* + * Switch to hex dump mode. + */ +void +ViewFilesDialog::OnFviewFmtHex(void) +{ + CComboBox* pCombo = (CComboBox*) GetDlgItem(IDC_FVIEW_FORMATSEL); + int sel = FindByVal(pCombo, ReformatHolder::kReformatHexDump); + if (sel < 0) + sel = 0; + pCombo->SetCurSel(sel); + OnFormatSelChange(); + pCombo->SetFocus(); +} + +/* + * Switch to raw mode. + */ +void +ViewFilesDialog::OnFviewFmtRaw(void) +{ + CComboBox* pCombo = (CComboBox*) GetDlgItem(IDC_FVIEW_FORMATSEL); + int sel = FindByVal(pCombo, ReformatHolder::kReformatRaw); + if (sel < 0) + sel = 0; + pCombo->SetCurSel(sel); + OnFormatSelChange(); + pCombo->SetFocus(); +} + +/* + * Enable or disable all of the format selection buttons. + */ +void +ViewFilesDialog::EnableFormatSelection(BOOL enable) +{ + GetDlgItem(IDC_FVIEW_FORMATSEL)->EnableWindow(enable); + GetDlgItem(IDC_FVIEW_FMT_BEST)->EnableWindow(enable); + GetDlgItem(IDC_FVIEW_FMT_HEX)->EnableWindow(enable); + GetDlgItem(IDC_FVIEW_FMT_RAW)->EnableWindow(enable); +} + +/* + * Return the combo box index for the entry whose "data" field matches "val". + * + * Returns -1 if the entry couldn't be found. + */ +int +ViewFilesDialog::FindByVal(CComboBox* pCombo, DWORD val) +{ + int count = pCombo->GetCount(); + int i; + + for (i = 0; i < count; i++) { + if (pCombo->GetItemData(i) == val) + return i; + } + return -1; +} + +/* + * Handle the "font" button. Choose a font, then apply the choice to + * all of the text in the box. + */ +void +ViewFilesDialog::OnFviewFont(void) +{ + LOGFONT logFont; + CFont font; + + /* + * Create a LOGFONT structure with the desired default characteristics, + * then use that to initialize the font dialog. + */ + CreateSimpleFont(&font, this, fTypeFace, fPointSize); + font.GetLogFont(&logFont); + + CFontDialog fontDlg(&logFont); + fontDlg.m_cf.Flags &= ~(CF_EFFECTS); + + if (fontDlg.DoModal() == IDOK) { + //fontDlg.GetCurrentFont(&logFont); + fTypeFace = fontDlg.GetFaceName(); + fPointSize = fontDlg.GetSize() / 10; + WMSG2("Now using %d-point '%s'\n", fPointSize, (const char*)fTypeFace); + + NewFontSelected(false); + } + +} + +/* + * Font selection has changed, update the richedit box. + */ +void +ViewFilesDialog::NewFontSelected(bool resetBold) +{ + CRichEditCtrl* pEdit = (CRichEditCtrl*) GetDlgItem(IDC_FVIEW_EDITBOX); + ASSERT(pEdit != nil); + + CHARFORMAT cf; + cf.cbSize = sizeof(CHARFORMAT); + cf.dwMask = CFM_FACE | CFM_SIZE; + if (resetBold) { + cf.dwMask |= CFM_BOLD; + cf.dwEffects = 0; + } + ::lstrcpy(cf.szFaceName, fTypeFace); + cf.yHeight = fPointSize * 20; // in twips + pEdit->SetSel(0, -1); // select all + pEdit->SetSelectionCharFormat(cf); + pEdit->SetSel(0, 0); // unselect +} + + +/* + * Print a ContentList. + */ +void +ViewFilesDialog::OnFviewPrint(void) +{ + MainWindow* pMainWindow = GET_MAIN_WINDOW(); + CPrintDialog dlg(FALSE); // use CPrintDialogEx for Win2K? CPageSetUpDialog? + PrintRichEdit pre; + CDC dc; + int numPages; + + dlg.m_pd.nFromPage = dlg.m_pd.nMinPage = 1; + dlg.m_pd.nToPage = dlg.m_pd.nMaxPage = 1; + + /* + * Getting the expected number of pages requires a print test-run. + * However, if we use GetDefaults to get the DC, the call to DoModal + * returns immediately with IDCANCEL. So, we do our pre-flighting + * in a separate DC with a separate print dialog object. + */ + { + CPrintDialog countDlg(FALSE); + CDC countDC; + + dlg.m_pd.hDevMode = pMainWindow->fhDevMode; + dlg.m_pd.hDevNames = pMainWindow->fhDevNames; + + if (countDlg.GetDefaults() == TRUE) { + CWaitCursor waitc; + + if (countDC.Attach(countDlg.GetPrinterDC()) != TRUE) { + ASSERT(false); + } + pre.Setup(&countDC, this); + pre.PrintPreflight(&fEditCtrl, &numPages); + WMSG1("Default printer generated %d pages\n", numPages); + + dlg.m_pd.nToPage = dlg.m_pd.nMaxPage = numPages; + } + + pMainWindow->fhDevMode = dlg.m_pd.hDevMode; + pMainWindow->fhDevNames = dlg.m_pd.hDevNames; + } + + long startChar, endChar; + fEditCtrl.GetSel(/*ref*/startChar, /*ref*/endChar); + + if (endChar != startChar) { + WMSG2("GetSel returned start=%ld end=%ld\n", startChar, endChar); + dlg.m_pd.Flags &= ~(PD_NOSELECTION); + } + + dlg.m_pd.hDevMode = pMainWindow->fhDevMode; + dlg.m_pd.hDevNames = pMainWindow->fhDevNames; + dlg.m_pd.Flags |= PD_USEDEVMODECOPIESANDCOLLATE; + dlg.m_pd.Flags &= ~(PD_NOPAGENUMS); + + + /* + * Show them the print dialog. + */ + if (dlg.DoModal() != IDOK) + return; + + /* + * Grab the chosen printer and prep ourselves. + */ + if (dc.Attach(dlg.GetPrinterDC()) != TRUE) { + CString msg; + msg.LoadString(IDS_PRINTER_NOT_USABLE); + ShowFailureMsg(this, msg, IDS_FAILED); + return; + } + pre.Setup(&dc, this); + + /* + * Do the printing. + */ + if (dlg.PrintRange()) + pre.PrintPages(&fEditCtrl, fTitle, dlg.GetFromPage(), dlg.GetToPage()); + else if (dlg.PrintSelection()) + pre.PrintSelection(&fEditCtrl, fTitle, startChar, endChar); + else // dlg.PrintAll() + pre.PrintAll(&fEditCtrl, fTitle); + + pMainWindow->fhDevMode = dlg.m_pd.hDevMode; + pMainWindow->fhDevNames = dlg.m_pd.hDevNames; +} + +/* + * User hit the "Find..." button. Open the modeless Find dialog. + */ +void +ViewFilesDialog::OnFviewFind(void) +{ + DWORD flags = 0; + + if (fpFindDialog != nil) + return; + + if (fFindDown) + flags |= FR_DOWN; + if (fFindMatchCase) + flags |= FR_MATCHCASE; + if (fFindMatchWholeWord) + flags |= FR_WHOLEWORD; + + /* + * I can't get this to work with FindText(). There's a lot of questions + * about this on web sites. Probably safest to just disable it. + */ + flags |= FR_HIDEUPDOWN; + + fpFindDialog = new CFindReplaceDialog; + + fpFindDialog->Create(TRUE, // "find" only + fFindLastStr, // default string to search for + NULL, // default string to replace + flags, // flags + this); // parent +} + +/* + * Handle activity in the modeless "find" dialog. + */ +LRESULT +ViewFilesDialog::OnFindDialogMessage(WPARAM wParam, LPARAM lParam) +{ + assert(fpFindDialog != nil); + + fFindDown = (fpFindDialog->SearchDown() != 0); + fFindMatchCase = (fpFindDialog->MatchCase() != 0); + fFindMatchWholeWord = (fpFindDialog->MatchWholeWord() != 0); + + if (fpFindDialog->IsTerminating()) { + WMSG0("VFD find dialog closing\n"); + fpFindDialog = nil; + return 0; + } + + if (fpFindDialog->FindNext()) { + fFindLastStr = fpFindDialog->GetFindString(); + FindNext(fFindLastStr, fFindDown, fFindMatchCase, fFindMatchWholeWord); + } else { + WMSG0("Unexpected find dialog activity\n"); + } + + return 0; +} + + +/* + * Find the next ocurrence of the specified string. + */ +void +ViewFilesDialog::FindNext(const char* str, bool down, bool matchCase, + bool wholeWord) +{ + WMSG4("FindText '%s' d=%d c=%d w=%d\n", str, down, matchCase, wholeWord); + + FINDTEXTEX findTextEx = { 0 }; + CHARRANGE selChrg; + DWORD flags = 0; + long start, result; + + if (matchCase) + flags |= FR_MATCHCASE; + if (wholeWord) + flags |= FR_WHOLEWORD; + + fEditCtrl.GetSel(selChrg); + WMSG2(" selection is %ld,%ld\n", + selChrg.cpMin, selChrg.cpMax); + if (selChrg.cpMin == selChrg.cpMax) + start = selChrg.cpMin; // start at caret + else + start = selChrg.cpMin +1; // start past selection + + findTextEx.chrg.cpMin = start; + findTextEx.chrg.cpMax = -1; + findTextEx.lpstrText = const_cast(str); + + /* MSVC++6 claims FindText doesn't exist, even though it's in the header */ + //result = fEditCtrl.FindText(flags, &findTextEx); + result = fEditCtrl.SendMessage(EM_FINDTEXTEX, (WPARAM) flags, + (LPARAM) &findTextEx); + + if (result == -1) { + /* didn't find it, wrap around to start */ + findTextEx.chrg.cpMin = 0; + findTextEx.chrg.cpMax = -1; + findTextEx.lpstrText = const_cast(str); + result = fEditCtrl.SendMessage(EM_FINDTEXTEX, (WPARAM) flags, + (LPARAM) &findTextEx); + } + + WMSG3(" result=%ld min=%ld max=%ld\n", result, + findTextEx.chrgText.cpMin, findTextEx.chrgText.cpMax); + if (result != -1) { + /* select the text we found */ + fEditCtrl.SetSel(findTextEx.chrgText); + } else { + /* remove selection, leaving caret at start of sel */ + selChrg.cpMax = selChrg.cpMin; + fEditCtrl.SetSel(selChrg); + + MainWindow* pMain = (MainWindow*)::AfxGetMainWnd(); + pMain->FailureBeep(); + } +} + +/* + * User pressed the "Help" button. + */ +void +ViewFilesDialog::OnHelp(void) +{ + WinHelp(HELP_TOPIC_FILE_VIEWER, HELP_CONTEXT); +} diff --git a/app/ViewFilesDialog.h b/app/ViewFilesDialog.h new file mode 100644 index 0000000..17e519c --- /dev/null +++ b/app/ViewFilesDialog.h @@ -0,0 +1,156 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Class for the "view files" dialog box. + */ +#ifndef __VIEWFILESDIALOG__ +#define __VIEWFILESDIALOG__ + +#include "GenericArchive.h" +#include "resource.h" + +class MainWindow; + +/* + * Implementation of the "view files" dialog box. + * + * The default window size is actually defined over in Preferences.cpp. + * The window size is a "sticky" pref (i.e. not stored in registry). + */ +class ViewFilesDialog : public CDialog { +public: + ViewFilesDialog(CWnd* pParentWnd = NULL) : + CDialog(IDD_FILE_VIEWER, pParentWnd) + { + //fpMainWindow = nil; + fpSelSet = nil; + fpHolder = nil; + fpOutput = nil; + fTypeFace = ""; + fPointSize = 0; + fNoWrapText = false; + fBusy = false; + fpRichEditOle = nil; + fFirstResize = false; + + fpFindDialog = nil; + fFindDown = false; + fFindMatchCase = false; + fFindMatchWholeWord = false; + } + virtual ~ViewFilesDialog(void) { + delete fpHolder; + delete fpOutput; + // Windows will handle destruction of fpFindDialog (child window) + } + + void SetSelectionSet(SelectionSet* pSelSet) { fpSelSet = pSelSet; } + + CString GetTextTypeFace(void) const { return fTypeFace; } + void SetTextTypeFace(const char* name) { fTypeFace = name; } + int GetTextPointSize(void) const { return fPointSize; } + void SetTextPointSize(int size) { fPointSize = size; } + //bool GetNoWrapText(void) const { return fNoWrapText; } + void SetNoWrapText(bool val) { fNoWrapText = val; } + +protected: + // overrides + virtual BOOL OnInitDialog(void); + virtual void OnOK(void); + virtual void OnCancel(void); + virtual void DoDataExchange(CDataExchange* pDX); + + afx_msg int OnCreate(LPCREATESTRUCT lpcs); + afx_msg void OnDestroy(void); + afx_msg void OnSize(UINT nType, int cx, int cy); + afx_msg void OnGetMinMaxInfo(MINMAXINFO* pMMI); + + afx_msg void OnFviewNext(void); + afx_msg void OnFviewPrev(void); + afx_msg void OnFviewFont(void); + afx_msg void OnFviewPrint(void); + afx_msg void OnFviewFind(void); + afx_msg void OnFviewData(void); + afx_msg void OnFviewRsrc(void); + afx_msg void OnFviewCmmt(void); + afx_msg void OnFviewFmtBest(void); + afx_msg void OnFviewFmtHex(void); + afx_msg void OnFviewFmtRaw(void); + afx_msg void OnFormatSelChange(void); + afx_msg void OnHelp(void); + //afx_msg void OnFviewWrap(void); + afx_msg LRESULT OnFindDialogMessage(WPARAM wParam, LPARAM lParam); + +private: + void ShiftControls(int deltaX, int deltaY); + //void MoveControl(int id, int deltaX, int deltaY); + //void StretchControl(int id, int deltaX, int deltaY); + void NewFontSelected(bool resetBold); + void DisplayText(const char* fileName); + int ReformatPrep(GenericEntry* pEntry); + int Reformat(const GenericEntry* pEntry, + ReformatHolder::ReformatPart part, ReformatHolder::ReformatID id); + void ConfigurePartButtons(const GenericEntry* pEntry); + ReformatHolder::ReformatPart GetSelectedPart(void); + void ForkSelectCommon(ReformatHolder::ReformatPart part); + ReformatHolder::ReformatID ConfigureFormatSel( + ReformatHolder::ReformatPart part); + int FindByVal(CComboBox* pCombo, DWORD val); + void EnableFormatSelection(BOOL enable); + void FindNext(const char* str, bool down, bool matchCase, + bool wholeWord); + + // pointer to main window, so we can ask for text to view + //MainWindow* fpMainWindow; + + // stuff to display + SelectionSet* fpSelSet; + + // edit control + CRichEditCtrl fEditCtrl; + + // currently loaded file + ReformatHolder* fpHolder; + + // most recent conversion + ReformatOutput* fpOutput; + + // current title of window + CString fTitle; + + // used to display a "gripper" in the bottom right of the dialog + CGripper fGripper; + + // last size of the window, so we can shift things around + CRect fLastWinSize; + + // font characteristics + CString fTypeFace; // name of font + int fPointSize; // size, in points + + // do we want to scroll or wrap? + bool fNoWrapText; + + // the message pump in the progress updater can cause NufxLib reentrancy + // (alternate solution: disable the window while we load stuff) + bool fBusy; + + // this is *really* annoying + bool fFirstResize; + + // used for stuffing images in; points at something inside RichEdit ctrl + IRichEditOle* fpRichEditOle; + + CFindReplaceDialog* fpFindDialog; + CString fFindLastStr; + bool fFindDown; + bool fFindMatchCase; + bool fFindMatchWholeWord; + + DECLARE_MESSAGE_MAP() +}; + +#endif /*__VIEWFILESDIALOG__*/ \ No newline at end of file diff --git a/app/VolumeCopyDialog.cpp b/app/VolumeCopyDialog.cpp new file mode 100644 index 0000000..4cd3cd2 --- /dev/null +++ b/app/VolumeCopyDialog.cpp @@ -0,0 +1,880 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Dialog that allows copying volumes or sub-volumes to and from files on + * disk. + * + * NOTE: we probably shouldn't allow copying volumes that start at block 0 + * and are equal to the DiskImgLib limit on sizes (e.g. 8GB). We'd be + * copying a partial volume, which doesn't make sense. + */ +#include "stdafx.h" +#include "VolumeCopyDialog.h" +#include "HelpTopics.h" +#include "Main.h" + + +BEGIN_MESSAGE_MAP(VolumeCopyDialog, CDialog) + ON_COMMAND(IDHELP, OnHelp) + ON_COMMAND(IDC_VOLUEMCOPYSEL_TOFILE, OnCopyToFile) + ON_COMMAND(IDC_VOLUEMCOPYSEL_FROMFILE, OnCopyFromFile) + ON_NOTIFY(LVN_ITEMCHANGED, IDC_VOLUMECOPYSEL_LIST, OnListChange) + ON_MESSAGE(WMU_DIALOG_READY, OnDialogReady) +END_MESSAGE_MAP() + + +/* + * Sub-class the generic libutil CancelDialog class. + */ +class VolumeXferProgressDialog : public ProgressCancelDialog { +public: + BOOL Create(CWnd* pParentWnd = NULL) { + fAbortOperation = false; + return ProgressCancelDialog::Create(&fAbortOperation, + IDD_VOLUMECOPYPROG, IDC_VOLUMECOPYPROG_PROGRESS, pParentWnd); + } + + void SetCurrentFiles(const char* fromName, const char* toName) { + CWnd* pWnd = GetDlgItem(IDC_VOLUMECOPYPROG_FROM); + ASSERT(pWnd != nil); + pWnd->SetWindowText(fromName); + pWnd = GetDlgItem(IDC_VOLUMECOPYPROG_TO); + ASSERT(pWnd != nil); + pWnd->SetWindowText(toName); + } + +private: + void OnOK(void) { + WMSG0("Ignoring VolumeXferProgressDialog OnOK\n"); + } + + MainWindow* GetMainWindow(void) const { + return (MainWindow*)::AfxGetMainWnd(); + } + + bool fAbortOperation; +}; + + +/* + * Scan the source image. + */ +BOOL +VolumeCopyDialog::OnInitDialog(void) +{ + CRect rect; + + //this->GetWindowRect(&rect); + //WMSG4("RECT is %d, %d, %d, %d\n", rect.left, rect.top, rect.bottom, rect.right); + + ASSERT(fpDiskImg != nil); + ScanDiskInfo(false); + + CDialog::OnInitDialog(); // does DDX init + + CButton* pButton; + pButton = (CButton*) GetDlgItem(IDC_VOLUEMCOPYSEL_FROMFILE); + pButton->EnableWindow(FALSE); + pButton = (CButton*) GetDlgItem(IDC_VOLUEMCOPYSEL_TOFILE); + pButton->EnableWindow(FALSE); + + CString newTitle; + GetWindowText(newTitle); + newTitle += " - "; + newTitle += fPathName; + SetWindowText(newTitle); + + /* + * Prep the listview control. + * + * Columns: + * [icon] Volume name | Format | Size (MB/GB) | Block count + */ + CListCtrl* pListView = (CListCtrl*) GetDlgItem(IDC_VOLUMECOPYSEL_LIST); + ASSERT(pListView != nil); + ListView_SetExtendedListViewStyleEx(pListView->m_hWnd, + LVS_EX_FULLROWSELECT, LVS_EX_FULLROWSELECT); + + int width1, width2, width3; + //CRect rect; + + pListView->GetClientRect(&rect); + width1 = pListView->GetStringWidth("XXVolume NameXXmmmmm"); + width2 = pListView->GetStringWidth("XXFormatXXmmmmmmmmmm"); + width3 = pListView->GetStringWidth("XXSizeXXmmm"); + //width4 = pListView->GetStringWidth("XXBlock CountXX"); + + pListView->InsertColumn(0, "Volume Name", LVCFMT_LEFT, width1); + pListView->InsertColumn(1, "Format", LVCFMT_LEFT, width2); + pListView->InsertColumn(2, "Size", LVCFMT_LEFT, width3); + pListView->InsertColumn(3, "Block Count", LVCFMT_LEFT, + rect.Width() - (width1+width2+width3) + - ::GetSystemMetrics(SM_CXVSCROLL) ); + + /* add images for list; this MUST be loaded before header images */ + LoadListImages(); + pListView->SetImageList(&fListImageList, LVSIL_SMALL); + + LoadList(); + + CenterWindow(); + + int cc = PostMessage(WMU_DIALOG_READY, 0, 0); + ASSERT(cc != 0); + + return TRUE; +} + +/* + * We need to make sure we throw out the DiskFS we created before the modal + * dialog exits. This is necessary because we rely on an external DiskImg, + * and create DiskFS objects that point to it. + */ +void +VolumeCopyDialog::OnOK(void) +{ + Cleanup(); + CDialog::OnOK(); +} +void +VolumeCopyDialog::OnCancel(void) +{ + Cleanup(); + CDialog::OnCancel(); +} +void +VolumeCopyDialog::Cleanup(void) +{ + WMSG0(" VolumeCopyDialog is done, cleaning up DiskFS\n"); + delete fpDiskFS; + fpDiskFS = nil; +} + +/* + * Something changed in the list. Update the buttons. + */ +void +VolumeCopyDialog::OnListChange(NMHDR*, LRESULT* pResult) +{ + //CRect rect; + //this->GetWindowRect(&rect); + //WMSG4("RECT is %d, %d, %d, %d\n", rect.left, rect.top, rect.bottom, rect.right); + + CListCtrl* pListView = (CListCtrl*) GetDlgItem(IDC_VOLUMECOPYSEL_LIST); + ASSERT(pListView != nil); + CButton* pButton; + UINT selectedCount; + + selectedCount = pListView->GetSelectedCount(); + + pButton = (CButton*) GetDlgItem(IDC_VOLUEMCOPYSEL_TOFILE); + pButton->EnableWindow(selectedCount != 0); + + if (!fpDiskImg->GetReadOnly()) { + pButton = (CButton*) GetDlgItem(IDC_VOLUEMCOPYSEL_FROMFILE); + pButton->EnableWindow(selectedCount != 0); + } + + *pResult = 0; +} + +/* + * (Re-)scan the disk image and any sub-volumes. + * + * The top-level disk image should already have been analyzed and the + * format overridden (if necessary). We don't want to do it in here the + * first time around because the "override" dialog screws up placement + * of our dialog box. I guess opening windows from inside OnInitDialog + * isn't expected. Annoying. [Um, maybe we could call CenterWindow?? + * Actually, now I'm a little concerned about modal dialogs coming and + * going while we're in OnInitDialog, because MainWindow is disabled and + * we're not yet enabled. ++ATM] + */ +void +VolumeCopyDialog::ScanDiskInfo(bool scanTop) +{ + const Preferences* pPreferences = GET_PREFERENCES(); + MainWindow* pMain = (MainWindow*)::AfxGetMainWnd(); + DIError dierr; + CString errMsg, failed; + + assert(fpDiskImg != nil); + assert(fpDiskFS == nil); + + if (scanTop) { + DiskImg::FSFormat oldFormat; + oldFormat = fpDiskImg->GetFSFormat(); + + /* check to see if the top-level FS has changed */ + fpDiskImg->AnalyzeImageFS(); + + /* + * If requested (or necessary), verify the format. We only do this + * if we think the format has changed. This is possible, e.g. if + * somebody drops an MS-DOS volume into the first partition of a + * CFFA disk. + */ + if (oldFormat != fpDiskImg->GetFSFormat() && + (fpDiskImg->GetFSFormat() == DiskImg::kFormatUnknown || + fpDiskImg->GetSectorOrder() == DiskImg::kSectorOrderUnknown || + pPreferences->GetPrefBool(kPrQueryImageFormat))) + { + // ignore them if they hit "cancel" + (void) pMain->TryDiskImgOverride(fpDiskImg, fPathName, + DiskImg::kFormatUnknown, nil, true, &errMsg); + if (!errMsg.IsEmpty()) { + ShowFailureMsg(this, errMsg, IDS_FAILED); + return; + } + } + } + + /* + * Creating the "busy" window here is problematic, because we get called + * from OnInitDialog, at which point our window isn't yet established. + * Since we're modal, we disabled MainWindow, which means that when the + * "busy" box goes away there's no CiderPress window to take control. As + * a result we get a nasty flash. + * + * The only way around this is to defer the destruction of the modeless + * dialog until after we become visible. + */ + bool deferDestroy = false; + if (!IsWindowVisible() || !IsWindowEnabled()) { + WMSG0(" Deferring destroy on wait dialog\n"); + deferDestroy = true; + } else { + WMSG0(" Not deferring destroy on wait dialog\n"); + } + + fpWaitDlg = new ExclusiveModelessDialog; + fpWaitDlg->Create(IDD_LOADING, this); + fpWaitDlg->CenterWindow(pMain); + pMain->PeekAndPump(); + + CWaitCursor waitc; + + /* + * Create an appropriate DiskFS object. We only need to do this to get + * the sub-volume info, which is unfortunate since it can be slow. + */ + fpDiskFS = fpDiskImg->OpenAppropriateDiskFS(true); + if (fpDiskFS == nil) { + WMSG0("HEY: OpenAppropriateDiskFS failed!\n"); + /* this is fatal, but there's no easy way to die */ + /* (could we do a DestroyWindow from here?) */ + /* at any rate, with "allowUnknown" set, this shouldn't happen */ + } else { + fpDiskFS->SetScanForSubVolumes(DiskFS::kScanSubContainerOnly); + + dierr = fpDiskFS->Initialize(fpDiskImg, DiskFS::kInitFull); + if (dierr != kDIErrNone) { + CString appName, msg; + appName.LoadString(IDS_MB_APP_NAME); + msg.Format("Warning: error during disk scan: %s.", + DiskImgLib::DIStrError(dierr)); + fpWaitDlg->MessageBox(msg, appName, MB_OK | MB_ICONEXCLAMATION); + /* keep going */ + } + } + + if (!deferDestroy && fpWaitDlg != nil) { + fpWaitDlg->DestroyWindow(); + fpWaitDlg = nil; + } + + return; +} + +/* + * When the focus changes, e.g. after dialog construction completes, see if + * we have a modeless dialog lurking about. + */ +LONG +VolumeCopyDialog::OnDialogReady(UINT, LONG) +{ + if (fpWaitDlg != nil) { + WMSG0("OnDialogReady found active window, destroying\n"); + fpWaitDlg->DestroyWindow(); + fpWaitDlg = nil; + } + return 0; +} + +/* + * (Re-)load the volume and sub-volumes into the list. + * + * We currently only look at the first level of sub-volumes. We're not + * really set up to display a hierarchy in the list view. Very few people + * will ever need to access a sub-sub-volume in this way, so it's not + * worth sorting it out. + */ +void +VolumeCopyDialog::LoadList(void) +{ + CListCtrl* pListView = (CListCtrl*) GetDlgItem(IDC_VOLUMECOPYSEL_LIST); + ASSERT(pListView != nil); + int itemIndex = 0; + + CString unknown = "(unknown)"; + + pListView->DeleteAllItems(); + if (fpDiskFS == nil) { + /* can only happen if imported volume is unrecognizeable */ + return; + } + + AddToList(pListView, fpDiskImg, fpDiskFS, &itemIndex); + + DiskImgLib::DiskFS::SubVolume* pSubVolume; + pSubVolume = fpDiskFS->GetNextSubVolume(nil); + while (pSubVolume != nil) { + if (pSubVolume->GetDiskFS() == nil) { + WMSG0("WARNING: sub-volume DiskFS is nil?!\n"); + assert(false); + } else { + AddToList(pListView, pSubVolume->GetDiskImg(), + pSubVolume->GetDiskFS(), &itemIndex); + } + pSubVolume = fpDiskFS->GetNextSubVolume(pSubVolume); + } +} + +/* + * Create an entry for a diskimg/diskfs pair. + */ +void +VolumeCopyDialog::AddToList(CListCtrl* pListView, DiskImg* pDiskImg, + DiskFS* pDiskFS, int* pIndex) +{ + CString volName, format, sizeStr, blocksStr; + long numBlocks; + + assert(pListView != nil); + assert(pDiskImg != nil); + assert(pDiskFS != nil); + assert(pIndex != nil); + + numBlocks = pDiskImg->GetNumBlocks(); + + volName = pDiskFS->GetVolumeName(); + format = DiskImg::ToString(pDiskImg->GetFSFormat()); + blocksStr.Format("%ld", pDiskImg->GetNumBlocks()); + if (numBlocks > 1024*1024*2) + sizeStr.Format("%.2fGB", (double) numBlocks / (1024.0*1024.0*2.0)); + else if (numBlocks > 1024*2) + sizeStr.Format("%.2fMB", (double) numBlocks / (1024.0*2.0)); + else + sizeStr.Format("%.2fKB", (double) numBlocks / 2.0); + + /* add entry; first entry is the whole volume */ + pListView->InsertItem(*pIndex, volName, + *pIndex == 0 ? kListIconVolume : kListIconSubVolume); + pListView->SetItemText(*pIndex, 1, format); + pListView->SetItemText(*pIndex, 2, sizeStr); + pListView->SetItemText(*pIndex, 3, blocksStr); + pListView->SetItemData(*pIndex, (DWORD) pDiskFS); + (*pIndex)++; +} + + +/* + * Recover the DiskImg and DiskFS pointers for the volume or sub-volume + * currently selected in the list. + * + * Returns "true" on success, "false" on failure. + */ +bool +VolumeCopyDialog::GetSelectedDisk(DiskImg** ppDiskImg, DiskFS** ppDiskFS) +{ + CListCtrl* pListView = (CListCtrl*) GetDlgItem(IDC_VOLUMECOPYSEL_LIST); + ASSERT(pListView != nil); + + ASSERT(ppDiskImg != nil); + ASSERT(ppDiskFS != nil); + + if (pListView->GetSelectedCount() != 1) + return false; + + POSITION posn; + posn = pListView->GetFirstSelectedItemPosition(); + if (posn == nil) { + ASSERT(false); + return false; + } + int num = pListView->GetNextSelectedItem(posn); + DWORD data = pListView->GetItemData(num); + + *ppDiskFS = (DiskFS*) data; + assert(*ppDiskFS != nil); + *ppDiskImg = (*ppDiskFS)->GetDiskImg(); + return true; +} + +/* + * User pressed the "Help" button. + */ +void +VolumeCopyDialog::OnHelp(void) +{ + WinHelp(HELP_TOPIC_VOLUME_COPIER, HELP_CONTEXT); +} + + +/* + * User pressed the "copy to file" button. Copy the selected partition out to + * a file on disk. + */ +void +VolumeCopyDialog::OnCopyToFile(void) +{ + VolumeXferProgressDialog* pProgressDialog = nil; + Preferences* pPreferences = GET_PREFERENCES_WR(); + MainWindow* pMain = (MainWindow*)::AfxGetMainWnd(); + DiskImg::FSFormat originalFormat = DiskImg::kFormatUnknown; + DiskImg* pSrcImg = nil; + DiskFS* pSrcFS = nil; + DiskImg dstImg; + DIError dierr; + CString errMsg, saveName, msg, srcName; + int result; + + result = GetSelectedDisk(&pSrcImg, &pSrcFS); + if (!result) + return; + assert(pSrcImg != nil); + assert(pSrcFS != nil); + + srcName = pSrcFS->GetVolumeName(); + + /* force the format to be generic ProDOS-ordered blocks */ + originalFormat = pSrcImg->GetFSFormat(); + dierr = pSrcImg->OverrideFormat(pSrcImg->GetPhysicalFormat(), + DiskImg::kFormatGenericProDOSOrd, pSrcImg->GetSectorOrder()); + if (dierr != kDIErrNone) { + errMsg.Format("Internal error: couldn't switch to generic ProDOS: %s.", + DiskImgLib::DIStrError(dierr)); + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + WMSG2("Logical volume '%s' has %d 512-byte blocks\n", + srcName, pSrcImg->GetNumBlocks()); + + /* + * Select file to write blocks to. + */ + { + CFileDialog saveDlg(FALSE, "po", NULL, + OFN_OVERWRITEPROMPT|OFN_NOREADONLYRETURN|OFN_HIDEREADONLY, + "All Files (*.*)|*.*||", this); + + CString saveFolder; + static char* title = "New disk image (.po)"; + + saveDlg.m_ofn.lpstrTitle = title; + saveDlg.m_ofn.lpstrInitialDir = + pPreferences->GetPrefString(kPrOpenArchiveFolder); + + if (saveDlg.DoModal() != IDOK) { + WMSG0(" User bailed out of image save dialog\n"); + goto bail; + } + + saveFolder = saveDlg.m_ofn.lpstrFile; + saveFolder = saveFolder.Left(saveDlg.m_ofn.nFileOffset); + pPreferences->SetPrefString(kPrOpenArchiveFolder, saveFolder); + + saveName = saveDlg.GetPathName(); + } + WMSG1("File will be saved to '%s'\n", saveName); + + /* DiskImgLib does not like it if file already exists */ + errMsg = pMain->RemoveFile(saveName); + if (!errMsg.IsEmpty()) { + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + + /* + * Create a block image with the expected number of blocks. + */ + int dstNumBlocks; + dstNumBlocks = pSrcImg->GetNumBlocks(); + + { + ExclusiveModelessDialog* pWaitDlg = new ExclusiveModelessDialog; + pWaitDlg->Create(IDD_FORMATTING, this); + pWaitDlg->CenterWindow(pMain); + pMain->PeekAndPump(); // redraw + CWaitCursor waitc; + + dierr = dstImg.CreateImage(saveName, nil, + DiskImg::kOuterFormatNone, + DiskImg::kFileFormatUnadorned, + DiskImg::kPhysicalFormatSectors, + nil, + DiskImg::kSectorOrderProDOS, + DiskImg::kFormatGenericProDOSOrd, + dstNumBlocks, + true /* don't need to erase contents */); + + pWaitDlg->DestroyWindow(); + //pMain->PeekAndPump(); // redraw + } + if (dierr != kDIErrNone) { + errMsg.Format("Couldn't create disk image: %s.", + DiskImgLib::DIStrError(dierr)); + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + + /* initialize cancel dialog, and disable main window */ + pProgressDialog = new VolumeXferProgressDialog; + EnableWindow(FALSE); + if (pProgressDialog->Create(this) == FALSE) { + WMSG0("Progress dialog init failed?!\n"); + ASSERT(false); + goto bail; + } + pProgressDialog->SetCurrentFiles(srcName, saveName); + + time_t startWhen, endWhen; + startWhen = time(nil); + + /* + * Do the actual block copy. + */ + dierr = pMain->CopyDiskImage(&dstImg, pSrcImg, false, false, pProgressDialog); + if (dierr != kDIErrNone) { + if (dierr == kDIErrCancelled) { + errMsg.LoadString(IDS_OPERATION_CANCELLED); + ShowFailureMsg(pProgressDialog, errMsg, IDS_CANCELLED); + // remove the partially-written file + dstImg.CloseImage(); + unlink(saveName); + } else { + errMsg.Format("Copy failed: %s.", DiskImgLib::DIStrError(dierr)); + ShowFailureMsg(pProgressDialog, errMsg, IDS_FAILED); + } + goto bail; + } + + dierr = dstImg.CloseImage(); + if (dierr != kDIErrNone) { + errMsg.Format("ERROR: dstImg close failed (err=%d)\n", dierr); + ShowFailureMsg(pProgressDialog, errMsg, IDS_FAILED); + goto bail; + } + + /* put elapsed time in the debug log */ + endWhen = time(nil); + float elapsed; + if (endWhen == startWhen) + elapsed = 1.0; + else + elapsed = (float) (endWhen - startWhen); + msg.Format("Copied %ld blocks in %ld seconds (%.2fKB/sec)", + pSrcImg->GetNumBlocks(), endWhen - startWhen, + (pSrcImg->GetNumBlocks() / 2.0) / elapsed); + WMSG1("%s\n", (const char*) msg); +#ifdef _DEBUG + pProgressDialog->MessageBox(msg, "DEBUG: elapsed time", MB_OK); +#endif + + pMain->SuccessBeep(); + + +bail: + // restore the dialog window to prominence + EnableWindow(TRUE); + //SetActiveWindow(); + if (pProgressDialog != nil) + pProgressDialog->DestroyWindow(); + + /* un-override the source disk */ + if (originalFormat != DiskImg::kFormatUnknown) { + dierr = pSrcImg->OverrideFormat(pSrcImg->GetPhysicalFormat(), + originalFormat, pSrcImg->GetSectorOrder()); + if (dierr != kDIErrNone) { + WMSG1("ERROR: couldn't un-override source image (dierr=%d)\n", dierr); + // not much else to do; should be okay + } + } + return; +} + + +/* + * User pressed the "copy from file" button. Copy a file over the selected + * partition. We may need to reload the main window after this completes. + */ +void +VolumeCopyDialog::OnCopyFromFile(void) +{ + VolumeXferProgressDialog* pProgressDialog = nil; + Preferences* pPreferences = GET_PREFERENCES_WR(); + MainWindow* pMain = (MainWindow*)::AfxGetMainWnd(); + //DiskImg::FSFormat originalFormat = DiskImg::kFormatUnknown; + CString openFilters; + CString loadName, targetName, errMsg, warning; + DiskImg* pDstImg = nil; + DiskFS* pDstFS = nil; + DiskImg srcImg; + DIError dierr; + int result; + bool needReload = false; + bool isPartial = false; + + warning.LoadString(IDS_WARNING); + + /* + * Get the DiskImg and DiskFS pointers for the selected partition out of + * the control. The storage for these is part of fpDiskFS, which holds + * the tree of subvolumes. + */ + result = GetSelectedDisk(&pDstImg, &pDstFS); + if (!result) + return; + +// if (pDstFS == nil) +// targetName = "the target volume"; +// else + targetName = pDstFS->GetVolumeName(); + + + /* + * Select the image to copy from. + */ + openFilters = MainWindow::kOpenDiskImage; + openFilters += MainWindow::kOpenAll; + openFilters += MainWindow::kOpenEnd; + CFileDialog dlg(TRUE, "dsk", NULL, OFN_FILEMUSTEXIST, openFilters, this); + + /* source file gets opened read-only */ + dlg.m_ofn.Flags |= OFN_HIDEREADONLY; + dlg.m_ofn.lpstrTitle = "Select image to copy from"; + dlg.m_ofn.lpstrInitialDir = pPreferences->GetPrefString(kPrOpenArchiveFolder); + + if (dlg.DoModal() != IDOK) + goto bail; + loadName = dlg.GetPathName(); + + { + CWaitCursor waitc; + CString saveFolder; + + /* open the image file and analyze it */ + dierr = srcImg.OpenImage(loadName, PathProposal::kLocalFssep, true); + if (dierr != kDIErrNone) { + errMsg.Format("Unable to open disk image: %s.", + DiskImgLib::DIStrError(dierr)); + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + + if (srcImg.AnalyzeImage() != kDIErrNone) { + errMsg.Format("The file '%s' doesn't seem to hold a valid disk image.", + loadName); + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + + // save our folder choice in the preferences file + saveFolder = dlg.m_ofn.lpstrFile; + saveFolder = saveFolder.Left(dlg.m_ofn.nFileOffset); + pPreferences->SetPrefString(kPrOpenArchiveFolder, saveFolder); + } + + /* + * Require that the input be block-addressable. This isn't really the + * right test, because it's conceivable that somebody would want to put + * a nibble image onto a disk volume. I can't think of a good reason + * to do this -- you can't just splat a fixed-track-length .NIB file + * onto a 5.25" disk, assuming you could get the drive to work on a PC + * in the first place -- so I'm going to take the simple way out. The + * right test is to verify that the EOF on the input is the same as the + * EOF on the output. + */ + if (!srcImg.GetHasBlocks()) { + errMsg = "The disk image must be block-oriented. Nibble images" + " cannot be copied."; + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + + /* force source volume to generic ProDOS blocks */ + dierr = srcImg.OverrideFormat(srcImg.GetPhysicalFormat(), + DiskImg::kFormatGenericProDOSOrd, srcImg.GetSectorOrder()); + if (dierr != kDIErrNone) { + errMsg.Format("Internal error: couldn't switch source to generic ProDOS: %s.", + DiskImgLib::DIStrError(dierr)); + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + WMSG2("Source image '%s' has %d 512-byte blocks\n", + loadName, srcImg.GetNumBlocks()); + + WMSG1("Target volume has %d 512-byte blocks\n", pDstImg->GetNumBlocks()); + + if (srcImg.GetNumBlocks() > pDstImg->GetNumBlocks()) { + errMsg.Format("Error: the disk image file has %ld blocks, but the" + " target volume holds %ld blocks. The target must" + " have more space than the input file.", + srcImg.GetNumBlocks(), pDstImg->GetNumBlocks()); + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + if (pDstImg->GetNumBlocks() >= DiskImgLib::kVolumeMaxBlocks) { + errMsg.Format("Error: for safety reasons, copying disk images to" + " larger volumes is not supported when the target" + " is 8GB or larger."); + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + + if (srcImg.GetNumBlocks() != pDstImg->GetNumBlocks()) { + errMsg.LoadString(IDS_WARNING); + errMsg.Format("The disk image file has %ld blocks, but the target" + " volume holds %ld blocks. The leftover space may be" + " wasted, and non-ProDOS volumes may not be identified" + " correctly. Do you wish to continue?", + srcImg.GetNumBlocks(), pDstImg->GetNumBlocks()); + result = MessageBox(errMsg, warning, MB_OKCANCEL | MB_ICONQUESTION); + if (result != IDOK) { + WMSG0("User chickened out of oversized disk copy\n"); + goto bail; + } + isPartial = true; + } + + errMsg.LoadString(IDS_WARNING); + errMsg.Format("You are about to overwrite volume %s with the" + " contents of '%s'. This will destroy all data on" + " %s. Are you sure you wish to continue?", + targetName, loadName, targetName); + result = MessageBox(errMsg, warning, MB_OKCANCEL | MB_ICONEXCLAMATION); + if (result != IDOK) { + WMSG0("User chickened out of disk copy\n"); + goto bail; + } + + /* force the target disk image to be generic ProDOS-ordered blocks */ + dierr = pDstImg->OverrideFormat(pDstImg->GetPhysicalFormat(), + DiskImg::kFormatGenericProDOSOrd, pDstImg->GetSectorOrder()); + if (dierr != kDIErrNone) { + errMsg.Format("Internal error: couldn't switch target to generic ProDOS: %s.", + DiskImgLib::DIStrError(dierr)); + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + + /* from here on out, before we exit we must re-analyze this volume */ + needReload = true; + + // redraw main to erase previous dialog + pMain->PeekAndPump(); + + /* initialize cancel dialog, and disable dialog */ + pProgressDialog = new VolumeXferProgressDialog; + EnableWindow(FALSE); + if (pProgressDialog->Create(this) == FALSE) { + WMSG0("Progress dialog init failed?!\n"); + ASSERT(false); + return; + } +// if (pDstFS == nil) +// pProgressDialog->SetCurrentFiles(loadName, "target"); +// else + pProgressDialog->SetCurrentFiles(loadName, targetName); + + /* + * We want to delete fpDiskFS now, but we can't because it's holding the + * storage for the DiskImg/DiskFS pointers in the subvolume list. We + * flush it to ensure that it won't try to write to the disk after the + * copy completes. + */ + fpDiskFS->Flush(DiskImg::kFlushAll); + + time_t startWhen, endWhen; + startWhen = time(nil); + + /* + * Do the actual block copy. + */ + dierr = pMain->CopyDiskImage(pDstImg, &srcImg, false, isPartial, + pProgressDialog); + if (dierr != kDIErrNone) { + if (dierr == kDIErrCancelled) { + errMsg.LoadString(IDS_OPERATION_CANCELLED); + ShowFailureMsg(pProgressDialog, errMsg, IDS_CANCELLED); + } else { + errMsg.Format("Copy failed: %s.", DiskImgLib::DIStrError(dierr)); + ShowFailureMsg(pProgressDialog, errMsg, IDS_FAILED); + } + goto bail; + } + + dierr = srcImg.CloseImage(); + if (dierr != kDIErrNone) { + errMsg.Format("ERROR: srcImg close failed (err=%d)\n", dierr); + ShowFailureMsg(pProgressDialog, errMsg, IDS_FAILED); + goto bail; + } + + endWhen = time(nil); + float elapsed; + if (endWhen == startWhen) + elapsed = 1.0; + else + elapsed = (float) (endWhen - startWhen); + errMsg.Format("Copied %ld blocks in %ld seconds (%.2fKB/sec)", + srcImg.GetNumBlocks(), endWhen - startWhen, + (srcImg.GetNumBlocks() / 2.0) / elapsed); + WMSG1("%s\n", (const char*) errMsg); +#ifdef _DEBUG + pProgressDialog->MessageBox(errMsg, "DEBUG: elapsed time", MB_OK); +#endif + + pMain->SuccessBeep(); + + /* + * If a DiskFS insists on privately caching stuff (e.g. libhfs), we could + * end up corrupting the image we just wrote. We use SetAllReadOnly() to + * ensure that nothing will be written before we delete the DiskFS. + */ + assert(!fpDiskImg->GetReadOnly()); + fpDiskFS->SetAllReadOnly(true); + delete fpDiskFS; + fpDiskFS = nil; + assert(fpDiskImg->GetReadOnly()); + fpDiskImg->SetReadOnly(false); + +bail: + // restore the dialog window to prominence + EnableWindow(TRUE); + //SetActiveWindow(); + if (pProgressDialog != nil) + pProgressDialog->DestroyWindow(); + + /* + * Force a reload. We need to reload the disk information, then reload + * the list contents. + * + * By design, anything that would require un-overriding the format of + * the target DiskImg requires reloading it completely. Sort of heavy- + * handed, but it's reliable. + */ + if (needReload) { + WMSG0("RELOAD dialog\n"); + ScanDiskInfo(true); // reopens fpDiskFS + LoadList(); + + /* will we need to reopen the currently-open file list archive? */ + if (pMain->IsOpenPathName(fPathName)) + pMain->SetReopenFlag(); + } + return; +} diff --git a/app/VolumeCopyDialog.h b/app/VolumeCopyDialog.h new file mode 100644 index 0000000..d766929 --- /dev/null +++ b/app/VolumeCopyDialog.h @@ -0,0 +1,80 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Dialog that allows copying volumes or sub-volumes to and from files on + * disk. Handy for backing up and restoring floppy disks and CFFA partitions. + */ +#ifndef __VOLUMECOPYDIALOG__ +#define __VOLUMECOPYDIALOG__ + +#include +#include "../diskimg/DiskImg.h" +#include "resource.h" + +/* + * A dialog with a list control that we populate with the names of the + * volumes in the system. + */ +class VolumeCopyDialog : public CDialog { +public: + VolumeCopyDialog(CWnd* pParentWnd = NULL) : + CDialog(IDD_VOLUMECOPYSEL, pParentWnd), + fpDiskImg(nil), + fpDiskFS(nil), + fpWaitDlg(nil) + {} + ~VolumeCopyDialog(void) { assert(fpDiskFS == nil); } + + /* disk image to work with; we don't own it */ + DiskImgLib::DiskImg* fpDiskImg; + /* path name of input disk image or volume; mainly for display */ + CString fPathName; + +protected: + virtual BOOL OnInitDialog(void); + //virtual void DoDataExchange(CDataExchange* pDX); + virtual void OnOK(void); + virtual void OnCancel(void); + + void Cleanup(void); + + enum { WMU_DIALOG_READY = WM_USER+2 }; + + afx_msg void OnHelp(void); + afx_msg void OnListChange(NMHDR* pNotifyStruct, LRESULT* pResult); + afx_msg void OnCopyToFile(void); + afx_msg void OnCopyFromFile(void); + + afx_msg LONG OnDialogReady(UINT, LONG); + + void ScanDiskInfo(bool scanTop); + void LoadList(void); + void AddToList(CListCtrl* pListView, DiskImgLib::DiskImg* pDiskImg, + DiskImgLib::DiskFS* pDiskFS, int* pIndex); + bool GetSelectedDisk(DiskImgLib::DiskImg** ppDstImg, + DiskImgLib::DiskFS** ppDiskFS); + + // Load images to be used in the list. Apparently this must be called + // before we try to load any header images. + void LoadListImages(void) { + if (!fListImageList.Create(IDB_VOL_PICS, 16, 1, CLR_DEFAULT)) + WMSG0("GLITCH: list image create failed\n"); + fListImageList.SetBkColor(::GetSysColor(COLOR_WINDOW)); + } + enum { // defs for IDB_VOL_PICS + kListIconVolume = 0, + kListIconSubVolume = 1, + }; + CImageList fListImageList; + + DiskImgLib::DiskFS* fpDiskFS; + + ModelessDialog* fpWaitDlg; + + DECLARE_MESSAGE_MAP() +}; + +#endif /*__VOLUMECOPYDIALOG*/ diff --git a/app/app.dsp b/app/app.dsp new file mode 100644 index 0000000..587ae3f --- /dev/null +++ b/app/app.dsp @@ -0,0 +1,641 @@ +# Microsoft Developer Studio Project File - Name="app" - Package Owner=<4> +# Microsoft Developer Studio Generated Build File, Format Version 6.00 +# ** DO NOT EDIT ** + +# TARGTYPE "Win32 (x86) Application" 0x0101 + +CFG=app - Win32 Debug +!MESSAGE This is not a valid makefile. To build this project using NMAKE, +!MESSAGE use the Export Makefile command and run +!MESSAGE +!MESSAGE NMAKE /f "app.mak". +!MESSAGE +!MESSAGE You can specify a configuration when running NMAKE +!MESSAGE by defining the macro CFG on the command line. For example: +!MESSAGE +!MESSAGE NMAKE /f "app.mak" CFG="app - Win32 Debug" +!MESSAGE +!MESSAGE Possible choices for configuration are: +!MESSAGE +!MESSAGE "app - Win32 Release" (based on "Win32 (x86) Application") +!MESSAGE "app - Win32 Debug" (based on "Win32 (x86) Application") +!MESSAGE + +# Begin Project +# PROP AllowPerConfigDependencies 0 +# PROP Scc_ProjName "" +# PROP Scc_LocalPath "" +CPP=cl.exe +MTL=midl.exe +RSC=rc.exe + +!IF "$(CFG)" == "app - Win32 Release" + +# PROP BASE Use_MFC 0 +# PROP BASE Use_Debug_Libraries 0 +# PROP BASE Output_Dir "Release" +# PROP BASE Intermediate_Dir "Release" +# PROP BASE Target_Dir "" +# PROP Use_MFC 2 +# PROP Use_Debug_Libraries 0 +# PROP Output_Dir "Release" +# PROP Intermediate_Dir "Release" +# PROP Ignore_Export_Lib 0 +# PROP Target_Dir "" +# ADD BASE CPP /nologo /W3 /GX /O2 /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /D "_MBCS" /Yu"stdafx.h" /FD /c +# ADD CPP /nologo /MD /W3 /GX /O2 /D "WIN32" /D "NDEBUGX" /D "_WINDOWS" /D "_MBCS" /D "_AFXDLL" /FR /YX"stdafx.h" /FD /c +# ADD BASE MTL /nologo /D "NDEBUG" /mktyplib203 /win32 +# ADD MTL /nologo /D "NDEBUG" /mktyplib203 /win32 +# ADD BASE RSC /l 0x409 /d "NDEBUG" +# ADD RSC /l 0x409 /d "NDEBUG" /d "_AFXDLL" +BSC32=bscmake.exe +# ADD BASE BSC32 /nologo +# ADD BSC32 /nologo +LINK32=link.exe +# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /subsystem:windows /machine:I386 +# ADD LINK32 ..\prebuilt\nufxlib2.lib ..\prebuilt\zdll.lib /nologo /subsystem:windows /map /machine:I386 /out:"Release/CiderPress.exe" +# Begin Special Build Tool +SOURCE="$(InputPath)" +PostBuild_Desc=Copying DLLs and help +PostBuild_Cmds=copy Help\CiderPress.hlp Release copy Help\CiderPress.cnt Release copy ..\prebuilt\nufxlib2.dll . copy ..\prebuilt\zlib1.dll . +# End Special Build Tool + +!ELSEIF "$(CFG)" == "app - Win32 Debug" + +# PROP BASE Use_MFC 0 +# PROP BASE Use_Debug_Libraries 1 +# PROP BASE Output_Dir "Debug" +# PROP BASE Intermediate_Dir "Debug" +# PROP BASE Target_Dir "" +# PROP Use_MFC 2 +# PROP Use_Debug_Libraries 1 +# PROP Output_Dir "Debug" +# PROP Intermediate_Dir "Debug" +# PROP Ignore_Export_Lib 0 +# PROP Target_Dir "" +# ADD BASE CPP /nologo /W3 /Gm /GX /ZI /Od /D "WIN32" /D "_DEBUG" /D "_WINDOWS" /D "_MBCS" /Yu"stdafx.h" /FD /GZ /c +# ADD CPP /nologo /MDd /W3 /Gm /GX /ZI /Od /D "WIN32" /D "_DEBUG" /D "_WINDOWS" /D "_MBCS" /D "_AFXDLL" /FR /YX"stdafx.h" /FD /GZ /c +# ADD BASE MTL /nologo /D "_DEBUG" /mktyplib203 /win32 +# ADD MTL /nologo /D "_DEBUG" /mktyplib203 /win32 +# ADD BASE RSC /l 0x409 /d "_DEBUG" +# ADD RSC /l 0x409 /d "_DEBUG" /d "_AFXDLL" +BSC32=bscmake.exe +# ADD BASE BSC32 /nologo +# ADD BSC32 /nologo +LINK32=link.exe +# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /subsystem:windows /debug /machine:I386 /pdbtype:sept +# ADD LINK32 ..\prebuilt\nufxlib2D.lib ..\prebuilt\zdll.lib /nologo /subsystem:windows /debug /machine:I386 /out:"Debug/CiderPress.exe" /pdbtype:sept +# Begin Special Build Tool +SOURCE="$(InputPath)" +PostBuild_Desc=Copying debug DLLs and help +PostBuild_Cmds=copy Help\CiderPress.hlp Debug copy Help\CiderPress.cnt Debug copy ..\prebuilt\nufxlib2D.dll . copy ..\prebuilt\zlib1.dll . +# End Special Build Tool + +!ENDIF + +# Begin Target + +# Name "app - Win32 Release" +# Name "app - Win32 Debug" +# Begin Group "Source Files" + +# PROP Default_Filter "cpp;c;cxx;rc;def;r;odl;idl;hpj;bat" +# Begin Source File + +SOURCE=.\AboutDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\ActionProgressDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\Actions.cpp +# End Source File +# Begin Source File + +SOURCE=.\ACUArchive.cpp +# End Source File +# Begin Source File + +SOURCE=.\AddClashDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\AddFilesDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\ArchiveInfoDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\BasicImport.cpp +# End Source File +# Begin Source File + +SOURCE=.\BNYArchive.cpp +# End Source File +# Begin Source File + +SOURCE=.\CassetteDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\CassImpTargetDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\ChooseAddTargetDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\ChooseDirDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\CiderPress.rc +# End Source File +# Begin Source File + +SOURCE=.\Clipboard.cpp +# End Source File +# Begin Source File + +SOURCE=.\ConfirmOverwriteDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\ContentList.cpp +# End Source File +# Begin Source File + +SOURCE=.\ConvDiskOptionsDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\ConvFileOptionsDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\CreateImageDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\CreateSubdirDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\DEFileDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\DiskArchive.cpp +# End Source File +# Begin Source File + +SOURCE=.\DiskConvertDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\DiskEditDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\DiskEditOpenDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\DiskFSTree.cpp +# End Source File +# Begin Source File + +SOURCE=.\EditAssocDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\EditCommentDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\EditPropsDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\EnterRegDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\EOLScanDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\ExtractOptionsDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\FileNameConv.cpp +# End Source File +# Begin Source File + +SOURCE=.\GenericArchive.cpp +# End Source File +# Begin Source File + +SOURCE=.\ImageFormatDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\Main.cpp +# End Source File +# Begin Source File + +SOURCE=.\MyApp.cpp +# End Source File +# Begin Source File + +SOURCE=.\NewDiskSize.cpp +# End Source File +# Begin Source File + +SOURCE=.\NewFolderDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\NufxArchive.cpp +# End Source File +# Begin Source File + +SOURCE=.\OpenVolumeDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\PasteSpecialDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\Preferences.cpp +# End Source File +# Begin Source File + +SOURCE=.\PrefsDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\Print.cpp +# End Source File +# Begin Source File + +SOURCE=.\RecompressOptionsDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\Registry.cpp +# End Source File +# Begin Source File + +SOURCE=.\RenameEntryDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\RenameVolumeDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\Squeeze.cpp +# End Source File +# Begin Source File + +SOURCE=.\StdAfx.cpp +# End Source File +# Begin Source File + +SOURCE=.\SubVolumeDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\Tools.cpp +# End Source File +# Begin Source File + +SOURCE=.\TwoImgPropsDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\UseSelectionDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\ViewFilesDialog.cpp +# End Source File +# Begin Source File + +SOURCE=.\VolumeCopyDialog.cpp +# End Source File +# End Group +# Begin Group "Header Files" + +# PROP Default_Filter "h;hpp;hxx;hm;inl" +# Begin Source File + +SOURCE=.\AboutDialog.h +# End Source File +# Begin Source File + +SOURCE=.\ActionProgressDialog.h +# End Source File +# Begin Source File + +SOURCE=.\ACUArchive.h +# End Source File +# Begin Source File + +SOURCE=.\AddClashDialog.h +# End Source File +# Begin Source File + +SOURCE=.\AddFilesDialog.h +# End Source File +# Begin Source File + +SOURCE=.\ArchiveInfoDialog.h +# End Source File +# Begin Source File + +SOURCE=.\BasicImport.h +# End Source File +# Begin Source File + +SOURCE=.\BNYArchive.h +# End Source File +# Begin Source File + +SOURCE=.\CassetteDialog.h +# End Source File +# Begin Source File + +SOURCE=.\CassImpTargetDialog.h +# End Source File +# Begin Source File + +SOURCE=.\ChooseAddTargetDialog.h +# End Source File +# Begin Source File + +SOURCE=.\ChooseDirDialog.h +# End Source File +# Begin Source File + +SOURCE=.\ConfirmOverwriteDialog.h +# End Source File +# Begin Source File + +SOURCE=.\ContentList.h +# End Source File +# Begin Source File + +SOURCE=.\ConvDiskOptionsDialog.h +# End Source File +# Begin Source File + +SOURCE=.\ConvFileOptionsDialog.h +# End Source File +# Begin Source File + +SOURCE=.\CreateImageDialog.h +# End Source File +# Begin Source File + +SOURCE=.\CreateSubdirDialog.h +# End Source File +# Begin Source File + +SOURCE=.\DEFileDialog.h +# End Source File +# Begin Source File + +SOURCE=.\DiskArchive.h +# End Source File +# Begin Source File + +SOURCE=.\DiskConvertDialog.h +# End Source File +# Begin Source File + +SOURCE=.\DiskEditDialog.h +# End Source File +# Begin Source File + +SOURCE=.\DiskEditOpenDialog.h +# End Source File +# Begin Source File + +SOURCE=.\DiskFSTree.h +# End Source File +# Begin Source File + +SOURCE=.\DoneOpenDialog.h +# End Source File +# Begin Source File + +SOURCE=.\EditAssocDialog.h +# End Source File +# Begin Source File + +SOURCE=.\EditCommentDialog.h +# End Source File +# Begin Source File + +SOURCE=.\EditPropsDialog.h +# End Source File +# Begin Source File + +SOURCE=.\EnterRegDialog.h +# End Source File +# Begin Source File + +SOURCE=.\EOLScanDialog.h +# End Source File +# Begin Source File + +SOURCE=.\ExtractOptionsDialog.h +# End Source File +# Begin Source File + +SOURCE=.\FileNameConv.h +# End Source File +# Begin Source File + +SOURCE=.\GenericArchive.h +# End Source File +# Begin Source File + +SOURCE=.\HelpTopics.h +# End Source File +# Begin Source File + +SOURCE=.\ImageFormatDialog.h +# End Source File +# Begin Source File + +SOURCE=.\Main.h +# End Source File +# Begin Source File + +SOURCE=.\MyApp.h +# End Source File +# Begin Source File + +SOURCE=.\NewDiskSize.h +# End Source File +# Begin Source File + +SOURCE=.\NewFolderDialog.h +# End Source File +# Begin Source File + +SOURCE=.\NufxArchive.h +# End Source File +# Begin Source File + +SOURCE=.\OpenVolumeDialog.h +# End Source File +# Begin Source File + +SOURCE=.\PasteSpecialDialog.h +# End Source File +# Begin Source File + +SOURCE=.\Preferences.h +# End Source File +# Begin Source File + +SOURCE=.\PrefsDialog.h +# End Source File +# Begin Source File + +SOURCE=.\Print.h +# End Source File +# Begin Source File + +SOURCE=.\ProgressCounterDialog.h +# End Source File +# Begin Source File + +SOURCE=.\RecompressOptionsDialog.h +# End Source File +# Begin Source File + +SOURCE=.\Registry.h +# End Source File +# Begin Source File + +SOURCE=.\RenameEntryDialog.h +# End Source File +# Begin Source File + +SOURCE=.\RenameVolumeDialog.h +# End Source File +# Begin Source File + +SOURCE=.\resource.h +# End Source File +# Begin Source File + +SOURCE=.\Squeeze.h +# End Source File +# Begin Source File + +SOURCE=.\StdAfx.h +# End Source File +# Begin Source File + +SOURCE=.\SubVolumeDialog.h +# End Source File +# Begin Source File + +SOURCE=.\TwoImgPropsDialog.h +# End Source File +# Begin Source File + +SOURCE=.\UseSelectionDialog.h +# End Source File +# Begin Source File + +SOURCE=.\ViewFilesDialog.h +# End Source File +# Begin Source File + +SOURCE=.\VolumeCopyDialog.h +# End Source File +# End Group +# Begin Group "Resource Files" + +# PROP Default_Filter "ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe" +# Begin Source File + +SOURCE=.\Graphics\binary2.ico +# End Source File +# Begin Source File + +SOURCE=.\Graphics\ChooseFolder.bmp +# End Source File +# Begin Source File + +SOURCE=.\Graphics\CiderPress.ico +# End Source File +# Begin Source File + +SOURCE=.\Graphics\diskimage.ico +# End Source File +# Begin Source File + +SOURCE=.\Graphics\FileViewer.ico +# End Source File +# Begin Source File + +SOURCE=.\Graphics\fslogo.bmp +# End Source File +# Begin Source File + +SOURCE=.\Graphics\hdrbar.bmp +# End Source File +# Begin Source File + +SOURCE=.\Graphics\icon2.ico +# End Source File +# Begin Source File + +SOURCE=".\Graphics\list-pics.bmp" +# End Source File +# Begin Source File + +SOURCE=.\Graphics\NewFolder.bmp +# End Source File +# Begin Source File + +SOURCE=.\Graphics\nufx.ico +# End Source File +# Begin Source File + +SOURCE=.\Graphics\toolbar1.bmp +# End Source File +# Begin Source File + +SOURCE=.\Graphics\tree_pics.bmp +# End Source File +# Begin Source File + +SOURCE=.\Graphics\vol_pics.bmp +# End Source File +# End Group +# Begin Source File + +SOURCE=.\Help\CIDERPRESS.HLP +# End Source File +# Begin Source File + +SOURCE=..\LICENSE.txt +# End Source File +# End Target +# End Project diff --git a/app/app.dsw b/app/app.dsw new file mode 100644 index 0000000..8c7f063 --- /dev/null +++ b/app/app.dsw @@ -0,0 +1,29 @@ +Microsoft Developer Studio Workspace File, Format Version 6.00 +# WARNING: DO NOT EDIT OR DELETE THIS WORKSPACE FILE! + +############################################################################### + +Project: "app"=.\app.dsp - Package Owner=<4> + +Package=<5> +{{{ +}}} + +Package=<4> +{{{ +}}} + +############################################################################### + +Global: + +Package=<5> +{{{ +}}} + +Package=<3> +{{{ +}}} + +############################################################################### + diff --git a/app/app.vcproj b/app/app.vcproj new file mode 100644 index 0000000..f85c51b --- /dev/null +++ b/app/app.vcproj @@ -0,0 +1,1211 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/resource.h b/app/resource.h new file mode 100644 index 0000000..02ad768 --- /dev/null +++ b/app/resource.h @@ -0,0 +1,574 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Developer Studio generated include file. +// Used by CiderPress.rc +// +#define IDR_MAINFRAME 102 +#define IDD_ABOUTDLG 105 +#define IDB_FSLOGO 106 +#define IDR_TOOLBAR1 107 +#define IDI_FILE_NUFX 108 +#define IDI_FILE_BINARY2 109 +#define IDI_FILE_DISKIMAGE 110 +#define IDB_HDRBAR 110 +#define IDD_PREF_GENERAL 115 +#define IDD_PREF_COMPRESSION 116 +#define IDR_RIGHTCLICKMENU 117 +#define IDD_FILE_VIEWER 118 +#define IDI_FILE_VIEWER 120 +#define IDD_PREF_FVIEW 121 +#define IDB_LIST_PICS 123 +#define IDD_DISKEDIT 127 +#define IDD_DECONF 128 +#define IDD_SUBV 131 +#define IDD_DEFILE 132 +#define IDD_PREF_FILES 133 +#define IDD_CHOOSEDIR 135 +#define IDB_NEW_FOLDER 139 +#define IDB_CHOOSE_FOLDER 140 +#define IDD_NEWFOLDER 141 +#define IDD_EXTRACT_FILES 142 +#define IDD_ACTION_PROGRESS 143 +#define IDD_CONFIRM_OVERWRITE 144 +#define IDD_RENAME_OVERWRITE 145 +#define IDD_USE_SELECTION 147 +#define IDD_RENAME_ENTRY 148 +#define IDD_COMMENT_EDIT 149 +#define IDD_RECOMPRESS_OPTS 150 +#define IDD_PRINT_CANCEL 152 +#define IDD_ASSOCIATIONS 153 +#define IDD_REGISTRATION 157 +#define IDD_DISKCONV 158 +#define IDD_DONEOPEN 159 +#define IDD_PROPS_EDIT 160 +#define IDD_CONVFILE_OPTS 161 +#define IDD_CONVDISK_OPTS 162 +#define IDD_BULKCONV 163 +#define IDD_OPENVOLUMEDLG 164 +#define IDD_VOLUMECOPYPROG 166 +#define IDD_DISKEDIT_OPENWHICH 167 +#define IDD_VOLUMECOPYSEL 169 +#define IDB_VOL_PICS 170 +#define IDD_FORMATTING 171 +#define IDD_CREATEIMAGE 172 +#define IDD_CHOOSE_ADD_TARGET 173 +#define IDB_TREE_PICS 174 +#define IDD_ARCHIVEINFO_NUFX 175 +#define IDD_ARCHIVEINFO_DISK 176 +#define IDD_ARCHIVEINFO_BNY 177 +#define IDD_PREF_DISKIMAGE 178 +#define IDD_CREATE_SUBDIR 179 +#define IDD_RENAME_VOLUME 180 +#define IDD_EOLSCAN 181 +#define IDD_TWOIMG_PROPS 182 +#define IDD_LOADING 183 +#define IDD_IMPORTCASSETTE 184 +#define IDD_CASSIMPTARGET 186 +#define IDD_ADD_CLASH 187 +#define IDD_ARCHIVEINFO_ACU 188 +#define IDD_IMPORT_BAS 189 +#define IDD_PASTE_SPECIAL 190 +#define IDD_PROGRESS_COUNTER 191 +#define IDC_NUFXLIB_VERS_TEXT 1001 +#define IDC_CONTENT_LIST 1002 +#define IDC_COL_PATHNAME 1005 +#define IDC_COL_TYPE 1006 +#define IDC_COL_AUXTYPE 1007 +#define IDC_COL_MODDATE 1008 +#define IDC_COL_FORMAT 1009 +#define IDC_COL_SIZE 1010 +#define IDC_COL_RATIO 1011 +#define IDC_COL_PACKED 1012 +#define IDC_COL_ACCESS 1013 +#define IDC_COL_DEFAULTS 1014 +#define IDC_DEFC_UNCOMPRESSED 1016 +#define IDC_DEFC_SQUEEZE 1017 +#define IDC_DEFC_LZW1 1018 +#define IDC_DEFC_LZW2 1019 +#define IDC_DEFC_LZC12 1020 +#define IDC_DEFC_LZC16 1021 +#define IDC_DEFC_DEFLATE 1022 +#define IDC_DEFC_BZIP2 1023 +#define IDC_PVIEW_NOWRAP_TEXT 1025 +#define IDC_PVIEW_BOLD_HEXDUMP 1026 +#define IDC_PVIEW_BOLD_BASIC 1027 +#define IDC_PVIEW_DISASM_ONEBYTEBRKCOP 1028 +#define IDC_PVIEW_HIRES_BW 1029 +#define IDC_PVIEW_DHR_CONV_COMBO 1030 +#define IDC_PVIEW_HITEXT 1036 +#define IDC_PVIEW_PASCALTEXT 1037 +#define IDC_PVIEW_APPLESOFT 1038 +#define IDC_PVIEW_INTEGER 1039 +#define IDC_PVIEW_HIRES 1040 +#define IDC_PVIEW_DHR 1041 +#define IDC_PVIEW_SHR 1042 +#define IDC_PVIEW_AWP 1043 +#define IDC_PVIEW_PRODOSFOLDER 1044 +#define IDC_PVIEW_RESOURCES 1045 +#define IDC_PVIEW_RELAX_GFX 1046 +#define IDC_PVIEW_ADB 1047 +#define IDC_PVIEW_SCASSEM 1048 +#define IDC_PVIEW_ASP 1049 +#define IDC_PVIEW_MACPAINT 1050 +#define IDC_PVIEW_PASCALCODE 1051 +#define IDC_PVIEW_CPMTEXT 1052 +#define IDC_PVIEW_GWP 1053 +#define IDC_PVIEW_DISASM 1054 +#define IDC_PVIEW_PRINTSHOP 1055 +#define IDC_PVIEW_TEXT8 1056 +#define IDC_PVIEW_SIZE_EDIT 1060 +#define IDC_PVIEW_SIZE_SPIN 1061 +#define IDC_DISKEDIT_DOREAD 1063 +#define IDC_DISKEDIT_DOWRITE 1064 +#define IDC_DISKEDIT_TRACK 1065 +#define IDC_DISKEDIT_TRACKSPIN 1066 +#define IDC_DISKEDIT_SECTOR 1067 +#define IDC_DISKEDIT_SECTORSPIN 1068 +#define IDC_DISKEDIT_OPENFILE 1069 +#define IDC_DISKEDIT_EDIT 1070 +#define IDC_DISKEDIT_PREV 1071 +#define IDC_DISKEDIT_NEXT 1072 +#define IDC_STEXT_SECTOR 1073 +#define IDC_STEXT_TRACK 1074 +#define IDC_DISKEDIT_DONE 1077 +#define IDC_DISKEDIT_HEX 1078 +#define IDC_DISKEDIT_SUBVOLUME 1081 +#define IDC_DECONF_FSFORMAT 1090 +#define IDC_DECONF_SECTORORDER 1091 +#define IDC_DECONF_PHYSICAL 1092 +#define IDC_DECONF_FILEFORMAT 1093 +#define IDC_DECONF_SOURCE 1094 +#define IDC_DISKIMG_VERS_TEXT 1095 +#define IDC_FVIEW_EDITBOX 1101 +#define IDC_SELECTED_COUNT 1102 +#define IDC_ABOUT_CREDITS 1111 +#define IDC_DECONF_HELP 1112 +#define IDC_SUBV_LIST 1114 +#define IDC_DEFILE_FILENAME 1115 +#define IDC_DEFILE_RSRC 1116 +#define IDC_CIDERPRESS_VERS_TEXT 1117 +#define IDC_PREF_TEMP_FOLDER 1118 +#define IDC_CHOOSEDIR_TREE 1121 +#define IDC_CHOOSEDIR_PATHEDIT 1123 +#define IDC_CHOOSEDIR_EXPAND_TREE 1124 +#define IDC_CHOOSEDIR_PATH 1125 +#define IDC_CHOOSEDIR_NEW_FOLDER 1127 +#define IDC_PREF_CHOOSE_TEMP_FOLDER 1128 +#define IDC_FVIEW_FONT 1129 +#define IDC_FVIEW_NEXT 1130 +#define IDC_FVIEW_PREV 1131 +#define IDC_NEWFOLDER_CURDIR 1132 +#define IDC_NEWFOLDER_NAME 1133 +#define IDC_EXT_PATH 1136 +#define IDC_EXT_CONVEOLTEXT 1137 +#define IDC_EXT_CONVEOLALL 1138 +#define IDC_EXT_STRIP_FOLDER 1142 +#define IDC_EXT_OVERWRITE_EXIST 1143 +#define IDC_EXT_SELECTED 1144 +#define IDC_EXT_ALL 1145 +#define IDC_EXT_REFORMAT 1147 +#define IDC_EXT_DATAFORK 1148 +#define IDC_EXT_RSRCFORK 1149 +#define IDC_EXT_CONVEOLNONE 1151 +#define IDC_EXT_CHOOSE_FOLDER 1152 +#define IDC_PROG_ARC_NAME 1153 +#define IDC_PROG_FILE_NAME 1154 +#define IDC_PROG_VERB 1155 +#define IDC_PROG_TOFROM 1156 +#define IDC_PROG_PROGRESS 1157 +#define IDC_OVWR_YES 1161 +#define IDC_OVWR_YESALL 1162 +#define IDC_OVWR_NO 1163 +#define IDC_OVWR_NOALL 1164 +#define IDC_OVWR_NEW_INFO 1166 +#define IDC_OVWR_RENAME 1167 +#define IDC_OVWR_EXIST_NAME 1168 +#define IDC_OVWR_EXIST_INFO 1169 +#define IDC_OVWR_NEW_NAME 1170 +#define IDC_RENOVWR_SOURCE_NAME 1171 +#define IDC_RENOVWR_ORIG_NAME 1172 +#define IDC_RENOVWR_NEW_NAME 1173 +#define IDC_SELECT_ACCEPT 1175 +#define IDC_ADDFILES_PREFIX 1177 +#define IDC_ADDFILES_INCLUDE_SUBFOLDERS 1180 +#define IDC_ADDFILES_STRIP_FOLDER 1181 +#define IDC_ADDFILES_NOPRESERVE 1182 +#define IDC_ADDFILES_PRESERVE 1183 +#define IDC_ADDFILES_PRESERVEPLUS 1184 +#define IDC_ADDFILES_STATIC1 1186 +#define IDC_ADDFILES_STATIC2 1187 +#define IDC_ADDFILES_STATIC3 1188 +#define IDC_ADDFILES_OVERWRITE 1189 +#define IDC_PREF_SHRINKIT_COMPAT 1190 +#define IDC_USE_SELECTED 1192 +#define IDC_USE_ALL 1193 +#define IDC_RENAME_OLD 1194 +#define IDC_RENAME_NEW 1195 +#define IDC_RENAME_PATHSEP 1196 +#define IDC_COMMENT_EDIT 1198 +#define IDC_COMMENT_DELETE 1199 +#define IDC_RECOMP_COMP 1201 +#define IDC_PREF_ASSOCIATIONS 1202 +#define IDC_ASSOCIATION_LIST 1209 +#define IDC_REG_COMPANY_NAME 1210 +#define IDC_REG_EXPIRES 1211 +#define IDC_ABOUT_ENTER_REG 1212 +#define IDC_REGENTER_USER 1213 +#define IDC_REGENTER_COMPANY 1214 +#define IDC_REGENTER_REG 1215 +#define IDC_REG_USER_NAME 1216 +#define IDC_ZLIB_VERS_TEXT 1218 +#define IDC_EXT_CONVHIGHASCII 1219 +#define IDC_EXT_DISKIMAGE 1220 +#define IDC_EXT_DISK_2MG 1221 +#define IDC_EXT_ADD_PRESERVE 1222 +#define IDC_EXT_ADD_EXTEN 1223 +#define IDC_EXT_CONFIG_PRESERVE 1224 +#define IDC_EXT_CONFIG_CONVERT 1225 +#define IDC_PREF_COERCE_DOS 1226 +#define IDC_PREF_SPACES_TO_UNDER 1227 +#define IDC_REGENTER_USERCRC 1228 +#define IDC_REGENTER_COMPCRC 1229 +#define IDC_REGENTER_REGCRC 1230 +#define IDC_RENAME_SKIP 1232 +#define IDC_DECONF_VIEWASBLOCKS 1233 +#define IDC_DECONF_VIEWASSECTORS 1234 +#define IDC_DECONF_VIEWASNIBBLES 1235 +#define IDC_DECONF_OUTERFORMAT 1236 +#define IDC_DECONF_VIEWAS 1237 +#define IDC_IMAGE_TYPE 1238 +#define IDC_DISKCONV_DOS 1239 +#define IDC_DISKCONV_DOS2MG 1240 +#define IDC_DISKCONV_PRODOS 1241 +#define IDC_DISKCONV_PRODOS2MG 1242 +#define IDC_DISKCONV_NIB 1243 +#define IDC_DISKCONV_NIB2MG 1244 +#define IDC_DISKCONV_D13 1245 +#define IDC_DISKCONV_DC42 1246 +#define IDC_DISKCONV_SDK 1247 +#define IDC_DISKCONV_TRACKSTAR 1248 +#define IDC_DISKCONV_HDV 1249 +#define IDC_DISKCONV_DDD 1250 +#define IDC_DISKCONV_GZIP 1251 +#define IDC_DISKEDIT_NIBBLE_PARMS 1252 +#define IDC_PROPS_PATHNAME 1255 +#define IDC_PROPS_FILETYPE 1256 +#define IDC_PROPS_AUXTYPE 1257 +#define IDC_PROPS_ACCESS_R 1258 +#define IDC_PROPS_ACCESS_W 1259 +#define IDC_PROPS_ACCESS_N 1260 +#define IDC_PROPS_ACCESS_D 1261 +#define IDC_PROPS_ACCESS_I 1262 +#define IDC_PROPS_ACCESS_B 1263 +#define IDC_PROPS_MODWHEN 1266 +#define IDC_PROPS_TYPEDESCR 1267 +#define IDC_CONVFILE_PRESERVEDIR 1270 +#define IDC_CONVDISK_140K 1271 +#define IDC_CONVDISK_800K 1273 +#define IDC_CONVDISK_1440K 1274 +#define IDC_CONVDISK_5MB 1275 +#define IDC_CONVDISK_16MB 1276 +#define IDC_CONVDISK_20MB 1277 +#define IDC_CONVDISK_32MB 1278 +#define IDC_CONVDISK_SPECIFY 1279 +#define IDC_IMAGE_SIZE_TEXT 1289 +#define IDC_BULKCONV_PATHNAME 1290 +#define IDC_PREF_EXTVIEWER_EXTS 1292 +#define IDC_VOLUME_LIST 1295 +#define IDC_OPENVOL_READONLY 1296 +#define IDC_VOLUMECOPYPROG_FROM 1297 +#define IDC_VOLUMECOPYPROG_TO 1298 +#define IDC_VOLUMECOPYPROG_PROGRESS 1299 +#define IDC_CONVDISK_SPECIFY_EDIT 1302 +#define IDC_CONVDISK_COMPUTE 1303 +#define IDC_DEOW_FILE 1303 +#define IDC_CONVDISK_SPACEREQ 1304 +#define IDC_DEOW_VOLUME 1304 +#define IDC_DEOW_CURRENT 1305 +#define IDC_CONVDISK_VOLNAME 1307 +#define IDC_VOLUME_FILTER 1307 +#define IDC_VOLUMECOPYSEL_LIST 1309 +#define IDC_VOLUEMCOPYSEL_TOFILE 1310 +#define IDC_VOLUEMCOPYSEL_FROMFILE 1311 +#define IDC_CREATEFS_DOS32 1312 +#define IDC_CREATEFS_DOS33 1313 +#define IDC_CREATEFS_PRODOS 1314 +#define IDC_CREATEFS_PASCAL 1315 +#define IDC_CREATEFS_HFS 1316 +#define IDC_CREATEFS_BLANK 1317 +#define IDC_CREATEFSDOS_ALLOCDOS 1321 +#define IDC_CREATEFSDOS_VOLNUM 1322 +#define IDC_CREATEFSPRODOS_VOLNAME 1323 +#define IDC_CREATEFSPASCAL_VOLNAME 1324 +#define IDC_ASPI_VERS_TEXT 1330 +#define IDC_PREF_SUCCESS_BEEP 1331 +#define IDC_ADD_TARGET_TREE 1333 +#define IDC_AIDISK_SUBVOLSEL 1334 +#define IDC_AIDISK_NOTES 1335 +#define IDC_AI_FILENAME 1336 +#define IDC_AIBNY_RECORDS 1337 +#define IDC_AINUFX_FORMAT 1338 +#define IDC_AINUFX_RECORDS 1339 +#define IDC_AINUFX_MASTERVERSION 1340 +#define IDC_AINUFX_CREATEWHEN 1341 +#define IDC_AINUFX_MODIFYWHEN 1342 +#define IDC_AINUFX_JUNKSKIPPED 1343 +#define IDC_AIDISK_OUTERFORMAT 1344 +#define IDC_AIDISK_FILEFORMAT 1345 +#define IDC_AIDISK_PHYSICALFORMAT 1346 +#define IDC_AIDISK_SECTORORDER 1347 +#define IDC_AIDISK_FSFORMAT 1348 +#define IDC_AIDISK_FILECOUNT 1349 +#define IDC_AIDISK_CAPACITY 1350 +#define IDC_AIDISK_FREESPACE 1351 +#define IDC_AIDISK_DAMAGED 1352 +#define IDC_AIDISK_WRITEABLE 1354 +#define IDC_PDISK_CONFIRM_FORMAT 1359 +#define IDC_PDISK_PRODOS_ALLOWLOWER 1360 +#define IDC_PDISK_PRODOS_USESPARSE 1361 +#define IDC_FVIEW_PRINT 1363 +#define IDC_CREATESUBDIR_BASE 1364 +#define IDC_CREATESUBDIR_NEW 1365 +#define IDC_RENAMEVOL_TREE 1366 +#define IDC_RENAMEVOL_NEW 1367 +#define IDC_ADDFILES_CONVEOLNONE 1368 +#define IDC_ADDFILES_CONVEOLTEXT 1369 +#define IDC_ADDFILES_CONVEOLALL 1370 +#define IDC_ADDFILES_STATIC4 1371 +#define IDC_PROPS_CREATEWHEN 1372 +#define IDC_EOLSCAN_CR 1374 +#define IDC_EOLSCAN_LF 1375 +#define IDC_EOLSCAN_CRLF 1376 +#define IDC_EOLSCAN_CHARS 1377 +#define IDC_PREF_PASTE_JUNKPATHS 1378 +#define IDC_EXT_CONVEOLTYPE 1379 +#define IDC_ADDFILES_CONVEOLTYPE 1380 +#define IDC_TWOIMG_LOCKED 1381 +#define IDC_TWOIMG_DOSVOLSET 1382 +#define IDC_TWOIMG_DOSVOLNUM 1383 +#define IDC_TWOIMG_COMMENT 1384 +#define IDC_TWOIMG_CREATOR 1385 +#define IDC_TWOIMG_VERSION 1386 +#define IDC_TWOIMG_FORMAT 1387 +#define IDC_TWOIMG_BLOCKS 1388 +#define IDC_FVIEW_DATA 1395 +#define IDC_FVIEW_RSRC 1396 +#define IDC_FVIEW_CMMT 1397 +#define IDC_FVIEW_FORMATSEL 1399 +#define IDC_FVIEW_FMT_HEX 1400 +#define IDC_FVIEW_FMT_RAW 1401 +#define IDC_FVIEW_FMT_BEST 1403 +#define IDC_PDISK_OPENVOL_RO 1405 +#define IDC_EOLSCAN_HIGHASCII 1407 +#define IDC_CASSETTE_LIST 1414 +#define IDC_IMPORT_CHUNK 1416 +#define IDC_CASSETTE_ALG 1418 +#define IDC_CASSETTE_INPUT 1419 +#define IDC_CASSIMPTARG_FILENAME 1420 +#define IDC_CASSIMPTARG_BAS 1421 +#define IDC_CASSIMPTARG_INT 1422 +#define IDC_CASSIMPTARG_BIN 1423 +#define IDC_CASSIMPTARG_BINADDR 1424 +#define IDC_CASSIMPTARG_RANGE 1425 +#define IDC_CLASH_RENAME 1426 +#define IDC_CLASH_SKIP 1427 +#define IDC_CLASH_WINNAME 1428 +#define IDC_CLASH_STORAGENAME 1429 +#define IDC_PREF_REDUCE_SHK_ERROR_CHECKS 1430 +#define IDC_IMPORT_BAS_RESULTS 1431 +#define IDC_IMPORT_BAS_SAVEAS 1432 +#define IDC_FVIEW_FIND 1438 +#define IDC_CREATEFSHFS_VOLNAME 1440 +#define IDC_PROPS_HFS_FILETYPE 1441 +#define IDC_PROPS_HFS_AUXTYPE 1442 +#define IDC_PROPS_HFS_MODE 1444 +#define IDC_PROPS_HFS_LABEL 1445 +#define IDC_PASTE_SPECIAL_COUNT 1446 +#define IDC_PASTE_SPECIAL_PATHS 1447 +#define IDC_PASTE_SPECIAL_NOPATHS 1448 +#define IDC_PROGRESS_COUNTER_COUNT 1449 +#define IDC_PROGRESS_COUNTER_DESC 1450 +#define IDC_PDISK_OPENVOL_PHYS0 1451 +#define IDC_PREF_SHK_BAD_MAC 1453 +#define IDS_READONLY 2021 +#define IDS_FAILED 2023 +#define IDS_ERROR 2024 +#define IDS_NOT_ALLOWED 2025 +#define IDS_WARNING 2026 +#define IDS_CANCELLED 2027 +#define IDS_SELECTED_COUNT 2041 +#define IDS_SELECTED_COUNTS_FMT 2042 +#define IDS_BLOCK 2045 +#define IDS_DEFILE_FIND_FAILED 2047 +#define IDS_DEFILE_OPEN_FAILED 2049 +#define IDS_DISKEDIT_NOREADTS 2057 +#define IDS_DISKEDIT_NOREADBLOCK 2058 +#define IDS_DISKEDIT_FIRDFAILED 2059 +#define IDS_EXT_SELECTED_COUNT 2060 +#define IDS_EXT_SELECTED_COUNTS_FMT 2061 +#define IDS_INDIC_RSRC 2062 +#define IDS_INDIC_DISK 2063 +#define IDS_INDIC_COMMENT 2064 +#define IDS_INDIC_DATA 2065 +#define IDS_NOW_ADDING 2066 +#define IDS_ADDING_AS 2067 +#define IDS_DEL_SELECTED_COUNTS_FMT 2068 +#define IDS_DEL_SELECTED_COUNT 2069 +#define IDS_DEL_ALL_FILES 2070 +#define IDS_DEL_OK 2071 +#define IDS_DEL_TITLE 2072 +#define IDS_TEST_SELECTED_COUNTS_FMT 2073 +#define IDS_TEST_SELECTED_COUNT 2074 +#define IDS_TEST_ALL_FILES 2075 +#define IDS_TEST_OK 2076 +#define IDS_TEST_TITLE 2077 +#define IDS_NOW_TESTING 2078 +#define IDS_MB_APP_NAME 2079 +#define IDS_NO_COMMENT_ADD 2080 +#define IDS_EDIT_COMMENT 2081 +#define IDS_DEL_COMMENT_OK 2082 +#define IDS_RECOMP_SELECTED_COUNT 2083 +#define IDS_RECOMP_SELECTED_COUNTS_FMT 2084 +#define IDS_RECOMP_ALL_FILES 2085 +#define IDS_RECOMP_OK 2086 +#define IDS_RECOMP_TITLE 2087 +#define IDS_NOW_EXPANDING 2088 +#define IDS_NOW_COMPRESSING 2089 +#define IDS_PRINT_CL_JOB_TITLE 2090 +#define IDS_REG_EXPIRED 2091 +#define IDS_REG_FAILURE 2092 +#define IDS_REG_INVALID 2093 +#define IDS_REG_EVAL_REM 2094 +#define IDS_REG_BAD_ENTRY 2095 +#define IDS_OPEN_AS_NUFX 2096 +#define IDS_ABOUT_UNREGISTERED 2097 +#define IDS_CDESC_140K 2098 +#define IDS_CDESC_800K 2099 +#define IDS_CDESC_BLOCKS 2100 +#define IDS_CDEC_140K_13 2101 +#define IDS_BAD_SST_IMAGE 2102 +#define IDS_NIBBLE_TO_SECTOR_WARNING 2103 +#define IDS_CDEC_RAWNIB 2104 +#define IDS_DISKEDITMSG_EMPTY 2105 +#define IDS_DISKEDITMSG_SPARSE 2106 +#define IDS_DISKEDITMSG_BADSECTOR 2107 +#define IDS_DISKEDITMSG_BADBLOCK 2108 +#define IDS_DISKEDITMSG_BADTRACK 2109 +#define IDS_CONVFILE_SELECTED_COUNT 2110 +#define IDS_CONVFILE_SELECTED_COUNTS_FMT 2111 +#define IDS_CONVFILE_ALL_FILES 2112 +#define IDS_CONVFILE_OK 2113 +#define IDS_CONVFILE_TITLE 2114 +#define IDS_CONVDISK_SELECTED_COUNT 2115 +#define IDS_CONVDISK_SELECTED_COUNTS_FMT 2116 +#define IDS_CONVDISK_ALL_FILES 2117 +#define IDS_CONVDISK_OK 2118 +#define IDS_CONVDISK_TITLE 2119 +#define IDS_CONVDISK_SPACEREQ 2120 +#define IDS_CDESC_40TRACK 2121 +#define IDS_TRACKSTAR_TO_OTHER_WARNING 2122 +#define IDS_DIFFERENT_NIBBLE_WARNING 2123 +#define IDS_VOLUME_NO_REMOTE 2124 +#define IDS_VOLUME_NO_CDROM 2125 +#define IDS_VOLUME_NO_RAMDISK 2126 +#define IDS_VOLUME_NO_GENERIC 2127 +#define IDS_VOLUME_NO_CDRIVE 2128 +#define IDS_VOLUME_SELECT_ONE 2129 +#define IDS_OPERATION_CANCELLED 2130 +#define IDS_NOT_READY 2131 +#define IDS_ASPI_NOT_LOADED 2132 +#define IDS_NOW_DELETING 2133 +#define IDS_VALID_FILENAME_PRODOS 2134 +#define IDS_VALID_FILENAME_DOS 2135 +#define IDS_VALID_FILENAME_PASCAL 2136 +#define IDS_VALID_VOLNAME_PRODOS 2137 +#define IDS_VALID_VOLNAME_DOS 2138 +#define IDS_VALID_VOLNAME_PASCAL 2139 +#define IDS_CLIPBOARD_REGFAILED 2140 +#define IDS_CLIPBOARD_OPENFAILED 2141 +#define IDS_CLIPBOARD_NOITEMS 2142 +#define IDS_CLIPBOARD_ALLOCFAILED 2143 +#define IDS_CLIPBOARD_NOTFOUND 2144 +#define IDS_CLIPBOARD_READFAILURE 2145 +#define IDS_CLIPBOARD_WRITEFAILURE 2146 +#define IDS_CLIPBOARD_WIN9XMAX 2147 +#define IDS_PRINTER_NOT_USABLE 2148 +#define IDS_NLIST_DATA_FAILED 2149 +#define IDS_PROPS_DOS_TYPE_CHANGE 2150 +#define IDS_FDI_TO_OTHER_WARNING 2151 +#define IDS_VALID_VOLNAME_HFS 2152 +#define IDS_VALID_FILENAME_HFS 2153 +#define IDS_PASTE_SPECIAL_COUNT 2154 +#define IDM_FILE_NEW_ARCHIVE 40001 +#define IDM_FILE_OPEN 40002 +#define IDM_FILE_CLOSE 40003 +#define IDM_FILE_PRINT 40004 +#define IDM_ACTIONS_ADD_FILES 40005 +#define IDM_FILE_EXIT 40006 +#define IDM_ACTIONS_ADD_DISKS 40007 +#define IDM_ACTIONS_EXTRACT 40008 +#define IDM_ACTIONS_VIEW 40009 +#define IDM_ACTIONS_TEST 40010 +#define IDM_ACTIONS_DELETE 40011 +#define IDM_EDIT_SELECT_ALL 40014 +#define IDM_ACTIONS_INVERT_SELECTION 40015 +#define IDM_EDIT_PREFERENCES 40016 +#define IDM_HELP_CONTENTS 40017 +#define IDM_HELP_ORDERING 40018 +#define IDM_HELP_ABOUT 40019 +#define IDM_ACTIONS_RECOMPRESS 40020 +#define IDM_SORT_PATHNAME 40025 +#define IDM_SORT_TYPE 40026 +#define IDM_SORT_AUXTYPE 40027 +#define IDM_SORT_MODDATE 40028 +#define IDM_SORT_FORMAT 40029 +#define IDM_SORT_SIZE 40030 +#define IDM_SORT_RATIO 40031 +#define IDM_SORT_PACKED 40032 +#define IDM_SORT_ACCESS 40033 +#define IDM_SORT_ORIGINAL 40034 +#define IDM_CONVERT_TOBSC 40039 +#define IDM_CONVERT_FROMBSC 40040 +#define ID_INDICATOR_COMPLETE 40043 +#define IDM_TOOLS_DISKEDIT 40044 +#define IDM_HELP_WEBSITE 40046 +#define IDM_EDIT_INVERT_SELECTION 40048 +#define IDM_ACTIONS_RENAME 40050 +#define IDM_ACTIONS_EDIT_COMMENT 40051 +#define IDM_TOOLS_DISKCONV 40054 +#define IDM_TOOLS_SST_MERGE 40055 +#define IDM_ACTIONS_OPENASDISK 40063 +#define IDM_ACTIONS_EDIT_PROPS 40064 +#define IDM_ACTIONS_CONV_DISK 40065 +#define IDM_ACTIONS_CONV_FILE 40066 +#define IDM_TOOLS_BULKDISKCONV 40067 +#define IDM_FILE_OPEN_VOLUME 40068 +#define IDM_TOOLS_IMAGECREATOR 40072 +#define IDM_TOOLS_VOLUMECOPIER_FILE 40074 +#define IDM_TOOLS_VOLUMECOPIER_VOLUME 40075 +#define IDM_FILE_ARCHIVEINFO 40079 +#define IDM_ACTIONS_CREATE_SUBDIR 40080 +#define IDM_ACTIONS_RENAME_VOLUME 40081 +#define IDM_TOOLS_EOLSCANNER 40083 +#define IDM_FILE_FLUSH 40084 +#define IDM_FILE_SAVE 40085 +#define IDM_EDIT_COPY 40086 +#define IDM_EDIT_PASTE 40087 +#define IDM_TOOLS_TWOMGPROPS 40088 +#define IDM_TOOLS_TWOIMGPROPS 40089 +#define IDM_ACTIONS_CONV_TOWAV 40094 +#define IDM_ACTIONS_CONV_FROMWAV 40095 +#define IDM_ACTIONS_IMPORT_BAS 40097 +#define IDM_FILE_REOPEN 40098 +#define IDM_EDIT_FIND 40100 +#define IDM_EDIT_PASTE_SPECIAL 40101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 192 +#define _APS_NEXT_COMMAND_VALUE 40102 +#define _APS_NEXT_CONTROL_VALUE 1454 +#define _APS_NEXT_SYMED_VALUE 102 +#endif +#endif diff --git a/app/resource.hm b/app/resource.hm new file mode 100644 index 0000000..63005db --- /dev/null +++ b/app/resource.hm @@ -0,0 +1,19 @@ +// Microsoft Developer Studio generated Help ID include file. +// Used by CiderPress.rc +// +#define HIDC_ABOUT_CREDITS 0x80690457 // IDD_ABOUTDLG +#define HIDC_DISKEDIT_DONE 0x807f0435 // IDD_DISKEDIT +#define HIDC_DISKEDIT_DOREAD 0x807f0427 // IDD_DISKEDIT +#define HIDC_DISKEDIT_DOWRITE 0x807f0428 // IDD_DISKEDIT +#define HIDC_DISKEDIT_EDIT 0x807f042e // IDD_DISKEDIT +#define HIDC_DISKEDIT_HEX 0x807f0436 // IDD_DISKEDIT +#define HIDC_DISKEDIT_NEXT 0x807f0430 // IDD_DISKEDIT +#define HIDC_DISKEDIT_OPENFILE 0x807f042d // IDD_DISKEDIT +#define HIDC_DISKEDIT_PREV 0x807f042f // IDD_DISKEDIT +#define HIDC_DISKEDIT_SECTOR 0x807f042b // IDD_DISKEDIT +#define HIDC_DISKEDIT_SUBVOLUME 0x807f0439 // IDD_DISKEDIT +#define HIDC_DISKEDIT_TRACK 0x807f0429 // IDD_DISKEDIT +#define HIDC_STATIC 0x8074ffff // IDD_PREF_COMPRESSION +#define HIDC_STEXT_SECTOR 0x807f0431 // IDD_DISKEDIT +#define HIDC_STEXT_TRACK 0x807f0432 // IDD_DISKEDIT +#define HIDHELP 0x807f0009 // IDD_DISKEDIT diff --git a/diskimg/ASPI.cpp b/diskimg/ASPI.cpp new file mode 100644 index 0000000..b1f870e --- /dev/null +++ b/diskimg/ASPI.cpp @@ -0,0 +1,626 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * ASPI I/O functions. + * + * Some notes on ASPI stuff: + * - The Nero ASPI provides an interface for IDE hard drives. It also + * throws in a couple of mystery devices on a host adapter at the end. + * It has "unknown" device type and doesn't respond to SCSI device + * inquiries, so it's easy to ignore. + * - The Win98 generic ASPI only finds CD-ROM drives on the IDE bus. + */ +#include "StdAfx.h" +#ifdef _WIN32 + +#include "DiskImgPriv.h" +#include "SCSIDefs.h" +#include "CP_wnaspi32.h" +#include "ASPI.h" + + +/* + * Initialize ASPI. + */ +DIError +ASPI::Init(void) +{ + DWORD aspiStatus; + static const char* kASPIDllName = "wnaspi32.dll"; + + /* + * Try to load the DLL. + */ + fhASPI = ::LoadLibrary(kASPIDllName); + if (fhASPI == nil) { + DWORD lastErr = ::GetLastError(); + if (lastErr == ERROR_MOD_NOT_FOUND) { + WMSG1("ASPI DLL '%s' not found\n", kASPIDllName); + } else { + WMSG2("ASPI LoadLibrary(%s) failed (err=%ld)\n", + kASPIDllName, GetLastError()); + } + return kDIErrGeneric; + } + + GetASPI32SupportInfo = (DWORD(*)(void))::GetProcAddress(fhASPI, "GetASPI32SupportInfo"); + SendASPI32Command = (DWORD(*)(LPSRB))::GetProcAddress(fhASPI, "SendASPI32Command"); + GetASPI32DLLVersion = (DWORD(*)(void))::GetProcAddress(fhASPI, "GetASPI32DLLVersion"); + if (GetASPI32SupportInfo == nil || SendASPI32Command == nil) { + WMSG0("ASPI functions not found in dll\n"); + ::FreeLibrary(fhASPI); + fhASPI = nil; + return kDIErrGeneric; + } + + if (GetASPI32DLLVersion != nil) { + fASPIVersion = GetASPI32DLLVersion(); + WMSG4(" ASPI version is %d.%d.%d.%d\n", + fASPIVersion & 0x0ff, + (fASPIVersion >> 8) & 0xff, + (fASPIVersion >> 16) & 0xff, + (fASPIVersion >> 24) & 0xff); + } else { + WMSG0("ASPI WARNING: couldn't find GetASPI32DLLVersion interface\n"); + } + + /* + * Successfully loaded the library. Start it up and see if it works. + */ + aspiStatus = GetASPI32SupportInfo(); + if (HIBYTE(LOWORD(aspiStatus)) != SS_COMP) { + WMSG1("ASPI loaded but not working (status=%d)\n", + HIBYTE(LOWORD(aspiStatus))); + ::FreeLibrary(fhASPI); + fhASPI = nil; + return kDIErrASPIFailure; + } + + fHostAdapterCount = LOBYTE(LOWORD(aspiStatus)); + WMSG1("ASPI loaded successfully, hostAdapterCount=%d\n", + fHostAdapterCount); + + return kDIErrNone; +} + +/* + * Destructor. Unload the ASPI DLL. + */ +ASPI::~ASPI(void) +{ + if (fhASPI != nil) { + WMSG0("Unloading ASPI DLL\n"); + ::FreeLibrary(fhASPI); + fhASPI = nil; + } +} + + +/* + * Issue an ASPI host adapter inquiry request for the specified adapter. + * + * Pass in a pointer to a struct that receives the result. + */ +DIError +ASPI::HostAdapterInquiry(unsigned char adapter, AdapterInfo* pAdapterInfo) +{ + SRB_HAInquiry req; + DWORD result; + + assert(adapter >= 0 && adapter < kMaxAdapters); + + memset(&req, 0, sizeof(req)); + req.SRB_Cmd = SC_HA_INQUIRY; + req.SRB_HaId = adapter; + + result = SendASPI32Command(&req); + if (result != SS_COMP) { + WMSG2("ASPI(SC_HA_INQUIRY on %d) failed with result=0x%lx\n", + adapter, result); + return kDIErrASPIFailure; + } + + pAdapterInfo->adapterScsiID = req.HA_SCSI_ID; + memcpy(pAdapterInfo->managerID, req.HA_ManagerId, + sizeof(pAdapterInfo->managerID)-1); + pAdapterInfo->managerID[sizeof(pAdapterInfo->managerID)-1] = '\0'; + memcpy(pAdapterInfo->identifier, req.HA_Identifier, + sizeof(pAdapterInfo->identifier)-1); + pAdapterInfo->identifier[sizeof(pAdapterInfo->identifier)-1] = '\0'; + pAdapterInfo->maxTargets = req.HA_Unique[3]; + pAdapterInfo->bufferAlignment = + (unsigned short) req.HA_Unique[1] << 8 | req.HA_Unique[0]; + + return kDIErrNone; +} + +/* + * Issue an ASPI query on device type. + */ +DIError +ASPI::GetDeviceType(unsigned char adapter, unsigned char target, + unsigned char lun, unsigned char* pType) +{ + SRB_GDEVBlock req; + DWORD result; + + assert(adapter >= 0 && adapter < kMaxAdapters); + assert(target >= 0 && target < kMaxTargets); + assert(lun >= 0 && lun < kMaxLuns); + assert(pType != nil); + + memset(&req, 0, sizeof(req)); + req.SRB_Cmd = SC_GET_DEV_TYPE; + req.SRB_HaId = adapter; + req.SRB_Target = target; + req.SRB_Lun = lun; + + result = SendASPI32Command(&req); + if (result != SS_COMP) + return kDIErrASPIFailure; + + *pType = req.SRB_DeviceType; + + return kDIErrNone; +} + +/* + * Return a printable string for the given device type. + */ +const char* +ASPI::DeviceTypeToString(unsigned char deviceType) +{ + switch (deviceType) { + case kScsiDevTypeDASD: return "Disk device"; + case kScsiDevTypeSEQD: return "Tape device"; + case kScsiDevTypePRNT: return "Printer"; + case kScsiDevTypePROC: return "Processor"; + case kScsiDevTypeWORM: return "Write-once read-multiple"; + case kScsiDevTypeCDROM: return "CD-ROM device"; + case kScsiDevTypeSCAN: return "Scanner device"; + case kScsiDevTypeOPTI: return "Optical memory device"; + case kScsiDevTypeJUKE: return "Medium changer device"; + case kScsiDevTypeCOMM: return "Communications device"; + case kScsiDevTypeUNKNOWN: return "Unknown or no device type"; + default: return "Invalid type"; + } +} + +/* + * Issue a SCSI device inquiry and return the interesting parts. + */ +DIError +ASPI::DeviceInquiry(unsigned char adapter, unsigned char target, + unsigned char lun, Inquiry* pInquiry) +{ + DIError dierr; + SRB_ExecSCSICmd srb; + CDB6Inquiry* pCDB; + unsigned char buf[96]; // enough to hold everything of interest, and more + CDB_InquiryData* pInqData = (CDB_InquiryData*) buf; + + assert(sizeof(CDB6Inquiry) == 6); + + memset(&srb, 0, sizeof(srb)); + srb.SRB_Cmd = SC_EXEC_SCSI_CMD; + srb.SRB_HaId = adapter; + srb.SRB_Target = target; + srb.SRB_Lun = lun; + srb.SRB_Flags = SRB_DIR_IN; + srb.SRB_BufLen = sizeof(buf); + srb.SRB_BufPointer = buf; + srb.SRB_SenseLen = SENSE_LEN; + srb.SRB_CDBLen = sizeof(*pCDB); + + pCDB = (CDB6Inquiry*) srb.CDBByte; + pCDB->operationCode = kScsiOpInquiry; + pCDB->allocationLength = sizeof(buf); + + // Don't set pCDB->logicalUnitNumber. It's only there for SCSI-1 + // devices. SCSI-2 uses an IDENTIFY command; I gather ASPI is doing + // this for us. + + dierr = ExecSCSICommand(&srb); + if (dierr != kDIErrNone) + return dierr; + + memcpy(pInquiry->vendorID, pInqData->vendorId, + sizeof(pInquiry->vendorID)-1); + pInquiry->vendorID[sizeof(pInquiry->vendorID)-1] = '\0'; + memcpy(pInquiry->productID, pInqData->productId, + sizeof(pInquiry->productID)-1); + pInquiry->productID[sizeof(pInquiry->productID)-1] = '\0'; + pInquiry->productRevision[0] = pInqData->productRevisionLevel[0]; + pInquiry->productRevision[1] = pInqData->productRevisionLevel[1]; + pInquiry->productRevision[2] = pInqData->productRevisionLevel[2]; + pInquiry->productRevision[3] = pInqData->productRevisionLevel[3]; + + return kDIErrNone; +} + + +/* + * Get the capacity of a SCSI block device. + */ +DIError +ASPI::GetDeviceCapacity(unsigned char adapter, unsigned char target, + unsigned char lun, unsigned long* pLastBlock, unsigned long* pBlockSize) +{ + DIError dierr; + SRB_ExecSCSICmd srb; + CDB10* pCDB; + CDB_ReadCapacityData dataBuf; + + assert(sizeof(dataBuf) == 8); // READ CAPACITY returns two longs + assert(sizeof(CDB10) == 10); + + memset(&srb, 0, sizeof(srb)); + srb.SRB_Cmd = SC_EXEC_SCSI_CMD; + srb.SRB_HaId = adapter; + srb.SRB_Target = target; + srb.SRB_Lun = lun; + srb.SRB_Flags = SRB_DIR_IN; + srb.SRB_BufLen = sizeof(dataBuf); + srb.SRB_BufPointer = (unsigned char*)&dataBuf; + srb.SRB_SenseLen = SENSE_LEN; + srb.SRB_CDBLen = sizeof(*pCDB); + + pCDB = (CDB10*) srb.CDBByte; + pCDB->operationCode = kScsiOpReadCapacity; + // rest of CDB is zero + + dierr = ExecSCSICommand(&srb); + if (dierr != kDIErrNone) + return dierr; + + *pLastBlock = + (unsigned long) dataBuf.logicalBlockAddr0 << 24 | + (unsigned long) dataBuf.logicalBlockAddr1 << 16 | + (unsigned long) dataBuf.logicalBlockAddr2 << 8 | + (unsigned long) dataBuf.logicalBlockAddr3; + *pBlockSize = + (unsigned long) dataBuf.bytesPerBlock0 << 24 | + (unsigned long) dataBuf.bytesPerBlock1 << 16 | + (unsigned long) dataBuf.bytesPerBlock2 << 8 | + (unsigned long) dataBuf.bytesPerBlock3; + return kDIErrNone; +} + +/* + * Test to see if a device is ready. + * + * Returns "true" if the device is ready, "false" if not. + */ +DIError +ASPI::TestUnitReady(unsigned char adapter, unsigned char target, + unsigned char lun, bool* pReady) +{ + DIError dierr; + SRB_ExecSCSICmd srb; + CDB6* pCDB; + + assert(sizeof(CDB6) == 6); + + memset(&srb, 0, sizeof(srb)); + srb.SRB_Cmd = SC_EXEC_SCSI_CMD; + srb.SRB_HaId = adapter; + srb.SRB_Target = target; + srb.SRB_Lun = lun; + srb.SRB_Flags = 0; //SRB_DIR_IN; + srb.SRB_BufLen = 0; + srb.SRB_BufPointer = nil; + srb.SRB_SenseLen = SENSE_LEN; + srb.SRB_CDBLen = sizeof(*pCDB); + + pCDB = (CDB6*) srb.CDBByte; + pCDB->operationCode = kScsiOpTestUnitReady; + // rest of CDB is zero + + dierr = ExecSCSICommand(&srb); + if (dierr != kDIErrNone) { + const CDB_SenseData* pSense = (const CDB_SenseData*) srb.SenseArea; + + if (srb.SRB_TargStat == kScsiStatCheckCondition && + pSense->senseKey == kScsiSenseNotReady) + { + // expect pSense->additionalSenseCode to be + // kScsiAdSenseNoMediaInDevice; no need to check it really. + WMSG3(" ASPI TestUnitReady: drive %d:%d:%d is NOT ready\n", + adapter, target, lun); + } else { + WMSG3(" ASPI TestUnitReady failed, status=0x%02x sense=0x%02x ASC=0x%02x\n", + srb.SRB_TargStat, pSense->senseKey, + pSense->additionalSenseCode); + } + *pReady = false; + } else { + const CDB_SenseData* pSense = (const CDB_SenseData*) srb.SenseArea; + WMSG3(" ASPI TestUnitReady: drive %d:%d:%d is ready\n", + adapter, target, lun); + //WMSG3(" status=0x%02x sense=0x%02x ASC=0x%02x\n", + // srb.SRB_TargStat, pSense->senseKey, pSense->additionalSenseCode); + *pReady = true; + } + + return kDIErrNone; +} + +/* + * Read one or more blocks from the device. + * + * The block size is going to be whatever the device's native size is + * (possibly modified by extents, but we'll ignore that). For a CD-ROM + * this means 2048-byte blocks. + */ +DIError +ASPI::ReadBlocks(unsigned char adapter, unsigned char target, + unsigned char lun, long startBlock, short numBlocks, long blockSize, + void* buf) +{ + SRB_ExecSCSICmd srb; + CDB10* pCDB; + + //WMSG3(" ASPI ReadBlocks start=%ld num=%d (size=%d)\n", + // startBlock, numBlocks, blockSize); + + assert(sizeof(CDB10) == 10); + assert(startBlock >= 0); + assert(numBlocks > 0); + assert(buf != nil); + + memset(&srb, 0, sizeof(srb)); + srb.SRB_Cmd = SC_EXEC_SCSI_CMD; + srb.SRB_HaId = adapter; + srb.SRB_Target = target; + srb.SRB_Lun = lun; + srb.SRB_Flags = SRB_DIR_IN; + srb.SRB_BufLen = numBlocks * blockSize; + srb.SRB_BufPointer = (unsigned char*)buf; + srb.SRB_SenseLen = SENSE_LEN; + srb.SRB_CDBLen = sizeof(*pCDB); + + pCDB = (CDB10*) srb.CDBByte; + pCDB->operationCode = kScsiOpRead; + pCDB->logicalBlockAddr0 = (unsigned char) (startBlock >> 24); // MSB + pCDB->logicalBlockAddr1 = (unsigned char) (startBlock >> 16); + pCDB->logicalBlockAddr2 = (unsigned char) (startBlock >> 8); + pCDB->logicalBlockAddr3 = (unsigned char) startBlock; // LSB + pCDB->transferLength0 = (unsigned char) (numBlocks >> 8); // MSB + pCDB->transferLength1 = (unsigned char) numBlocks; // LSB + + return ExecSCSICommand(&srb); +} + +/* + * Write one or more blocks to the device. + */ +DIError +ASPI::WriteBlocks(unsigned char adapter, unsigned char target, + unsigned char lun, long startBlock, short numBlocks, long blockSize, + const void* buf) +{ + SRB_ExecSCSICmd srb; + CDB10* pCDB; + + WMSG3(" ASPI WriteBlocks start=%ld num=%d (size=%d)\n", + startBlock, numBlocks, blockSize); + + assert(sizeof(CDB10) == 10); + assert(startBlock >= 0); + assert(numBlocks > 0); + assert(buf != nil); + + memset(&srb, 0, sizeof(srb)); + srb.SRB_Cmd = SC_EXEC_SCSI_CMD; + srb.SRB_HaId = adapter; + srb.SRB_Target = target; + srb.SRB_Lun = lun; + srb.SRB_Flags = SRB_DIR_IN; + srb.SRB_BufLen = numBlocks * blockSize; + srb.SRB_BufPointer = (unsigned char*)buf; + srb.SRB_SenseLen = SENSE_LEN; + srb.SRB_CDBLen = sizeof(*pCDB); + + pCDB = (CDB10*) srb.CDBByte; + pCDB->operationCode = kScsiOpWrite; + pCDB->logicalBlockAddr0 = (unsigned char) (startBlock >> 24); // MSB + pCDB->logicalBlockAddr1 = (unsigned char) (startBlock >> 16); + pCDB->logicalBlockAddr2 = (unsigned char) (startBlock >> 8); + pCDB->logicalBlockAddr3 = (unsigned char) startBlock; // LSB + pCDB->transferLength0 = (unsigned char) (numBlocks >> 8); // MSB + pCDB->transferLength1 = (unsigned char) numBlocks; // LSB + + return ExecSCSICommand(&srb); +} + + +/* + * Execute a SCSI command. + * + * Returns an error if ASPI reports an error or the SCSI status isn't + * kScsiStatGood. + * + * The Nero ASPI layer typically returns immediately, and hands back an + * SS_ERR when something fails. Win98 ASPI does the SS_PENDING thang. + */ +DIError +ASPI::ExecSCSICommand(SRB_ExecSCSICmd* pSRB) +{ + HANDLE completionEvent = nil; + DWORD eventStatus; + DWORD aspiStatus; + + assert(pSRB->SRB_Cmd == SC_EXEC_SCSI_CMD); + assert(pSRB->SRB_Flags == SRB_DIR_IN || + pSRB->SRB_Flags == SRB_DIR_OUT || + pSRB->SRB_Flags == 0); + + /* + * Set up event-waiting stuff, as described in the Adaptec ASPI docs. + */ + pSRB->SRB_Flags |= SRB_EVENT_NOTIFY; + + completionEvent = ::CreateEvent(NULL, FALSE, FALSE, NULL); + if (completionEvent == nil) { + WMSG0("Failed creating a completion event?\n"); + return kDIErrGeneric; + } + + pSRB->SRB_PostProc = completionEvent; + + /* + * Send the request. + */ + (void)SendASPI32Command((LPSRB) pSRB); + aspiStatus = pSRB->SRB_Status; + if (aspiStatus == SS_PENDING) { + //WMSG0(" (waiting for completion)\n"); + eventStatus = ::WaitForSingleObject(completionEvent, kTimeout * 1000); + + ::CloseHandle(completionEvent); + + if (eventStatus == WAIT_TIMEOUT) { + WMSG0(" ASPI exec timed out!\n"); + return kDIErrSCSIFailure; + } else if (eventStatus != WAIT_OBJECT_0) { + WMSG1(" ASPI exec returned weird wait state %ld\n", eventStatus); + return kDIErrGeneric; + } + } + + /* + * Check the final status. + */ + aspiStatus = pSRB->SRB_Status; + + if (aspiStatus == SS_COMP) { + /* success! */ + } else if (aspiStatus == SS_ERR) { + const CDB_SenseData* pSense = (const CDB_SenseData*) pSRB->SenseArea; + + WMSG4(" ASPI SCSI command 0x%02x failed: scsiStatus=0x%02x" + " senseKey=0x%02x ASC=0x%02x\n", + pSRB->CDBByte[0], pSRB->SRB_TargStat, + pSense->senseKey, pSense->additionalSenseCode); + return kDIErrSCSIFailure; + } else { + // SS_ABORTED, SS_ABORT_FAIL, SS_NO_DEVICE, ... + WMSG3(" ASPI failed on command 0x%02x: aspiStatus=%d scsiStatus=%d\n", + pSRB->CDBByte[0], aspiStatus, pSRB->SRB_TargStat); + return kDIErrASPIFailure; + } + + return kDIErrNone; +} + + +/* + * Return an array of accessible devices we found. + * + * Only return the devices matching device types in "deviceMask". + */ +DIError +ASPI::GetAccessibleDevices(int deviceMask, ASPIDevice** ppDeviceArray, + int* pNumDevices) +{ + DIError dierr; + ASPIDevice* deviceArray = nil; + int idx = 0; + + assert(deviceMask != 0); + assert((deviceMask & ~(kDevMaskCDROM | kDevMaskHardDrive)) == 0); + assert(ppDeviceArray != nil); + assert(pNumDevices != nil); + + deviceArray = new ASPIDevice[kMaxAccessibleDrives]; + if (deviceArray == nil) + return kDIErrMalloc; + + WMSG1("ASPI scanning %d host adapters\n", fHostAdapterCount); + + for (int ha = 0; ha < fHostAdapterCount; ha++) { + AdapterInfo adi; + + dierr = HostAdapterInquiry(ha, &adi); + if (dierr != kDIErrNone) { + WMSG1(" ASPI inquiry on %d failed\n", ha); + continue; + } + + WMSG2(" ASPI host adapter %d (SCSI ID=%d)\n", ha, adi.adapterScsiID); + WMSG2(" identifier='%s' managerID='%s'\n", + adi.identifier, adi.managerID); + WMSG2(" maxTargets=%d bufferAlignment=%d\n", + adi.maxTargets, adi.bufferAlignment); + + int maxTargets = adi.maxTargets; + if (!maxTargets) { + /* Win98 ASPI reports zero here for ATAPI */ + maxTargets = 8; + } + if (maxTargets > kMaxTargets) + maxTargets = kMaxTargets; + for (int targ = 0; targ < maxTargets; targ++) { + for (int lun = 0; lun < kMaxLuns; lun++) { + Inquiry inq; + unsigned char deviceType; + char addrString[48]; + bool deviceReady; + + dierr = GetDeviceType(ha, targ, lun, &deviceType); + if (dierr != kDIErrNone) + continue; + + sprintf(addrString, "%d:%d:%d", ha, targ, lun); + + dierr = DeviceInquiry(ha, targ, lun, &inq); + if (dierr != kDIErrNone) { + WMSG2(" ASPI DeviceInquiry for '%s' (type=%d) failed\n", + addrString, deviceType); + continue; + } + + WMSG4(" Device %s is %s '%s' '%s'\n", + addrString, DeviceTypeToString(deviceType), + inq.vendorID, inq.productID); + + if ((deviceMask & kDevMaskCDROM) != 0 && + deviceType == kScsiDevTypeCDROM) + { + /* found CD-ROM */ + } else if ((deviceMask & kDevMaskHardDrive) != 0 && + deviceType == kScsiDevTypeDASD) + { + /* found hard drive */ + } else + continue; + + if (idx >= kMaxAccessibleDrives) { + WMSG0("GLITCH: ran out of places to stuff CD-ROM drives\n"); + assert(false); + goto done; + } + + dierr = TestUnitReady(ha, targ, lun, &deviceReady); + if (dierr != kDIErrNone) { + WMSG1(" ASPI TestUnitReady for '%s' failed\n", addrString); + continue; + } + + deviceArray[idx].Init(ha, targ, lun, inq.vendorID, + inq.productID, deviceType, deviceReady); + idx++; + } + } + } + +done: + *ppDeviceArray = deviceArray; + *pNumDevices = idx; + return kDIErrNone; +} + +#endif /*_WIN32*/ diff --git a/diskimg/ASPI.h b/diskimg/ASPI.h new file mode 100644 index 0000000..3384304 --- /dev/null +++ b/diskimg/ASPI.h @@ -0,0 +1,199 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * ASPI (Advanced SCSI Programming Interface) definitions. + * + * This may be included directly by an application. It must not be necessary + * to include the lower-level headers, e.g. wnaspi32.h. + */ +#ifndef __ASPI__ +#define __ASPI__ + +#ifndef _WIN32 +/* + * Placeholder definition to keep Linux build happy. + */ +namespace DiskImgLib { + class DISKIMG_API ASPI { + public: + ASPI(void) {} + virtual ~ASPI(void) {} + }; +}; + +#else + + + +#ifndef __WNASPI32_H__ +struct SRB_ExecSCSICmd; // fwd +#endif + +namespace DiskImgLib { + +/* + * Descriptor for one SCSI device. + */ +class DISKIMG_API ASPIDevice { +public: + ASPIDevice(void) : fVendorID(nil), fProductID(nil), + fAdapter(0xff), fTarget(0xff), fLun(0xff), fDeviceReady(false) + {} + virtual ~ASPIDevice(void) { + delete[] fVendorID; + delete[] fProductID; + } + + void Init(unsigned char adapter, unsigned char target, unsigned char lun, + const unsigned char* vendor, unsigned const char* product, + int deviceType, bool ready) + { + fAdapter = adapter; + fTarget = target; + fLun = lun; + assert(fVendorID == nil); + fVendorID = new char[strlen((const char*)vendor)+1]; + strcpy(fVendorID, (const char*)vendor); + assert(fProductID == nil); + fProductID = new char[strlen((const char*)product)+1]; + strcpy(fProductID, (const char*)product); + fDeviceReady = ready; + fDeviceType = deviceType; + } + + enum { + kTypeDASD = 0, // kScsiDevTypeDASD + kTypeCDROM = 5, // kScsiDevTypeCDROM + }; + + unsigned char GetAdapter(void) const { return fAdapter; } + unsigned char GetTarget(void) const { return fTarget; } + unsigned char GetLun(void) const { return fLun; } + const char* GetVendorID(void) const { return fVendorID; } + const char* GetProductID(void) const { return fProductID; } + bool GetDeviceReady(void) const { return fDeviceReady; } + int GetDeviceType(void) const { return fDeviceType; } + +private: + // strings from SCSI inquiry, padded with spaces at end + char* fVendorID; + char* fProductID; + + unsigned char fAdapter; // physical or logical host adapter (0-15) + unsigned char fTarget; // SCSI ID on adapter (0-15) + unsigned char fLun; // logical unit (0-7) + + int fDeviceType; // e.g. kScsiDevTypeCDROM + bool fDeviceReady; +}; + +/* + * There should be only one instance of this in the library (part of the + * DiskImgLib Globals). It wouldn't actually break anything to have more than + * one, but there's no need for it. + */ +class DISKIMG_API ASPI { +public: + ASPI(void) : + fhASPI(nil), + GetASPI32SupportInfo(nil), + SendASPI32Command(nil), + GetASPI32DLLVersion(nil), + fASPIVersion(0), + fHostAdapterCount(-1) + {} + virtual ~ASPI(void); + + // load ASPI DLL if it exists + DIError Init(void); + + // return the version returned by the loaded DLL + DWORD GetVersion(void) const { + assert(fhASPI != nil); + return fASPIVersion; + } + + // Return an *array* of ASPIDevice structures for drives in system; + // the caller is expected to delete[] it when done. + enum { kDevMaskCDROM = 0x01, kDevMaskHardDrive = 0x02 }; + DIError GetAccessibleDevices(int deviceMask, ASPIDevice** ppDeviceArray, + int* pNumDevices); + + // Get the type of the device (DTYPE_*, 0x00-0x1f) using ASPI query + DIError GetDeviceType(unsigned char adapter, unsigned char target, + unsigned char lun, unsigned char* pType); + // convert DTYPE_* to a string + const char* DeviceTypeToString(unsigned char deviceType); + + // Get the capacity, expressed as the highest-available LBA and the device + // block size. + DIError GetDeviceCapacity(unsigned char adapter, unsigned char target, + unsigned char lun, unsigned long* pLastBlock, unsigned long* pBlockSize); + + // Read blocks from the device. + DIError ReadBlocks(unsigned char adapter, unsigned char target, + unsigned char lun, long startBlock, short numBlocks, long blockSize, + void* buf); + + // Write blocks to the device. + DIError WriteBlocks(unsigned char adapter, unsigned char target, + unsigned char lun, long startBlock, short numBlocks, long blockSize, + const void* buf); + +private: + /* + * The interesting bits that come out of an SC_HA_INQUIRY request. + */ + typedef struct AdapterInfo { + unsigned char adapterScsiID; // SCSI ID of the adapter itself + unsigned char managerID[16+1]; // string describing manager + unsigned char identifier[16+1]; // string describing host adapter + unsigned char maxTargets; // max #of targets on this adapter + unsigned short bufferAlignment; // buffer alignment requirement + } AdapterInfo; + /* + * The interesting bits from a SCSI device INQUIRY command. + */ + typedef struct Inquiry { + unsigned char vendorID[8+1]; // vendor ID string + unsigned char productID[16+1]; // product ID string + unsigned char productRevision[4]; // product revision bytes + } Inquiry; + + // Issue an ASPI adapter inquiry request. + DIError HostAdapterInquiry(unsigned char adapter, + AdapterInfo* pAdapterInfo); + // Issue a SCSI device inquiry request. + DIError DeviceInquiry(unsigned char adapter, unsigned char target, + unsigned char lun, Inquiry* pInquiry); + // Issue a SCSI test unit ready request. + DIError TestUnitReady(unsigned char adapter, unsigned char target, + unsigned char lun, bool* pReady); + // execute a SCSI command + DIError ExecSCSICommand(SRB_ExecSCSICmd* pSRB); + + + enum { + kMaxAdapters = 16, + kMaxTargets = 16, + kMaxLuns = 8, + kTimeout = 30, // timeout, in seconds + kMaxAccessibleDrives = 16, + }; + + HMODULE fhASPI; + DWORD (*GetASPI32SupportInfo)(void); + DWORD (*SendASPI32Command)(void* lpsrb); + DWORD (*GetASPI32DLLVersion)(void); + DWORD fASPIVersion; + int fHostAdapterCount; +}; + +}; // namespace DiskImgLib + +#endif /*__ASPI__*/ + +#endif /*_WIN32*/ diff --git a/diskimg/CFFA.cpp b/diskimg/CFFA.cpp new file mode 100644 index 0000000..c0a0c8c --- /dev/null +++ b/diskimg/CFFA.cpp @@ -0,0 +1,597 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * The "CFFA" DiskFS is a container class for multiple ProDOS and HFS volumes. + * + * The CFFA card doesn't have any RAM, so the author used a fixed partitioning + * scheme. You get 4 or 8 volumes -- depending on which firmware you jumper + * in -- at 32MB each. CF cards usually hold less than you would expect, so + * a 64MB card would have one 32MB volume and one less-than-32MB volume. + * + * With Dave's GS/OS driver, you get an extra drive or two at the end, at up + * to 1GB each. The driver only works in 4-volume mode. + * + * There is no magic CFFA block at the front, so it looks like a plain + * ProDOS or HFS volume. If the size is less than 32MB -- meaning there's + * only one volume -- we don't need to take an interest in the file, + * because the regular filesystem goodies will handle it just fine. If it's + * more than 32MB, we need to create a structure in which multiple volumes + * reside. + * + * The trick is finding all the volumes. The first four are easy. The + * fifth one is either another 32MB volume (if you're in 8-volume mode) + * or a volume whose size is somewhere between the amount of space left + * and 1GB. Not an issue until we get to CF cards > 128MB. We have to + * rely on the CFFA card making volumes as large as it can. + * + * I think it's reasonable to require that the first volume be either ProDOS + * or HFS. That way we don't go digging through large non-CFFA files when + * auto-probing. + */ +#include "StdAfx.h" +#include "DiskImgPriv.h" + +/* + * Figure out if this is a CFFA volume, and if so, whether it was formatted + * in 4-partition or 8-partition mode. + * + * The "imageOrder" parameter has no use here, because (in the current + * version) embedded parent volumes are implicitly ProDOS-ordered. + * + * "*pFormatFound" should be either a CFFA format or "unknown" on entry. + * If it's not "unknown", we will look for the specified format first. + * Otherwise, we look for 4-partition then 8-partition. The first one + * we find successfully is returned. + * + * Ideally we'd have some way to express ambiguity here, so that we could + * force the "disk format verification" dialog to come up. No such + * mechanism exists, and for now it doesn't seem worthwhile to add one. + */ +/*static*/ DIError +DiskFSCFFA::TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder, + DiskImg::FSFormat* pFormatFound) +{ + DIError dierr; + long totalBlocks = pImg->GetNumBlocks(); + long startBlock, maxBlocks, totalBlocksLeft; + long fsNumBlocks; + DiskFS* pNewFS = nil; + DiskImg* pNewImg = nil; + //bool fiveIs32MB; + + assert(totalBlocks > kEarlyVolExpectedSize); + + // could be "generic" from an earlier override + if (*pFormatFound != DiskImg::kFormatCFFA4 && + *pFormatFound != DiskImg::kFormatCFFA8) + { + *pFormatFound = DiskImg::kFormatUnknown; + } + + WMSG1("----- BEGIN CFFA SCAN (fmt=%d) -----\n", *pFormatFound); + + startBlock = 0; + totalBlocksLeft = totalBlocks; + + /* + * Look for a 32MB ProDOS or HFS volume in the first slot. If we + * don't find this, it's probably not CFFA. It's possible they just + * didn't format the first one, but that seems unlikely, and it's not + * unreasonable to insist that they format the first partition. + */ + maxBlocks = totalBlocksLeft; + if (maxBlocks > kEarlyVolExpectedSize) + maxBlocks = kEarlyVolExpectedSize; + + dierr = OpenSubVolume(pImg, startBlock, maxBlocks, true, + &pNewImg, &pNewFS); + if (dierr != kDIErrNone) { + WMSG0(" CFFA failed opening sub-volume #1\n"); + goto bail; + } + fsNumBlocks = pNewFS->GetFSNumBlocks(); + delete pNewFS; + delete pNewImg; + if (fsNumBlocks != kEarlyVolExpectedSize && + fsNumBlocks != kEarlyVolExpectedSize-1) + { + WMSG1(" CFFA found fsNumBlocks=%ld in slot #1\n", fsNumBlocks); + dierr = kDIErrFilesystemNotFound; + goto bail; + } + WMSG0(" CFFA found good volume in slot #1\n"); + + startBlock += maxBlocks; + totalBlocksLeft -= maxBlocks; + assert(totalBlocksLeft > 0); + + /* + * Look for a ProDOS or HFS volume <= 32MB in the second slot. If + * we don't find something here, and this is a 64MB card, then there's + * no advantage to using CFFA (in fact, the single-volume handling may + * be more convenient). If there's at least another 32MB, we continue + * looking. + */ + maxBlocks = totalBlocksLeft; + if (maxBlocks > kEarlyVolExpectedSize) + maxBlocks = kEarlyVolExpectedSize; + + dierr = OpenSubVolume(pImg, startBlock, maxBlocks, true, + &pNewImg, &pNewFS); + if (dierr != kDIErrNone) { + WMSG0(" CFFA failed opening sub-volume #2\n"); + if (maxBlocks < kEarlyVolExpectedSize) + goto bail; + // otherwise, assume they just didn't format #2, and keep going + } else { + fsNumBlocks = pNewFS->GetFSNumBlocks(); + delete pNewFS; + delete pNewImg; +#if 0 + if (fsNumBlocks != kEarlyVolExpectedSize && + fsNumBlocks != kEarlyVolExpectedSize-1) + { + WMSG1(" CFFA found fsNumBlocks=%ld in slot #2\n", fsNumBlocks); + dierr = kDIErrFilesystemNotFound; + goto bail; + } +#endif + WMSG0(" CFFA found good volume in slot #2\n"); + } + + startBlock += maxBlocks; + totalBlocksLeft -= maxBlocks; + if (totalBlocksLeft == 0) { + *pFormatFound = DiskImg::kFormatCFFA4; + goto bail; + } + + /* + * Skip #3 and #4. + */ + WMSG0(" CFFA skipping over slot #3\n"); + maxBlocks = kEarlyVolExpectedSize*2; + if (maxBlocks > totalBlocksLeft) + maxBlocks = totalBlocksLeft; + startBlock += maxBlocks; + totalBlocksLeft -= maxBlocks; + if (totalBlocksLeft == 0) { + // no more partitions to find; we're done + *pFormatFound = DiskImg::kFormatCFFA4; + goto bail; + } + WMSG0(" CFFA skipping over slot #4\n"); + + /* + * Partition #5. We know where it starts, but not how large it is. + * Could be 32MB, could be 1GB, could be anything between. + * + * CF cards come in power-of-two sizes -- 128MB, 256MB, etc. -- but + * we don't want to make assumptions here. It's possible we're + * looking at an odd-sized image file that some clever person is + * expecting to access with CiderPress. + */ + maxBlocks = totalBlocksLeft; + if (maxBlocks > kOneGB) + maxBlocks = kOneGB; + if (maxBlocks <= kEarlyVolExpectedSize) { + /* + * Only enough room for one <= 32MB volume. Not expected for a + * real CFFA card, unless they come in 160MB sizes. + * + * Treat it like 4-partition; it'll look like somebody slapped a + * 32MB volume into the first 1GB area. + */ + WMSG0(" CFFA assuming odd-sized slot #5\n"); + *pFormatFound = DiskImg::kFormatCFFA4; + goto bail; + } + + /* + * We could be looking at a 32MB ProDOS partition, 32MB HFS partition, + * or an up to 1GB HFS partition. We have to specify the size in + * the OpenSubVolume request, which means trying it both ways and + * finding a match from GetFSNumBlocks(). Complicating matters is + * truncation (ProDOS max 65535, not 65536) and round-off (not sure + * how HFS deals with left-over blocks). + * + * Start with <= 1GB. + */ + dierr = OpenSubVolume(pImg, startBlock, maxBlocks, true, + &pNewImg, &pNewFS); + if (dierr != kDIErrNone) { + if (dierr == kDIErrCancelled) + goto bail; + WMSG0(" CFFA failed opening large sub-volume #5\n"); + // if we can't get #5, don't bother looking for #6 + // (we could try anyway, but it's easier to just skip it) + dierr = kDIErrNone; + *pFormatFound = DiskImg::kFormatCFFA4; + goto bail; + } + + fsNumBlocks = pNewFS->GetFSNumBlocks(); + delete pNewFS; + delete pNewImg; + if (fsNumBlocks < 2 || fsNumBlocks > maxBlocks) { + WMSG1(" CFFA WARNING: FSNumBlocks #5 reported blocks=%ld\n", + fsNumBlocks); + } + if (fsNumBlocks == kEarlyVolExpectedSize-1 || + fsNumBlocks == kEarlyVolExpectedSize) + { + WMSG0(" CFFA #5 is a 32MB volume\n"); + // tells us nothing -- could still be 4 or 8, so we keep going + maxBlocks = kEarlyVolExpectedSize; + } else if (fsNumBlocks > kEarlyVolExpectedSize) { + // must be a GS/OS 1GB area + WMSG0(" CFFA #5 is larger than 32MB\n"); + *pFormatFound = DiskImg::kFormatCFFA4; + goto bail; + } else { + WMSG1(" CFFA #5 was unexpectedly small (%ld blocks)\n", fsNumBlocks); + // just stop now + *pFormatFound = DiskImg::kFormatCFFA4; + goto bail; + } + + startBlock += maxBlocks; + totalBlocksLeft -= maxBlocks; + + if (!totalBlocksLeft) { + WMSG0(" CFFA got 5 volumes\n"); + *pFormatFound = DiskImg::kFormatCFFA4; + goto bail; + } + + /* + * Various possibilities for slots 5 and up: + * A. Card in 4-partition mode. 5th partition isn't formatted. Don't + * bother looking for 6th. [already handled] + * B. Card in 4-partition mode. 5th partition is >32MB HFS. 6th + * partition will be at +1GB. [already handled] + * C. Card in 4-partition mode. 5th partition is 32MB ProDOS. 6th + * partition will be at +1GB. + * D. Card in 8-partition mode. 5th partition is 32MB HFS. 6th + * partition will be at +32MB. + * E. Card in 8-partition mode. 5th partition is 32MB ProDOS. 6th + * partition will be at +32MB. + * + * I'm ignoring D on the off chance somebody could create a 32MB HFS + * partition in the 1GB space. D and E are handled alike. + * + * The difference between C and D/E can *usually* be determined by + * opening up a 6th partition in the two expected locations. + */ + WMSG0(" CFFA probing 6th slot for 4-vs-8\n"); + /* + * Look in two different places. If we find something at the + * +32MB mark, assume it's in "8 mode". If we find something at + * the +1GB mark, assume it's in "4 + GS/OS mode". If both exist + * we have a problem. + */ + bool foundSmall, foundGig; + + foundSmall = false; + maxBlocks = totalBlocksLeft; + if (maxBlocks > kEarlyVolExpectedSize) + maxBlocks = kEarlyVolExpectedSize; + dierr = OpenSubVolume(pImg, startBlock + kEarlyVolExpectedSize, + maxBlocks, true, &pNewImg, &pNewFS); + if (dierr != kDIErrNone) { + if (dierr == kDIErrCancelled) + goto bail; + WMSG0(" CFFA no vol #6 found at +32MB\n"); + } else { + foundSmall = true; + delete pNewFS; + delete pNewImg; + } + + foundGig = false; + // no need to look if we don't have at least 1GB left! + if (totalBlocksLeft >= kOneGB) { + maxBlocks = totalBlocksLeft; + if (maxBlocks > kOneGB) + maxBlocks = kOneGB; + dierr = OpenSubVolume(pImg, startBlock + kOneGB, + maxBlocks, true, &pNewImg, &pNewFS); + if (dierr != kDIErrNone) { + if (dierr == kDIErrCancelled) + goto bail; + WMSG0(" CFFA no vol #6 found at +1GB\n"); + } else { + foundGig = true; + delete pNewFS; + delete pNewImg; + } + } + + dierr = kDIErrNone; + + if (!foundSmall && !foundGig) { + WMSG0(" CFFA no valid filesystem found in 6th position\n"); + *pFormatFound = DiskImg::kFormatCFFA4; + // don't bother looking for 7 and 8 + } else if (foundSmall && foundGig) { + WMSG0(" CFFA WARNING: found valid volumes at +32MB *and* +1GB\n"); + // default to 4-partition mode + if (*pFormatFound == DiskImg::kFormatUnknown) + *pFormatFound = DiskImg::kFormatCFFA4; + } else if (foundGig) { + WMSG0(" CFFA found 6th volume at +1GB, assuming 4-mode w/GSOS\n"); + if (fsNumBlocks < 2 || fsNumBlocks > kOneGB) { + WMSG1(" CFFA WARNING: FSNumBlocks #6 reported as %ld\n", + fsNumBlocks); + } + *pFormatFound = DiskImg::kFormatCFFA4; + } else if (foundSmall) { + WMSG0(" CFFA found 6th volume at +32MB, assuming 8-mode\n"); + if (fsNumBlocks < 2 || fsNumBlocks > kEarlyVolExpectedSize) { + WMSG1(" CFFA WARNING: FSNumBlocks #6 reported as %ld\n", + fsNumBlocks); + } + *pFormatFound = DiskImg::kFormatCFFA8; + } else { + assert(false); // how'd we get here?? + } + + // done! + +bail: + WMSG2("----- END CFFA SCAN (err=%d format=%d) -----\n", + dierr, *pFormatFound); + if (dierr == kDIErrNone) { + assert(*pFormatFound != DiskImg::kFormatUnknown); + } else { + *pFormatFound = DiskImg::kFormatUnknown; + } + return dierr; +} + + +/* + * Open up a sub-volume. + * + * If "scanOnly" is set, the full DiskFS initialization isn't performed. + * We just do enough to get the volume size info. + */ +/*static*/ DIError +DiskFSCFFA::OpenSubVolume(DiskImg* pImg, long startBlock, long numBlocks, + bool scanOnly, DiskImg** ppNewImg, DiskFS** ppNewFS) +{ + DIError dierr = kDIErrNone; + DiskFS* pNewFS = nil; + DiskImg* pNewImg = nil; + + pNewImg = new DiskImg; + if (pNewImg == nil) { + dierr = kDIErrMalloc; + goto bail; + } + + dierr = pNewImg->OpenImage(pImg, startBlock, numBlocks); + if (dierr != kDIErrNone) { + WMSG3(" CFFASub: OpenImage(%ld,%ld) failed (err=%d)\n", + startBlock, numBlocks, dierr); + goto bail; + } + //WMSG2(" +++ CFFASub: new image has ro=%d (parent=%d)\n", + // pNewImg->GetReadOnly(), pImg->GetReadOnly()); + + dierr = pNewImg->AnalyzeImage(); + if (dierr != kDIErrNone) { + WMSG1(" CFFASub: analysis failed (err=%d)\n", dierr); + goto bail; + } + + if (pNewImg->GetFSFormat() == DiskImg::kFormatUnknown || + pNewImg->GetSectorOrder() == DiskImg::kSectorOrderUnknown) + { + WMSG1(" CFFASub: unable to identify filesystem at %ld\n", + startBlock); + dierr = kDIErrUnsupportedFSFmt; + goto bail; + } + + /* open a DiskFS for the sub-image */ + WMSG2(" CFFASub (%ld,%ld) analyze succeeded!\n", startBlock, numBlocks); + pNewFS = pNewImg->OpenAppropriateDiskFS(); + if (pNewFS == nil) { + WMSG0(" CFFASub: OpenAppropriateDiskFS failed\n"); + dierr = kDIErrUnsupportedFSFmt; + goto bail; + } + + /* we encapsulate arbitrary stuff, so encourage child to scan */ + pNewFS->SetScanForSubVolumes(kScanSubEnabled); + + /* + * Load the files from the sub-image. When doing our initial tests, + * or when loading data for the volume copier, we don't want to dig + * into our sub-volumes, just figure out what they are and where. + */ + InitMode initMode; + if (scanOnly) + initMode = kInitHeaderOnly; + else + initMode = kInitFull; + dierr = pNewFS->Initialize(pNewImg, initMode); + if (dierr != kDIErrNone) { + WMSG1(" CFFASub: error %d reading list of files from disk\n", dierr); + goto bail; + } + +bail: + if (dierr != kDIErrNone) { + delete pNewFS; + delete pNewImg; + } else { + *ppNewImg = pNewImg; + *ppNewFS = pNewFS; + } + return dierr; +} + +/* + * Check to see if this is a CFFA volume. + */ +/*static*/ DIError +DiskFSCFFA::TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency) +{ + if (pImg->GetNumBlocks() < kMinInterestingBlocks) + return kDIErrFilesystemNotFound; + if (pImg->GetIsEmbedded()) // don't look for CFFA inside CFFA! + return kDIErrFilesystemNotFound; + + /* assume ProDOS -- shouldn't matter, since it's embedded */ + if (TestImage(pImg, DiskImg::kSectorOrderProDOS, pFormat) == kDIErrNone) { + assert(*pFormat == DiskImg::kFormatCFFA4 || + *pFormat == DiskImg::kFormatCFFA8); + *pOrder = DiskImg::kSectorOrderProDOS; + return kDIErrNone; + } + + // make sure we didn't tamper with it + assert(*pFormat == DiskImg::kFormatUnknown); + + WMSG0(" FS didn't find valid CFFA\n"); + return kDIErrFilesystemNotFound; +} + + +/* + * Prep the CFFA "container" for use. + */ +DIError +DiskFSCFFA::Initialize(void) +{ + DIError dierr = kDIErrNone; + + WMSG1("CFFA initializing (scanForSub=%d)\n", fScanForSubVolumes); + + /* seems pointless *not* to, but we just do what we're told */ + if (fScanForSubVolumes != kScanSubDisabled) { + dierr = FindSubVolumes(); + if (dierr != kDIErrNone) + return dierr; + } + + /* blank out the volume usage map */ + SetVolumeUsageMap(); + + return dierr; +} + + +/* + * Find the various sub-volumes and open them. + * + * We don't handle the volume specially unless it's at least 32MB, which + * means there are at least 2 partitions. + */ +DIError +DiskFSCFFA::FindSubVolumes(void) +{ + DIError dierr; + long startBlock, blocksLeft; + + startBlock = 0; + blocksLeft = fpImg->GetNumBlocks(); + + if (fpImg->GetFSFormat() == DiskImg::kFormatCFFA4) { + WMSG0(" CFFA opening 4+2 volumes\n"); + dierr = AddVolumeSeries(0, 4, kEarlyVolExpectedSize, /*ref*/startBlock, + /*ref*/blocksLeft); + if (dierr != kDIErrNone) + goto bail; + + WMSG2(" CFFA after first 4, startBlock=%ld blocksLeft=%ld\n", + startBlock, blocksLeft); + if (blocksLeft > 0) { + dierr = AddVolumeSeries(4, 2, kOneGB, /*ref*/startBlock, + /*ref*/blocksLeft); + if (dierr != kDIErrNone) + goto bail; + } + } else if (fpImg->GetFSFormat() == DiskImg::kFormatCFFA8) { + WMSG0(" CFFA opening 8 volumes\n"); + dierr = AddVolumeSeries(0, 8, kEarlyVolExpectedSize, /*ref*/startBlock, + /*ref*/blocksLeft); + if (dierr != kDIErrNone) + goto bail; + } else { + assert(false); + return kDIErrInternal; + } + + if (blocksLeft != 0) { + WMSG1(" CFFA ignoring leftover %ld blocks\n", blocksLeft); + } + +bail: + return dierr; +} + +/* + * Add a series of equal-sized volumes. + * + * Updates "startBlock" and "totalBlocksLeft". + */ +DIError +DiskFSCFFA::AddVolumeSeries(int start, int count, long blocksPerVolume, + long& startBlock, long& totalBlocksLeft) +{ + DIError dierr = kDIErrNone; + DiskFS* pNewFS = nil; + DiskImg* pNewImg = nil; + long maxBlocks, fsNumBlocks; + bool scanOnly = false; + + /* used by volume copier, to avoid deep scan */ + if (GetScanForSubVolumes() == kScanSubContainerOnly) + scanOnly = true; + + for (int i = start; i < start+count; i++) { + maxBlocks = blocksPerVolume; + if (maxBlocks > totalBlocksLeft) + maxBlocks = totalBlocksLeft; + + dierr = OpenSubVolume(fpImg, startBlock, maxBlocks, scanOnly, + &pNewImg, &pNewFS); + if (dierr != kDIErrNone) { + if (dierr == kDIErrCancelled) + goto bail; + WMSG1(" CFFA failed opening sub-volume %d (not formatted?)\n", i); + /* create a fake one to represent the partition */ + dierr = CreatePlaceholder(startBlock, maxBlocks, NULL, NULL, + &pNewImg, &pNewFS); + if (dierr == kDIErrNone) { + AddSubVolumeToList(pNewImg, pNewFS); + } else { + WMSG3(" CFFA unable to create placeholder (%ld, %ld) (err=%d)\n", + startBlock, maxBlocks, dierr); + goto bail; + } + } else { + fsNumBlocks = pNewFS->GetFSNumBlocks(); + if (fsNumBlocks < 2 || fsNumBlocks > blocksPerVolume) { + WMSG2(" CFFA WARNING: FSNumBlocks #%d reported as %ld\n", + i, fsNumBlocks); + } + AddSubVolumeToList(pNewImg, pNewFS); + } + + startBlock += maxBlocks; + totalBlocksLeft -= maxBlocks; + if (!totalBlocksLeft) + break; // all done + } + +bail: + return dierr; +} diff --git a/diskimg/CPM.cpp b/diskimg/CPM.cpp new file mode 100644 index 0000000..01e7566 --- /dev/null +++ b/diskimg/CPM.cpp @@ -0,0 +1,772 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Apple II CP/M disk format. + * + * Limitations: + * - Read-only. + * - Does not do much with user numbers. + * - Rumor has it that "sparse" files are possible. Not handled. + * - I'm currently treating the directory as fixed-length. This may + * not be correct. + * + * As I have no practical experience with CP/M, this is the weakest of the + * filesystem implementations. + */ +#include "StdAfx.h" +#include "DiskImgPriv.h" + + +/* + * =========================================================================== + * DiskFSCPM + * =========================================================================== + */ + +const int kBlkSize = 512; // really ought to be 1024 +const int kVolDirBlock = 24; // track 3 sector 0 +const int kVolDirCount = 4; // 4 prodos blocks +const int kNoDataByte = 0xe5; +const int kMaxUserNumber = 31; // 0-15 on some systems, 0-31 on others +const int kMaxExtent = 31; // extent counter, 0-31 + +/* + * See if this looks like a CP/M volume. + * + * We test a few fields in the volume directory for validity. + */ +static DIError +TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder) +{ + DIError dierr = kDIErrNone; + unsigned char dirBuf[kBlkSize * kVolDirCount]; + unsigned char* dptr; + int i; + + assert(sizeof(dirBuf) == DiskFSCPM::kFullDirSize); + + for (i = 0; i < kVolDirCount; i++) { + dierr = pImg->ReadBlockSwapped(kVolDirBlock + i, dirBuf + kBlkSize*i, + imageOrder, DiskImg::kSectorOrderCPM); + if (dierr != kDIErrNone) + goto bail; + } + + dptr = dirBuf; + for (i = 0; i < DiskFSCPM::kFullDirSize/DiskFSCPM::kDirectoryEntryLen; i++) + { + if (*dptr != kNoDataByte) { + /* usually userNumber == 0, but sometimes not; must be <= 31 */ + if (*dptr > kMaxUserNumber) { + dierr = kDIErrFilesystemNotFound; + break; + } + + /* extent counter, 0-31 */ + if (dptr[12] > kMaxExtent) { + dierr = kDIErrFilesystemNotFound; + break; + } + /* value in S1 must be zero */ + if (dptr[13] != 0) { + dierr = kDIErrFilesystemNotFound; + break; + } + + /* check for a valid filename here; high bit may be set on some bytes */ + unsigned char firstLet = *(dptr+1) & 0x7f; + if (firstLet < 0x20) { + dierr = kDIErrFilesystemNotFound; + break; + } + } + dptr += DiskFSCPM::kDirectoryEntryLen; + } + if (dierr == kDIErrNone) { + WMSG1(" CPM found clean directory, imageOrder=%d\n", imageOrder); + } + +bail: + return dierr; +} + +/* + * Test to see if the image is a CP/M disk. + * + * On the Apple II, these were always on 5.25" disks. However, it's possible + * to create hard drive volumes up to 8MB. + */ +/*static*/ DIError +DiskFSCPM::TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency) +{ + /* CP/M disks use 1K blocks, so ignore anything with odd count */ + if (pImg->GetNumBlocks() == 0 || + (pImg->GetNumBlocks() & 0x01) != 0) + { + WMSG1(" CPM rejecting image with numBlocks=%ld\n", + pImg->GetNumBlocks()); + return kDIErrFilesystemNotFound; + } + + DiskImg::SectorOrder ordering[DiskImg::kSectorOrderMax]; + + DiskImg::GetSectorOrderArray(ordering, *pOrder); + + for (int i = 0; i < DiskImg::kSectorOrderMax; i++) { + if (ordering[i] == DiskImg::kSectorOrderUnknown) + continue; + if (TestImage(pImg, ordering[i]) == kDIErrNone) { + *pOrder = ordering[i]; + *pFormat = DiskImg::kFormatCPM; + return kDIErrNone; + } + } + + WMSG0(" CPM didn't find valid FS\n"); + return kDIErrFilesystemNotFound; +} + +/* + * Get things rolling. + * + * Since we're assured that this is a valid disk, errors encountered from here + * on out must be handled somehow, possibly by claiming that the disk is + * completely full and has no files on it. + */ +DIError +DiskFSCPM::Initialize(void) +{ + DIError dierr = kDIErrNone; + + dierr = ReadCatalog(); + if (dierr != kDIErrNone) + goto bail; + + fVolumeUsage.Create(fpImg->GetNumBlocks()); + dierr = ScanFileUsage(); + if (dierr != kDIErrNone) { + /* this might not be fatal; just means that *some* files are bad */ + dierr = kDIErrNone; + goto bail; + } + + fDiskIsGood = CheckDiskIsGood(); + + fVolumeUsage.Dump(); + + //A2File* pFile; + //pFile = GetNextFile(nil); + //while (pFile != nil) { + // pFile->Dump(); + // pFile = GetNextFile(pFile); + //} + +bail: + return dierr; +} + +/* + * Read the entire CP/M catalog (all 2K of it) into memory, and parse + * out the individual files. + * + * A single file can have more than one directory entry. We only want + * to create an A2File object for the first one. + */ +DIError +DiskFSCPM::ReadCatalog(void) +{ + DIError dierr = kDIErrNone; + unsigned char dirBuf[kFullDirSize]; + unsigned char* dptr; + int i; + + for (i = 0; i < kVolDirCount; i++) { + dierr = fpImg->ReadBlock(kVolDirBlock + i, dirBuf + kBlkSize*i); + if (dierr != kDIErrNone) + goto bail; + } + + dptr = dirBuf; + for (i = 0; i < kNumDirEntries; i++) { + fDirEntry[i].userNumber = dptr[0x00]; + /* copy the filename, stripping the high bits off */ + for (int j = 0; j < kDirFileNameLen; j++) + fDirEntry[i].fileName[j] = dptr[0x01 + j] & 0x7f; + fDirEntry[i].fileName[kDirFileNameLen] = '\0'; + fDirEntry[i].extent = dptr[0x0c] + dptr[0x0e] * kExtentsInLowByte; + fDirEntry[i].S1 = dptr[0x0d]; + fDirEntry[i].records = dptr[0x0f]; + memcpy(fDirEntry[i].blocks, &dptr[0x10], kDirEntryBlockCount); + fDirEntry[i].readOnly = (dptr[0x09] & 0x80) != 0; + fDirEntry[i].system = (dptr[0x0a] & 0x80) != 0; + fDirEntry[i].badBlockList = false; // set if block list is bad + + dptr += kDirectoryEntryLen; + } + + /* create an entry for the first extent of each file */ + for (i = 0; i < kNumDirEntries; i++) { + A2FileCPM* pFile; + + if (fDirEntry[i].userNumber == kNoDataByte || fDirEntry[i].extent != 0) + continue; + + pFile = new A2FileCPM(this, fDirEntry); + FormatName(pFile->fFileName, (char*)fDirEntry[i].fileName); + pFile->fReadOnly = fDirEntry[i].readOnly; + pFile->fDirIdx = i; + + pFile->fLength = 0; + dierr = ComputeLength(pFile); + if (dierr != kDIErrNone) { + pFile->SetQuality(A2File::kQualityDamaged); + dierr = kDIErrNone; + } + AddFileToList(pFile); + } + + /* + * Validate the list of blocks. + */ + int maxCpmBlock; + maxCpmBlock = (fpImg->GetNumBlocks() - kVolDirBlock) / 2; + for (i = 0; i < kNumDirEntries; i++) { + if (fDirEntry[i].userNumber == kNoDataByte) + continue; + for (int j = 0; j < kDirEntryBlockCount; j++) { + if (fDirEntry[i].blocks[j] >= maxCpmBlock) { + WMSG2(" CPM invalid block %d in file '%s'\n", + fDirEntry[i].blocks[j], fDirEntry[i].fileName); + //pFile->SetQuality(A2File::kQualityDamaged); + fDirEntry[i].badBlockList = true; + break; + } + } + } + +bail: + return dierr; +} + +/* + * Reformat from 11 chars with spaces into clean xxxxx.yyy format. + */ +void +DiskFSCPM::FormatName(char* dstBuf, const char* srcBuf) +{ + char workBuf[kDirFileNameLen+1]; + char* cp; + + assert(strlen(srcBuf) < sizeof(workBuf)); + strcpy(workBuf, srcBuf); + + cp = workBuf; + while (*cp != '\0') { + //*cp &= 0x7f; // [no longer necessary] + if (*cp == ' ') + *cp = '\0'; + if (*cp == ':') // don't think this is allowed, but check + *cp = 'X'; // for it anyway + cp++; + } + + strcpy(dstBuf, workBuf); + dstBuf[8] = '\0'; // in case filename part is full 8 chars + strcat(dstBuf, "."); + strcat(dstBuf, workBuf+8); + + assert(strlen(dstBuf) <= A2FileCPM::kMaxFileName); +} + +/* + * Compute the length of a file. Sets "pFile->fLength". + * + * This requires walking through the list of extents and looking for the + * last one. We use the "records" field of the last extent to determine + * the file length. + * + * (Should probably just get the block list and then walk that, rather than + * having directory parse code in two places.) + */ +DIError +DiskFSCPM::ComputeLength(A2FileCPM* pFile) +{ + int i; + int best, maxExtent; + + best = maxExtent = -1; + + for (i = 0; i < DiskFSCPM::kNumDirEntries; i++) { + if (fDirEntry[i].userNumber == kNoDataByte) + continue; + + if (strcmp((const char*)fDirEntry[i].fileName, + (const char*)fDirEntry[pFile->fDirIdx].fileName) == 0 && + fDirEntry[i].userNumber == fDirEntry[pFile->fDirIdx].userNumber) + { + /* this entry is part of the file */ + if (fDirEntry[i].extent > maxExtent) { + best = i; + maxExtent = fDirEntry[i].extent; + } + } + } + + if (maxExtent < 0 || best < 0) { + WMSG1(" CPM couldn't find existing file '%s'!\n", pFile->fFileName); + assert(false); + return kDIErrInternal; + } + + pFile->fLength = kDirEntryBlockCount * 1024 * maxExtent + + fDirEntry[best].records * 128; + + return kDIErrNone; +} + + +/* + * Scan file usage into the volume usage map. + * + * Tracks 0, 1, and 2 are always used by the boot loader. The volume directory + * is on the first half of track 3 (blocks 0 and 1). + */ +DIError +DiskFSCPM::ScanFileUsage(void) +{ + int cpmBlock; + int i, j; + + for (i = 0; i < kVolDirBlock; i++) + SetBlockUsage(i, VolumeUsage::kChunkPurposeSystem); + for (i = kVolDirBlock; i < kVolDirBlock + kVolDirCount; i++) + SetBlockUsage(i, VolumeUsage::kChunkPurposeVolumeDir); + + for (i = 0; i < kNumDirEntries; i++) { + if (fDirEntry[i].userNumber == kNoDataByte) + continue; + if (fDirEntry[i].badBlockList) + continue; + + for (j = 0; j < kDirEntryBlockCount; j++) { + cpmBlock = fDirEntry[i].blocks[j]; + if (cpmBlock == 0) + break; + SetBlockUsage(CPMToProDOSBlock(cpmBlock), + VolumeUsage::kChunkPurposeUserData); + SetBlockUsage(CPMToProDOSBlock(cpmBlock)+1, + VolumeUsage::kChunkPurposeUserData); + } + } + + return kDIErrNone; +} + +/* + * Update an entry in the usage map. + * + * "block" is a 512-byte block, so you will have to call here twice for every + * 1K CP/M block. + */ +void +DiskFSCPM::SetBlockUsage(long block, VolumeUsage::ChunkPurpose purpose) +{ + VolumeUsage::ChunkState cstate; + + if (fVolumeUsage.GetChunkState(block, &cstate) != kDIErrNone) { + WMSG1(" CPM ERROR: unable to set state on block %ld\n", block); + return; + } + + if (cstate.isUsed) { + cstate.purpose = VolumeUsage::kChunkPurposeConflict; + WMSG1(" CPM conflicting uses for block=%ld\n", block); + } else { + cstate.isUsed = true; + cstate.isMarkedUsed = true; // no volume bitmap + cstate.purpose = purpose; + } + fVolumeUsage.SetChunkState(block, &cstate); +} + + +/* + * Scan for damaged files and conflicting file allocation entries. + * + * Appends some entries to the DiskImg notes, so this should only be run + * once per DiskFS. + * + * Returns "true" if disk appears to be perfect, "false" otherwise. + */ +bool +DiskFSCPM::CheckDiskIsGood(void) +{ + //DIError dierr; + bool result = true; + + //if (fEarlyDamage) + // result = false; + + /* + * TO DO: look for multiple files occupying the same blocks. + */ + + /* + * Scan for "damaged" or "suspicious" files diagnosed earlier. + */ + bool damaged, suspicious; + ScanForDamagedFiles(&damaged, &suspicious); + + if (damaged) { + fpImg->AddNote(DiskImg::kNoteWarning, + "One or more files are damaged."); + result = false; + } else if (suspicious) { + fpImg->AddNote(DiskImg::kNoteWarning, + "One or more files look suspicious."); + result = false; + } + + return result; +} + + +/* + * =========================================================================== + * A2FileCPM + * =========================================================================== + */ + +/* + * Not a whole lot to do, since there's no fancy index blocks. + * + * Calling GetBlockList twice is probably not the best way to go through life. + * This needs an overhaul. + */ +DIError +A2FileCPM::Open(A2FileDescr** ppOpenFile, bool readOnly, + bool rsrcFork /*=false*/) +{ + DIError dierr; + A2FDCPM* pOpenFile = nil; + + if (fpOpenFile != nil) + return kDIErrAlreadyOpen; + if (rsrcFork) + return kDIErrForkNotFound; + + assert(readOnly); + + pOpenFile = new A2FDCPM(this); + + dierr = GetBlockList(&pOpenFile->fBlockCount, nil); + if (dierr != kDIErrNone) + goto bail; + + pOpenFile->fBlockList = new unsigned char[pOpenFile->fBlockCount+1]; + pOpenFile->fBlockList[pOpenFile->fBlockCount] = 0xff; + + dierr = GetBlockList(&pOpenFile->fBlockCount, pOpenFile->fBlockList); + if (dierr != kDIErrNone) + goto bail; + + assert(pOpenFile->fBlockList[pOpenFile->fBlockCount] == 0xff); + + pOpenFile->fOffset = 0; + //fOpen = true; + + fpOpenFile = pOpenFile; + *ppOpenFile = pOpenFile; + pOpenFile = nil; + +bail: + delete pOpenFile; + return dierr; +} + + +/* + * Get the complete block list for a file. This will involve reading + * one or more directory entries. + * + * Call this once with "blockBuf" equal to "nil" to get the block count, + * then call a second time after allocating blockBuf. + */ +DIError +A2FileCPM::GetBlockList(long* pBlockCount, unsigned char* blockBuf) const +{ + di_off_t length = fLength; + int blockCount = 0; + int i, j; + + /* + * Run through the entries, pulling blocks out until we account for the + * entire length of the file. + * + * [Should probably pay more attention to extent numbers, making sure + * that they make sense. Not vital until we allow writes.] + */ + for (i = 0; i < DiskFSCPM::kNumDirEntries; i++) { + if (length <= 0) + break; + if (fpDirEntry[i].userNumber == kNoDataByte) + continue; + + if (strcmp((const char*)fpDirEntry[i].fileName, + (const char*)fpDirEntry[fDirIdx].fileName) == 0 && + fpDirEntry[i].userNumber == fpDirEntry[fDirIdx].userNumber) + { + /* this entry is part of the file */ + for (j = 0; j < DiskFSCPM::kDirEntryBlockCount; j++) { + if (fpDirEntry[i].blocks[j] == 0) { + WMSG2(" CPM found sparse block %d/%d\n", i, j); + } + blockCount++; + + if (blockBuf != nil) { + long listOffset = j + + fpDirEntry[i].extent * DiskFSCPM::kDirEntryBlockCount; + blockBuf[listOffset] = fpDirEntry[i].blocks[j]; + } + + length -= 1024; + if (length <= 0) + break; + } + } + } + + if (length > 0) { + WMSG1(" CPM WARNING: can't account for %ld bytes!\n", (long) length); + //assert(false); + } + + //WMSG2(" Returning blockCount=%d for '%s'\n", blockCount, + // fpDirEntry[fDirIdx].fileName); + if (pBlockCount != nil) { + assert(blockBuf == nil || *pBlockCount == blockCount); + *pBlockCount = blockCount; + } + + return kDIErrNone; +} + +/* + * Dump the contents of the A2File structure. + */ +void +A2FileCPM::Dump(void) const +{ + WMSG2("A2FileCPM '%s' length=%ld\n", fFileName, (long) fLength); +} + + +/* + * =========================================================================== + * A2FDCPM + * =========================================================================== + */ + +/* + * Read a chunk of data from the current offset. + */ +DIError +A2FDCPM::Read(void* buf, size_t len, size_t* pActual) +{ + WMSG3(" CP/M reading %d bytes from '%s' (offset=%ld)\n", + len, fpFile->GetPathName(), (long) fOffset); + + A2FileCPM* pFile = (A2FileCPM*) fpFile; + + /* don't allow them to read past the end of the file */ + if (fOffset + (long)len > pFile->fLength) { + if (pActual == nil) + return kDIErrDataUnderrun; + len = (size_t) (pFile->fLength - fOffset); + } + if (pActual != nil) + *pActual = len; + long incrLen = len; + + DIError dierr = kDIErrNone; + const int kCPMBlockSize = kBlkSize*2; + assert(kCPMBlockSize == 1024); + unsigned char blkBuf[kCPMBlockSize]; + int blkIndex = (int) (fOffset / kCPMBlockSize); + int bufOffset = (int) (fOffset % kCPMBlockSize); // (& 0x3ff) + size_t thisCount; + long prodosBlock; + + if (len == 0) + return kDIErrNone; + assert(pFile->fLength != 0); + + while (len) { + if (blkIndex >= fBlockCount) { + /* ran out of data */ + return kDIErrDataUnderrun; + } + + if (fBlockList[blkIndex] == 0) { + /* + * Sparse block. + */ + memset(blkBuf, kNoDataByte, sizeof(blkBuf)); + } else { + /* + * Read one CP/M block (two ProDOS blocks) and pull out the + * set of data that the user wants. + */ + prodosBlock = DiskFSCPM::CPMToProDOSBlock(fBlockList[blkIndex]); + + dierr = fpFile->GetDiskFS()->GetDiskImg()->ReadBlock(prodosBlock, + blkBuf); + if (dierr != kDIErrNone) { + WMSG1(" CP/M error1 reading file '%s'\n", pFile->fFileName); + return dierr; + } + dierr = fpFile->GetDiskFS()->GetDiskImg()->ReadBlock(prodosBlock+1, + blkBuf + kBlkSize); + if (dierr != kDIErrNone) { + WMSG1(" CP/M error2 reading file '%s'\n", pFile->fFileName); + return dierr; + } + } + + thisCount = kCPMBlockSize - bufOffset; + if (thisCount > len) + thisCount = len; + + memcpy(buf, blkBuf + bufOffset, thisCount); + len -= thisCount; + buf = (char*)buf + thisCount; + + bufOffset = 0; + blkIndex++; + } + + fOffset += incrLen; + + return dierr; +} + +/* + * Write data at the current offset. + */ +DIError +A2FDCPM::Write(const void* buf, size_t len, size_t* pActual) +{ + return kDIErrNotSupported; +} + +/* + * Seek to a new offset. + */ +DIError +A2FDCPM::Seek(di_off_t offset, DIWhence whence) +{ + di_off_t fileLength = ((A2FileCPM*) fpFile)->fLength; + + switch (whence) { + case kSeekSet: + if (offset < 0 || offset > fileLength) + return kDIErrInvalidArg; + fOffset = offset; + break; + case kSeekEnd: + if (offset > 0 || offset < -fileLength) + return kDIErrInvalidArg; + fOffset = fileLength + offset; + break; + case kSeekCur: + if (offset < -fOffset || + offset >= (fileLength - fOffset)) + { + return kDIErrInvalidArg; + } + fOffset += offset; + break; + default: + assert(false); + return kDIErrInvalidArg; + } + + assert(fOffset >= 0 && fOffset <= fileLength); + return kDIErrNone; +} + +/* + * Return current offset. + */ +di_off_t +A2FDCPM::Tell(void) +{ + return fOffset; +} + +/* + * Release file state, such as it is. + */ +DIError +A2FDCPM::Close(void) +{ + fpFile->CloseDescr(this); + return kDIErrNone; +} + +/* + * Return the #of sectors/blocks in the file. + */ +long +A2FDCPM::GetSectorCount(void) const +{ + return fBlockCount * 4; +} +long +A2FDCPM::GetBlockCount(void) const +{ + return fBlockCount * 2; +} + +/* + * Return the Nth track/sector in this file. + */ +DIError +A2FDCPM::GetStorage(long sectorIdx, long* pTrack, long* pSector) const +{ + long cpmIdx = sectorIdx / 4; // 4 256-byte sectors per 1K CP/M block + if (cpmIdx >= fBlockCount) + return kDIErrInvalidIndex; // CP/M files can have *no* storage + + long cpmBlock = fBlockList[cpmIdx]; + long prodosBlock = DiskFSCPM::CPMToProDOSBlock(cpmBlock); + if (sectorIdx & 0x02) + prodosBlock++; + + BlockToTrackSector(prodosBlock, (sectorIdx & 0x01) != 0, pTrack, pSector); + return kDIErrNone; +} +/* + * Return the Nth 512-byte block in this file. Since things aren't stored + * in 512-byte blocks, we grab the appropriate 1K block and pick half. + */ +DIError +A2FDCPM::GetStorage(long blockIdx, long* pBlock) const +{ + long cpmIdx = blockIdx / 2; // 4 256-byte sectors per 1K CP/M block + if (cpmIdx >= fBlockCount) + return kDIErrInvalidIndex; + + long cpmBlock = fBlockList[cpmIdx]; + long prodosBlock = DiskFSCPM::CPMToProDOSBlock(cpmBlock); + if (blockIdx & 0x01) + prodosBlock++; + + *pBlock = prodosBlock; + assert(*pBlock < fpFile->GetDiskFS()->GetDiskImg()->GetNumBlocks()); + return kDIErrNone; +} diff --git a/diskimg/CP_WNASPI32.H b/diskimg/CP_WNASPI32.H new file mode 100644 index 0000000..ae5ca0b --- /dev/null +++ b/diskimg/CP_WNASPI32.H @@ -0,0 +1,325 @@ +/****************************************************************************** +** +** Module Name: wnaspi32.h +** +** Description: Header file for ASPI for Win32. This header includes +** macro and type declarations, and can be included without +** modification when using Borland C++ or Microsoft Visual +** C++ with 32-bit compilation. If you are using a different +** compiler then you MUST ensure that structures are packed +** onto byte alignments, and that C++ name mangling is turned +** off. +** +** Notes: This file created using 4 spaces per tab. +** +******************************************************************************/ + +#ifndef __WNASPI32_H__ +#define __WNASPI32_H__ + +/* +** Make sure structures are packed and undecorated. +*/ + +#ifdef __BORLANDC__ +#pragma option -a1 +#endif //__BORLANDC__ + +#ifdef _MSC_VER +#pragma pack(1) +#endif //__MSC_VER + +#ifdef __cplusplus +extern "C" { +#endif //__cplusplus + +//***************************************************************************** +// %%% SCSI MISCELLANEOUS EQUATES %%% +//***************************************************************************** + +#define SENSE_LEN 18 // Default sense buffer length (ATM: was 14) +#define SRB_DIR_SCSI 0x00 // Direction determined by SCSI +#define SRB_POSTING 0x01 // Enable ASPI posting +#define SRB_ENABLE_RESIDUAL_COUNT 0x04 // Enable residual byte count reporting +#define SRB_DIR_IN 0x08 // Transfer from SCSI target to host +#define SRB_DIR_OUT 0x10 // Transfer from host to SCSI target +#define SRB_EVENT_NOTIFY 0x40 // Enable ASPI event notification + +#define RESIDUAL_COUNT_SUPPORTED 0x02 // Extended buffer flag +#define MAX_SRB_TIMEOUT 108000lu // 30 hour maximum timeout in s +#define DEFAULT_SRB_TIMEOUT 108000lu // Max timeout by default + + +//***************************************************************************** +// %%% ASPI Command Definitions %%% +//***************************************************************************** + +#define SC_HA_INQUIRY 0x00 // Host adapter inquiry +#define SC_GET_DEV_TYPE 0x01 // Get device type +#define SC_EXEC_SCSI_CMD 0x02 // Execute SCSI command +#define SC_ABORT_SRB 0x03 // Abort an SRB +#define SC_RESET_DEV 0x04 // SCSI bus device reset +#define SC_SET_HA_PARMS 0x05 // Set HA parameters +#define SC_GET_DISK_INFO 0x06 // Get Disk information +#define SC_RESCAN_SCSI_BUS 0x07 // ReBuild SCSI device map +#define SC_GETSET_TIMEOUTS 0x08 // Get/Set target timeouts + +//***************************************************************************** +// %%% SRB Status %%% +//***************************************************************************** + +#define SS_PENDING 0x00 // SRB being processed +#define SS_COMP 0x01 // SRB completed without error +#define SS_ABORTED 0x02 // SRB aborted +#define SS_ABORT_FAIL 0x03 // Unable to abort SRB +#define SS_ERR 0x04 // SRB completed with error + +#define SS_INVALID_CMD 0x80 // Invalid ASPI command +#define SS_INVALID_HA 0x81 // Invalid host adapter number +#define SS_NO_DEVICE 0x82 // SCSI device not installed + +#define SS_INVALID_SRB 0xE0 // Invalid parameter set in SRB +#define SS_OLD_MANAGER 0xE1 // ASPI manager doesn't support Windows +#define SS_BUFFER_ALIGN 0xE1 // Buffer not aligned (replaces OLD_MANAGER in Win32) +#define SS_ILLEGAL_MODE 0xE2 // Unsupported Windows mode +#define SS_NO_ASPI 0xE3 // No ASPI managers resident +#define SS_FAILED_INIT 0xE4 // ASPI for windows failed init +#define SS_ASPI_IS_BUSY 0xE5 // No resources available to execute cmd +#define SS_BUFFER_TO_BIG 0xE6 // Buffer size to big to handle! +#define SS_MISMATCHED_COMPONENTS 0xE7 // The DLLs/EXEs of ASPI don't version check +#define SS_NO_ADAPTERS 0xE8 // No host adapters to manage +#define SS_INSUFFICIENT_RESOURCES 0xE9 // Couldn't allocate resources needed to init +#define SS_ASPI_IS_SHUTDOWN 0xEA // Call came to ASPI after PROCESS_DETACH +#define SS_BAD_INSTALL 0xEB // The DLL or other components are installed wrong + +//***************************************************************************** +// %%% Host Adapter Status %%% +//***************************************************************************** + +#define HASTAT_OK 0x00 // Host adapter did not detect an error +#define HASTAT_SEL_TO 0x11 // Selection Timeout +#define HASTAT_DO_DU 0x12 // Data overrun data underrun +#define HASTAT_BUS_FREE 0x13 // Unexpected bus free +#define HASTAT_PHASE_ERR 0x14 // Target bus phase sequence failure +#define HASTAT_TIMEOUT 0x09 // Timed out while SRB was waiting to be processed. +#define HASTAT_COMMAND_TIMEOUT 0x0B // Adapter timed out processing SRB. +#define HASTAT_MESSAGE_REJECT 0x0D // While processing SRB, the adapter received a MESSAGE +#define HASTAT_BUS_RESET 0x0E // A bus reset was detected. +#define HASTAT_PARITY_ERROR 0x0F // A parity error was detected. +#define HASTAT_REQUEST_SENSE_FAILED 0x10 // The adapter failed in issuing + +//***************************************************************************** +// %%% SRB - HOST ADAPTER INQUIRY - SC_HA_INQUIRY (0) %%% +//***************************************************************************** + +typedef struct // Offset +{ // HX/DEC + BYTE SRB_Cmd; // 00/000 ASPI command code = SC_HA_INQUIRY + BYTE SRB_Status; // 01/001 ASPI command status byte + BYTE SRB_HaId; // 02/002 ASPI host adapter number + BYTE SRB_Flags; // 03/003 ASPI request flags + DWORD SRB_Hdr_Rsvd; // 04/004 Reserved, MUST = 0 + BYTE HA_Count; // 08/008 Number of host adapters present + BYTE HA_SCSI_ID; // 09/009 SCSI ID of host adapter + BYTE HA_ManagerId[16]; // 0A/010 String describing the manager + BYTE HA_Identifier[16]; // 1A/026 String describing the host adapter + BYTE HA_Unique[16]; // 2A/042 Host Adapter Unique parameters + WORD HA_Rsvd1; // 3A/058 Reserved, MUST = 0 +} +SRB_HAInquiry, *PSRB_HAInquiry, FAR *LPSRB_HAInquiry; + +//***************************************************************************** +// %%% SRB - GET DEVICE TYPE - SC_GET_DEV_TYPE (1) %%% +//***************************************************************************** + +typedef struct // Offset +{ // HX/DEC + BYTE SRB_Cmd; // 00/000 ASPI command code = SC_GET_DEV_TYPE + BYTE SRB_Status; // 01/001 ASPI command status byte + BYTE SRB_HaId; // 02/002 ASPI host adapter number + BYTE SRB_Flags; // 03/003 Reserved, MUST = 0 + DWORD SRB_Hdr_Rsvd; // 04/004 Reserved, MUST = 0 + BYTE SRB_Target; // 08/008 Target's SCSI ID + BYTE SRB_Lun; // 09/009 Target's LUN number + BYTE SRB_DeviceType; // 0A/010 Target's peripheral device type + BYTE SRB_Rsvd1; // 0B/011 Reserved, MUST = 0 +} +SRB_GDEVBlock, *PSRB_GDEVBlock, FAR *LPSRB_GDEVBlock; + +//***************************************************************************** +// %%% SRB - EXECUTE SCSI COMMAND - SC_EXEC_SCSI_CMD (2) %%% +//***************************************************************************** + +typedef struct // Offset +{ // HX/DEC + BYTE SRB_Cmd; // 00/000 ASPI command code = SC_EXEC_SCSI_CMD + BYTE SRB_Status; // 01/001 ASPI command status byte + BYTE SRB_HaId; // 02/002 ASPI host adapter number + BYTE SRB_Flags; // 03/003 ASPI request flags + DWORD SRB_Hdr_Rsvd; // 04/004 Reserved + BYTE SRB_Target; // 08/008 Target's SCSI ID + BYTE SRB_Lun; // 09/009 Target's LUN number + WORD SRB_Rsvd1; // 0A/010 Reserved for Alignment + DWORD SRB_BufLen; // 0C/012 Data Allocation Length + BYTE FAR *SRB_BufPointer; // 10/016 Data Buffer Pointer + BYTE SRB_SenseLen; // 14/020 Sense Allocation Length + BYTE SRB_CDBLen; // 15/021 CDB Length + BYTE SRB_HaStat; // 16/022 Host Adapter Status + BYTE SRB_TargStat; // 17/023 Target Status + VOID FAR *SRB_PostProc; // 18/024 Post routine + BYTE SRB_Rsvd2[20]; // 1C/028 Reserved, MUST = 0 + BYTE CDBByte[16]; // 30/048 SCSI CDB + BYTE SenseArea[SENSE_LEN+2]; // 50/064 Request Sense buffer +} +SRB_ExecSCSICmd, *PSRB_ExecSCSICmd, FAR *LPSRB_ExecSCSICmd; + +//***************************************************************************** +// %%% SRB - ABORT AN SRB - SC_ABORT_SRB (3) %%% +//***************************************************************************** + +typedef struct // Offset +{ // HX/DEC + BYTE SRB_Cmd; // 00/000 ASPI command code = SC_ABORT_SRB + BYTE SRB_Status; // 01/001 ASPI command status byte + BYTE SRB_HaId; // 02/002 ASPI host adapter number + BYTE SRB_Flags; // 03/003 Reserved + DWORD SRB_Hdr_Rsvd; // 04/004 Reserved + VOID FAR *SRB_ToAbort; // 08/008 Pointer to SRB to abort +} +SRB_Abort, *PSRB_Abort, FAR *LPSRB_Abort; + +//***************************************************************************** +// %%% SRB - BUS DEVICE RESET - SC_RESET_DEV (4) %%% +//***************************************************************************** + +typedef struct // Offset +{ // HX/DEC + BYTE SRB_Cmd; // 00/000 ASPI command code = SC_RESET_DEV + BYTE SRB_Status; // 01/001 ASPI command status byte + BYTE SRB_HaId; // 02/002 ASPI host adapter number + BYTE SRB_Flags; // 03/003 ASPI request flags + DWORD SRB_Hdr_Rsvd; // 04/004 Reserved + BYTE SRB_Target; // 08/008 Target's SCSI ID + BYTE SRB_Lun; // 09/009 Target's LUN number + BYTE SRB_Rsvd1[12]; // 0A/010 Reserved for Alignment + BYTE SRB_HaStat; // 16/022 Host Adapter Status + BYTE SRB_TargStat; // 17/023 Target Status + VOID FAR *SRB_PostProc; // 18/024 Post routine + BYTE SRB_Rsvd2[36]; // 1C/028 Reserved, MUST = 0 +} +SRB_BusDeviceReset, *PSRB_BusDeviceReset, FAR *LPSRB_BusDeviceReset; + +//***************************************************************************** +// %%% SRB - GET DISK INFORMATION - SC_GET_DISK_INFO %%% +//***************************************************************************** + +typedef struct // Offset +{ // HX/DEC + BYTE SRB_Cmd; // 00/000 ASPI command code = SC_GET_DISK_INFO + BYTE SRB_Status; // 01/001 ASPI command status byte + BYTE SRB_HaId; // 02/002 ASPI host adapter number + BYTE SRB_Flags; // 03/003 Reserved, MUST = 0 + DWORD SRB_Hdr_Rsvd; // 04/004 Reserved, MUST = 0 + BYTE SRB_Target; // 08/008 Target's SCSI ID + BYTE SRB_Lun; // 09/009 Target's LUN number + BYTE SRB_DriveFlags; // 0A/010 Driver flags + BYTE SRB_Int13HDriveInfo; // 0B/011 Host Adapter Status + BYTE SRB_Heads; // 0C/012 Preferred number of heads translation + BYTE SRB_Sectors; // 0D/013 Preferred number of sectors translation + BYTE SRB_Rsvd1[10]; // 0E/014 Reserved, MUST = 0 +} +SRB_GetDiskInfo, *PSRB_GetDiskInfo, FAR *LPSRB_GetDiskInfo; + +//***************************************************************************** +// %%% SRB - RESCAN SCSI BUS(ES) ON SCSIPORT %%% +//***************************************************************************** + +typedef struct // Offset +{ // HX/DEC + BYTE SRB_Cmd; // 00/000 ASPI command code = SC_RESCAN_SCSI_BUS + BYTE SRB_Status; // 01/001 ASPI command status byte + BYTE SRB_HaId; // 02/002 ASPI host adapter number + BYTE SRB_Flags; // 03/003 Reserved, MUST = 0 + DWORD SRB_Hdr_Rsvd; // 04/004 Reserved, MUST = 0 +} +SRB_RescanPort, *PSRB_RescanPort, FAR *LPSRB_RescanPort; + +//***************************************************************************** +// %%% SRB - GET/SET TARGET TIMEOUTS %%% +//***************************************************************************** + +typedef struct // Offset +{ // HX/DEC + BYTE SRB_Cmd; // 00/000 ASPI command code = SC_GETSET_TIMEOUTS + BYTE SRB_Status; // 01/001 ASPI command status byte + BYTE SRB_HaId; // 02/002 ASPI host adapter number + BYTE SRB_Flags; // 03/003 ASPI request flags + DWORD SRB_Hdr_Rsvd; // 04/004 Reserved, MUST = 0 + BYTE SRB_Target; // 08/008 Target's SCSI ID + BYTE SRB_Lun; // 09/009 Target's LUN number + DWORD SRB_Timeout; // 0A/010 Timeout in half seconds +} +SRB_GetSetTimeouts, *PSRB_GetSetTimeouts, FAR *LPSRB_GetSetTimeouts; + +//***************************************************************************** +// %%% ASPIBUFF - Structure For Controllng I/O Buffers %%% +//***************************************************************************** + +typedef struct tag_ASPI32BUFF // Offset +{ // HX/DEC + PBYTE AB_BufPointer; // 00/000 Pointer to the ASPI allocated buffer + DWORD AB_BufLen; // 04/004 Length in bytes of the buffer + DWORD AB_ZeroFill; // 08/008 Flag set to 1 if buffer should be zeroed + DWORD AB_Reserved; // 0C/012 Reserved +} +ASPI32BUFF, *PASPI32BUFF, FAR *LPASPI32BUFF; + +//***************************************************************************** +// %%% PROTOTYPES - User Callable ASPI for Win32 Functions %%% +//***************************************************************************** + +typedef void *LPSRB; + +#if defined(__BORLANDC__) + +DWORD _import GetASPI32SupportInfo( void ); +DWORD _import SendASPI32Command( LPSRB ); +BOOL _import GetASPI32Buffer( PASPI32BUFF ); +BOOL _import FreeASPI32Buffer( PASPI32BUFF ); +BOOL _import TranslateASPI32Address( PDWORD, PDWORD ); + +#elif defined(_MSC_VER) + +__declspec(dllimport) DWORD GetASPI32SupportInfo( void ); +__declspec(dllimport) DWORD SendASPI32Command( LPSRB ); +__declspec(dllimport) BOOL GetASPI32Buffer( PASPI32BUFF ); +__declspec(dllimport) BOOL FreeASPI32Buffer( PASPI32BUFF ); +__declspec(dllimport) BOOL TranslateASPI32Address( PDWORD, PDWORD ); + +#else + +extern DWORD GetASPI32SupportInfo( void ); +extern DWORD SendASPI32Command( LPSRB ); +extern BOOL GetASPI32Buffer( PASPI32BUFF ); +extern BOOL FreeASPI32Buffer( PASPI32BUFF ); +extern BOOL TranslateASPI32Address( PDWORD, PDWORD ); + +#endif + +/* +** Restore compiler default packing and close off the C declarations. +*/ + +#ifdef __BORLANDC__ +#pragma option -a. +#endif //__BORLANDC__ + +#ifdef _MSC_VER +#pragma pack() +#endif //_MSC_VER + +#ifdef __cplusplus +} +#endif //__cplusplus + +#endif //__WNASPI32_H__ diff --git a/diskimg/CP_ntddscsi.h b/diskimg/CP_ntddscsi.h new file mode 100644 index 0000000..ccdda6c --- /dev/null +++ b/diskimg/CP_ntddscsi.h @@ -0,0 +1,184 @@ +/* + * ntddscsi.h + * + * SCSI port IOCTL interface. + * + * This file is part of the w32api package. + * + * Contributors: + * Created by Casper S. Hornstrup + * + * THIS SOFTWARE IS NOT COPYRIGHTED + * + * This source code is offered for use in the public domain. You may + * use, modify or distribute it freely. + * + * This code is distributed in the hope that it will be useful but + * WITHOUT ANY WARRANTY. ALL WARRANTIES, EXPRESS OR IMPLIED ARE HEREBY + * DISCLAIMED. This includes but is not limited to warranties of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + */ + +#ifndef __NTDDSCSI_H +#define __NTDDSCSI_H + +#if __GNUC__ >=3 +#pragma GCC system_header +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#pragma pack(push,4) + +//#include "ntddk.h" + +#ifndef ULONG_PTR +# define ULONG_PTR DWORD +#endif + + +#define DD_SCSI_DEVICE_NAME "\\Device\\ScsiPort" +#define DD_SCSI_DEVICE_NAME_U L"\\Device\\ScsiPort" + +#define IOCTL_SCSI_BASE FILE_DEVICE_CONTROLLER + +#define IOCTL_SCSI_GET_INQUIRY_DATA \ + CTL_CODE(IOCTL_SCSI_BASE, 0x0403, METHOD_BUFFERED, FILE_ANY_ACCESS) + +#define IOCTL_SCSI_GET_CAPABILITIES \ + CTL_CODE(IOCTL_SCSI_BASE, 0x0404, METHOD_BUFFERED, FILE_ANY_ACCESS) + +#define IOCTL_SCSI_GET_ADDRESS \ + CTL_CODE(IOCTL_SCSI_BASE, 0x0406, METHOD_BUFFERED, FILE_ANY_ACCESS) + +#define IOCTL_SCSI_MINIPORT \ + CTL_CODE(IOCTL_SCSI_BASE, 0x0402, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS) + +#define IOCTL_SCSI_PASS_THROUGH \ + CTL_CODE(IOCTL_SCSI_BASE, 0x0401, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS) + +#define IOCTL_SCSI_PASS_THROUGH_DIRECT \ + CTL_CODE(IOCTL_SCSI_BASE, 0x0405, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS) + +#define IOCTL_SCSI_RESCAN_BUS \ + CTL_CODE(IOCTL_SCSI_BASE, 0x0407, METHOD_BUFFERED, FILE_ANY_ACCESS) + + +//DEFINE_GUID(ScsiRawInterfaceGuid, \ +// 0x53f56309L, 0xb6bf, 0x11d0, 0x94, 0xf2, 0x00, 0xa0, 0xc9, 0x1e, 0xfb, 0x8b); + +//DEFINE_GUID(WmiScsiAddressGuid, \ +// 0x53f5630fL, 0xb6bf, 0x11d0, 0x94, 0xf2, 0x00, 0xa0, 0xc9, 0x1e, 0xfb, 0x8b); + +typedef struct _SCSI_PASS_THROUGH { + USHORT Length; + UCHAR ScsiStatus; + UCHAR PathId; + UCHAR TargetId; + UCHAR Lun; + UCHAR CdbLength; + UCHAR SenseInfoLength; + UCHAR DataIn; + ULONG DataTransferLength; + ULONG TimeOutValue; + ULONG_PTR DataBufferOffset; + ULONG SenseInfoOffset; + UCHAR Cdb[16]; +} SCSI_PASS_THROUGH, *PSCSI_PASS_THROUGH; + +typedef struct _SCSI_PASS_THROUGH_DIRECT { + USHORT Length; + UCHAR ScsiStatus; + UCHAR PathId; + UCHAR TargetId; + UCHAR Lun; + UCHAR CdbLength; + UCHAR SenseInfoLength; + UCHAR DataIn; + ULONG DataTransferLength; + ULONG TimeOutValue; + PVOID DataBuffer; + ULONG SenseInfoOffset; + UCHAR Cdb[16]; +} SCSI_PASS_THROUGH_DIRECT, *PSCSI_PASS_THROUGH_DIRECT; + +typedef struct _SRB_IO_CONTROL { + ULONG HeaderLength; + UCHAR Signature[8]; + ULONG Timeout; + ULONG ControlCode; + ULONG ReturnCode; + ULONG Length; +} SRB_IO_CONTROL, *PSRB_IO_CONTROL; + +typedef struct _SCSI_ADDRESS { + ULONG Length; + UCHAR PortNumber; + UCHAR PathId; + UCHAR TargetId; + UCHAR Lun; +} SCSI_ADDRESS, *PSCSI_ADDRESS; + +typedef struct _SCSI_BUS_DATA { + UCHAR NumberOfLogicalUnits; + UCHAR InitiatorBusId; + ULONG InquiryDataOffset; +}SCSI_BUS_DATA, *PSCSI_BUS_DATA; + +typedef struct _SCSI_ADAPTER_BUS_INFO { + UCHAR NumberOfBuses; + SCSI_BUS_DATA BusData[1]; +} SCSI_ADAPTER_BUS_INFO, *PSCSI_ADAPTER_BUS_INFO; + +typedef struct _IO_SCSI_CAPABILITIES { + ULONG Length; + ULONG MaximumTransferLength; + ULONG MaximumPhysicalPages; + ULONG SupportedAsynchronousEvents; + ULONG AlignmentMask; + BOOLEAN TaggedQueuing; + BOOLEAN AdapterScansDown; + BOOLEAN AdapterUsesPio; +} IO_SCSI_CAPABILITIES, *PIO_SCSI_CAPABILITIES; + +typedef struct _SCSI_INQUIRY_DATA { + UCHAR PathId; + UCHAR TargetId; + UCHAR Lun; + BOOLEAN DeviceClaimed; + ULONG InquiryDataLength; + ULONG NextInquiryDataOffset; + UCHAR InquiryData[1]; +} SCSI_INQUIRY_DATA, *PSCSI_INQUIRY_DATA; + +#define SCSI_IOCTL_DATA_OUT 0 +#define SCSI_IOCTL_DATA_IN 1 +#define SCSI_IOCTL_DATA_UNSPECIFIED 2 + +struct ADAPTER_OBJECT; +typedef struct ADAPTER_OBJECT* PADAPTER_OBJECT; + +typedef struct _DUMP_POINTERS { + PADAPTER_OBJECT AdapterObject; + PVOID MappedRegisterBase; + PVOID DumpData; + PVOID CommonBufferVa; + LARGE_INTEGER CommonBufferPa; + ULONG CommonBufferSize; + BOOLEAN AllocateCommonBuffers; + BOOLEAN UseDiskDump; + UCHAR Spare1[2]; + PVOID DeviceObject; +} DUMP_POINTERS, *PDUMP_POINTERS; + + +#pragma pack(pop) + +#ifdef __cplusplus +} +#endif + +#endif /* __NTDDSCSI_H */ diff --git a/diskimg/Container.cpp b/diskimg/Container.cpp new file mode 100644 index 0000000..016215d --- /dev/null +++ b/diskimg/Container.cpp @@ -0,0 +1,122 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Base "container FS" support. + */ +#include "StdAfx.h" +#include "DiskImgPriv.h" + + +/* + * Blank out the volume usage map, setting all entries to "embedded". + */ +void +DiskFSContainer::SetVolumeUsageMap(void) +{ + VolumeUsage::ChunkState cstate; + long block; + + fVolumeUsage.Create(fpImg->GetNumBlocks()); + + cstate.isUsed = true; + cstate.isMarkedUsed = true; + cstate.purpose = VolumeUsage::kChunkPurposeEmbedded; + + for (block = fpImg->GetNumBlocks()-1; block >= 0; block--) + fVolumeUsage.SetChunkState(block, &cstate); +} + + +/* + * Create a "placeholder" sub-volume. Useful for some of the tools when + * dealing with unformatted (or unknown-formatted) partitions. + */ +DIError +DiskFSContainer::CreatePlaceholder(long startBlock, long numBlocks, + const char* partName, const char* partType, + DiskImg** ppNewImg, DiskFS** ppNewFS) +{ + DIError dierr = kDIErrNone; + DiskFS* pNewFS = nil; + DiskImg* pNewImg = nil; + + WMSG3(" %s/CrPl creating placeholder for %ld +%ld\n", GetDebugName(), + startBlock, numBlocks); + + if (startBlock > fpImg->GetNumBlocks()) { + WMSG3(" %s/CrPl start block out of range (%ld vs %ld)\n", + GetDebugName(), startBlock, fpImg->GetNumBlocks()); + return kDIErrBadPartition; + } + + pNewImg = new DiskImg; + if (pNewImg == nil) { + dierr = kDIErrMalloc; + goto bail; + } + + if (partName != nil) { + if (partType != nil) + pNewImg->AddNote(DiskImg::kNoteInfo, + "Partition name='%s' type='%s'.", partName, partType); + else + pNewImg->AddNote(DiskImg::kNoteInfo, + "Partition name='%s'.", partName); + } + + dierr = pNewImg->OpenImage(fpImg, startBlock, numBlocks); + if (dierr != kDIErrNone) { + WMSG4(" %s/CrPl: OpenImage(%ld,%ld) failed (err=%d)\n", + GetDebugName(), startBlock, numBlocks, dierr); + goto bail; + } + + /* + * If this slot isn't formatted at all, the call will return with + * "unknown FS". If it's formatted enough to pass the initial test, + * but fails during "Initialize", we'll have a non-unknown value + * for the FS format. We need to stomp that. + */ + dierr = pNewImg->AnalyzeImage(); + if (dierr != kDIErrNone) { + WMSG2(" %s/CrPl: analysis failed (err=%d)\n", GetDebugName(), dierr); + goto bail; + } + if (pNewImg->GetFSFormat() != DiskImg::kFormatUnknown) { + dierr = pNewImg->OverrideFormat(pNewImg->GetPhysicalFormat(), + DiskImg::kFormatUnknown, pNewImg->GetSectorOrder()); + if (dierr != kDIErrNone) { + WMSG1(" %s/CrPl: unable to override to unknown\n", + GetDebugName()); + goto bail; + } + } + + /* open a DiskFS for the sub-image, allowing "unknown" */ + pNewFS = pNewImg->OpenAppropriateDiskFS(true); + if (pNewFS == nil) { + WMSG1(" %s/CrPl: OpenAppropriateDiskFS failed\n", GetDebugName()); + dierr = kDIErrUnsupportedFSFmt; + goto bail; + } + + /* sets the DiskImg ptr (and very little else) */ + dierr = pNewFS->Initialize(pNewImg, kInitFull); + if (dierr != kDIErrNone) { + WMSG2(" %s/CrPl: init failed (err=%d)\n", GetDebugName(), dierr); + goto bail; + } + +bail: + if (dierr != kDIErrNone) { + delete pNewFS; + delete pNewImg; + } else { + *ppNewImg = pNewImg; + *ppNewFS = pNewFS; + } + return dierr; +} diff --git a/diskimg/DDD.cpp b/diskimg/DDD.cpp new file mode 100644 index 0000000..a8370d5 --- /dev/null +++ b/diskimg/DDD.cpp @@ -0,0 +1,638 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Pack and unpack DDD format. + */ +/* +The trouble with unpacking DOS DDD 2.x files: + +[ Most of this is no longer relevant, but the discussion is enlightening. ] + +DDD writes its files as DOS 3.3 binary (type 'B') files, which have +starting address and length embedded as the first 4 bytes. Unfortunately, it +cannot write the length, because the largest possible 16-bit length value is +only 64K. Instead, DDD sets it to zero. DDD v2.0 does store a copy of the +length *in sectors* in the filename (e.g. "<397>"), but this doesn't really +help much. When CiderPress goes to extract or view the file, it just sees a +zero-length binary file. + +CiderPress could make an exception and assume that any binary file with +zero length and more than one sector allocated has a length equal to the +number of sectors times 256. This could cause problems for other things, +but it's probably pretty safe. However, we still don't have an accurate +idea of where the end of the file is. + +Knowing where the file ends is important because there is no identifying +information or checksum in a DDD file. The only way to know that it's a +DDD compressed disk is to try to unpack it and see if you end up at exactly +140K at the same time that you run out of input. Without knowing where the +file really ends this test is much less certain. (DDD 2.5 appears to have +added some sort of checksum, which was appended to the DOS filename, but +without knowing how it was calculated there's no way to verify it.) + +The only safe way to make this work would be to skip the automatic format +detection and tell CiderPress that the file is definitely DDD format. +There's currently no easy way to do that without complicating the user +interface. Filename extensions might be useful in making the decision, +but they're rare under DOS 3.3, and I don't know if the "<397>" convention +is common to all versions of DDD. + +Complicating the matter is that, if a DOS DDD file (type 'B') is converted +to ProDOS, the first 4 bytes will be stripped off. Without unpacking +the file and knowing to within a byte where it ends, there's no way to +automatically tell whether to start at byte 0 or byte 4. (DDD Pro files +have four bytes of garbage at the very start, probably in an attempt to +retain compatibility with the DOS version. Because it uses REL files the +4 bytes of extra DOS stuff aren't added when the files are copied around, +so this was a reasonably smart thing to do, but it complicates matters +for CiderPress because a file extracted from DOS and a file extracted +from ProDOS will come out differently because only the DOS version has the +leading 4 bytes stripped. This could be avoided if the DOS file uses the +'R' or 'S' file type, but we still lack an accurate file length.) + +To unpack a file created by DOS DDD v2.x with CiderPress: + - In an emulator, copy the file to a ProDOS disk, using something that + guesses at the actual length when one isn't provided (Copy ][+ 9.0 + may work). + - Reduce the length to within a byte or two of the actual end of file. + This is nearly impossible, because DDD doesn't zero out the remaining + data in the last sector. + - Insert 4 bytes of garbage at the front of the file. My copy of DDD + Pro 1.1 seems to like 03 c9 bf d0. +Probably not worth the effort. Just unpack it with an emulator. + +In general DDD is a rather poor choice, because the compression isn't +very good and there's no checksum. ShrinkIt is a much better way to go. + +NOTE: DOS DDD v2.0 seems to have a bug where it doesn't always write the last +run correctly. On the DOS system master this caused the last half of the +last sector (FID's T/S list) to have garbage written instead of zero bytes, +which caused CP to label FID as damaged. DDD v2.1 and later doesn't +appear to have this issue. Unfortunate that DDD 2.0 is what shipped on the +SST disk. + */ +#include "StdAfx.h" +#include "DiskImgPriv.h" + +const int kNumSymbols = 256; +const int kNumFavorites = 20; +const int kRLEDelim = 0x97; // value MUST have high bit set +const int kMaxExcessByteCount = WrapperDDD::kMaxDDDZeroCount + 1; +//const int kTrackLen = 4096; +//const int kNumTracks = 35; + +/* I suspect this is random garbage, but it's appearing consistently */ +const unsigned long kDDDProSignature = 0xd0bfc903; + + +/* + * =========================================================================== + * BitBuffer + * =========================================================================== + */ + +/* + * Class for getting and putting bits to and from a file. + */ +class WrapperDDD::BitBuffer { +public: + BitBuffer(void) : fpGFD(nil), fBits(0), fBitCount(0), fIOFailure(false) {} + ~BitBuffer(void) {} + + void SetFile(GenericFD* pGFD) { fpGFD = pGFD; } + void PutBits(unsigned char bits, int numBits); + unsigned char GetBits(int numBits); + + bool IOFailure(void) const { return fIOFailure; } + + static unsigned char Reverse(unsigned char val); + +private: + GenericFD* fpGFD; + unsigned char fBits; + int fBitCount; + bool fIOFailure; +}; + +/* + * Add bits to the buffer. + * + * We roll the low bits out of "bits" and shift them to the left (in the + * reverse order in which they were passed in). As soon as we get 8 bits + * we flush. + */ +void +WrapperDDD::BitBuffer::PutBits(unsigned char bits, int numBits) +{ + assert(fBitCount >= 0 && fBitCount < 8); + assert(numBits > 0 && numBits <= 8); + assert(fpGFD != nil); + + DIError dierr; + + while (numBits--) { + fBits = (fBits << 1) | (bits & 0x01); + fBitCount++; + + if (fBitCount == 8) { + dierr = fpGFD->Write(&fBits, 1); + fIOFailure = (dierr != kDIErrNone); + fBitCount = 0; + } + + bits >>= 1; + } +} + +/* + * Get bits from the buffer. + * + * These come out in the order in which they appear in the file, which + * means that in some cases they will have to be reversed. + */ +unsigned char +WrapperDDD::BitBuffer::GetBits(int numBits) +{ + assert(fBitCount >= 0 && fBitCount < 8); + assert(numBits > 0 && numBits <= 8); + assert(fpGFD != nil); + + DIError dierr; + unsigned char retVal; + + if (fBitCount == 0) { + /* have no bits */ + dierr = fpGFD->Read(&fBits, 1); + fIOFailure = (dierr != kDIErrNone); + fBitCount = 8; + } + + if (numBits <= fBitCount) { + /* just serve up what we've already got */ + retVal = fBits >> (8 - numBits); + fBits <<= numBits; + fBitCount -= numBits; + } else { + /* some old, some new; load what we have right-aligned */ + retVal = fBits >> (8 - fBitCount); + numBits -= fBitCount; + + dierr = fpGFD->Read(&fBits, 1); + fIOFailure = (dierr != kDIErrNone); + fBitCount = 8; + + /* make room for the rest (also zeroes out the low bits) */ + retVal <<= numBits; + + /* add the high bits from the new byte */ + retVal |= fBits >> (8 - numBits); + fBits <<= numBits; + fBitCount -= numBits; + } + + return retVal; +} + +/* + * Utility function to reverse the order of bits in a byte. + */ +/*static*/ unsigned char +WrapperDDD::BitBuffer::Reverse(unsigned char val) +{ + int i; + unsigned char result = 0; // init is to make valgrind happy + + for (i = 0; i < 8; i++) { + result = (result << 1) + (val & 0x01); + val >>= 1; + } + + return result; +} + + +/* + * =========================================================================== + * DDD compression functions + * =========================================================================== + */ + +/* + * These are all odd, which when they're written in reverse order means + * they all have their hi bits set. + */ +static const unsigned char kFavoriteBitEnc[kNumFavorites] = { + 0x03, 0x09, 0x1f, 0x0f, 0x07, 0x1b, 0x0b, 0x0d, 0x15, 0x37, + 0x3d, 0x25, 0x05, 0xb1, 0x11, 0x21, 0x01, 0x57, 0x5d, 0x1d +}; +static const int kFavoriteBitEncLen[kNumFavorites] = { + 4, 4, 5, 5, 5, 5, 5, 5, 5, 6, + 6, 6, 6, 6, 6, 6, 6, 7, 7, 7 +}; + + +/* + * Pack a disk image with DDD. + * + * Assumes pSrcGFD points to DOS-ordered sectors. (This is enforced when the + * disk image is first being created.) + */ +/*static*/ DIError +WrapperDDD::PackDisk(GenericFD* pSrcGFD, GenericFD* pWrapperGFD, + short diskVolNum) +{ + DIError dierr = kDIErrNone; + BitBuffer bitBuffer; + + assert(diskVolNum >= 0 && diskVolNum < 256); + + /* write four zeroes to replace the DOS addr/len bytes */ + /* (actually, let's write the apparent DDD Pro v1.1 signature instead) */ + WriteLongLE(pWrapperGFD, kDDDProSignature); + + bitBuffer.SetFile(pWrapperGFD); + + bitBuffer.PutBits(0x00, 3); + bitBuffer.PutBits((unsigned char)diskVolNum, 8); + + /* + * Process all tracks. + */ + for (int track = 0; track < kNumTracks; track++) { + unsigned char trackBuf[kTrackLen]; + + dierr = pSrcGFD->Read(trackBuf, kTrackLen); + if (dierr != kDIErrNone) { + WMSG1(" DDD error during read (err=%d)\n", dierr); + goto bail; + } + + PackTrack(trackBuf, &bitBuffer); + } + + /* write 8 bits of zeroes to flush remaining data out of buffer */ + bitBuffer.PutBits(0x00, 8); + + /* write another zero byte because that's what DDD Pro v1.1 does */ + long zero; + zero = 0; + dierr = pWrapperGFD->Write(&zero, 1); + if (dierr != kDIErrNone) + goto bail; + + assert(dierr == kDIErrNone); +bail: + return dierr; +} + +/* + * Compress a track full of data. + */ +/*static*/ void +WrapperDDD::PackTrack(const unsigned char* trackBuf, BitBuffer* pBitBuf) +{ + unsigned short freqCounts[kNumSymbols]; + unsigned char favorites[kNumFavorites]; + int i, fav; + + ComputeFreqCounts(trackBuf, freqCounts); + ComputeFavorites(freqCounts, favorites); + + /* write favorites */ + for (fav = 0; fav < kNumFavorites; fav++) + pBitBuf->PutBits(favorites[fav], 8); + + /* + * Compress track data. Store runs as { 0x97 char count }, where + * a count of zero means 256. + */ + const unsigned char* ucp = trackBuf; + for (i = 0; i < kTrackLen; i++, ucp++) { + if (i < (kTrackLen-3) && + *ucp == *(ucp+1) && + *ucp == *(ucp+2) && + *ucp == *(ucp+3)) + { + int runLen = 4; + i += 3; + ucp += 3; + + while (i < kTrackLen-1 && *ucp == *(ucp+1)) { + runLen++; + ucp++; + i++; + + if (runLen == 256) { + runLen = 0; + break; + } + } + + pBitBuf->PutBits(kRLEDelim, 8); // note kRLEDelim has hi bit set + pBitBuf->PutBits(*ucp, 8); + pBitBuf->PutBits(runLen, 8); + + } else { + /* + * Not a run, see if it's one of our favorites. + */ + for (fav = 0; fav < kNumFavorites; fav++) { + if (*ucp == favorites[fav]) + break; + } + if (fav == kNumFavorites) { + /* just a plain byte */ + pBitBuf->PutBits(0x00, 1); + pBitBuf->PutBits(*ucp, 8); + } else { + /* found a favorite; leading hi bit is implied */ + pBitBuf->PutBits(kFavoriteBitEnc[fav], kFavoriteBitEncLen[fav]); + } + } + } +} + + +/* + * Compute the #of times each byte appears in trackBuf. Runs of four + * bytes or longer are completely ignored. + * + * "trackBuf" holds kTrackLen bytes of data, and "freqCounts" holds + * kNumSymbols (256) unsigned shorts. + */ +/*static*/ void +WrapperDDD::ComputeFreqCounts(const unsigned char* trackBuf, + unsigned short* freqCounts) +{ + const unsigned char* ucp; + int i; + + memset(freqCounts, 0, 256 * sizeof(unsigned short)); + + ucp = trackBuf; + for (i = 0; i < kTrackLen; i++, ucp++) { + if (i < (kTrackLen-3) && + *ucp == *(ucp+1) && + *ucp == *(ucp+2) && + *ucp == *(ucp+3)) + { + int runLen = 4; // DEBUG only + i += 3; + ucp += 3; + + while (i < kTrackLen-1 && *ucp == *(ucp+1)) { + runLen++; + ucp++; + i++; + + if (runLen == 256) { + runLen = 0; + break; + } + } + + //WMSG2("Found run of %d of 0x%02x\n", runLen, *ucp); + } else { + /* not a run, just update stats */ + freqCounts[*ucp]++; + } + } +} + +/* + * Find the 20 most frequently occurring symbols, in order. + * + * Modifies "freqCounts". + */ +/*static*/ void +WrapperDDD::ComputeFavorites(unsigned short* freqCounts, + unsigned char* favorites) +{ + int i, fav; + + for (fav = 0; fav < kNumFavorites; fav++) { + unsigned short bestCount = 0; + unsigned char bestSym = 0; + + for (i = 0; i < kNumSymbols; i++) { + if (freqCounts[i] >= bestCount) { + bestSym = (unsigned char) i; + bestCount = freqCounts[i]; + } + } + + favorites[fav] = bestSym; + freqCounts[bestSym] = 0; + } + + //WMSG0("FAVORITES: "); + //for (fav = 0; fav < kNumFavorites; fav++) + // WMSG1("%02x\n", favorites[fav]); + //WMSG0("\n"); +} + + +/* + * =========================================================================== + * DDD expansion functions + * =========================================================================== + */ + +/* + * This is the reverse of the kFavoriteBitEnc table. The bits are + * reversed and lack the high bit. + */ +static const unsigned char kFavoriteBitDec[kNumFavorites] = { + 0x04, 0x01, 0x0f, 0x0e, 0x0c, 0x0b, 0x0a, 0x06, 0x05, 0x1b, + 0x0f, 0x09, 0x08, 0x03, 0x02, 0x01, 0x00, 0x35, 0x1d, 0x1c +}; + +/* + * Entry point for unpacking a disk image compressed with DDD. + * + * The result is an unadorned DOS-ordered image. + */ +/*static*/ DIError +WrapperDDD::UnpackDisk(GenericFD* pGFD, GenericFD* pNewGFD, + short* pDiskVolNum) +{ + DIError dierr = kDIErrNone; + BitBuffer bitBuffer; + unsigned char val; + long lbuf; + + assert(pGFD != nil); + assert(pNewGFD != nil); + + /* read four zeroes to skip the DOS addr/len bytes */ + assert(sizeof(lbuf) >= 4); + dierr = pGFD->Read(&lbuf, 4); + if (dierr != kDIErrNone) + goto bail; + + bitBuffer.SetFile(pGFD); + + val = bitBuffer.GetBits(3); + if (val != 0) { + WMSG1(" DDD bits not zero, this isn't a DDD II file (0x%02x)\n", val); + dierr = kDIErrGeneric; + goto bail; + } + val = bitBuffer.GetBits(8); + *pDiskVolNum = bitBuffer.Reverse(val); + WMSG1(" DDD found disk volume num = %d\n", *pDiskVolNum); + + int track; + for (track = 0; track < kNumTracks; track++) { + unsigned char trackBuf[kTrackLen]; + + if (!UnpackTrack(&bitBuffer, trackBuf)) { + WMSG1(" DDD failed unpacking track %d\n", track); + dierr = kDIErrBadCompressedData; + goto bail; + } + if (bitBuffer.IOFailure()) { + WMSG0(" DDD failure or EOF on input file\n"); + dierr = kDIErrBadCompressedData; + goto bail; + } + dierr = pNewGFD->Write(trackBuf, kTrackLen); + if (dierr != kDIErrNone) + goto bail; + } + + /* + * We should be within a byte or two of the end of the file. Try + * to read more and expect it to fail. + * + * Unfortunately, if this was a DOS DDD file, we could be up to 256 + * bytes off (the 1 additional byte it adds plus the remaining 255 + * bytes in the sector). We have to choose between a tight auto-detect + * and the ability to process DOS DDD files. + * + * Fortunately the need to hit track boundaries exactly and the quick test + * for long runs of bytes provides some opportunity for correct + * detection. + */ + size_t actual; + char sctBuf[256 + 16]; + dierr = pGFD->Read(&sctBuf, sizeof(sctBuf), &actual); + if (dierr == kDIErrNone) { + if (actual > /*kMaxExcessByteCount*/ 256) { + WMSG1(" DDD looks like too much data in input file (%d extra)\n", + actual); + dierr = kDIErrBadCompressedData; + goto bail; + } else { + WMSG1(" DDD excess bytes (%d) within normal parameters\n", actual); + } + } + + WMSG0(" DDD looks like a DDD archive!\n"); + dierr = kDIErrNone; + +bail: + return dierr; +} + +/* + * Unpack a single track. + * + * Returns "true" if all went well, "false" if something failed. + */ +/*static*/ bool +WrapperDDD::UnpackTrack(BitBuffer* pBitBuffer, unsigned char* trackBuf) +{ + unsigned char favorites[kNumFavorites]; + unsigned char val; + unsigned char* trackPtr; + int fav; + + /* + * Start by pulling our favorites out, in reverse order. + */ + for (fav = 0; fav < kNumFavorites; fav++) { + val = pBitBuffer->GetBits(8); + val = pBitBuffer->Reverse(val); + favorites[fav] = val; + } + + trackPtr = trackBuf; + + /* + * Keep pulling data out until the track is full. + */ + while (trackPtr < trackBuf + kTrackLen) { + val = pBitBuffer->GetBits(1); + if (!val) { + /* simple byte */ + val = pBitBuffer->GetBits(8); + val = pBitBuffer->Reverse(val); + *trackPtr++ = val; + } else { + /* try for a prefix match */ + int extraBits; + + val = pBitBuffer->GetBits(2); + + for (extraBits = 0; extraBits < 4; extraBits++) { + val = (val << 1) | pBitBuffer->GetBits(1); + int start, end; + + if (extraBits == 0) { + start = 0; + end = 2; + } else if (extraBits == 1) { + start = 2; + end = 9; + } else if (extraBits == 2) { + start = 9; + end = 17; + } else { + start = 17; + end = 20; + } + + while (start < end) { + if (val == kFavoriteBitDec[start]) { + /* winner! */ + *trackPtr++ = favorites[start]; + break; + } + start++; + } + if (start != end) + break; // we got it, break out of for loop + } + if (extraBits == 4) { + /* we didn't get it, this must be RLE */ + unsigned char rleChar; + int rleCount; + + (void) pBitBuffer->GetBits(1); // get last bit of 0x97 + val = pBitBuffer->GetBits(8); + rleChar = pBitBuffer->Reverse(val); + val = pBitBuffer->GetBits(8); + rleCount = pBitBuffer->Reverse(val); + //WMSG2(" DDD found run of %d of 0x%02x\n", rleCount, rleChar); + + if (rleCount == 0) + rleCount = 256; + + /* make sure we won't overrun */ + if (trackPtr + rleCount > trackBuf + kTrackLen) { + WMSG0(" DDD overrun in RLE\n"); + return false; + } + while (rleCount--) + *trackPtr++ = rleChar; + } + } + } + + return true; +} + diff --git a/diskimg/DIUtil.cpp b/diskimg/DIUtil.cpp new file mode 100644 index 0000000..911797b --- /dev/null +++ b/diskimg/DIUtil.cpp @@ -0,0 +1,379 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * DiskImgLib global utility functions. + */ +#include "StdAfx.h" +#include "DiskImgPriv.h" + +#define kFilenameExtDelim '.' /* separates extension from filename */ + +/* + * Get values from a memory buffer. + */ +unsigned short +DiskImgLib::GetShortLE(const unsigned char* ptr) +{ + return *ptr | (unsigned short) *(ptr+1) << 8; +} + +unsigned long +DiskImgLib::GetLongLE(const unsigned char* ptr) +{ + return *ptr | + (unsigned long) *(ptr+1) << 8 | + (unsigned long) *(ptr+2) << 16 | + (unsigned long) *(ptr+3) << 24; +} + +unsigned short +DiskImgLib::GetShortBE(const unsigned char* ptr) +{ + return *(ptr+1) | (unsigned short) *ptr << 8; +} + +unsigned long +DiskImgLib::GetLongBE(const unsigned char* ptr) +{ + return *(ptr+3) | + (unsigned long) *(ptr+2) << 8 | + (unsigned long) *(ptr+1) << 16 | + (unsigned long) *ptr << 24; +} + +unsigned long +DiskImgLib::Get24BE(const unsigned char* ptr) +{ + return *(ptr+2) | + (unsigned long) *(ptr+1) << 8 | + (unsigned long) *ptr << 16; +} + +void +DiskImgLib::PutShortLE(unsigned char* ptr, unsigned short val) +{ + *ptr++ = (unsigned char) val; + *ptr = val >> 8; +} + +void +DiskImgLib::PutLongLE(unsigned char* ptr, unsigned long val) +{ + *ptr++ = (unsigned char) val; + *ptr++ = (unsigned char) (val >> 8); + *ptr++ = (unsigned char) (val >> 16); + *ptr = (unsigned char) (val >> 24); +} + +void +DiskImgLib::PutShortBE(unsigned char* ptr, unsigned short val) +{ + *ptr++ = val >> 8; + *ptr = (unsigned char) val; +} + +void +DiskImgLib::PutLongBE(unsigned char* ptr, unsigned long val) +{ + *ptr++ = (unsigned char) (val >> 24); + *ptr++ = (unsigned char) (val >> 16); + *ptr++ = (unsigned char) (val >> 8); + *ptr = (unsigned char) val; +} + + +/* + * Read a two-byte little-endian value. + */ +DIError +DiskImgLib::ReadShortLE(GenericFD* pGFD, short* pBuf) +{ + DIError dierr; + unsigned char val[2]; + + dierr = pGFD->Read(&val[0], 1); + if (dierr == kDIErrNone) + dierr = pGFD->Read(&val[1], 1); + + *pBuf = val[0] | (short) val[1] << 8; + return dierr; +} + +/* + * Read a four-byte little-endian value. + */ +DIError +DiskImgLib::ReadLongLE(GenericFD* pGFD, long* pBuf) +{ + DIError dierr; + unsigned char val[4]; + + dierr = pGFD->Read(&val[0], 1); + if (dierr == kDIErrNone) + dierr = pGFD->Read(&val[1], 1); + if (dierr == kDIErrNone) + dierr = pGFD->Read(&val[2], 1); + if (dierr == kDIErrNone) + dierr = pGFD->Read(&val[3], 1); + + *pBuf = val[0] | (long)val[1] << 8 | (long)val[2] << 16 | (long)val[3] << 24; + return dierr; +} + +/* + * Write a two-byte little-endian value. + */ +DIError +DiskImgLib::WriteShortLE(FILE* fp, unsigned short val) +{ + putc(val, fp); + putc(val >> 8, fp); + return kDIErrNone; +} + +/* + * Write a four-byte little-endian value. + */ +DIError +DiskImgLib::WriteLongLE(FILE* fp, unsigned long val) +{ + putc(val, fp); + putc(val >> 8, fp); + putc(val >> 16, fp); + putc(val >> 24, fp); + return kDIErrNone; +} + +/* + * Write a two-byte little-endian value. + */ +DIError +DiskImgLib::WriteShortLE(GenericFD* pGFD, unsigned short val) +{ + unsigned char buf; + + buf = (unsigned char) val; + pGFD->Write(&buf, 1); + buf = val >> 8; + return pGFD->Write(&buf, 1); +} + +/* + * Write a four-byte little-endian value. + */ +DIError +DiskImgLib::WriteLongLE(GenericFD* pGFD, unsigned long val) +{ + unsigned char buf; + + buf = (unsigned char) val; + pGFD->Write(&buf, 1); + buf = (unsigned char) (val >> 8); + pGFD->Write(&buf, 1); + buf = (unsigned char) (val >> 16); + pGFD->Write(&buf, 1); + buf = (unsigned char) (val >> 24); + return pGFD->Write(&buf, 1); +} + +/* + * Write a two-byte big-endian value. + */ +DIError +DiskImgLib::WriteShortBE(GenericFD* pGFD, unsigned short val) +{ + unsigned char buf; + + buf = val >> 8; + pGFD->Write(&buf, 1); + buf = (unsigned char) val; + return pGFD->Write(&buf, 1); +} + +/* + * Write a four-byte big-endian value. + */ +DIError +DiskImgLib::WriteLongBE(GenericFD* pGFD, unsigned long val) +{ + unsigned char buf; + + buf = (unsigned char) (val >> 24); + pGFD->Write(&buf, 1); + buf = (unsigned char) (val >> 16); + pGFD->Write(&buf, 1); + buf = (unsigned char) (val >> 8); + pGFD->Write(&buf, 1); + buf = (unsigned char) val; + return pGFD->Write(&buf, 1); +} + + +/* + * Find the filename component of a local pathname. Uses the fssep passed + * in. If the fssep is '\0' (as is the case for DOS 3.3), then the entire + * pathname is returned. + * + * Always returns a pointer to a string; never returns nil. + */ +const char* +DiskImgLib::FilenameOnly(const char* pathname, char fssep) +{ + const char* retstr; + const char* pSlash; + char* tmpStr = nil; + + assert(pathname != nil); + if (fssep == '\0') { + retstr = pathname; + goto bail; + } + + pSlash = strrchr(pathname, fssep); + if (pSlash == nil) { + retstr = pathname; /* whole thing is the filename */ + goto bail; + } + + pSlash++; + if (*pSlash == '\0') { + if (strlen(pathname) < 2) { + retstr = pathname; /* the pathname is just "/"? Whatever */ + goto bail; + } + + /* some bonehead put an fssep on the very end; back up before it */ + /* (not efficient, but this should be rare, and I'm feeling lazy) */ + tmpStr = strdup(pathname); + tmpStr[strlen(pathname)-1] = '\0'; + pSlash = strrchr(tmpStr, fssep); + + if (pSlash == nil) { + retstr = pathname; /* just a filename with a '/' after it */ + goto bail; + } + + pSlash++; + if (*pSlash == '\0') { + retstr = pathname; /* I give up! */ + goto bail; + } + + retstr = pathname + (pSlash - tmpStr); + + } else { + retstr = pSlash; + } + +bail: + free(tmpStr); + return retstr; +} + +/* + * Return the filename extension found in a full pathname. + * + * An extension is the stuff following the last '.' in the filename. If + * there is nothing following the last '.', then there is no extension. + * + * Returns a pointer to the '.' preceding the extension, or nil if no + * extension was found. + * + * We guarantee that there is at least one character after the '.'. + */ +const char* +DiskImgLib::FindExtension(const char* pathname, char fssep) +{ + const char* pFilename; + const char* pExt; + + /* + * We have to isolate the filename so that we don't get excited + * about "/foo.bar/file". + */ + pFilename = FilenameOnly(pathname, fssep); + assert(pFilename != nil); + pExt = strrchr(pFilename, kFilenameExtDelim); + + /* also check for "/blah/foo.", which doesn't count */ + if (pExt != nil && *(pExt+1) != '\0') + return pExt; + + return nil; +} + +/* + * Like strcpy(), but allocate with new[] instead. + * + * If "str" is nil, or "new" fails, this returns nil. + */ +char* +DiskImgLib::StrcpyNew(const char* str) +{ + char* newStr; + + if (str == nil) + return nil; + newStr = new char[strlen(str)+1]; + if (newStr != nil) + strcpy(newStr, str); + return newStr; +} + + +#ifdef _WIN32 +/* + * Convert the value from GetLastError() to its DIError counterpart. + */ +DIError +DiskImgLib::LastErrorToDIError(void) +{ + DWORD lastErr = ::GetLastError(); + + switch (lastErr) { + case ERROR_FILE_NOT_FOUND: return kDIErrFileNotFound; // 2 + case ERROR_ACCESS_DENIED: return kDIErrAccessDenied; // 5 + case ERROR_WRITE_PROTECT: return kDIErrWriteProtected; // 19 + case ERROR_SECTOR_NOT_FOUND: return kDIErrGeneric; // 27 + case ERROR_SHARING_VIOLATION: return kDIErrSharingViolation; // 32 + case ERROR_HANDLE_EOF: return kDIErrEOF; // 38 + case ERROR_INVALID_PARAMETER: return kDIErrInvalidArg; // 87 + case ERROR_SEM_TIMEOUT: return kDIErrGenericIO; // 121 + // ERROR_SEM_TIMEOUT seen read bad blocks from floptical under Win2K + + case ERROR_INVALID_HANDLE: // 6 + WMSG0("HEY: got ERROR_INVALID_HANDLE!\n"); + return kDIErrInternal; + case ERROR_NEGATIVE_SEEK: // 131 + WMSG0("HEY: got ERROR_NEGATIVE_SEEK!\n"); + return kDIErrInternal; + default: + WMSG2("LastErrorToDIError: not converting 0x%08lx (%ld)\n", + lastErr, lastErr); + return kDIErrGeneric; + } +} + +/* + * Returns "true" if we're running on Win9x (Win95, Win98, WinME), "false" + * if not (could be WinNT/2K/XP or even Win31 with Win32s). + */ +bool +DiskImgLib::IsWin9x(void) +{ + OSVERSIONINFO osvers; + BOOL result; + + osvers.dwOSVersionInfoSize = sizeof(osvers); + result = ::GetVersionEx(&osvers); + assert(result != FALSE); + + if (osvers.dwPlatformId == VER_PLATFORM_WIN32_WINDOWS) + return true; + else + return false; +} +#endif diff --git a/diskimg/DOS33.cpp b/diskimg/DOS33.cpp new file mode 100644 index 0000000..bd5b983 --- /dev/null +++ b/diskimg/DOS33.cpp @@ -0,0 +1,3447 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Implementation of DiskFSDOS33 and A2FileDOS classes. + * + * Works for DOS 3.2 and "wide DOS" as well. + * + * BUG: does not keep VolumeUsage up to date. + */ +#include "StdAfx.h" +#include "DiskImgPriv.h" + + +/* + * =========================================================================== + * DiskFSDOS33 + * =========================================================================== + */ + +const int kMaxSectors = 32; + +const int kSctSize = 256; +/* do we need a way to override these? */ +const int kVTOCTrack = 17; +const int kVTOCSector = 0; +const int kCatalogEntryOffset = 0x0b; // first entry in cat sect starts here +const int kCatalogEntrySize = 0x23; // length in bytes of catalog entries +const int kCatalogEntriesPerSect = 7; // #of entries per catalog sector +const int kEntryDeleted = 0xff; // this is used for track# of deleted files +const int kEntryUnused = 0x00; // this is track# in never-used entries +const int kMaxTSPairs = 0x7a; // 122 entries for 256-byte sectors +const int kTSOffset = 0x0c; // first T/S entry in a T/S list + +const int kMaxTSIterations = 32; + +/* + * Get a pointer to the Nth entry in a catalog sector. + */ +static inline unsigned char* +GetCatalogEntryPtr(unsigned char* basePtr, int entryNum) +{ + assert(entryNum >= 0 && entryNum < kCatalogEntriesPerSect); + return basePtr + kCatalogEntryOffset + entryNum * kCatalogEntrySize; +} + + +/* + * Test this image for DOS3.3-ness. + * + * Some notes on tricky disks... + * + * DISK019B (Ultima II player master) has a copy of the VTOC in track 11 + * sector 1, which causes a loop back to track 11 sector f. We may want + * to be clever here and allow it, but we have to be careful because + * we must be similarly clever in the VTOC read routines. (Need a more + * sophisticated loop detector, since a loop will crank our "foundGood" up.) + * + * DISK038B (Congo Bongo) has some "crack" titles and a valid VTOC, but not + * much else. Could allow it if the user explicitly told us to use DOS33, + * but it's a little thin. + * + * DISK112B.X (Ultima I player master) has a catalog that jumps around a lot. + * It's perfectly valid, but we don't really detect it properly. Forcing + * DOS interpretation should be acceptable. + * + * DISK175A (Standing Stones) has an extremely short but valid catalog track. + * + * DISK198B (Aliens+docs) gets 3 and bails with a self-reference. + */ +static DIError +TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder, int* pGoodCount) +{ + DIError dierr = kDIErrNone; + unsigned char sctBuf[kSctSize]; + int numTracks, numSectors; + int catTrack, catSect; + int foundGood = 0; + int iterations = 0; + + *pGoodCount = 0; + + dierr = pImg->ReadTrackSectorSwapped(kVTOCTrack, kVTOCSector, + sctBuf, imageOrder, DiskImg::kSectorOrderDOS); + if (dierr != kDIErrNone) + goto bail; + + catTrack = sctBuf[0x01]; + catSect = sctBuf[0x02]; + numTracks = sctBuf[0x34]; + numSectors = sctBuf[0x35]; + + if (!(sctBuf[0x27] == kMaxTSPairs) || + /*!(sctBuf[0x36] == 0 && sctBuf[0x37] == 1) ||*/ // bytes per sect + !(numTracks <= DiskFSDOS33::kMaxTracks) || + !(numSectors == 13 || numSectors == 16 || numSectors == 32) || + !(catTrack < numTracks && catSect < numSectors) || + 0) + { + WMSG1(" DOS header test failed (order=%d)\n", imageOrder); + dierr = kDIErrFilesystemNotFound; + goto bail; + } + + foundGood++; // score one for a valid-looking VTOC + + /* + * Walk through the catalog track to try to figure out ordering. + */ + while (catTrack != 0 && catSect != 0 && + iterations < DiskFSDOS33::kMaxCatalogSectors) + { + dierr = pImg->ReadTrackSectorSwapped(catTrack, catSect, sctBuf, + imageOrder, DiskImg::kSectorOrderDOS); + if (dierr != kDIErrNone) { + dierr = kDIErrNone; + break; /* allow it if earlier stuff was okay */ + } + + if (catTrack == sctBuf[1] && catSect == sctBuf[2] +1) + foundGood++; + else if (catTrack == sctBuf[1] && catSect == sctBuf[2]) { + WMSG2(" DOS detected self-reference on cat (%d,%d)\n", + catTrack, catSect); + break; + } + catTrack = sctBuf[1]; + catSect = sctBuf[2]; + iterations++; // watch for infinite loops + } + if (iterations >= DiskFSDOS33::kMaxCatalogSectors) { + /* possible cause: LF->CR conversion screws up link to sector $0a */ + dierr = kDIErrDirectoryLoop; + WMSG1(" DOS directory links cause a loop (order=%d)\n", imageOrder); + goto bail; + } + + WMSG2(" DOS foundGood=%d order=%d\n", foundGood, imageOrder); + *pGoodCount = foundGood; + +bail: + return dierr; +} + +/* + * Test to see if the image is a DOS 3.2 or DOS 3.3 disk. + */ +/*static*/ DIError +DiskFSDOS33::TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency) +{ + if (pImg->GetNumTracks() > kMaxInterestingTracks) + return kDIErrFilesystemNotFound; + + DiskImg::SectorOrder ordering[DiskImg::kSectorOrderMax]; + + DiskImg::GetSectorOrderArray(ordering, *pOrder); + + DiskImg::SectorOrder bestOrder = DiskImg::kSectorOrderUnknown; + int bestCount = 0; + + for (int i = 0; i < DiskImg::kSectorOrderMax; i++) { + int goodCount = 0; + + if (ordering[i] == DiskImg::kSectorOrderUnknown) + continue; + if (TestImage(pImg, ordering[i], &goodCount) == kDIErrNone) { + if (goodCount > bestCount) { + bestCount = goodCount; + bestOrder = ordering[i]; + } + } + } + + if (bestCount >= 4 || + (leniency == kLeniencyVery && bestCount >= 2)) + { + WMSG2(" DOS test: bestCount=%d for order=%d\n", bestCount, bestOrder); + assert(bestOrder != DiskImg::kSectorOrderUnknown); + *pOrder = bestOrder; + *pFormat = DiskImg::kFormatDOS33; + if (pImg->GetNumSectPerTrack() == 13) + *pFormat = DiskImg::kFormatDOS32; + return kDIErrNone; + } + + WMSG0(" DOS33 didn't find valid DOS3.2 or DOS3.3\n"); + return kDIErrFilesystemNotFound; +} + + +/* + * Get things rolling. + * + * Since we're assured that this is a valid disk, errors encountered from here + * on out must be handled somehow, possibly by claiming that the disk is + * completely full and has no files on it. + */ +DIError +DiskFSDOS33::Initialize(InitMode initMode) +{ + DIError dierr = kDIErrNone; + + fVolumeUsage.Create(fpImg->GetNumTracks(), fpImg->GetNumSectPerTrack()); + + dierr = ReadVTOC(); + if (dierr != kDIErrNone) + goto bail; + //DumpVTOC(); + + dierr = ScanVolBitmap(); + if (dierr != kDIErrNone) + goto bail; + + if (initMode == kInitHeaderOnly) { + WMSG0(" DOS - headerOnly set, skipping file load\n"); + goto bail; + } + + /* read the contents of the catalog, creating our A2File list */ + dierr = ReadCatalog(); + if (dierr != kDIErrNone) + goto bail; + + /* run through and get file lengths and data offsets */ + dierr = GetFileLengths(); + if (dierr != kDIErrNone) + goto bail; + + /* mark DOS tracks appropriately */ + FixVolumeUsageMap(); + + fDiskIsGood = CheckDiskIsGood(); + + fVolumeUsage.Dump(); + +// A2File* pFile; +// pFile = GetNextFile(nil); +// while (pFile != nil) { +// pFile->Dump(); +// pFile = GetNextFile(pFile); +// } + +bail: + return dierr; +} + +/* + * Read some fields from the disk Volume Table of Contents. + */ +DIError +DiskFSDOS33::ReadVTOC(void) +{ + DIError dierr; + + dierr = LoadVolBitmap(); + if (dierr != kDIErrNone) + goto bail; + + fFirstCatTrack = fVTOC[0x01]; + fFirstCatSector = fVTOC[0x02]; + fVTOCVolumeNumber = fVTOC[0x06]; + fVTOCNumTracks = fVTOC[0x34]; + fVTOCNumSectors = fVTOC[0x35]; + + if (fFirstCatTrack >= fpImg->GetNumTracks()) + return kDIErrBadDiskImage; + if (fFirstCatSector >= fpImg->GetNumSectPerTrack()) + return kDIErrBadDiskImage; + + if (fVTOCNumTracks != fpImg->GetNumTracks()) { + WMSG2(" DOS33 warning: VTOC numtracks %d vs %ld\n", + fVTOCNumTracks, fpImg->GetNumTracks()); + } + if (fVTOCNumSectors != fpImg->GetNumSectPerTrack()) { + WMSG2(" DOS33 warning: VTOC numsect %d vs %d\n", + fVTOCNumSectors, fpImg->GetNumSectPerTrack()); + } + + // call SetDiskVolumeNum with the appropriate thing + UpdateVolumeNum(); + +bail: + FreeVolBitmap(); + return dierr; +} + +/* + * Call this if fpImg's volume num (derived from nibble formats) or + * the VTOC's volume number changes. + */ +void +DiskFSDOS33::UpdateVolumeNum(void) +{ + /* use the sector-embedded volume number, if available */ + if (fpImg->GetDOSVolumeNum() == DiskImg::kVolumeNumNotSet) + SetDiskVolumeNum(fVTOCVolumeNumber); + else + SetDiskVolumeNum(fpImg->GetDOSVolumeNum()); + if (fDiskVolumeNum != fVTOCVolumeNumber) { + WMSG2(" NOTE: ignoring VTOC vol (%d) in favor of embedded (%d)\n", + fVTOCVolumeNumber, fDiskVolumeNum); + } +} + +/* + * Set the disk volume number (fDiskVolumeNum) and derived fields. + */ +void +DiskFSDOS33::SetDiskVolumeNum(int val) +{ + if (val < 0 || val > 255) { + // Actual valid range should be 1-254, but it's possible for a + // sector edit to put invalid stuff here. It's just one byte + // though, so 0-255 should be guaranteed. + assert(false); + return; + } + fDiskVolumeNum = val; + sprintf(fDiskVolumeName, "DOS%03d", fDiskVolumeNum); + if (fpImg->GetFSFormat() == DiskImg::kFormatDOS32) + sprintf(fDiskVolumeID, "DOS 3.2 Volume %03d", fDiskVolumeNum); + else + sprintf(fDiskVolumeID, "DOS 3.3 Volume %03d", fDiskVolumeNum); +} + + +/* + * Dump some VTOC fields. + */ +void +DiskFSDOS33::DumpVTOC(void) +{ + + WMSG2("VTOC catalog: track=%d sector=%d\n", + fFirstCatTrack, fFirstCatSector); + WMSG3(" volnum=%d numTracks=%d numSects=%d\n", + fVTOCVolumeNumber, fVTOCNumTracks, fVTOCNumSectors); +} + +/* + * Update an entry in the VolumeUsage map, watching for conflicts. + */ +void +DiskFSDOS33::SetSectorUsage(long track, long sector, + VolumeUsage::ChunkPurpose purpose) +{ + VolumeUsage::ChunkState cstate; + + //WMSG3(" DOS setting usage %d,%d to %d\n", track, sector, purpose); + + fVolumeUsage.GetChunkState(track, sector, &cstate); + if (cstate.isUsed) { + cstate.purpose = VolumeUsage::kChunkPurposeConflict; +// WMSG2(" DOS conflicting uses for t=%d s=%d\n", track, sector); + } else { + cstate.isUsed = true; + cstate.purpose = purpose; + } + fVolumeUsage.SetChunkState(track, sector, &cstate); +} + +/* + * Examine the volume bitmap, setting fields in the VolumeUsage map + * as appropriate. We mark "isMarkedUsed", but leave "isUsed" clear. The + * "isUsed" flag gets set by the DOS catalog track processor and the file + * scanners. + * + * We can't mark the DOS tracks, because there's no reliable way to tell by + * looking at a DOS disk whether it has a bootable DOS image. It's possible + * the tracks are marked in-use because files are stored there. Some + * tweaked versions of DOS freed up a few sectors on track 2, so partial + * allocation isn't a good indicator. + * + * What we have to do is wait until we have all the information for the + * various files, and mark the tracks as owned by DOS if nobody else + * claims them. + */ +DIError +DiskFSDOS33::ScanVolBitmap(void) +{ + DIError dierr; + VolumeUsage::ChunkState cstate; + char freemap[32+1] = "--------------------------------"; + + cstate.isUsed = false; + cstate.isMarkedUsed = true; + cstate.purpose = (VolumeUsage::ChunkPurpose) 0; + + dierr = LoadVolBitmap(); + if (dierr != kDIErrNone) + goto bail; + + WMSG0(" map 0123456789abcdef\n"); + + int i; + for (i = 0; i < kMaxTracks; i++) { + unsigned long val, origVal; + int bit; + + val = (unsigned long) fVTOC[0x38 + i*4] << 24; + val |= (unsigned long) fVTOC[0x39 + i*4] << 16; + val |= (unsigned long) fVTOC[0x3a + i*4] << 8; + val |= (unsigned long) fVTOC[0x3b + i*4]; + origVal = val; + + /* init the VolumeUsage stuff */ + for (bit = fpImg->GetNumSectPerTrack()-1; bit >= 0; bit--) { + freemap[bit] = val & 0x80000000 ? '.' : 'X'; + + if (i < fpImg->GetNumTracks() && !(val & 0x80000000)) { + /* mark the sector as in-use */ + if (fVolumeUsage.SetChunkState(i, bit, &cstate) != kDIErrNone) { + assert(false); + } + } + val <<= 1; + } + WMSG3(" %2d: %s (0x%08lx)\n", i, freemap, origVal); + } + + /* we know the VTOC is used, so mark it now */ + SetSectorUsage(kVTOCTrack, kVTOCSector, VolumeUsage::kChunkPurposeVolumeDir); + +bail: + FreeVolBitmap(); + return dierr; +} + + +/* + * Load the VTOC into the buffer. + */ +DIError +DiskFSDOS33::LoadVolBitmap(void) +{ + DIError dierr; + + assert(!fVTOCLoaded); + + dierr = fpImg->ReadTrackSector(kVTOCTrack, kVTOCSector, fVTOC); + if (dierr != kDIErrNone) + return dierr; + + fVTOCLoaded = true; + return kDIErrNone; +} + +/* + * Save our copy of the volume bitmap. + */ +DIError +DiskFSDOS33::SaveVolBitmap(void) +{ + if (!fVTOCLoaded) { + assert(false); + return kDIErrNotReady; + } + + return fpImg->WriteTrackSector(kVTOCTrack, kVTOCSector, fVTOC); +} + +/* + * Throw away the volume bitmap, discarding any unsaved changes. + * + * It's okay to call this if the bitmap isn't loaded. + */ +void +DiskFSDOS33::FreeVolBitmap(void) +{ + fVTOCLoaded = false; + +#ifdef _DEBUG + memset(fVTOC, 0x99, sizeof(fVTOC)); +#endif +} + +/* + * Return entry N from the VTOC. + */ +inline unsigned long +DiskFSDOS33::GetVTOCEntry(const unsigned char* pVTOC, long track) const +{ + unsigned long val; + val = (unsigned long) pVTOC[0x38 + track*4] << 24; + val |= (unsigned long) pVTOC[0x39 + track*4] << 16; + val |= (unsigned long) pVTOC[0x3a + track*4] << 8; + val |= (unsigned long) pVTOC[0x3b + track*4]; + + return val; +} + +/* + * Allocate a new sector from the unused pool. + * + * Only touches the in-memory copy. + */ +DIError +DiskFSDOS33::AllocSector(TrackSector* pTS) +{ + unsigned long val; + unsigned long mask; + long track, numSectPerTrack; + + /* we could compute "mask", but it's faster and easier to do this */ + numSectPerTrack = GetDiskImg()->GetNumSectPerTrack(); + if (numSectPerTrack == 13) + mask = 0xfff80000; + else if (numSectPerTrack == 16) + mask = 0xffff0000; + else if (numSectPerTrack == 32) + mask = 0xffffffff; + else { + assert(false); + return kDIErrInternal; + } + + /* + * Start by finding a track with a free sector. We know it's free + * because the bits aren't all zero. + * + * In theory we don't need "mask", because the DOS format routine is + * good about leaving the unused bits clear, and nobody else disturbs + * them. However, it's best not to rely on it. + */ + for (track = kVTOCTrack; track > 0; track--) { + val = GetVTOCEntry(fVTOC, track); + if ((val & mask) != 0) + break; + } + if (track == 0) { + long numTracks = GetDiskImg()->GetNumTracks(); + for (track = kVTOCTrack; track < numTracks; track++) + { + val = GetVTOCEntry(fVTOC, track); + if ((val & mask) != 0) + break; + } + if (track == numTracks) { + WMSG0("DOS33 AllocSector unable to find empty sector\n"); + return kDIErrDiskFull; + } + } + + /* + * We've got the track. Now find the first free sector. + */ + int sector; + sector = numSectPerTrack-1; + while (sector >= 0) { + if (val & 0x80000000) { + //WMSG2("+++ allocating T=%d S=%d\n", track, sector); + SetSectorUseEntry(track, sector, true); + break; + } + + val <<= 1; + sector--; + } + if (sector < 0) { + assert(false); + return kDIErrInternal; // should not have failed + } + + /* + * Mostly for fun, update the VTOC allocation thingy. + */ + fVTOC[0x30] = (unsigned char) track; // last track where alloc happened + if (track < kVTOCTrack) + fVTOC[0x31] = 0xff; // descending + else + fVTOC[0x31] = 0x01; // ascending + + pTS->track = (char) track; + pTS->sector = (char) sector; + + return kDIErrNone; +} + +/* + * Create an in-use map for an empty disk. Sets up the VTOC map only. + * + * If "withDOS" is set, mark the first 3 tracks as in-use. + */ +DIError +DiskFSDOS33::CreateEmptyBlockMap(bool withDOS) +{ + DIError dierr; + long track, sector, maxTrack; + + dierr = LoadVolBitmap(); + if (dierr != kDIErrNone) + return dierr; + + if (withDOS) + maxTrack = 3; + else + maxTrack = 1; + + /* + * Set each bit individually. Slower, but exercises standard functions. + * + * Clear all "in use" flags, except for track 0, track 17, and (if + * withDOS is set) tracks 1 and 2. + */ + for (track = fpImg->GetNumTracks()-1; track >= 0; track--) { + for (sector = fpImg->GetNumSectPerTrack()-1; sector >= 0; sector--) { + if (track < maxTrack || track == kVTOCTrack) + SetSectorUseEntry(track, sector, true); + else + SetSectorUseEntry(track, sector, false); + } + } + + dierr = SaveVolBitmap(); + FreeVolBitmap(); + if (dierr != kDIErrNone) + return dierr; + + return kDIErrNone; +} + +/* + * Get the state of an entry in the VTOC sector use map. + * + * Returns "true" if it's in use, "false" otherwise. + */ +bool +DiskFSDOS33::GetSectorUseEntry(long track, int sector) const +{ + assert(fVTOCLoaded); + assert(track >= 0 && track < fpImg->GetNumTracks()); + assert(sector >= 0 && sector < fpImg->GetNumSectPerTrack()); + + unsigned long val, mask; + + val = GetVTOCEntry(fVTOC, track); + //val = (unsigned long) fVTOC[0x38 + track*4] << 24; + //val |= (unsigned long) fVTOC[0x39 + track*4] << 16; + //val |= (unsigned long) fVTOC[0x3a + track*4] << 8; + //val |= (unsigned long) fVTOC[0x3b + track*4]; + + /* + * The highest-numbered sector is now in the high bit. If this is a + * 16-sector disk, the high bit holds the state of sector 15. + * + * A '1' indicates the sector is free, '0' indicates it's in use. + */ + mask = 1L << (32 - fpImg->GetNumSectPerTrack() + sector); + return (val & mask) == 0; +} + +/* + * Change the state of an entry in the VTOC sector use map. + */ +void +DiskFSDOS33::SetSectorUseEntry(long track, int sector, bool inUse) +{ + assert(fVTOCLoaded); + assert(track >= 0 && track < fpImg->GetNumTracks()); + assert(sector >= 0 && sector < fpImg->GetNumSectPerTrack()); + + unsigned long val, mask; + + val = GetVTOCEntry(fVTOC, track); + + /* highest sector is always in the high bit */ + mask = 1L << (32 - fpImg->GetNumSectPerTrack() + sector); + if (inUse) + val &= ~mask; + else + val |= mask; + + fVTOC[0x38 + track*4] = (unsigned char) (val >> 24); + fVTOC[0x39 + track*4] = (unsigned char) (val >> 16); + fVTOC[0x3a + track*4] = (unsigned char) (val >> 8); + fVTOC[0x3b + track*4] = (unsigned char) val; +} + + +/* + * Get the amount of free space remaining. + */ +DIError +DiskFSDOS33::GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const +{ + DIError dierr; + long track, sector, freeSectors; + + dierr = const_cast(this)->LoadVolBitmap(); + if (dierr != kDIErrNone) + return dierr; + + freeSectors = 0; + for (track = GetDiskImg()->GetNumTracks()-1; track >= 0; track--) { + for (sector = GetDiskImg()->GetNumSectPerTrack()-1; sector >= 0; sector--) + { + if (!GetSectorUseEntry(track, sector)) + freeSectors++; + } + } + + *pTotalUnits = fpImg->GetNumTracks() * fpImg->GetNumSectPerTrack(); + *pFreeUnits = freeSectors; + *pUnitSize = kSectorSize; + + const_cast(this)->FreeVolBitmap(); + return kDIErrNone; +} + + +/* + * Fix up the DOS tracks. + * + * Any sectors marked used but not actually in use by a file are marked as + * in use by the system. We have to be somewhat careful here because some + * disks had DOS removed to add space, un-set the last few sectors of track 2 + * that weren't actually used by DOS, or did some other funky thing. + */ +void +DiskFSDOS33::FixVolumeUsageMap(void) +{ + VolumeUsage::ChunkState cstate; + int track, sector; + + for (track = 0; track < 3; track++) { + for (sector = 0; sector < fpImg->GetNumSectPerTrack(); sector++) { + fVolumeUsage.GetChunkState(track, sector, &cstate); + if (cstate.isMarkedUsed && !cstate.isUsed) { + cstate.isUsed = true; + cstate.purpose = VolumeUsage::kChunkPurposeSystem; + fVolumeUsage.SetChunkState(track, sector, &cstate); + } + } + } +} + + +/* + * Read the disk's catalog. + * + * NOTE: supposedly DOS stops reading the catalog track when it finds the + * first entry with a 00 byte, which is why deleted files use ff. If so, + * it *might* make sense to mimic this behavior, though on a health disk + * we shouldn't be finding garbage anyway. + * + * Fills out "fCatalogSectors" as it works. + */ +DIError +DiskFSDOS33::ReadCatalog(void) +{ + DIError dierr = kDIErrNone; + unsigned char sctBuf[kSctSize]; + int catTrack, catSect; + int iterations; + + catTrack = fFirstCatTrack; + catSect = fFirstCatSector; + iterations = 0; + + memset(fCatalogSectors, 0, sizeof(fCatalogSectors)); + + while (catTrack != 0 && catSect != 0 && iterations < kMaxCatalogSectors) + { + SetSectorUsage(catTrack, catSect, VolumeUsage::kChunkPurposeVolumeDir); + + WMSG2(" DOS33 reading catalog sector T=%d S=%d\n", catTrack, catSect); + dierr = fpImg->ReadTrackSector(catTrack, catSect, sctBuf); + if (dierr != kDIErrNone) + goto bail; + + /* + * Watch for flaws that the DOS detector allows. + */ + if (catTrack == sctBuf[0x01] && catSect == sctBuf[0x02]) { + WMSG2(" DOS detected self-reference on cat (%d,%d)\n", + catTrack, catSect); + break; + } + + /* + * Check the next track/sector in the chain. If the pointer is + * broken, there's a very good chance that this isn't really a + * catalog sector, so we want to bail out now. + */ + if (sctBuf[0x01] >= fpImg->GetNumTracks() || + sctBuf[0x02] >= fpImg->GetNumSectPerTrack()) + { + WMSG0(" DOS bailing out early on catalog read due to funky T/S\n"); + break; + } + + dierr = ProcessCatalogSector(catTrack, catSect, sctBuf); + if (dierr != kDIErrNone) + goto bail; + + fCatalogSectors[iterations].track = catTrack; + fCatalogSectors[iterations].sector = catSect; + + catTrack = sctBuf[0x01]; + catSect = sctBuf[0x02]; + + iterations++; // watch for infinite loops + + } + if (iterations >= kMaxCatalogSectors) { + dierr = kDIErrDirectoryLoop; + goto bail; + } + +bail: + return dierr; +} + +/* + * Process the list of files in one sector of the catalog. + * + * Pass in the track, sector, and the contents of that track and sector. + * (We only use "catTrack" and "catSect" to fill out some fields.) + */ +DIError +DiskFSDOS33::ProcessCatalogSector(int catTrack, int catSect, + const unsigned char* sctBuf) +{ + A2FileDOS* pFile; + const unsigned char* pEntry; + int i; + + pEntry = &sctBuf[kCatalogEntryOffset]; + + for (i = 0; i < kCatalogEntriesPerSect; i++) { + if (pEntry[0x00] != kEntryUnused && pEntry[0x00] != kEntryDeleted) { + pFile = new A2FileDOS(this); + + pFile->SetQuality(A2File::kQualityGood); + + pFile->fTSListTrack = pEntry[0x00]; + pFile->fTSListSector = pEntry[0x01]; + pFile->fLocked = (pEntry[0x02] & 0x80) != 0; + switch (pEntry[0x02] & 0x7f) { + case 0x00: pFile->fFileType = A2FileDOS::kTypeText; break; + case 0x01: pFile->fFileType = A2FileDOS::kTypeInteger; break; + case 0x02: pFile->fFileType = A2FileDOS::kTypeApplesoft; break; + case 0x04: pFile->fFileType = A2FileDOS::kTypeBinary; break; + case 0x08: pFile->fFileType = A2FileDOS::kTypeS; break; + case 0x10: pFile->fFileType = A2FileDOS::kTypeReloc; break; + case 0x20: pFile->fFileType = A2FileDOS::kTypeA; break; + case 0x40: pFile->fFileType = A2FileDOS::kTypeB; break; + default: + /* some odd arrangement of bit flags? */ + WMSG1(" DOS33 peculiar filetype byte 0x%02x\n", pEntry[0x02]); + pFile->fFileType = A2FileDOS::kTypeUnknown; + pFile->SetQuality(A2File::kQualitySuspicious); + break; + } + + memcpy(pFile->fFileName, &pEntry[0x03], A2FileDOS::kMaxFileName); + pFile->fFileName[A2FileDOS::kMaxFileName] = '\0'; + pFile->FixFilename(); + + pFile->fLengthInSectors = pEntry[0x21]; + pFile->fLengthInSectors |= (unsigned short) pEntry[0x22] << 8; + + pFile->fCatTS.track = catTrack; + pFile->fCatTS.sector = catSect; + pFile->fCatEntryNum = i; + + /* can't do these yet, so just set to defaults */ + pFile->fLength = 0; + pFile->fSparseLength = 0; + pFile->fDataOffset = 0; + + AddFileToList(pFile); + } + + pEntry += kCatalogEntrySize; + } + + return kDIErrNone; +} + + +/* + * Perform consistency checks on the filesystem. + * + * Returns "true" if disk appears to be perfect, "false" otherwise. + */ +bool +DiskFSDOS33::CheckDiskIsGood(void) +{ + DIError dierr; + const DiskImg* pDiskImg = GetDiskImg(); + bool result = true; + int i; + + dierr = LoadVolBitmap(); + if (dierr != kDIErrNone) + goto bail; + + /* + * Make sure the VTOC is marked in use, or things could go badly. + * Ditto for the catalog tracks. + */ + if (!GetSectorUseEntry(kVTOCTrack, kVTOCSector)) { + fpImg->AddNote(DiskImg::kNoteWarning, "VTOC sector marked as free."); + result = false; + } + for (i = 0; i < kMaxCatalogSectors; i++) { + if (!GetSectorUseEntry(fCatalogSectors[i].track, + fCatalogSectors[i].sector)) + { + fpImg->AddNote(DiskImg::kNoteWarning, + "Catalog sector %d,%d is marked as free.", + fCatalogSectors[i].track, fCatalogSectors[i].sector); + result = false; + } + } + + /* + * Check for used blocks that aren't marked in-use. + * + * This requires that VolumeUsage be accurate. Since this function is + * only run during initial startup, any later deviation between VU and + * the block use map is irrelevant. + */ + VolumeUsage::ChunkState cstate; + long track, sector; + long notMarked, extraUsed, conflicts; + notMarked = extraUsed = conflicts = 0; + for (track = 0; track < pDiskImg->GetNumTracks(); track++) { + for (sector = 0; sector < pDiskImg->GetNumSectPerTrack(); sector++) { + dierr = fVolumeUsage.GetChunkState(track, sector, &cstate); + if (dierr != kDIErrNone) { + fpImg->AddNote(DiskImg::kNoteWarning, + "Internal volume usage error on t=%ld s=%ld.", + track, sector); + result = false; + goto bail; + } + + if (cstate.isUsed && !cstate.isMarkedUsed) + notMarked++; + if (!cstate.isUsed && cstate.isMarkedUsed) + extraUsed++; + if (cstate.purpose == VolumeUsage::kChunkPurposeConflict) + conflicts++; + } + } + if (extraUsed > 0) { + fpImg->AddNote(DiskImg::kNoteInfo, + "%ld sector%s marked used but not part of any file.", + extraUsed, extraUsed == 1 ? " is" : "s are"); + // not a problem, really + } + if (notMarked > 0) { + fpImg->AddNote(DiskImg::kNoteWarning, + "%ld sector%s used by files but not marked used.", + notMarked, notMarked == 1 ? " is" : "s are"); + result = false; + } + if (conflicts > 0) { + fpImg->AddNote(DiskImg::kNoteWarning, + "%ld sector%s used by more than one file.", + conflicts, conflicts == 1 ? " is" : "s are"); + result = false; + } + + /* + * Scan for "damaged" files or "suspicious" files diagnosed earlier. + */ + bool damaged, suspicious; + ScanForDamagedFiles(&damaged, &suspicious); + + if (damaged) { + fpImg->AddNote(DiskImg::kNoteWarning, + "One or more files are damaged."); + result = false; + } else if (suspicious) { + fpImg->AddNote(DiskImg::kNoteWarning, + "One or more files look suspicious."); + result = false; + } + +bail: + FreeVolBitmap(); + return result; +} + + +/* + * Run through our list of files, computing the lengths and marking file + * usage in the VolumeUsage object. + */ +DIError +DiskFSDOS33::GetFileLengths(void) +{ + A2FileDOS* pFile; + TrackSector* tsList = nil; + TrackSector* indexList = nil; + int tsCount; + int indexCount; + + pFile = (A2FileDOS*) GetNextFile(nil); + while (pFile != nil) { + DIError dierr; + dierr = pFile->LoadTSList(&tsList, &tsCount, &indexList, &indexCount); + if (dierr != kDIErrNone) { + WMSG1("DOS failed loading TS list for '%s'\n", + pFile->GetPathName()); + pFile->SetQuality(A2File::kQualityDamaged); + } else { + MarkFileUsage(pFile, tsList, tsCount, indexList, indexCount); + dierr = ComputeLength(pFile, tsList, tsCount); + if (dierr != kDIErrNone) { + WMSG1("DOS unable to get length for '%s'\n", + pFile->GetPathName()); + pFile->SetQuality(A2File::kQualityDamaged); + } + } + + if (pFile->fLengthInSectors != indexCount + tsCount) { + WMSG3("DOS NOTE: file '%s' has len-in-sect=%d but actual=%d\n", + pFile->GetPathName(), pFile->fLengthInSectors, + indexCount + tsCount); + // expected on sparse random-access text files + } + + delete[] tsList; + delete[] indexList; + tsList = indexList = nil; + + pFile = (A2FileDOS*) GetNextFile(pFile); + } + + return kDIErrNone; +} + +/* + * Compute the length and starting data offset of the file. + * + * For Text, there are two situations: sequential and random. For + * sequential text files, we just need to find the first 00 byte. For + * random, there can be 00s everywhere, and in fact there can be holes + * in the T/S list. The plan: since DOS doesn't let you "truncate" a + * text file, just scan the last sector for 00. The length is the + * number of previous T/S entries * 256 plus the sector offset. + * --> This does the wrong thing for random-access text files, which + * need to retain their full length, and doesn't work right for sequential + * text files that (somehow) had their last block over-allocated. It does + * the right thing most of the time, but we either need to be more clever + * here or provide a way to override the default (bool fTrimTextFiles?). + * + * For Applesoft and Integer, the file length is stored as the first two + * bytes of the file. + * + * For Binary, the file length is stored in the second two bytes (after + * the two-byte address). Some files (with low-memory loaders) used a + * fake length, and DDD 2.x sets both address and length to zero. + * + * For Reloc, S, A2, B2, and "unknown", we just multiply the sector count. + * We get an accurate sector count from the T/S list (the value in the + * directory entry might have been tampered with). + * + * To handle DDD 2.x files correctly, we need to identify them as such by + * looking for 'B' with address=0 and length=0, a T/S count of at least 8 + * (the smallest possible compression of a 35-track disk is 2385 bytes), + * and a '<' in the filename. If found, we start from offset=0 + * (because DDD Pro 1.x includes the 4 leading bytes) and include all + * sectors, we'll get the actual file plus at most 256 garbage bytes. + * + * On success, we set the following: + * pFile->fLength + * pFile->fSparseLength + * pFile->fDataOffset + */ +DIError +DiskFSDOS33::ComputeLength(A2FileDOS* pFile, const TrackSector* tsList, + int tsCount) +{ + DIError dierr = kDIErrNone; + unsigned char sctBuf[kSctSize]; + + assert(pFile != nil); + assert(tsList != nil); + assert(tsCount >= 0); + + pFile->fDataOffset = 0; + + pFile->fAuxType = 0; + if (pFile->fFileType == A2FileDOS::kTypeApplesoft) + pFile->fAuxType = 0x0801; + /* for text files it's default record length; assume zero */ + + if (tsCount == 0) { + /* no data at all */ + pFile->fLength = 0; + } else if (pFile->fFileType == A2FileDOS::kTypeApplesoft || + pFile->fFileType == A2FileDOS::kTypeInteger || + pFile->fFileType == A2FileDOS::kTypeBinary) + { + /* read first sector and analyze it */ + //WMSG0(" DOS reading first file sector\n"); + dierr = fpImg->ReadTrackSector(tsList[0].track, tsList[0].sector, + sctBuf); + if (dierr != kDIErrNone) + goto bail; + + if (pFile->fFileType == A2FileDOS::kTypeBinary) { + pFile->fAuxType = + sctBuf[0x00] | (unsigned short) sctBuf[0x01] << 8; + pFile->fLength = + sctBuf[0x02] | (unsigned short) sctBuf[0x03] << 8; + pFile->fDataOffset = 4; // take the above into account + } else { + pFile->fLength = + sctBuf[0x00] | (unsigned short) sctBuf[0x01] << 8; + pFile->fDataOffset = 2; // take the above into account + } + + if (pFile->fFileType == A2FileDOS::kTypeBinary && + pFile->fLength == 0 && pFile->fAuxType == 0 && + tsCount >= 8 && + strchr(pFile->fFileName, '<') != nil && + strchr(pFile->fFileName, '>') != nil) + { + WMSG2(" DOS found probable DDD archive, tweaking '%s' (lis=%u)\n", + pFile->GetPathName(), pFile->fLengthInSectors); + //dierr = TrimLastSectorDown(pFile, tsBuf, WrapperDDD::kMaxDDDZeroCount); + //if (dierr != kDIErrNone) + // goto bail; + //WMSG3(" DOS scanned DDD file '%s' to length %ld (tsCount=%d)\n", + // pFile->fFileName, pFile->fLength, pFile->fTSListCount); + pFile->fLength = tsCount * kSctSize; + pFile->fDataOffset = 0; + } + + /* catch bogus lengths in damaged A/I/B files */ + if (pFile->fLength > tsCount * kSctSize) { + WMSG3(" DOS33 capping max len from %ld to %d in '%s'\n", + (long) pFile->fLength, tsCount * kSctSize, + pFile->fFileName); + pFile->fLength = tsCount * kSctSize - pFile->fDataOffset; + if (pFile->fLength < 0) // can't happen here? + pFile->fLength = 0; + + /* + * This could cause a problem, because if the user changes a 'T' + * file to 'B', the bogus file length will mark the file as + * "suspicious" and we won't allow writing to the disk (which + * makes it hard to switch the file type back). We really don't + * want to weaken this test though. + */ + pFile->SetQuality(A2File::kQualitySuspicious); + } + + } else if (pFile->fFileType == A2FileDOS::kTypeText) { + /* scan text file */ + pFile->fLength = tsCount * kSctSize; + dierr = TrimLastSectorUp(pFile, tsList[tsCount-1]); + if (dierr != kDIErrNone) + goto bail; + + WMSG4(" DOS scanned text file '%s' down to %d+%ld = %ld\n", + pFile->fFileName, + (tsCount-1) * kSctSize, + (long)pFile->fLength - (tsCount-1) * kSctSize, + (long)pFile->fLength); + + /* TO DO: something clever to discern random access record length? */ + } else { + /* S/R/A/B: just use the TS count */ + pFile->fLength = tsCount * kSctSize; + } + + /* + * Compute the sparse length for random-access text files. + */ + int i, sparseCount; + sparseCount = 0; + for (i = 0; i < tsCount; i++) { + if (tsList[i].track == 0 && tsList[i].sector == 0) + sparseCount++; + } + pFile->fSparseLength = pFile->fLength - sparseCount * kSctSize; + if (pFile->fSparseLength == -pFile->fDataOffset) { + /* + * This can happen for a completely sparse file. Looks sort of + * stupid to have a length of "-4", so force it to zero. + */ + pFile->fSparseLength = 0; + } + +bail: + return dierr; +} + +/* + * Trim the zeroes off the end of the last sector. We begin at the start + * of the sector and stop at the first zero found. + * + * Modifies pFile->fLength, which should be set to a roughly accurate + * value on entry. + * + * The caller should endeavor to strip out T=0 S=0 entries that come after + * the body of the file. They're valid in the middle for random-access + * text files. + */ +DIError +DiskFSDOS33::TrimLastSectorUp(A2FileDOS* pFile, TrackSector lastTS) +{ + DIError dierr; + unsigned char sctBuf[kSctSize]; + int i; + + if (lastTS.track == 0) { + /* happens on files with lots of "sparse" space at the end */ + return kDIErrNone; + } + + //WMSG0(" DOS reading LAST file sector\n"); + dierr = fpImg->ReadTrackSector(lastTS.track, lastTS.sector, sctBuf); + if (dierr != kDIErrNone) + goto bail; + + /* start with EOF equal to previous sectors */ + pFile->fLength -= kSctSize; + for (i = 0; i < kSctSize; i++) { + if (sctBuf[i] == 0x00) + break; + else + pFile->fLength++; + } + +bail: + return dierr; +} + +/* + * Given lists of tracks and sector for data and TS index sectors, set the + * entries in the volume usage map. + */ +void +DiskFSDOS33::MarkFileUsage(A2FileDOS* pFile, TrackSector* tsList, int tsCount, + TrackSector* indexList, int indexCount) +{ + int i; + + for (i = 0; i < tsCount; i++) { + /* mark all sectors as in-use by file */ + if (tsList[i].track == 0 && tsList[i].sector == 0) { + /* sparse sector in random-access text file */ + } else { + SetSectorUsage(tsList[i].track, tsList[i].sector, + VolumeUsage::kChunkPurposeUserData); + } + } + + for (i = 0; i < indexCount; i++) { + /* mark the T/S sectors as in-use by file structures */ + SetSectorUsage(indexList[i].track, indexList[i].sector, + VolumeUsage::kChunkPurposeFileStruct); + } +} + + + +#if 0 +/* + * Trim the zeroes off the end of the last sector. We begin at the end + * of the sector and back up. + * + * It is possible (one out of between 128 and 256 times) that we have just + * the trailing zero in this sector, and we need to back up to the previous + * sector to find the actual end. We know a file can end with three zeroes + * and we suspect it might be possible to end with four, which means we could + * have between 0 and 3 zeroes in the previous sector, and between 1 and 4 + * in this sector. If we just tack on three more zeroes, we weaken our + * length test slightly, because we must allow a "slop" of up to seven bytes. + * It's a little more work, but scanning the next-to-last sector is probably + * worthwhile given the otherwise flaky nature of DDD storage. + */ +DIError +DiskFSDOS33::TrimLastSectorDown(A2FileDOS* pFile, unsigned short* tsBuf, + int maxZeroCount) +{ + DIError dierr; + unsigned char sctBuf[kSctSize]; + int i; + + //WMSG0(" DOS reading LAST file sector\n"); + dierr = fpImg->ReadTrackSector( + pFile->TSTrack(tsBuf[pFile->fTSListCount-1]), + pFile->TSSector(tsBuf[pFile->fTSListCount-1]), + sctBuf); + if (dierr != kDIErrNone) + goto bail; + + /* find the first trailing zero by finding the last non-zero */ + for (i = kSctSize-1; i >= 0; i--) { + if (sctBuf[i] != 0x00) + break; + } + if (i < 0) { + /* sector was nothing but zeroes */ + DebugBreak(); + } else { + /* peg it at 256; if it went over that, DDD would've added a sector */ + i += maxZeroCount; + if (i > kSctSize) + i = kSctSize; + pFile->fLength = (pFile->fTSListCount-1) * kSctSize + i; + } + +bail: + return dierr; +} +#endif + + +/* + * Convert high ASCII to low ASCII. + * + * Some people put inverse and flashing text into filenames, not to mention + * control characters, so we have to cope with those too. + * + * We modify the first "len" bytes of "buf" in place. + */ +/*static*/ void +DiskFSDOS33::LowerASCII(unsigned char* buf, long len) +{ + while (len--) { + if (*buf & 0x80) { + if (*buf >= 0xa0) + *buf &= 0x7f; + else + *buf = (*buf & 0x7f) + 0x20; + } else + *buf = ((*buf & 0x3f) ^ 0x20) + 0x20; + + buf++; + } +} + + +/* + * Determine whether or not "name" is a valid DOS 3.3 filename. + * + * Names can be up to 30 characters and can contain absolutely anything. + * To make life easier on DOS users, we ban the use of the comma, block + * control characters and high ASCII, and don't allow completely blank + * names. Later on we will later convert to upper case, so we allow lower + * case letters here. + * + * Filenames simply pad out to 30 characters with spaces, so the only + * "invalid" character is a trailing space. Because we're using C-style + * strings, we implicitly ban the use of '\0' in the name. + */ +/*static*/ bool +DiskFSDOS33::IsValidFileName(const char* name) +{ + bool nonSpace = false; + int len = 0; + + /* count letters, skipping control chars */ + while (*name != '\0') { + char ch = *name++; + + if (ch < 0x20 || ch >= 0x7f || ch == ',') + return false; + if (ch != 0x20) + nonSpace = true; + len++; + } + if (len == 0 || len > A2FileDOS::kMaxFileName) + return false; // can't be empty, can't be huge + if (!nonSpace) + return false; // must have one non-ctrl non-space char + if (*(name-1) == ' ') + return false; // no trailing spaces + + return true; +} + +/* + * Determine whether "name" is a valid volume number. + */ +/*static*/ bool +DiskFSDOS33::IsValidVolumeName(const char* name) +{ + long val; + char* endp; + + val = strtol(name, &endp, 10); + if (*endp != '\0' || val < 1 || val > 254) + return false; + + return true; +} + + +/* + * Put a DOS 3.2/3.3 filesystem image on the specified DiskImg. + * + * If "volName" is "DOS", a basic DOS image will be written to the first three + * tracks of the disk, and the in-use map will be updated appropriately. + * + * It would seem at first glance that putting the volume number into the + * volume name string would make the interface more consistent with the + * rest of the filesystems. The first glance is substantially correct, but + * the DOS stuff has a separate "set volume number" interface already, used + * to deal with the various locations where volume numbers can be stored + * (2MG header, VTOC, sector address headers) in the various formats. + * + * So, instead of stuffing the volume number into "volName" and creating + * some other path for specifying "add DOS image", I continue to use the + * defined ways of setting the volume number and abuse "volName" slightly. + */ +DIError +DiskFSDOS33::Format(DiskImg* pDiskImg, const char* volName) +{ + DIError dierr = kDIErrNone; + unsigned char sctBuf[256]; + bool addDOS = false; + + if (pDiskImg->GetNumTracks() < kMinTracks || + pDiskImg->GetNumTracks() > kMaxTracks) + { + WMSG1(" DOS33 can't format numTracks=%ld\n", pDiskImg->GetNumTracks()); + return kDIErrInvalidArg; + } + if (pDiskImg->GetNumSectPerTrack() != 13 && + pDiskImg->GetNumSectPerTrack() != 16 && + pDiskImg->GetNumSectPerTrack() != 32) + { + WMSG1(" DOS33 can't format sectors=%d\n", + pDiskImg->GetNumSectPerTrack()); + return kDIErrInvalidArg; + } + + if (volName != nil && strcmp(volName, "DOS") == 0) { + if (pDiskImg->GetNumSectPerTrack() != 16 && + pDiskImg->GetNumSectPerTrack() != 13) + { + WMSG1("NOTE: numSectPerTrack = %d, can't write DOS tracks\n", + pDiskImg->GetNumSectPerTrack()); + return kDIErrInvalidArg; + } + addDOS = true; + } + + /* set fpImg so calls that rely on it will work; we un-set it later */ + assert(fpImg == nil); + SetDiskImg(pDiskImg); + + WMSG1(" DOS33 formatting disk image (sectorOrder=%d)\n", + fpImg->GetSectorOrder()); + + /* write DOS sectors */ + dierr = fpImg->OverrideFormat(fpImg->GetPhysicalFormat(), + DiskImg::kFormatGenericDOSOrd, fpImg->GetSectorOrder()); + if (dierr != kDIErrNone) + goto bail; + + /* + * We should now zero out the disk blocks, but on a 32MB volume that can + * take a little while. The blocks are zeroed for us when a disk is + * created, so this is really only needed if we're re-formatting an + * existing disk. CiderPress currently doesn't do that, so we're going + * to skip it here. + */ +// dierr = fpImg->ZeroImage(); + WMSG0(" DOS33 (not zeroing blocks)\n"); + + if (addDOS) { + dierr = WriteDOSTracks(pDiskImg->GetNumSectPerTrack()); + if (dierr != kDIErrNone) + goto bail; + } + + /* + * Set up the static fields in the VTOC. + */ + dierr = LoadVolBitmap(); + if (dierr != kDIErrNone) + goto bail; + fVTOC[0x00] = 0x04; // (no reason) + fVTOC[0x01] = kVTOCTrack; // first cat track + fVTOC[0x02] = fpImg->GetNumSectPerTrack()-1; // first cat sector + fVTOC[0x03] = 3; // version + if (fpImg->GetDOSVolumeNum() == DiskImg::kVolumeNumNotSet) + fVTOC[0x06] = kDefaultVolumeNum; // VTOC volume number + else + fVTOC[0x06] = (unsigned char) fpImg->GetDOSVolumeNum(); + fVTOC[0x27] = 122; // max T/S pairs + fVTOC[0x30] = kVTOCTrack+1; // last alloc + fVTOC[0x31] = 1; // ascending + fVTOC[0x34] = (unsigned char)fpImg->GetNumTracks(); // #of tracks + fVTOC[0x35] = fpImg->GetNumSectPerTrack(); // #of sectors + fVTOC[0x36] = 0x00; // bytes/sector (lo) + fVTOC[0x37] = 0x01; // bytes/sector (hi) + if (pDiskImg->GetNumSectPerTrack() == 13) { + // minor changes for DOS 3.2 + fVTOC[0x00] = 0x02; + fVTOC[0x03] = 2; + } + + dierr = SaveVolBitmap(); + FreeVolBitmap(); + if (dierr != kDIErrNone) + goto bail; + + /* + * Fill the sectors in the catalog track. + */ + int sect; + memset(sctBuf, 0, sizeof(sctBuf)); + sctBuf[0x01] = kVTOCTrack; + for (sect = fpImg->GetNumSectPerTrack()-1; sect > 1; sect--) { + sctBuf[0x02] = sect-1; + + dierr = fpImg->WriteTrackSector(kVTOCTrack, sect, sctBuf); + if (dierr != kDIErrNone) + goto bail; + } + + /* + * Generate the initial block usage map. The only entries in use are + * right at the start of the disk. + */ + CreateEmptyBlockMap(addDOS); + + /* check our work, and set some object fields, by reading what we wrote */ + dierr = ReadVTOC(); + if (dierr != kDIErrNone) { + WMSG1(" GLITCH: couldn't read header we just wrote (err=%d)\n", dierr); + goto bail; + } + + /* don't do this -- assume they're going to call Initialize() later */ + //ScanVolBitmap(); + +bail: + SetDiskImg(nil); // shouldn't really be set by us + return dierr; +} + +/* + * Write a DOS image into tracks 0-2. + * + * This takes the number of sectors per track as an argument so we can figure + * out which version of DOS to write. This probably ought to be an enum so + * we can specify various versions of DOS. + */ +DIError +DiskFSDOS33::WriteDOSTracks(int sectPerTrack) +{ + DIError dierr = kDIErrNone; + long track, sector; + const unsigned char* buf = gDOS33Tracks; + + if (sectPerTrack == 13) { + WMSG0(" DOS33 writing DOS 3.3 tracks\n"); + buf = gDOS32Tracks; + + for (track = 0; track < 3; track++) { + for (sector = 0; sector < 13; sector++) { + dierr = fpImg->WriteTrackSector(track, sector, buf); + if (dierr != kDIErrNone) + goto bail; + buf += kSctSize; + } + } + } else if (sectPerTrack == 16) { + WMSG0(" DOS33 writing DOS 3.3 tracks\n"); + buf = gDOS33Tracks; + + // this should be used for 32-sector disks + + for (track = 0; track < 3; track++) { + for (sector = 0; sector < 16; sector++) { + dierr = fpImg->WriteTrackSector(track, sector, buf); + if (dierr != kDIErrNone) + goto bail; + buf += kSctSize; + } + } + } else { + WMSG1(" DOS33 *not* writing DOS tracks to %d-sector disk\n", + sectPerTrack); + assert(false); + } + +bail: + return dierr; +} + +/* + * Normalize a DOS 3.3 path. Used when adding files from DiskArchive. + * The path may contain subdirectory components, which we need to strip away. + * + * "*pNormalizedBufLen" is used to pass in the length of the buffer and + * pass out the length of the string (should the buffer prove inadequate). + */ +DIError +DiskFSDOS33::NormalizePath(const char* path, char fssep, + char* normalizedBuf, int* pNormalizedBufLen) +{ + DIError dierr = kDIErrNone; + char tmpBuf[A2FileDOS::kMaxFileName+1]; + int len; + + DoNormalizePath(path, fssep, tmpBuf); + len = strlen(tmpBuf)+1; + + if (*pNormalizedBufLen < len) + dierr = kDIErrDataOverrun; + else + strcpy(normalizedBuf, tmpBuf); + *pNormalizedBufLen = len; + + return dierr; +} + +/* + * Normalize a DOS 3.3 pathname. Lower case becomes upper case, control + * characters and high ASCII get stripped, and ',' becomes '_'. + * + * "outBuf" must be able to hold kMaxFileName+1 characters. + */ +void +DiskFSDOS33::DoNormalizePath(const char* name, char fssep, char* outBuf) +{ + char* outp = outBuf; + char* cp; + + /* throw out leading pathname, if any */ + if (fssep != '\0') { + cp = strrchr(name, fssep); + if (cp != nil) + name = cp+1; + } + + while (*name != '\0' && (outp - outBuf) <= A2FileDOS::kMaxFileName) { + if (*name >= 0x20 && *name < 0x7f) { + if (*name == ',') + *outp = '_'; + else + *outp = toupper(*name); + + outp++; + } + name++; + } + *outp = '\0'; + + if (*outBuf == '\0') { + /* nothing left */ + strcpy(outBuf, "BLANK"); + } +} + +/* + * Create a file on a DOS 3.2/3.3 disk. + * + * The file will be created with an empty T/S list. + * + * It is not possible to set the aux type here. Aux types only apply to 'B' + * files, and since they're stored in the first data sector (which we don't + * create), there's nowhere to put it. We stuff it into the aux type value + * in the linear file list, on the assumption that somebody will come along + * and politely Write to the file, even if it's zero bytes long. + * + * (Technically speaking, setting the file type here is bogus, because a + * 'B' file with no data sectors is invalid. However, we don't want to + * handle arbitrary changes later -- switching from 'T' to 'B' requires + * either rewriting the entire file, or confusing the user by changing the + * type without adjusting the first 4 bytes -- so we set it now. It's also + * helpful to set it now because the Write routine needs to know how many + * bytes offset from the start of the file it needs to be. We could avoid + * most of this weirdness by just going ahead and allocating the first + * sector of the file now, and modifying the Write() function to understand + * that the first block is already there. Need to do that someday.) + */ +DIError +DiskFSDOS33::CreateFile(const CreateParms* pParms, A2File** ppNewFile) +{ + DIError dierr = kDIErrNone; + const bool createUnique = (GetParameter(kParm_CreateUnique) != 0); + char normalName[A2FileDOS::kMaxFileName+1]; +// char storageName[A2FileDOS::kMaxFileName+1]; + A2FileDOS::FileType fileType; + A2FileDOS* pNewFile = nil; + + if (fpImg->GetReadOnly()) + return kDIErrAccessDenied; + if (!fDiskIsGood) + return kDIErrBadDiskImage; + + assert(pParms != nil); + assert(pParms->pathName != nil); + assert(pParms->storageType == A2FileProDOS::kStorageSeedling); + WMSG1(" DOS33 ---v--- CreateFile '%s'\n", pParms->pathName); + + *ppNewFile = nil; + + DoNormalizePath(pParms->pathName, pParms->fssep, normalName); + + /* + * See if the file already exists. + * + * If "create unique" is set, we append digits until the name doesn't + * match any others. The name will be modified in place. + */ + if (createUnique) { + MakeFileNameUnique(normalName); + } else { + if (GetFileByName(normalName) != nil) { + WMSG1(" DOS33 create: normalized name '%s' already exists\n", + normalName); + dierr = kDIErrFileExists; + goto bail; + } + } + + fileType = A2FileDOS::ConvertFileType(pParms->fileType, 0); + + /* + * Allocate a directory entry and T/S list. + */ + unsigned char sctBuf[kSctSize]; + TrackSector catSect; + TrackSector tsSect; + int catEntry; + A2FileDOS* pPrevEntry; + + dierr = LoadVolBitmap(); + if (dierr != kDIErrNone) + goto bail; + + /* allocate a sector for the T/S list, and zero it out */ + dierr = AllocSector(&tsSect); + if (dierr != kDIErrNone) + goto bail; + + memset(sctBuf, 0, kSctSize); + dierr = fpImg->WriteTrackSector(tsSect.track, tsSect.sector, sctBuf); + if (dierr != kDIErrNone) + goto bail; + + /* + * Find the first free catalog entry. Also returns a pointer to the + * previous entry. + */ + dierr = GetFreeCatalogEntry(&catSect, &catEntry, sctBuf, &pPrevEntry); + if (dierr != kDIErrNone) { + WMSG0("DOS unable to find an empty entry in the catalog\n"); + goto bail; + } + WMSG4(" DOS found free catalog entry T=%d S=%d ent=%d prev=0x%08lx\n", + catSect.track, catSect.sector, catEntry, (long) pPrevEntry); + + /* create the new dir entry at the specified location */ + CreateDirEntry(sctBuf, catEntry, normalName, &tsSect, + (unsigned char) fileType, pParms->access); + + /* + * Flush everything to disk. + */ + dierr = fpImg->WriteTrackSector(catSect.track, catSect.sector, sctBuf); + if (dierr != kDIErrNone) + goto bail; + + dierr = SaveVolBitmap(); + if (dierr != kDIErrNone) + goto bail; + + /* + * Create a new entry for our file list. + */ + pNewFile = new A2FileDOS(this); + if (pNewFile == nil) { + dierr = kDIErrMalloc; + goto bail; + } + pNewFile->fTSListTrack = tsSect.track; + pNewFile->fTSListSector = tsSect.sector; + pNewFile->fLengthInSectors = 1; + pNewFile->fLocked = false; + strcpy(pNewFile->fFileName, normalName); + pNewFile->fFileType = fileType; + + pNewFile->fCatTS.track = catSect.track; + pNewFile->fCatTS.sector = catSect.sector; + pNewFile->fCatEntryNum = catEntry; + + pNewFile->fAuxType = (unsigned short) pParms->auxType; + pNewFile->fDataOffset = 0; + switch (pNewFile->fFileType) { + case A2FileDOS::kTypeInteger: + pNewFile->fDataOffset = 2; + break; + case A2FileDOS::kTypeApplesoft: + pNewFile->fDataOffset = 2; + pNewFile->fAuxType = 0x0801; + break; + case A2FileDOS::kTypeBinary: + pNewFile->fDataOffset = 4; + break; + default: + break; + } + pNewFile->fLength = 0; + pNewFile->fSparseLength = 0; + + /* + * Insert it in the proper place, so that the order of the files matches + * the order of entries in the catalog. + */ + InsertFileInList(pNewFile, pPrevEntry); + + *ppNewFile = pNewFile; + pNewFile = nil; + +bail: + delete pNewFile; + FreeVolBitmap(); + return dierr; +} + +/* + * Make the name pointed to by "fileName" unique. The name should already + * be FS-normalized, and be in a buffer that can hold at least kMaxFileName+1 + * bytes. + * + * (This is nearly identical to the code in the ProDOS implementation. I'd + * like to make it a general DiskFS function, but making the loop condition + * work requires setting up callbacks, which isn't hard here but is a little + * annoying in ProDOS because of the subdir buffer. So it's cut & paste + * for now.) + * + * Returns an error on failure, which should be impossible. + */ +DIError +DiskFSDOS33::MakeFileNameUnique(char* fileName) +{ + assert(fileName != nil); + assert(strlen(fileName) <= A2FileDOS::kMaxFileName); + + if (GetFileByName(fileName) == nil) + return kDIErrNone; + + WMSG1(" DOS found duplicate of '%s', making unique\n", fileName); + + int nameLen = strlen(fileName); + int dotOffset=0, dotLen=0; + char dotBuf[kMaxExtensionLen+1]; + + /* ensure the result will be null-terminated */ + memset(fileName + nameLen, 0, (A2FileDOS::kMaxFileName - nameLen) +1); + + /* + * If this has what looks like a filename extension, grab it. We want + * to preserve ".gif", ".c", etc. + */ + const char* cp = strrchr(fileName, '.'); + if (cp != nil) { + int tmpOffset = cp - fileName; + if (tmpOffset > 0 && nameLen - tmpOffset <= kMaxExtensionLen) { + WMSG1(" DOS (keeping extension '%s')\n", cp); + assert(strlen(cp) <= kMaxExtensionLen); + strcpy(dotBuf, cp); + dotOffset = tmpOffset; + dotLen = nameLen - dotOffset; + } + } + + const int kMaxDigits = 999; + int digits = 0; + int digitLen; + int copyOffset; + char digitBuf[4]; + do { + if (digits == kMaxDigits) + return kDIErrFileExists; + digits++; + + /* not the most efficient way to do this, but it'll do */ + sprintf(digitBuf, "%d", digits); + digitLen = strlen(digitBuf); + if (nameLen + digitLen > A2FileDOS::kMaxFileName) + copyOffset = A2FileDOS::kMaxFileName - dotLen - digitLen; + else + copyOffset = nameLen - dotLen; + memcpy(fileName + copyOffset, digitBuf, digitLen); + if (dotLen != 0) + memcpy(fileName + copyOffset + digitLen, dotBuf, dotLen); + } while (GetFileByName(fileName) != nil); + + WMSG1(" DOS converted to unique name: %s\n", fileName); + + return kDIErrNone; +} + +/* + * Find the first free entry in the catalog. + * + * Also returns an A2File pointer for the previous entry in the catalog. + * + * The contents of the catalog sector will be in "sctBuf". + */ +DIError +DiskFSDOS33::GetFreeCatalogEntry(TrackSector* pCatSect, int* pCatEntry, + unsigned char* sctBuf, A2FileDOS** ppPrevEntry) +{ + DIError dierr = kDIErrNone; + unsigned char* pEntry; + int sct, ent; + bool found = false; + + for (sct = 0; sct < kMaxCatalogSectors; sct++) { + if (fCatalogSectors[sct].track == 0 && + fCatalogSectors[sct].sector == 0) + { + /* end of list reached */ + WMSG0("DOS catalog is full\n"); + dierr = kDIErrVolumeDirFull; + goto bail; + } + dierr = fpImg->ReadTrackSector(fCatalogSectors[sct].track, + fCatalogSectors[sct].sector, sctBuf); + if (dierr != kDIErrNone) + goto bail; + + pEntry = &sctBuf[kCatalogEntryOffset]; + for (ent = 0; ent < kCatalogEntriesPerSect; ent++) { + if (pEntry[0x00] == 0x00 || pEntry[0x00] == kEntryDeleted) { + /* winner! */ + *pCatSect = fCatalogSectors[sct]; + *pCatEntry = ent; + found = true; + break; + } + + pEntry += kCatalogEntrySize; + } + + if (found) + break; + } + + if (sct == kMaxCatalogSectors) { + /* didn't find anything, assume the disk is full */ + dierr = kDIErrVolumeDirFull; + // fall through to "bail" + } else { + /* figure out what the previous entry is */ + TrackSector prevTS; + int prevEntry; + + if (*pCatEntry != 0) { + prevTS = *pCatSect; + prevEntry = *pCatEntry -1; + } else if (sct != 0) { + prevTS = fCatalogSectors[sct-1]; + prevEntry = kCatalogEntriesPerSect-1; + } else { + /* disk was empty; there's no previous entry */ + prevTS.track = 0; + prevTS.sector = 0; + prevEntry = -1; + } + + /* now find it in the linear file list */ + *ppPrevEntry = nil; + if (prevEntry >= 0) { + A2FileDOS* pFile = (A2FileDOS*) GetNextFile(nil); + while (pFile != nil) { + if (pFile->fCatTS.track == prevTS.track && + pFile->fCatTS.sector == prevTS.sector && + pFile->fCatEntryNum == prevEntry) + { + *ppPrevEntry = pFile; + break; + } + pFile = (A2FileDOS*) GetNextFile(pFile); + } + assert(*ppPrevEntry != nil); + } + } + +bail: + return dierr; +} + +/* + * Fill out the catalog entry in the location specified. + */ +void +DiskFSDOS33::CreateDirEntry(unsigned char* sctBuf, int catEntry, + const char* fileName, TrackSector* pTSSect, unsigned char fileType, + int access) +{ + char highName[A2FileDOS::kMaxFileName+1]; + unsigned char* pEntry; + + pEntry = GetCatalogEntryPtr(sctBuf, catEntry); + if (pEntry[0x00] != 0x00 && pEntry[0x00] != kEntryDeleted) { + /* somebody screwed up */ + assert(false); + return; + } + + A2FileDOS::MakeDOSName(highName, fileName); + + pEntry[0x00] = pTSSect->track; + pEntry[0x01] = pTSSect->sector; + pEntry[0x02] = fileType; + if ((access & A2FileProDOS::kAccessWrite) == 0) + pEntry[0x02] |= (unsigned char) A2FileDOS::kTypeLocked; + memcpy(&pEntry[0x03], highName, A2FileDOS::kMaxFileName); + PutShortLE(&pEntry[0x21], 1); // assume file is 1 sector long +} + +/* + * Delete a file. + * + * This entails freeing up the allocated sectors and changing a byte in + * the directory entry. We then remove it from the DiskFS file list. + */ +DIError +DiskFSDOS33::DeleteFile(A2File* pGenericFile) +{ + DIError dierr = kDIErrNone; + A2FileDOS* pFile = (A2FileDOS*) pGenericFile; + TrackSector* tsList = nil; + TrackSector* indexList = nil; + int tsCount, indexCount; + unsigned char sctBuf[kSctSize]; + unsigned char* pEntry; + + if (pGenericFile == nil) { + assert(false); + return kDIErrInvalidArg; + } + + if (fpImg->GetReadOnly()) + return kDIErrAccessDenied; + if (!fDiskIsGood) + return kDIErrBadDiskImage; + if (pGenericFile->IsFileOpen()) + return kDIErrFileOpen; + + WMSG1(" Deleting '%s'\n", pFile->GetPathName()); + + /* + * Update the block usage map. Nothing is permanent until we flush + * the data to disk. + */ + dierr = LoadVolBitmap(); + if (dierr != kDIErrNone) + goto bail; + + dierr = pFile->LoadTSList(&tsList, &tsCount, &indexList, &indexCount); + if (dierr != kDIErrNone) { + WMSG1("Failed loading TS lists while deleting '%s'\n", + pFile->GetPathName()); + goto bail; + } + + FreeTrackSectors(tsList, tsCount); + FreeTrackSectors(indexList, indexCount); + + /* + * Mark the entry as deleted. + */ + dierr = fpImg->ReadTrackSector(pFile->fCatTS.track, pFile->fCatTS.sector, + sctBuf); + if (dierr != kDIErrNone) + goto bail; + pEntry = GetCatalogEntryPtr(sctBuf, pFile->fCatEntryNum); + assert(pEntry[0x00] != 0x00 && pEntry[0x00] != kEntryDeleted); + pEntry[0x00] = kEntryDeleted; + dierr = fpImg->WriteTrackSector(pFile->fCatTS.track, pFile->fCatTS.sector, + sctBuf); + if (dierr != kDIErrNone) + goto bail; + + /* + * Save our updated copy of the volume bitmap to disk. + */ + dierr = SaveVolBitmap(); + if (dierr != kDIErrNone) + goto bail; + + /* + * Remove the A2File* from the list. + */ + DeleteFileFromList(pFile); + +bail: + FreeVolBitmap(); + delete[] tsList; + delete[] indexList; + return dierr; +} + +/* + * Mark all of the track/sector entries in "pList" as free. + */ +void +DiskFSDOS33::FreeTrackSectors(TrackSector* pList, int count) +{ + VolumeUsage::ChunkState cstate; + int i; + + cstate.isUsed = false; + cstate.isMarkedUsed = false; + cstate.purpose = VolumeUsage::kChunkPurposeUnknown; + + for (i = 0; i < count; i++) { + if (pList[i].track == 0 && pList[i].sector == 0) + continue; // sparse file + + if (!GetSectorUseEntry(pList[i].track, pList[i].sector)) { + WMSG2("WARNING: freeing unallocated sector T=%d S=%d\n", + pList[i].track, pList[i].sector); + assert(false); // impossible unless disk is "damaged" + } + SetSectorUseEntry(pList[i].track, pList[i].sector, false); + + fVolumeUsage.SetChunkState(pList[i].track, pList[i].sector, &cstate); + } +} + +/* + * Rename a file. + * + * "newName" must already be normalized. + */ +DIError +DiskFSDOS33::RenameFile(A2File* pGenericFile, const char* newName) +{ + DIError dierr = kDIErrNone; + A2FileDOS* pFile = (A2FileDOS*) pGenericFile; + char normalName[A2FileDOS::kMaxFileName+1]; + char dosName[A2FileDOS::kMaxFileName+1]; + unsigned char sctBuf[kSctSize]; + unsigned char* pEntry; + + if (pFile == nil || newName == nil) + return kDIErrInvalidArg; + if (!IsValidFileName(newName)) + return kDIErrInvalidArg; + if (fpImg->GetReadOnly()) + return kDIErrAccessDenied; + if (!fDiskIsGood) + return kDIErrBadDiskImage; + + WMSG2(" DOS renaming '%s' to '%s'\n", pFile->GetPathName(), newName); + + /* + * Update the disk catalog entry. + */ + dierr = fpImg->ReadTrackSector(pFile->fCatTS.track, pFile->fCatTS.sector, + sctBuf); + if (dierr != kDIErrNone) + goto bail; + + pEntry = GetCatalogEntryPtr(sctBuf, pFile->fCatEntryNum); + + DoNormalizePath(newName, '\0', normalName); + A2FileDOS::MakeDOSName(dosName, normalName); + memcpy(&pEntry[0x03], dosName, A2FileDOS::kMaxFileName); + + dierr = fpImg->WriteTrackSector(pFile->fCatTS.track, pFile->fCatTS.sector, + sctBuf); + if (dierr != kDIErrNone) + goto bail; + + /* + * Update our internal copy. + */ + char storedName[A2FileDOS::kMaxFileName+1]; + strcpy(storedName, dosName); + LowerASCII((unsigned char*)storedName, A2FileDOS::kMaxFileName); + A2FileDOS::TrimTrailingSpaces(storedName); + + strcpy(pFile->fFileName, storedName); + +bail: + return dierr; +} + +/* + * Set the file's attributes. + * + * We allow the file to be locked or unlocked, and we allow the file type + * to be changed. We don't try to rewrite the file if they're changing to or + * from a format with embedded data (e.g. BAS or BIN); instead, we just + * change the type letter. We do need to re-evaluate the end-of-file + * value afterward. + * + * Changing the aux type is only allowed for BIN files. + */ +DIError +DiskFSDOS33::SetFileInfo(A2File* pGenericFile, long fileType, long auxType, + long accessFlags) +{ + DIError dierr = kDIErrNone; + A2FileDOS* pFile = (A2FileDOS*) pGenericFile; + TrackSector* tsList = nil; + int tsCount; + bool nowLocked; + bool typeChanged; + + if (pFile == nil) + return kDIErrInvalidArg; + if (fpImg->GetReadOnly()) + return kDIErrAccessDenied; + + WMSG4("DOS SetFileInfo '%s' type=0x%02lx aux=0x%04lx access=0x%02lx\n", + pFile->GetPathName(), fileType, auxType, accessFlags); + + /* + * We can ignore the file/aux type, or we can verify that they're not + * trying to change it. The latter is a little more work but makes + * the API a little more communicative. + */ + if (!A2FileDOS::IsValidType(fileType)) { + WMSG0("DOS SetFileInfo invalid file type\n"); + dierr = kDIErrInvalidArg; + goto bail; + } + if (auxType != pFile->GetAuxType() && fileType != 0x06) { + /* this only makes sense for BIN files */ + WMSG0("DOS SetFileInfo aux type mismatch; ignoring\n"); + //dierr = kDIErrNotSupported; + //goto bail; + } + + nowLocked = (accessFlags & A2FileProDOS::kAccessWrite) == 0; + typeChanged = (fileType != pFile->GetFileType()); + + /* + * Update the file type and locked status, if necessary. + */ + if (nowLocked != pFile->fLocked || typeChanged) { + A2FileDOS::FileType newFileType; + unsigned char sctBuf[kSctSize]; + unsigned char* pEntry; + + WMSG1("Updating file '%s'\n", pFile->GetPathName()); + + dierr = fpImg->ReadTrackSector(pFile->fCatTS.track, pFile->fCatTS.sector, + sctBuf); + if (dierr != kDIErrNone) + goto bail; + + pEntry = GetCatalogEntryPtr(sctBuf, pFile->fCatEntryNum); + + newFileType = A2FileDOS::ConvertFileType(fileType, 0); + pEntry[0x02] = (unsigned char) newFileType; + if (nowLocked) + pEntry[0x02] |= 0x80; + + dierr = fpImg->WriteTrackSector(pFile->fCatTS.track, pFile->fCatTS.sector, + sctBuf); + if (dierr != kDIErrNone) + goto bail; + + /* update our local copy */ + pFile->fLocked = nowLocked; + } + + if (!typeChanged && auxType == pFile->GetAuxType()) { + /* only the locked status has changed; skip the rest */ + goto bail; + } + + /* + * If the file has type BIN (either because it was before and we left it + * alone, or we changed it to BIN), we need to figure out what the aux + * type should be. There are two situations: + * + * (1) User specified an aux type. If the aux type passed in doesn't match + * what's in the A2FileDOS structure, we assume they meant to change it. + * (2) User didn't specify an aux type change. If the file was BIN before, + * we don't need to do anything, but if it was just changed to BIN then + * we need to extract the aux type from the first sector of the file. + * + * There's also a 3rd situation: they changed the aux type for a non-BIN + * file. This should have been blocked earlier. + * + * On top of all this, if we changed the file type at all then we need to + * re-scan the file length and "data offset" value. + */ + unsigned short newAuxType; + newAuxType = (unsigned short) auxType; + + dierr = pFile->LoadTSList(&tsList, &tsCount); + if (dierr != kDIErrNone) { + WMSG1(" DOS SFI: unable to load TS list (err=%d)\n", dierr); + goto bail; + } + + if (fileType == 0x06 && tsCount > 0) { + unsigned char sctBuf[kSctSize]; + + dierr = fpImg->ReadTrackSector(tsList[0].track, + tsList[0].sector, sctBuf); + if (dierr != kDIErrNone) { + WMSG0("DOS SFI: unable to get first sector of file\n"); + goto bail; + } + + if (auxType == pFile->GetAuxType()) { + newAuxType = GetShortLE(&sctBuf[0x00]); + WMSG1(" Aux type not changed, extracting from file (0x%04x)\n", + newAuxType); + } else { + WMSG1(" Aux type changed (to 0x%04x), changing file\n", + newAuxType); + + PutShortLE(&sctBuf[0x00], newAuxType); + dierr = fpImg->WriteTrackSector(tsList[0].track, + tsList[0].sector, sctBuf); + if (dierr != kDIErrNone) { + WMSG0("DOS SFI: unable to write first sector of file\n"); + goto bail; + } + } + } else { + /* not BIN or file has no sectors */ + if (pFile->fFileType == A2FileDOS::kTypeApplesoft) + newAuxType = 0x0801; + else + newAuxType = 0x0000; + } + + /* update our local copy */ + pFile->fFileType = A2FileDOS::ConvertFileType(fileType, 0); + pFile->fAuxType = newAuxType; + + /* + * Recalculate the file's length and "data offset". This may also mark + * the file as "suspicious". We wouldn't be here if the file was + * suspicious when we opened the disk image -- the image would have + * been marked read-only -- so if it's suspicious now, it's probably + * from a previous file type change attempt in the current session. + * Clear the flag so it doesn't "stick". + */ + pFile->ResetQuality(); + (void) ComputeLength(pFile, tsList, tsCount); + +bail: + delete[] tsList; + return dierr; +} + +/* + * Change the disk volume name (number). + * + * We can't change the 2MG header, and we can't change the values embedded + * in the sector headers, so all we do is change the VTOC entry. + */ +DIError +DiskFSDOS33::RenameVolume(const char* newName) +{ + DIError dierr = kDIErrNone; + unsigned char sctBuf[kSctSize]; + long newNumber; + char* endp; + + if (!IsValidVolumeName(newName)) + return kDIErrInvalidArg; + if (fpImg->GetReadOnly()) + return kDIErrAccessDenied; + + // convert the number; we already ascertained that it's valid + newNumber = strtol(newName, &endp, 10); + + dierr = fpImg->ReadTrackSector(kVTOCTrack, kVTOCSector, sctBuf); + if (dierr != kDIErrNone) + goto bail; + + sctBuf[0x06] = (unsigned char) newNumber; + + dierr = fpImg->WriteTrackSector(kVTOCTrack, kVTOCSector, sctBuf); + if (dierr != kDIErrNone) + goto bail; + + fVTOCVolumeNumber = newNumber; + UpdateVolumeNum(); + +bail: + return dierr; +} + + +/* + * =========================================================================== + * A2FileDOS + * =========================================================================== + */ + +/* + * Constructor. + */ +A2FileDOS::A2FileDOS(DiskFS* pDiskFS) : A2File(pDiskFS) +{ + fTSListTrack = -1; + fTSListSector = -1; + fLengthInSectors = 0; + fLocked = false; + fFileName[0] = '\0'; + fFileType = kTypeUnknown; + + fCatTS.track = fCatTS.sector = 0; + fCatEntryNum = -1; + + fAuxType = 0; + fDataOffset = 0; + fLength = -1; + fSparseLength = -1; + + fpOpenFile = nil; +} + +/* + * Destructor. Make sure an "open" file gets "closed". + */ +A2FileDOS::~A2FileDOS(void) +{ + delete fpOpenFile; +} + + +/* + * Convert the filetype enum to a ProDOS type. + * + * Remember that the DOS filetype field is actually a bit field, so we need + * to handle situations where more than one bit is set. + * + * Ideally this is a reversible transformation, so files copied to ProDOS + * volumes can be copied back to DOS with no loss of information. The reverse + * is *not* true, because of file type reduction and the potential loss of + * accurate file length info. + * + * I'm not entirely certain about the conversion of 'R' to REL, largely + * because I can't find any information on the REL format. However, Copy ][+ + * does convert to REL, and the Binary ][ standard says I should as well. + */ +long +A2FileDOS::GetFileType(void) const +{ + long retval; + + switch (fFileType) { + case kTypeText: retval = 0x04; break; // TXT + case kTypeInteger: retval = 0xfa; break; // INT + case kTypeApplesoft: retval = 0xfc; break; // BAS + case kTypeBinary: retval = 0x06; break; // BIN + case kTypeS: retval = 0xf2; break; // $f2 + case kTypeReloc: retval = 0xfe; break; // REL + case kTypeA: retval = 0xf3; break; // $f3 + case kTypeB: retval = 0xf4; break; // $f4 + case kTypeUnknown: + default: + retval = 0x00; // NON + break; + } + + return retval; +} + +/* + * Convert a ProDOS 8 file type to its DOS equivalent. + * + * We need to know the file length because files over 64K can't fit into + * DOS A/I/B files. Text files can be as long as they want, and the + * other types don't have a length word defined, so they're fine. + * + * We can't just convert them later, because by that point they've already + * got a 2-byte or 4-byte header reserved. + * + * Because we don't generally know the eventual length of the file at + * the time we're creating it, this doesn't work nearly as well as could + * be hoped. We can make life a little less confusing for the caller by + * using type 'S' for any unknown type. + */ +/*static*/ A2FileDOS::FileType +A2FileDOS::ConvertFileType(long prodosType, di_off_t fileLen) +{ + const long kMaxBinary = 65535; + FileType newType; + + switch (prodosType) { + case 0xb0: newType = kTypeText; break; // SRC + case 0x04: newType = kTypeText; break; // TXT + case 0xfa: newType = kTypeInteger; break; // INT + case 0xfc: newType = kTypeApplesoft; break; // BAS + case 0x06: newType = kTypeBinary; break; // BIN + case 0xf2: newType = kTypeS; break; // $f2 + case 0xfe: newType = kTypeReloc; break; // REL + case 0xf3: newType = kTypeA; break; // $f3 + case 0xf4: newType = kTypeB; break; // $f4 + default: newType = kTypeS; break; + } + + if (fileLen > kMaxBinary && + (newType == kTypeInteger || newType == kTypeApplesoft || + newType == kTypeBinary)) + { + WMSG0(" DOS setting type for large A/I/B file to S\n"); + newType = kTypeS; + } + + return newType; +} + +/* + * Determine whether the specified type has a valid DOS mapping. + */ +/*static*/ bool +A2FileDOS::IsValidType(long prodosType) +{ + switch (prodosType) { + case 0xb0: // SRC + case 0x04: // TXT + case 0xfa: // INT + case 0xfc: // BAS + case 0x06: // BIN + case 0xf2: // $f2 + case 0xfe: // REL + case 0xf3: // $f3 + case 0xf4: // $f4 + return true; + default: + return false; + } +} + +/* + * Match the ProDOS equivalents of "locked" and "unlocked". + */ +long +A2FileDOS::GetAccess(void) const +{ + if (fLocked) + return DiskFS::kFileAccessLocked; // 0x01 read + else + return DiskFS::kFileAccessUnlocked; // 0xc3 read/write/rename/destroy +} + +/* + * "Fix" a DOS3.3 filename. Convert DOS-ASCII to normal ASCII, and strip + * trailing spaces. + */ +void +A2FileDOS::FixFilename(void) +{ + DiskFSDOS33::LowerASCII((unsigned char*)fFileName, kMaxFileName); + TrimTrailingSpaces(fFileName); +} + +/* + * Trim the spaces off the end of a filename. + * + * Assumes the filename has already been converted to low ASCII. + */ +/*static*/ void +A2FileDOS::TrimTrailingSpaces(char* filename) +{ + char* lastspc = filename + strlen(filename); + + assert(*lastspc == '\0'); + + while (--lastspc) { + if (*lastspc != ' ') + break; + } + + *(lastspc+1) = '\0'; +} + +/* + * Encode a filename into high ASCII, padded out with spaces to + * kMaxFileName chars. Lower case is converted to upper case. This + * does not filter out control characters or other chunk. + * + * "buf" must be able to hold kMaxFileName+1 chars. + */ +/*static*/ void +A2FileDOS::MakeDOSName(char* buf, const char* name) +{ + for (int i = 0; i < kMaxFileName; i++) { + if (*name == '\0') + *buf++ = (char) 0xa0; + else + *buf++ = toupper(*name++) | 0x80; + } + *buf = '\0'; +} + + +/* + * Set up state for this file. + */ +DIError +A2FileDOS::Open(A2FileDescr** ppOpenFile, bool readOnly, + bool rsrcFork /*=false*/) +{ + DIError dierr = kDIErrNone; + A2FDDOS* pOpenFile = nil; + + if (!readOnly) { + if (fpDiskFS->GetDiskImg()->GetReadOnly()) + return kDIErrAccessDenied; + if (fpDiskFS->GetFSDamaged()) + return kDIErrBadDiskImage; + } + + if (fpOpenFile != nil) { + dierr = kDIErrAlreadyOpen; + goto bail; + } + + if (rsrcFork) + return kDIErrForkNotFound; + + pOpenFile = new A2FDDOS(this); + + dierr = LoadTSList(&pOpenFile->fTSList, &pOpenFile->fTSCount, + &pOpenFile->fIndexList, &pOpenFile->fIndexCount); + if (dierr != kDIErrNone) { + WMSG1("DOS33 unable to load TS for '%s' open\n", GetPathName()); + goto bail; + } + + pOpenFile->fOffset = 0; + pOpenFile->fOpenEOF = fLength; + pOpenFile->fOpenSectorsUsed = fLengthInSectors; + + fpOpenFile = pOpenFile; // add it to our single-member "open file set" + *ppOpenFile = pOpenFile; + pOpenFile = nil; + +bail: + delete pOpenFile; + return dierr; +} + +/* + * Dump the contents of an A2FileDOS. + */ +void +A2FileDOS::Dump(void) const +{ + WMSG1("A2FileDOS '%s'\n", fFileName); + WMSG2(" TS T=%-2d S=%-2d\n", fTSListTrack, fTSListSector); + WMSG2(" Cat T=%-2d S=%-2d\n", fCatTS.track, fCatTS.sector); + WMSG3(" type=%d lck=%d slen=%d\n", fFileType, fLocked, fLengthInSectors); + WMSG2(" auxtype=0x%04x length=%ld\n", + fAuxType, (long) fLength); +} + + +/* + * Load the T/S list for this file. + * + * A single T/S sector holds 122 entries, enough to store a 30.5K file. + * It's very unlikely that a file will need more than two, although it's + * possible for a random-access text file to have a very large number of + * entries. + * + * If "pIndexList" and "pIndexCount" are non-nil, the list of index blocks is + * also loaded. + * + * It's entirely possible to get a large T/S list back that is filled + * entirely with zeroes. This can happen if we have a large set of T/S + * index sectors that are all zero. We have to leave space for them so + * that the Write function can use the existing allocated index blocks. + * + * THOUGHT: we may want to use the file type to tighten this up a bit. + * For example, we're currently very careful around random-access text + * files, but if the file doesn't have type 'T' then random access is + * impossible. Currently this isn't a problem, but for e.g. T/S lists + * with garbage at the end would could deal with the problem more generally. + */ +DIError +A2FileDOS::LoadTSList(TrackSector** pTSList, int* pTSCount, + TrackSector** pIndexList, int* pIndexCount) +{ + DIError dierr = kDIErrNone; + DiskImg* pDiskImg; + const int kDefaultTSAlloc = 2; + const int kDefaultIndexAlloc = 8; + TrackSector* tsList = nil; + TrackSector* indexList = nil; + int tsCount, tsAlloc; + int indexCount, indexAlloc; + unsigned char sctBuf[kSctSize]; + int track, sector, iterations; + + WMSG1("--- DOS loading T/S list for '%s'\n", GetPathName()); + + /* over-alloc for small files to reduce reallocs */ + tsAlloc = kMaxTSPairs * kDefaultTSAlloc; + tsList = new TrackSector[tsAlloc]; + tsCount = 0; + + indexAlloc = kDefaultIndexAlloc; + indexList = new TrackSector[indexAlloc]; + indexCount = 0; + + if (tsList == nil || indexList == nil) { + dierr = kDIErrMalloc; + goto bail; + } + + assert(fpDiskFS != nil); + pDiskImg = fpDiskFS->GetDiskImg(); + assert(pDiskImg != nil); + + /* get the first T/S sector for this file */ + track = fTSListTrack; + sector = fTSListSector; + if (track >= pDiskImg->GetNumTracks() || + sector >= pDiskImg->GetNumSectPerTrack()) + { + WMSG3(" DOS33 invalid initial T/S %d,%d in '%s'\n", track, sector, + fFileName); + dierr = kDIErrBadFile; + goto bail; + } + + /* + * Run through the set of t/s pairs. + */ + iterations = 0; + do { + unsigned short sectorOffset; + int lastNonZero; + + /* + * Add the current T/S sector to the index list. + */ + if (indexCount == indexAlloc) { + WMSG0("+++ expanding index list\n"); + TrackSector* newList; + indexAlloc += kDefaultIndexAlloc; + newList = new TrackSector[indexAlloc]; + if (newList == nil) { + dierr = kDIErrMalloc; + goto bail; + } + memcpy(newList, indexList, indexCount * sizeof(TrackSector)); + delete[] indexList; + indexList = newList; + } + indexList[indexCount].track = track; + indexList[indexCount].sector = sector; + indexCount++; + + + //WMSG2("+++ scanning T/S at T=%d S=%d\n", track, sector); + dierr = pDiskImg->ReadTrackSector(track, sector, sctBuf); + if (dierr != kDIErrNone) + goto bail; + + /* grab next track/sector */ + track = sctBuf[0x01]; + sector = sctBuf[0x02]; + sectorOffset = GetShortLE(&sctBuf[0x05]); + + /* if T/S link is bogus, whole sector is probably bad */ + if (track >= pDiskImg->GetNumTracks() || + sector >= pDiskImg->GetNumSectPerTrack()) + { + // bogus T/S, mark file as damaged and stop + WMSG3(" DOS33 invalid T/S link %d,%d in '%s'\n", track, sector, + GetPathName()); + dierr = kDIErrBadFile; + goto bail; + } + if ((sectorOffset % kMaxTSPairs) != 0) { + WMSG2(" DOS33 invalid T/S header sector offset %u in '%s'\n", + sectorOffset, GetPathName()); + // not fatal, just weird + } + + /* + * Make sure we have enough room to hold an entire sector full of + * T/S pairs in the list. + */ + if (tsCount + kMaxTSPairs > tsAlloc) { + WMSG0("+++ expanding ts list\n"); + TrackSector* newList; + tsAlloc += kMaxTSPairs * kDefaultTSAlloc; + newList = new TrackSector[tsAlloc]; + if (newList == nil) { + dierr = kDIErrMalloc; + goto bail; + } + memcpy(newList, tsList, tsCount * sizeof(TrackSector)); + delete[] tsList; + tsList = newList; + } + + /* + * Add the entries. If there's another T/S list linked, we just + * grab the entire sector. If not, we grab every entry until the + * last 0,0. (Can't stop at the first (0,0), or we'll drop a + * piece of a random access text file.) + */ + dierr = ExtractTSPairs(sctBuf, &tsList[tsCount], &lastNonZero); + if (dierr != kDIErrNone) + goto bail; + + if (track != 0 && sector != 0) { + /* more T/S lists to come, so we keep all entries */ + tsCount += kMaxTSPairs; + } else { + /* this was the last one */ + if (lastNonZero == -1) { + /* this is ALWAYS the case for a newly-created file */ + //WMSG1(" DOS33 odd -- last T/S sector of '%s' was empty\n", + // GetPathName()); + } + tsCount += lastNonZero +1; + } + + iterations++; // watch for infinite loops + } while (!(track == 0 && sector == 0) && iterations < kMaxTSIterations); + + if (iterations == kMaxTSIterations) { + dierr = kDIErrFileLoop; + goto bail; + } + + *pTSList = tsList; + *pTSCount = tsCount; + tsList = nil; + + if (pIndexList != nil) { + *pIndexList = indexList; + *pIndexCount = indexCount; + indexList = nil; + } + +bail: + delete[] tsList; + delete[] indexList; + return dierr; +} + +/* + * Extract the track/sector pairs from the TS list in "sctBuf". The entries + * are copied to "tsList", which is assumed to have enough space to hold + * at least kMaxTSPairs entries. + * + * The last non-zero entry will be identified and stored in "*pLastNonZero". + * If all entries are zero, it will be set to -1. + * + * Sometimes files will have junk at the tail end of an otherwise valid + * T/S list. We can't just stop when we hit the first (0,0) entry because + * that'll screw up random-access text file handling. What we can do is + * try to detect the situation, and mark the file as "suspicious" without + * returning an error if we see it. + * + * If a TS entry appears to be invalid, this returns an error after all + * entries have been copied. If it looks to be partially valid, only the + * valid parts are copied out, with the rest zeroed. + */ +DIError +A2FileDOS::ExtractTSPairs(const unsigned char* sctBuf, TrackSector* tsList, + int* pLastNonZero) +{ + DIError dierr = kDIErrNone; + const DiskImg* pDiskImg = fpDiskFS->GetDiskImg(); + const unsigned char* ptr; + int i, track, sector; + + *pLastNonZero = -1; + memset(tsList, 0, sizeof(TrackSector) * kMaxTSPairs); + + ptr = &sctBuf[kTSOffset]; // offset of first T/S entry (0x0c) + + for (i = 0; i < kMaxTSPairs; i++) { + track = *ptr++; + sector = *ptr++; + + if (dierr == kDIErrNone && + (track >= pDiskImg->GetNumTracks() || + sector >= pDiskImg->GetNumSectPerTrack() || + (track == 0 && sector != 0))) + { + WMSG3(" DOS33 invalid T/S %d,%d in '%s'\n", track, sector, + fFileName); + + if (i > 0 && tsList[i-1].track == 0 && tsList[i-1].sector == 0) { + WMSG0(" T/S list looks partially valid\n"); + SetQuality(kQualitySuspicious); + goto bail; // quit immediately + } else { + dierr = kDIErrBadFile; + // keep going, just so caller has the full set to stare at + } + } + + if (track != 0 || sector != 0) + *pLastNonZero = i; + + tsList[i].track = track; + tsList[i].sector = sector; + } + +bail: + return dierr; +} + + +/* + * =========================================================================== + * A2FDDOS + * =========================================================================== + */ + +/* + * Read data from the current offset. + * + * Files read back as they would from ProDOS, i.e. if you read a binary + * file you won't see the 4 bytes of length and address. + */ +DIError +A2FDDOS::Read(void* buf, size_t len, size_t* pActual) +{ + WMSG3(" DOS reading %d bytes from '%s' (offset=%ld)\n", + len, fpFile->GetPathName(), (long) fOffset); + + A2FileDOS* pFile = (A2FileDOS*) fpFile; + + /* + * Don't allow them to read past the end of the file. The length value + * stored in pFile->fLength already has pFile->fDataOffset subtracted + * from the actual data length, so don't factor it in again. + */ + if (fOffset + (long)len > fOpenEOF) { + if (pActual == nil) + return kDIErrDataUnderrun; + len = (size_t) (fOpenEOF - fOffset); + } + if (pActual != nil) + *pActual = len; + long incrLen = len; + + DIError dierr = kDIErrNone; + unsigned char sctBuf[kSctSize]; + di_off_t actualOffset = fOffset + pFile->fDataOffset; // adjust for embedded len + int tsIndex = (int) (actualOffset / kSctSize); + int bufOffset = (int) (actualOffset % kSctSize); // (& 0xff) + size_t thisCount; + + if (len == 0) + return kDIErrNone; + assert(fOpenEOF != 0); + + assert(tsIndex >= 0 && tsIndex < fTSCount); + + /* could be more clever in here and avoid double-buffering */ + while (len) { + if (tsIndex >= fTSCount) { + /* should've caught this earlier */ + assert(false); + WMSG1(" DOS ran off the end (fTSCount=%d)\n", fTSCount); + return kDIErrDataUnderrun; + } + + if (fTSList[tsIndex].track == 0 && fTSList[tsIndex].sector == 0) { + //WMSG2(" DOS sparse sector T=%d S=%d\n", + // TSTrack(fTSList[tsIndex]), TSSector(fTSList[tsIndex])); + memset(sctBuf, 0, sizeof(sctBuf)); + } else { + dierr = pFile->GetDiskFS()->GetDiskImg()->ReadTrackSector( + fTSList[tsIndex].track, + fTSList[tsIndex].sector, + sctBuf); + if (dierr != kDIErrNone) { + WMSG1(" DOS error reading file '%s'\n", pFile->GetPathName()); + return dierr; + } + } + thisCount = kSctSize - bufOffset; + if (thisCount > len) + thisCount = len; + memcpy(buf, sctBuf + bufOffset, thisCount); + len -= thisCount; + buf = (char*)buf + thisCount; + + bufOffset = 0; + tsIndex++; + } + + fOffset += incrLen; + + return dierr; +} + +/* + * Write data at the current offset. + * + * For simplicity, we assume that we're writing a brand-new file in one + * shot. As it happens, that's all we're currently required to do, so even + * if we wrote a more sophisticated function it wouldn't get exercised. + * Because of the way we write, there's no way to mimic the behavior of + * random-access text file allocation, so that isn't supported. + * + * The data in "buf" should *not* include the 2-4 bytes of header present + * on A/I/B files. That's already factored in. + * + * Modifies fOpenEOF, fOpenSectorsUsed, and sets fModified. + */ +DIError +A2FDDOS::Write(const void* buf, size_t len, size_t* pActual) +{ + DIError dierr = kDIErrNone; + A2FileDOS* pFile = (A2FileDOS*) fpFile; + DiskFSDOS33* pDiskFS = (DiskFSDOS33*) fpFile->GetDiskFS(); + unsigned char sctBuf[kSctSize]; + + WMSG2(" DOS Write len=%u %s\n", len, pFile->GetPathName()); + + if (len >= 0x01000000) { // 16MB + assert(false); + return kDIErrInvalidArg; + } + assert(fOffset == 0); // big simplifying assumption + assert(fOpenEOF == 0); // another one + assert(fTSCount == 0); // must hold for our newly-created files + assert(fIndexCount == 1); // must hold for our newly-created files + assert(fOpenSectorsUsed == fTSCount + fIndexCount); + assert(buf != nil); + + long actualLen = (long) len + pFile->fDataOffset; + long numSectors = (actualLen + kSctSize -1) / kSctSize; + TrackSector firstIndex; + int i; + + /* + * Nothing to do for zero-length write; don't even set fModified. Note, + * however, that a zero-length 'B' file is actually 4 bytes long, and + * must have a data block allocated. + */ + if (actualLen == 0) + goto bail; + assert(numSectors > 0); + + dierr = pDiskFS->LoadVolBitmap(); + if (dierr != kDIErrNone) + goto bail; + + /* + * Start by allocating a full T/S list. The existing T/S list is + * empty, but we do have one T/S index sector to fill before we + * allocate any others. + * + * Since we determined above that there was nothing interesting in + * our T/S list, we just grab the one allocated block, throw out + * the lists, and reallocate them. + */ + firstIndex = fIndexList[0]; + delete[] fTSList; + delete[] fIndexList; + fTSList = fIndexList = nil; + + fTSCount = numSectors; + fTSList = new TrackSector[fTSCount]; + fIndexCount = (numSectors + kMaxTSPairs -1) / kMaxTSPairs; + assert(fIndexCount > 0); + fIndexList = new TrackSector[fIndexCount]; + if (fTSList == nil || fIndexList == nil) { + dierr = kDIErrMalloc; + goto bail; + } + + /* + * Allocate all of the index sectors. In theory we should to this along + * with the file sectors, so that the index and file sectors are + * interspersed with the data, but in practice 99% of the file have + * only one or two index blocks. By grouping them together we improve + * the performance for emulators and CiderPress. + */ + fIndexList[0] = firstIndex; + for (i = 1; i < fIndexCount; i++) { + TrackSector allocTS; + + dierr = pDiskFS->AllocSector(&allocTS); + if (dierr != kDIErrNone) + goto bail; + fIndexList[i] = allocTS; + } + /* + * Allocate the data sectors. + */ + for (i = 0; i < fTSCount; i++) { + TrackSector allocTS; + + dierr = pDiskFS->AllocSector(&allocTS); + if (dierr != kDIErrNone) + goto bail; + fTSList[i] = allocTS; + } + + /* + * Write the sectors into the T/S list. + */ + const unsigned char* curPtr; + int sectorIdx; + + curPtr = (const unsigned char*) buf; + sectorIdx = 0; + + if (pFile->fDataOffset > 0) { + /* handle first sector specially */ + assert(pFile->fDataOffset < kSctSize); + int dataInFirstSct = kSctSize - pFile->fDataOffset; + if (dataInFirstSct > actualLen - pFile->fDataOffset) + dataInFirstSct = actualLen - pFile->fDataOffset; + + // dataInFirstSct could be zero (== len) + memset(sctBuf, 0, sizeof(sctBuf)); + memcpy(sctBuf + pFile->fDataOffset, curPtr, + dataInFirstSct); + dierr = pDiskFS->GetDiskImg()->WriteTrackSector(fTSList[sectorIdx].track, + fTSList[sectorIdx].sector, sctBuf); + if (dierr != kDIErrNone) + goto bail; + + sectorIdx++; + actualLen -= dataInFirstSct + pFile->fDataOffset; + curPtr += dataInFirstSct; + } + while (actualLen > 0) { + if (actualLen >= kSctSize) { + /* write directly from input */ + dierr = pDiskFS->GetDiskImg()->WriteTrackSector(fTSList[sectorIdx].track, + fTSList[sectorIdx].sector, curPtr); + if (dierr != kDIErrNone) + goto bail; + } else { + /* make a copy of the partial buffer */ + memset(sctBuf, 0, sizeof(sctBuf)); + memcpy(sctBuf, curPtr, actualLen); + dierr = pDiskFS->GetDiskImg()->WriteTrackSector(fTSList[sectorIdx].track, + fTSList[sectorIdx].sector, sctBuf); + if (dierr != kDIErrNone) + goto bail; + } + + sectorIdx++; + actualLen -= kSctSize; // goes negative; that's fine + curPtr += kSctSize; + } + assert(sectorIdx == fTSCount); + + /* + * Fill out the T/S list sectors. Failure here presents a potential + * problem because, once we've written the first T/S entry, the file + * appears to have storage that it actually doesn't. The easiest way + * to handle this safely is to start by writing the last index block + * first. + */ + for (i = fIndexCount-1; i >= 0; i--) { + int tsOffset = i * kMaxTSPairs; + assert(tsOffset < fTSCount); + + memset(sctBuf, 0, kSctSize); + if (i != fIndexCount-1) { + sctBuf[0x01] = fIndexList[i+1].track; + sctBuf[0x02] = fIndexList[i+1].sector; + } + PutShortLE(&sctBuf[0x05], kMaxTSPairs * i); + + int ent = i * kMaxTSPairs; // start here + for (int j = 0; j < kMaxTSPairs; j++) { + if (ent == fTSCount) + break; + sctBuf[kTSOffset + j*2] = fTSList[ent].track; + sctBuf[kTSOffset + j*2 +1] = fTSList[ent].sector; + ent++; + } + + dierr = pDiskFS->GetDiskImg()->WriteTrackSector(fIndexList[i].track, + fIndexList[i].sector, sctBuf); + if (dierr != kDIErrNone) + goto bail; + } + + dierr = pDiskFS->SaveVolBitmap(); + if (dierr != kDIErrNone) { + /* + * This is awkward -- we wrote the first T/S list, so the file + * now appears to have content, but the blocks aren't marked used. + * We read the VTOC successfully though, so it's VERY unlikely + * that this will fail. If it does, it's likely that any attempt + * to mitigate the problem will also fail. (Maybe we could force + * the object into read-only mode?) + */ + goto bail; + } + + /* finish up */ + fOpenSectorsUsed = fIndexCount + fTSCount; + fOpenEOF = len; + fOffset += len; + fModified = true; + + if (!UpdateProgress(fOffset)) + dierr = kDIErrCancelled; + +bail: + pDiskFS->FreeVolBitmap(); + return dierr; +} + +/* + * Seek to the specified offset. + */ +DIError +A2FDDOS::Seek(di_off_t offset, DIWhence whence) +{ + //di_off_t fileLength = fpFile->GetDataLength(); + + switch (whence) { + case kSeekSet: + if (offset < 0 || offset > fOpenEOF) + return kDIErrInvalidArg; + fOffset = offset; + break; + case kSeekEnd: + if (offset > 0 || offset < -fOpenEOF) + return kDIErrInvalidArg; + fOffset = fOpenEOF + offset; + break; + case kSeekCur: + if (offset < -fOffset || + offset >= (fOpenEOF - fOffset)) + { + return kDIErrInvalidArg; + } + fOffset += offset; + break; + default: + assert(false); + return kDIErrInvalidArg; + } + + assert(fOffset >= 0 && fOffset <= fOpenEOF); + return kDIErrNone; +} + +/* + * Return current offset. + */ +di_off_t +A2FDDOS::Tell(void) +{ + return fOffset; +} + +/* + * Release file state. + * + * If the file was modified, we need to update the sector usage count in + * the catalog track, and possibly a length word in the first sector of + * the file (for A/I/B). + * + * Given the current "write all at once" implementation of Write, we could + * have handled the length word back when initially writing the data, but + * someday we may fix that and I don't want to have to rewrite this part. + * + * Most applications don't check the value of "Close", or call it from a + * destructor, so we call CloseDescr whether we succeed or not. + */ +DIError +A2FDDOS::Close(void) +{ + DIError dierr = kDIErrNone; + + if (fModified) { + DiskFSDOS33* pDiskFS = (DiskFSDOS33*) fpFile->GetDiskFS(); + A2FileDOS* pFile = (A2FileDOS*) fpFile; + unsigned char sctBuf[kSctSize]; + unsigned char* pEntry; + + /* + * Fill in the length and address, if needed for this type of file. + */ + if (pFile->fFileType == A2FileDOS::kTypeInteger || + pFile->fFileType == A2FileDOS::kTypeApplesoft || + pFile->fFileType == A2FileDOS::kTypeBinary) + { + assert(fTSCount > 0); + assert(pFile->fDataOffset > 0); + //assert(fOpenEOF < 65536); + if (fOpenEOF > 65535) { + WMSG1("WARNING: DOS Close trimming A/I/B file from %ld to 65535\n", + (long) fOpenEOF); + fOpenEOF = 65535; + } + dierr = pDiskFS->GetDiskImg()->ReadTrackSector(fTSList[0].track, + fTSList[0].sector, sctBuf); + if (dierr != kDIErrNone) { + WMSG0("DOS Close: unable to get first sector of file\n"); + goto bail; + } + + if (pFile->fFileType == A2FileDOS::kTypeInteger || + pFile->fFileType == A2FileDOS::kTypeApplesoft) + { + PutShortLE(&sctBuf[0x00], (unsigned short) fOpenEOF); + } else { + PutShortLE(&sctBuf[0x00], pFile->fAuxType); + PutShortLE(&sctBuf[0x02], (unsigned short) fOpenEOF); + } + + dierr = pDiskFS->GetDiskImg()->WriteTrackSector(fTSList[0].track, + fTSList[0].sector, sctBuf); + if (dierr != kDIErrNone) { + WMSG0("DOS Close: unable to write first sector of file\n"); + goto bail; + } + } else if (pFile->fFileType == A2FileDOS::kTypeText) { + /* + * The length of text files can be determined by looking for the + * first $00. A file of exactly 256 bytes occupies only one + * sector though, so running out of sectors also works -- the + * last $00 is not mandatory. + * + * Bottom line is that the value we just wrote for fOpenEOF is + * *probably* recoverable, so we can stuff it into "fLength" + * with some assurance that it will be there when we reopen the + * file. + */ + } else { + /* + * The remaining file types have a length based solely on + * sector count. We need to round off our length value. + */ + fOpenEOF = ((fOpenEOF + kSctSize-1) / kSctSize) * kSctSize; + } + + /* + * Update our internal copies of stuff. + */ + pFile->fLength = fOpenEOF; + pFile->fSparseLength = pFile->fLength; + pFile->fLengthInSectors = (unsigned short) fOpenSectorsUsed; + + /* + * Update the sector count in the directory entry. + */ + dierr = pDiskFS->GetDiskImg()->ReadTrackSector(pFile->fCatTS.track, + pFile->fCatTS.sector, sctBuf); + if (dierr != kDIErrNone) + goto bail; + + pEntry = GetCatalogEntryPtr(sctBuf, pFile->fCatEntryNum); + assert(GetShortLE(&pEntry[0x21]) == 1); // holds for new file + PutShortLE(&pEntry[0x21], pFile->fLengthInSectors); + dierr = pDiskFS->GetDiskImg()->WriteTrackSector(pFile->fCatTS.track, + pFile->fCatTS.sector, sctBuf); + } + +bail: + fpFile->CloseDescr(this); + return dierr; +} + + +/* + * Return the #of sectors/blocks in the file. + */ +long +A2FDDOS::GetSectorCount(void) const +{ + return fTSCount; +} +long +A2FDDOS::GetBlockCount(void) const +{ + return (fTSCount+1)/2; +} + +/* + * Return the Nth track/sector in this file. + * + * Returns (0,0) for a sparse sector. + */ +DIError +A2FDDOS::GetStorage(long sectorIdx, long* pTrack, long* pSector) const +{ + if (sectorIdx < 0 || sectorIdx >= fTSCount) + return kDIErrInvalidIndex; + + *pTrack = fTSList[sectorIdx].track; + *pSector = fTSList[sectorIdx].sector; + return kDIErrNone; +} +/* + * Return the Nth 512-byte block in this file. Since things aren't stored + * in 512-byte blocks, we're reduced to finding storage at (tsIndex*2) and + * converting it to a block number. + */ +DIError +A2FDDOS::GetStorage(long blockIdx, long* pBlock) const +{ + long sectorIdx = blockIdx * 2; + if (sectorIdx < 0 || sectorIdx >= fTSCount) + return kDIErrInvalidIndex; + + bool dummy; + TrackSectorToBlock(fTSList[sectorIdx].track, + fTSList[sectorIdx].sector, pBlock, &dummy); + assert(*pBlock < fpFile->GetDiskFS()->GetDiskImg()->GetNumBlocks()); + return kDIErrNone; +} + + +/* + * Dump the T/S list for an open file. + */ +void +A2FDDOS::DumpTSList(void) const +{ + //A2FileDOS* pFile = (A2FileDOS*) fpFile; + WMSG2(" DOS T/S list for '%s' (count=%d)\n", + ((A2FileDOS*)fpFile)->fFileName, fTSCount); + + int i; + for (i = 0; i <= fTSCount; i++) { + WMSG3(" %3d: T=%-2d S=%d\n", i, fTSList[i].track, fTSList[i].sector); + } +} diff --git a/diskimg/DOSImage.cpp b/diskimg/DOSImage.cpp new file mode 100644 index 0000000..de5062f --- /dev/null +++ b/diskimg/DOSImage.cpp @@ -0,0 +1,2905 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * DOS images. + */ +#include "StdAfx.h" +#include "DiskImgPriv.h" + +namespace DiskImgLib { + +/* + * Three 16-sector tracks, in DOS order (i.e. track 0 sector 0 followed + * by track 0 sector 1). + * + * Obtained from tracks 0-2 of a newly-formatted disk created + * by "INIT HELLO" after booting the DOS 3.3 system master. + */ +/*static*/ const unsigned char DiskFSDOS33::gDOS33Tracks[16 * 3 * 256] = { + 0x01, 0xa5, 0x27, 0xc9, 0x09, 0xd0, 0x18, 0xa5, + 0x2b, 0x4a, 0x4a, 0x4a, 0x4a, 0x09, 0xc0, 0x85, + 0x3f, 0xa9, 0x5c, 0x85, 0x3e, 0x18, 0xad, 0xfe, + 0x08, 0x6d, 0xff, 0x08, 0x8d, 0xfe, 0x08, 0xae, + 0xff, 0x08, 0x30, 0x15, 0xbd, 0x4d, 0x08, 0x85, + 0x3d, 0xce, 0xff, 0x08, 0xad, 0xfe, 0x08, 0x85, + 0x27, 0xce, 0xfe, 0x08, 0xa6, 0x2b, 0x6c, 0x3e, + 0x00, 0xee, 0xfe, 0x08, 0xee, 0xfe, 0x08, 0x20, + 0x89, 0xfe, 0x20, 0x93, 0xfe, 0x20, 0x2f, 0xfb, + 0xa6, 0x2b, 0x6c, 0xfd, 0x08, 0x00, 0x0d, 0x0b, + 0x09, 0x07, 0x05, 0x03, 0x01, 0x0e, 0x0c, 0x0a, + 0x08, 0x06, 0x04, 0x02, 0x0f, 0x00, 0x20, 0x64, + 0xa7, 0xb0, 0x08, 0xa9, 0x00, 0xa8, 0x8d, 0x5d, + 0xb6, 0x91, 0x40, 0xad, 0xc5, 0xb5, 0x4c, 0xd2, + 0xa6, 0xad, 0x5d, 0xb6, 0xf0, 0x08, 0xee, 0xbd, + 0xb5, 0xd0, 0x03, 0xee, 0xbe, 0xb5, 0xa9, 0x00, + 0x8d, 0x5d, 0xb6, 0x4c, 0x46, 0xa5, 0x8d, 0xbc, + 0xb5, 0x20, 0xa8, 0xa6, 0x20, 0xea, 0xa2, 0x4c, + 0x7d, 0xa2, 0xa0, 0x13, 0xb1, 0x42, 0xd0, 0x14, + 0xc8, 0xc0, 0x17, 0xd0, 0xf7, 0xa0, 0x19, 0xb1, + 0x42, 0x99, 0xa4, 0xb5, 0xc8, 0xc0, 0x1d, 0xd0, + 0xf6, 0x4c, 0xbc, 0xa6, 0xa2, 0xff, 0x8e, 0x5d, + 0xb6, 0xd0, 0xf6, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x20, 0x58, 0xfc, 0xa9, 0xc2, 0x20, 0xed, 0xfd, + 0xa9, 0x01, 0x20, 0xda, 0xfd, 0xa9, 0xad, 0x20, + 0xed, 0xfd, 0xa9, 0x00, 0x20, 0xda, 0xfd, 0x60, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb6, 0x09, + + 0x8e, 0xe9, 0xb7, 0x8e, 0xf7, 0xb7, 0xa9, 0x01, + 0x8d, 0xf8, 0xb7, 0x8d, 0xea, 0xb7, 0xad, 0xe0, + 0xb7, 0x8d, 0xe1, 0xb7, 0xa9, 0x02, 0x8d, 0xec, + 0xb7, 0xa9, 0x04, 0x8d, 0xed, 0xb7, 0xac, 0xe7, + 0xb7, 0x88, 0x8c, 0xf1, 0xb7, 0xa9, 0x01, 0x8d, + 0xf4, 0xb7, 0x8a, 0x4a, 0x4a, 0x4a, 0x4a, 0xaa, + 0xa9, 0x00, 0x9d, 0xf8, 0x04, 0x9d, 0x78, 0x04, + 0x20, 0x93, 0xb7, 0xa2, 0xff, 0x9a, 0x8e, 0xeb, + 0xb7, 0x4c, 0xc8, 0xbf, 0x20, 0x89, 0xfe, 0x4c, + 0x84, 0x9d, 0xad, 0xe7, 0xb7, 0x38, 0xed, 0xf1, + 0xb7, 0x8d, 0xe1, 0xb7, 0xad, 0xe7, 0xb7, 0x8d, + 0xf1, 0xb7, 0xce, 0xf1, 0xb7, 0xa9, 0x02, 0x8d, + 0xec, 0xb7, 0xa9, 0x04, 0x8d, 0xed, 0xb7, 0xa9, + 0x02, 0x8d, 0xf4, 0xb7, 0x20, 0x93, 0xb7, 0xad, + 0xe7, 0xb7, 0x8d, 0xfe, 0xb6, 0x18, 0x69, 0x09, + 0x8d, 0xf1, 0xb7, 0xa9, 0x0a, 0x8d, 0xe1, 0xb7, + 0x38, 0xe9, 0x01, 0x8d, 0xff, 0xb6, 0x8d, 0xed, + 0xb7, 0x20, 0x93, 0xb7, 0x60, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xad, 0xe5, 0xb7, 0xac, 0xe4, + 0xb7, 0x20, 0xb5, 0xb7, 0xac, 0xed, 0xb7, 0x88, + 0x10, 0x07, 0xa0, 0x0f, 0xea, 0xea, 0xce, 0xec, + 0xb7, 0x8c, 0xed, 0xb7, 0xce, 0xf1, 0xb7, 0xce, + 0xe1, 0xb7, 0xd0, 0xdf, 0x60, 0x08, 0x78, 0x20, + 0x00, 0xbd, 0xb0, 0x03, 0x28, 0x18, 0x60, 0x28, + 0x38, 0x60, 0xad, 0xbc, 0xb5, 0x8d, 0xf1, 0xb7, + 0xa9, 0x00, 0x8d, 0xf0, 0xb7, 0xad, 0xf9, 0xb5, + 0x49, 0xff, 0x8d, 0xeb, 0xb7, 0x60, 0xa9, 0x00, + 0xa8, 0x91, 0x42, 0xc8, 0xd0, 0xfb, 0x60, 0x00, + 0x1b, 0x02, 0x0a, 0x1b, 0xe8, 0xb7, 0x00, 0xb6, + 0x01, 0x60, 0x02, 0xfe, 0x00, 0x01, 0xfb, 0xb7, + 0x00, 0xb7, 0x00, 0x00, 0x02, 0xeb, 0xfe, 0x60, + 0x02, 0x00, 0x00, 0x00, 0x01, 0xef, 0xd8, 0x00, + + 0xa2, 0x00, 0xa0, 0x02, 0x88, 0xb1, 0x3e, 0x4a, + 0x3e, 0x00, 0xbc, 0x4a, 0x3e, 0x00, 0xbc, 0x99, + 0x00, 0xbb, 0xe8, 0xe0, 0x56, 0x90, 0xed, 0xa2, + 0x00, 0x98, 0xd0, 0xe8, 0xa2, 0x55, 0xbd, 0x00, + 0xbc, 0x29, 0x3f, 0x9d, 0x00, 0xbc, 0xca, 0x10, + 0xf5, 0x60, 0x38, 0x86, 0x27, 0x8e, 0x78, 0x06, + 0xbd, 0x8d, 0xc0, 0xbd, 0x8e, 0xc0, 0x30, 0x7c, + 0xad, 0x00, 0xbc, 0x85, 0x26, 0xa9, 0xff, 0x9d, + 0x8f, 0xc0, 0x1d, 0x8c, 0xc0, 0x48, 0x68, 0xea, + 0xa0, 0x04, 0x48, 0x68, 0x20, 0xb9, 0xb8, 0x88, + 0xd0, 0xf8, 0xa9, 0xd5, 0x20, 0xb8, 0xb8, 0xa9, + 0xaa, 0x20, 0xb8, 0xb8, 0xa9, 0xad, 0x20, 0xb8, + 0xb8, 0x98, 0xa0, 0x56, 0xd0, 0x03, 0xb9, 0x00, + 0xbc, 0x59, 0xff, 0xbb, 0xaa, 0xbd, 0x29, 0xba, + 0xa6, 0x27, 0x9d, 0x8d, 0xc0, 0xbd, 0x8c, 0xc0, + 0x88, 0xd0, 0xeb, 0xa5, 0x26, 0xea, 0x59, 0x00, + 0xbb, 0xaa, 0xbd, 0x29, 0xba, 0xae, 0x78, 0x06, + 0x9d, 0x8d, 0xc0, 0xbd, 0x8c, 0xc0, 0xb9, 0x00, + 0xbb, 0xc8, 0xd0, 0xea, 0xaa, 0xbd, 0x29, 0xba, + 0xa6, 0x27, 0x20, 0xbb, 0xb8, 0xa9, 0xde, 0x20, + 0xb8, 0xb8, 0xa9, 0xaa, 0x20, 0xb8, 0xb8, 0xa9, + 0xeb, 0x20, 0xb8, 0xb8, 0xa9, 0xff, 0x20, 0xb8, + 0xb8, 0xbd, 0x8e, 0xc0, 0xbd, 0x8c, 0xc0, 0x60, + 0x18, 0x48, 0x68, 0x9d, 0x8d, 0xc0, 0x1d, 0x8c, + 0xc0, 0x60, 0xa0, 0x00, 0xa2, 0x56, 0xca, 0x30, + 0xfb, 0xb9, 0x00, 0xbb, 0x5e, 0x00, 0xbc, 0x2a, + 0x5e, 0x00, 0xbc, 0x2a, 0x91, 0x3e, 0xc8, 0xc4, + 0x26, 0xd0, 0xeb, 0x60, 0xa0, 0x20, 0x88, 0xf0, + 0x61, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0x49, 0xd5, + 0xd0, 0xf4, 0xea, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, + 0xc9, 0xaa, 0xd0, 0xf2, 0xa0, 0x56, 0xbd, 0x8c, + 0xc0, 0x10, 0xfb, 0xc9, 0xad, 0xd0, 0xe7, 0xa9, + + 0x00, 0x88, 0x84, 0x26, 0xbc, 0x8c, 0xc0, 0x10, + 0xfb, 0x59, 0x00, 0xba, 0xa4, 0x26, 0x99, 0x00, + 0xbc, 0xd0, 0xee, 0x84, 0x26, 0xbc, 0x8c, 0xc0, + 0x10, 0xfb, 0x59, 0x00, 0xba, 0xa4, 0x26, 0x99, + 0x00, 0xbb, 0xc8, 0xd0, 0xee, 0xbc, 0x8c, 0xc0, + 0x10, 0xfb, 0xd9, 0x00, 0xba, 0xd0, 0x13, 0xbd, + 0x8c, 0xc0, 0x10, 0xfb, 0xc9, 0xde, 0xd0, 0x0a, + 0xea, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0xc9, 0xaa, + 0xf0, 0x5c, 0x38, 0x60, 0xa0, 0xfc, 0x84, 0x26, + 0xc8, 0xd0, 0x04, 0xe6, 0x26, 0xf0, 0xf3, 0xbd, + 0x8c, 0xc0, 0x10, 0xfb, 0xc9, 0xd5, 0xd0, 0xf0, + 0xea, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0xc9, 0xaa, + 0xd0, 0xf2, 0xa0, 0x03, 0xbd, 0x8c, 0xc0, 0x10, + 0xfb, 0xc9, 0x96, 0xd0, 0xe7, 0xa9, 0x00, 0x85, + 0x27, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0x2a, 0x85, + 0x26, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0x25, 0x26, + 0x99, 0x2c, 0x00, 0x45, 0x27, 0x88, 0x10, 0xe7, + 0xa8, 0xd0, 0xb7, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, + 0xc9, 0xde, 0xd0, 0xae, 0xea, 0xbd, 0x8c, 0xc0, + 0x10, 0xfb, 0xc9, 0xaa, 0xd0, 0xa4, 0x18, 0x60, + 0x86, 0x2b, 0x85, 0x2a, 0xcd, 0x78, 0x04, 0xf0, + 0x53, 0xa9, 0x00, 0x85, 0x26, 0xad, 0x78, 0x04, + 0x85, 0x27, 0x38, 0xe5, 0x2a, 0xf0, 0x33, 0xb0, + 0x07, 0x49, 0xff, 0xee, 0x78, 0x04, 0x90, 0x05, + 0x69, 0xfe, 0xce, 0x78, 0x04, 0xc5, 0x26, 0x90, + 0x02, 0xa5, 0x26, 0xc9, 0x0c, 0xb0, 0x01, 0xa8, + 0x38, 0x20, 0xee, 0xb9, 0xb9, 0x11, 0xba, 0x20, + 0x00, 0xba, 0xa5, 0x27, 0x18, 0x20, 0xf1, 0xb9, + 0xb9, 0x1d, 0xba, 0x20, 0x00, 0xba, 0xe6, 0x26, + 0xd0, 0xc3, 0x20, 0x00, 0xba, 0x18, 0xad, 0x78, + 0x04, 0x29, 0x03, 0x2a, 0x05, 0x2b, 0xaa, 0xbd, + 0x80, 0xc0, 0xa6, 0x2b, 0x60, 0xaa, 0xa0, 0xa0, + + 0xa2, 0x11, 0xca, 0xd0, 0xfd, 0xe6, 0x46, 0xd0, + 0x02, 0xe6, 0x47, 0x38, 0xe9, 0x01, 0xd0, 0xf0, + 0x60, 0x01, 0x30, 0x28, 0x24, 0x20, 0x1e, 0x1d, + 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x70, 0x2c, 0x26, + 0x22, 0x1f, 0x1e, 0x1d, 0x1c, 0x1c, 0x1c, 0x1c, + 0x1c, 0x96, 0x97, 0x9a, 0x9b, 0x9d, 0x9e, 0x9f, + 0xa6, 0xa7, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb2, + 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb9, 0xba, 0xbb, + 0xbc, 0xbd, 0xbe, 0xbf, 0xcb, 0xcd, 0xce, 0xcf, + 0xd3, 0xd6, 0xd7, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, + 0xde, 0xdf, 0xe5, 0xe6, 0xe7, 0xe9, 0xea, 0xeb, + 0xec, 0xed, 0xee, 0xef, 0xf2, 0xf3, 0xf4, 0xf5, + 0xf6, 0xf7, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, + 0xff, 0xb3, 0xb3, 0xa0, 0xe0, 0xb3, 0xc3, 0xc5, + 0xb3, 0xa0, 0xe0, 0xb3, 0xc3, 0xc5, 0xb3, 0xa0, + 0xe0, 0xb3, 0xb3, 0xc5, 0xaa, 0xa0, 0x82, 0xb3, + 0xb3, 0xc5, 0xaa, 0xa0, 0x82, 0xc5, 0xb3, 0xb3, + 0xaa, 0x88, 0x82, 0xc5, 0xb3, 0xb3, 0xaa, 0x88, + 0x82, 0xc5, 0xc4, 0xb3, 0xb0, 0x88, 0x00, 0x01, + 0x98, 0x99, 0x02, 0x03, 0x9c, 0x04, 0x05, 0x06, + 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0x07, 0x08, + 0xa8, 0xa9, 0xaa, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, + 0xb0, 0xb1, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, + 0xb8, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, + 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, + 0xc8, 0xc9, 0xca, 0x1b, 0xcc, 0x1c, 0x1d, 0x1e, + 0xd0, 0xd1, 0xd2, 0x1f, 0xd4, 0xd5, 0x20, 0x21, + 0xd8, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, + 0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0x29, 0x2a, 0x2b, + 0xe8, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, + 0xf0, 0xf1, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, + 0xf8, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, + + 0x01, 0x0a, 0x11, 0x0a, 0x08, 0x20, 0x20, 0x0e, + 0x18, 0x06, 0x02, 0x31, 0x02, 0x09, 0x08, 0x27, + 0x22, 0x00, 0x12, 0x0a, 0x0a, 0x04, 0x00, 0x00, + 0x03, 0x2a, 0x00, 0x04, 0x00, 0x00, 0x22, 0x08, + 0x10, 0x28, 0x12, 0x02, 0x00, 0x02, 0x08, 0x11, + 0x0a, 0x08, 0x02, 0x28, 0x11, 0x01, 0x39, 0x22, + 0x31, 0x01, 0x05, 0x18, 0x20, 0x28, 0x02, 0x10, + 0x06, 0x02, 0x09, 0x02, 0x05, 0x2c, 0x10, 0x00, + 0x08, 0x2e, 0x00, 0x05, 0x02, 0x28, 0x18, 0x02, + 0x30, 0x23, 0x02, 0x20, 0x32, 0x04, 0x11, 0x02, + 0x14, 0x02, 0x08, 0x09, 0x12, 0x20, 0x0e, 0x2f, + 0x23, 0x30, 0x2f, 0x23, 0x30, 0x0c, 0x17, 0x2a, + 0x3f, 0x27, 0x23, 0x30, 0x37, 0x23, 0x30, 0x12, + 0x1a, 0x08, 0x30, 0x2f, 0x08, 0x30, 0x2f, 0x27, + 0x23, 0x30, 0x37, 0x23, 0x30, 0x3a, 0x22, 0x34, + 0x3c, 0x2a, 0x35, 0x08, 0x35, 0x2f, 0x2a, 0x2a, + 0x08, 0x35, 0x2f, 0x2a, 0x25, 0x08, 0x35, 0x2f, + 0x29, 0x10, 0x08, 0x31, 0x2f, 0x29, 0x11, 0x08, + 0x31, 0x2f, 0x29, 0x0f, 0x08, 0x31, 0x2f, 0x29, + 0x10, 0x11, 0x11, 0x11, 0x0f, 0x12, 0x12, 0x01, + 0x0f, 0x27, 0x23, 0x30, 0x2f, 0x23, 0x30, 0x1a, + 0x02, 0x2a, 0x08, 0x35, 0x2f, 0x2a, 0x37, 0x08, + 0x35, 0x2f, 0x2a, 0x2a, 0x08, 0x35, 0x2f, 0x2a, + 0x3a, 0x08, 0x35, 0x2f, 0x06, 0x2f, 0x23, 0x30, + 0x2f, 0x23, 0x30, 0x18, 0x12, 0x12, 0x01, 0x0f, + 0x27, 0x23, 0x30, 0x37, 0x23, 0x30, 0x1a, 0x3a, + 0x3a, 0x3a, 0x02, 0x2a, 0x3a, 0x3a, 0x12, 0x1a, + 0x27, 0x23, 0x30, 0x37, 0x23, 0x30, 0x18, 0x22, + 0x29, 0x3a, 0x24, 0x28, 0x25, 0x22, 0x25, 0x3a, + 0x24, 0x28, 0x25, 0x22, 0x25, 0x24, 0x24, 0x32, + 0x25, 0x34, 0x25, 0x24, 0x24, 0x32, 0x25, 0x34, + 0x25, 0x24, 0x28, 0x32, 0x28, 0x29, 0x21, 0x29, + + 0x10, 0xa1, 0x45, 0x28, 0x21, 0x82, 0x80, 0x38, + 0x62, 0x19, 0x0b, 0xc5, 0x0b, 0x24, 0x21, 0x9c, + 0x88, 0x00, 0x48, 0x28, 0x2b, 0x10, 0x00, 0x03, + 0x0c, 0xa9, 0x01, 0x10, 0x01, 0x00, 0x88, 0x22, + 0x40, 0xa0, 0x48, 0x09, 0x01, 0x08, 0x21, 0x44, + 0x29, 0x22, 0x08, 0xa0, 0x45, 0x06, 0xe4, 0x8a, + 0xc4, 0x06, 0x16, 0x60, 0x80, 0xa0, 0x09, 0x40, + 0x18, 0x0a, 0x24, 0x0a, 0x16, 0xb0, 0x43, 0x00, + 0x20, 0xbb, 0x00, 0x14, 0x08, 0xa0, 0x60, 0x0a, + 0xc0, 0x8f, 0x0a, 0x83, 0xca, 0x11, 0x44, 0x08, + 0x51, 0x0a, 0x20, 0x26, 0x4a, 0x80, 0x38, 0xbd, + 0x8d, 0xc0, 0xbd, 0x8e, 0xc0, 0x30, 0x5e, 0xa9, + 0xff, 0x9d, 0x8f, 0xc0, 0xdd, 0x8c, 0xc0, 0x48, + 0x68, 0x20, 0xc3, 0xbc, 0x20, 0xc3, 0xbc, 0x9d, + 0x8d, 0xc0, 0xdd, 0x8c, 0xc0, 0xea, 0x88, 0xd0, + 0xf0, 0xa9, 0xd5, 0x20, 0xd5, 0xbc, 0xa9, 0xaa, + 0x20, 0xd5, 0xbc, 0xa9, 0x96, 0x20, 0xd5, 0xbc, + 0xa5, 0x41, 0x20, 0xc4, 0xbc, 0xa5, 0x44, 0x20, + 0xc4, 0xbc, 0xa5, 0x3f, 0x20, 0xc4, 0xbc, 0xa5, + 0x41, 0x45, 0x44, 0x45, 0x3f, 0x48, 0x4a, 0x05, + 0x3e, 0x9d, 0x8d, 0xc0, 0xbd, 0x8c, 0xc0, 0x68, + 0x09, 0xaa, 0x20, 0xd4, 0xbc, 0xa9, 0xde, 0x20, + 0xd5, 0xbc, 0xa9, 0xaa, 0x20, 0xd5, 0xbc, 0xa9, + 0xeb, 0x20, 0xd5, 0xbc, 0x18, 0xbd, 0x8e, 0xc0, + 0xbd, 0x8c, 0xc0, 0x60, 0x48, 0x4a, 0x05, 0x3e, + 0x9d, 0x8d, 0xc0, 0xdd, 0x8c, 0xc0, 0x68, 0xea, + 0xea, 0xea, 0x09, 0xaa, 0xea, 0xea, 0x48, 0x68, + 0x9d, 0x8d, 0xc0, 0xdd, 0x8c, 0xc0, 0x60, 0x88, + 0xa5, 0xe8, 0x91, 0xa0, 0x94, 0x88, 0x96, 0xe8, + 0x91, 0xa0, 0x94, 0x88, 0x96, 0x91, 0x91, 0xc8, + 0x94, 0xd0, 0x96, 0x91, 0x91, 0xc8, 0x94, 0xd0, + 0x96, 0x91, 0xa3, 0xc8, 0xa0, 0xa5, 0x85, 0xa4, + + 0x84, 0x48, 0x85, 0x49, 0xa0, 0x02, 0x8c, 0xf8, + 0x06, 0xa0, 0x04, 0x8c, 0xf8, 0x04, 0xa0, 0x01, + 0xb1, 0x48, 0xaa, 0xa0, 0x0f, 0xd1, 0x48, 0xf0, + 0x1b, 0x8a, 0x48, 0xb1, 0x48, 0xaa, 0x68, 0x48, + 0x91, 0x48, 0xbd, 0x8e, 0xc0, 0xa0, 0x08, 0xbd, + 0x8c, 0xc0, 0xdd, 0x8c, 0xc0, 0xd0, 0xf6, 0x88, + 0xd0, 0xf8, 0x68, 0xaa, 0xbd, 0x8e, 0xc0, 0xbd, + 0x8c, 0xc0, 0xa0, 0x08, 0xbd, 0x8c, 0xc0, 0x48, + 0x68, 0x48, 0x68, 0x8e, 0xf8, 0x05, 0xdd, 0x8c, + 0xc0, 0xd0, 0x03, 0x88, 0xd0, 0xee, 0x08, 0xbd, + 0x89, 0xc0, 0xa0, 0x06, 0xb1, 0x48, 0x99, 0x36, + 0x00, 0xc8, 0xc0, 0x0a, 0xd0, 0xf6, 0xa0, 0x03, + 0xb1, 0x3c, 0x85, 0x47, 0xa0, 0x02, 0xb1, 0x48, + 0xa0, 0x10, 0xd1, 0x48, 0xf0, 0x06, 0x91, 0x48, + 0x28, 0xa0, 0x00, 0x08, 0x6a, 0x90, 0x05, 0xbd, + 0x8a, 0xc0, 0xb0, 0x03, 0xbd, 0x8b, 0xc0, 0x66, + 0x35, 0x28, 0x08, 0xd0, 0x0b, 0xa0, 0x07, 0x20, + 0x00, 0xba, 0x88, 0xd0, 0xfa, 0xae, 0xf8, 0x05, + 0xa0, 0x04, 0xb1, 0x48, 0x20, 0x5a, 0xbe, 0x28, + 0xd0, 0x11, 0xa4, 0x47, 0x10, 0x0d, 0xa0, 0x12, + 0x88, 0xd0, 0xfd, 0xe6, 0x46, 0xd0, 0xf7, 0xe6, + 0x47, 0xd0, 0xf3, 0xa0, 0x0c, 0xb1, 0x48, 0xf0, + 0x5a, 0xc9, 0x04, 0xf0, 0x58, 0x6a, 0x08, 0xb0, + 0x03, 0x20, 0x00, 0xb8, 0xa0, 0x30, 0x8c, 0x78, + 0x05, 0xae, 0xf8, 0x05, 0x20, 0x44, 0xb9, 0x90, + 0x24, 0xce, 0x78, 0x05, 0x10, 0xf3, 0xad, 0x78, + 0x04, 0x48, 0xa9, 0x60, 0x20, 0x95, 0xbe, 0xce, + 0xf8, 0x06, 0xf0, 0x28, 0xa9, 0x04, 0x8d, 0xf8, + 0x04, 0xa9, 0x00, 0x20, 0x5a, 0xbe, 0x68, 0x20, + 0x5a, 0xbe, 0x4c, 0xbc, 0xbd, 0xa4, 0x2e, 0xcc, + 0x78, 0x04, 0xf0, 0x1c, 0xad, 0x78, 0x04, 0x48, + 0x98, 0x20, 0x95, 0xbe, 0x68, 0xce, 0xf8, 0x04, + + 0xd0, 0xe5, 0xf0, 0xca, 0x68, 0xa9, 0x40, 0x28, + 0x4c, 0x48, 0xbe, 0xf0, 0x39, 0x4c, 0xaf, 0xbe, + 0xa0, 0x03, 0xb1, 0x48, 0x48, 0xa5, 0x2f, 0xa0, + 0x0e, 0x91, 0x48, 0x68, 0xf0, 0x08, 0xc5, 0x2f, + 0xf0, 0x04, 0xa9, 0x20, 0xd0, 0xe1, 0xa0, 0x05, + 0xb1, 0x48, 0xa8, 0xb9, 0xb8, 0xbf, 0xc5, 0x2d, + 0xd0, 0x97, 0x28, 0x90, 0x1c, 0x20, 0xdc, 0xb8, + 0x08, 0xb0, 0x8e, 0x28, 0xa2, 0x00, 0x86, 0x26, + 0x20, 0xc2, 0xb8, 0xae, 0xf8, 0x05, 0x18, 0x24, + 0x38, 0xa0, 0x0d, 0x91, 0x48, 0xbd, 0x88, 0xc0, + 0x60, 0x20, 0x2a, 0xb8, 0x90, 0xf0, 0xa9, 0x10, + 0xb0, 0xee, 0x48, 0xa0, 0x01, 0xb1, 0x3c, 0x6a, + 0x68, 0x90, 0x08, 0x0a, 0x20, 0x6b, 0xbe, 0x4e, + 0x78, 0x04, 0x60, 0x85, 0x2a, 0x20, 0x8e, 0xbe, + 0xb9, 0x78, 0x04, 0x24, 0x35, 0x30, 0x03, 0xb9, + 0xf8, 0x04, 0x8d, 0x78, 0x04, 0xa5, 0x2a, 0x24, + 0x35, 0x30, 0x05, 0x99, 0xf8, 0x04, 0x10, 0x03, + 0x99, 0x78, 0x04, 0x4c, 0xa0, 0xb9, 0x8a, 0x4a, + 0x4a, 0x4a, 0x4a, 0xa8, 0x60, 0x48, 0xa0, 0x02, + 0xb1, 0x48, 0x6a, 0x66, 0x35, 0x20, 0x8e, 0xbe, + 0x68, 0x0a, 0x24, 0x35, 0x30, 0x05, 0x99, 0xf8, + 0x04, 0x10, 0x03, 0x99, 0x78, 0x04, 0x60, 0xa0, + 0x03, 0xb1, 0x48, 0x85, 0x41, 0xa9, 0xaa, 0x85, + 0x3e, 0xa0, 0x56, 0xa9, 0x00, 0x85, 0x44, 0x99, + 0xff, 0xbb, 0x88, 0xd0, 0xfa, 0x99, 0x00, 0xbb, + 0x88, 0xd0, 0xfa, 0xa9, 0x50, 0x20, 0x95, 0xbe, + 0xa9, 0x28, 0x85, 0x45, 0xa5, 0x44, 0x20, 0x5a, + 0xbe, 0x20, 0x0d, 0xbf, 0xa9, 0x08, 0xb0, 0x24, + 0xa9, 0x30, 0x8d, 0x78, 0x05, 0x38, 0xce, 0x78, + 0x05, 0xf0, 0x19, 0x20, 0x44, 0xb9, 0xb0, 0xf5, + 0xa5, 0x2d, 0xd0, 0xf1, 0x20, 0xdc, 0xb8, 0xb0, + 0xec, 0xe6, 0x44, 0xa5, 0x44, 0xc9, 0x23, 0x90, + + 0xd3, 0x18, 0x90, 0x05, 0xa0, 0x0d, 0x91, 0x48, + 0x38, 0xbd, 0x88, 0xc0, 0x60, 0xa9, 0x00, 0x85, + 0x3f, 0xa0, 0x80, 0xd0, 0x02, 0xa4, 0x45, 0x20, + 0x56, 0xbc, 0xb0, 0x6b, 0x20, 0x2a, 0xb8, 0xb0, + 0x66, 0xe6, 0x3f, 0xa5, 0x3f, 0xc9, 0x10, 0x90, + 0xec, 0xa0, 0x0f, 0x84, 0x3f, 0xa9, 0x30, 0x8d, + 0x78, 0x05, 0x99, 0xa8, 0xbf, 0x88, 0x10, 0xfa, + 0xa4, 0x45, 0x20, 0x87, 0xbf, 0x20, 0x87, 0xbf, + 0x20, 0x87, 0xbf, 0x48, 0x68, 0xea, 0x88, 0xd0, + 0xf1, 0x20, 0x44, 0xb9, 0xb0, 0x23, 0xa5, 0x2d, + 0xf0, 0x15, 0xa9, 0x10, 0xc5, 0x45, 0xa5, 0x45, + 0xe9, 0x01, 0x85, 0x45, 0xc9, 0x05, 0xb0, 0x11, + 0x38, 0x60, 0x20, 0x44, 0xb9, 0xb0, 0x05, 0x20, + 0xdc, 0xb8, 0x90, 0x1c, 0xce, 0x78, 0x05, 0xd0, + 0xf1, 0x20, 0x44, 0xb9, 0xb0, 0x0b, 0xa5, 0x2d, + 0xc9, 0x0f, 0xd0, 0x05, 0x20, 0xdc, 0xb8, 0x90, + 0x8c, 0xce, 0x78, 0x05, 0xd0, 0xeb, 0x38, 0x60, + 0xa4, 0x2d, 0xb9, 0xa8, 0xbf, 0x30, 0xdd, 0xa9, + 0xff, 0x99, 0xa8, 0xbf, 0xc6, 0x3f, 0x10, 0xca, + 0xa5, 0x44, 0xd0, 0x0a, 0xa5, 0x45, 0xc9, 0x10, + 0x90, 0xe5, 0xc6, 0x45, 0xc6, 0x45, 0x18, 0x60, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x0d, 0x0b, 0x09, 0x07, 0x05, 0x03, 0x01, + 0x0e, 0x0c, 0x0a, 0x08, 0x06, 0x04, 0x02, 0x0f, + 0x20, 0x93, 0xfe, 0xad, 0x81, 0xc0, 0xad, 0x81, + 0xc0, 0xa9, 0x00, 0x8d, 0x00, 0xe0, 0x4c, 0x44, + 0xb7, 0x00, 0x00, 0x00, 0x8d, 0x63, 0xaa, 0x8d, + 0x70, 0xaa, 0x8d, 0x71, 0xaa, 0x60, 0x20, 0x5b, + 0xa7, 0x8c, 0xb7, 0xaa, 0x60, 0x20, 0x7e, 0xae, + 0xae, 0x9b, 0xb3, 0x9a, 0x20, 0x16, 0xa3, 0xba, + 0x8e, 0x9b, 0xb3, 0xa9, 0x09, 0x4c, 0x85, 0xb3, + + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + 0xd3, 0x9c, 0x81, 0x9e, 0xbd, 0x9e, 0x75, 0xaa, + 0x93, 0xaa, 0x60, 0xaa, 0x00, 0x9d, 0xbb, 0xb5, + 0xea, 0x9e, 0x11, 0x9f, 0x22, 0x9f, 0x2e, 0x9f, + 0x51, 0x9f, 0x60, 0x9f, 0x70, 0x9f, 0x4e, 0xa5, + 0x12, 0xa4, 0x96, 0xa3, 0xd0, 0xa4, 0xef, 0xa4, + 0x62, 0xa2, 0x70, 0xa2, 0x74, 0xa2, 0xe9, 0xa2, + 0x1a, 0xa5, 0xc5, 0xa5, 0x0f, 0xa5, 0xdc, 0xa5, + 0xa2, 0xa2, 0x97, 0xa2, 0x80, 0xa2, 0x6d, 0xa5, + 0x32, 0xa2, 0x3c, 0xa2, 0x28, 0xa2, 0x2d, 0xa2, + 0x50, 0xa2, 0x79, 0xa5, 0x9d, 0xa5, 0x30, 0xa3, + 0x5c, 0xa3, 0x8d, 0xa3, 0x7c, 0xa2, 0xfc, 0xa4, + 0xfc, 0xa4, 0x65, 0xd8, 0x00, 0xe0, 0x3c, 0xd4, + 0xf2, 0xd4, 0x36, 0xe8, 0xe5, 0xa4, 0xe3, 0xe3, + 0x00, 0xe0, 0x03, 0xe0, 0xfc, 0xa4, 0xfc, 0xa4, + 0x65, 0xd8, 0x00, 0xe0, 0x3c, 0xd4, 0xf2, 0xd4, + 0x06, 0xa5, 0x06, 0xa5, 0x67, 0x10, 0x84, 0x9d, + 0x3c, 0x0c, 0xf2, 0x0c, 0xad, 0xe9, 0xb7, 0x4a, + 0x4a, 0x4a, 0x4a, 0x8d, 0x6a, 0xaa, 0xad, 0xea, + 0xb7, 0x8d, 0x68, 0xaa, 0xad, 0x00, 0xe0, 0x49, + 0x20, 0xd0, 0x11, 0x8d, 0xb6, 0xaa, 0xa2, 0x0a, + 0xbd, 0x61, 0x9d, 0x9d, 0x55, 0x9d, 0xca, 0xd0, + 0xf7, 0x4c, 0xbc, 0x9d, 0xa9, 0x40, 0x8d, 0xb6, + 0xaa, 0xa2, 0x0c, 0xbd, 0x6b, 0x9d, 0x9d, 0x55, + 0x9d, 0xca, 0xd0, 0xf7, 0x38, 0xb0, 0x12, 0xad, + 0xb6, 0xaa, 0xd0, 0x04, 0xa9, 0x20, 0xd0, 0x05, + 0x0a, 0x10, 0x05, 0xa9, 0x4c, 0x20, 0xb2, 0xa5, + 0x18, 0x08, 0x20, 0x51, 0xa8, 0xa9, 0x00, 0x8d, + 0x5e, 0xaa, 0x8d, 0x52, 0xaa, 0x28, 0x6a, 0x8d, + 0x51, 0xaa, 0x30, 0x03, 0x6c, 0x5e, 0x9d, 0x6c, + 0x5c, 0x9d, 0x0a, 0x10, 0x19, 0x8d, 0xb6, 0xaa, + 0xa2, 0x0c, 0xbd, 0x77, 0x9d, 0x9d, 0x55, 0x9d, + 0xca, 0xd0, 0xf7, 0xa2, 0x1d, 0xbd, 0x93, 0xaa, + + 0x9d, 0x75, 0xaa, 0xca, 0x10, 0xf7, 0xad, 0xb1, + 0xaa, 0x8d, 0x57, 0xaa, 0x20, 0xd4, 0xa7, 0xad, + 0xb3, 0xaa, 0xf0, 0x09, 0x48, 0x20, 0x9d, 0xa6, + 0x68, 0xa0, 0x00, 0x91, 0x40, 0x20, 0x5b, 0xa7, + 0xad, 0x5f, 0xaa, 0xd0, 0x20, 0xa2, 0x2f, 0xbd, + 0x51, 0x9e, 0x9d, 0xd0, 0x03, 0xca, 0x10, 0xf7, + 0xad, 0x53, 0x9e, 0x8d, 0xf3, 0x03, 0x49, 0xa5, + 0x8d, 0xf4, 0x03, 0xad, 0x52, 0x9e, 0x8d, 0xf2, + 0x03, 0xa9, 0x06, 0xd0, 0x05, 0xad, 0x62, 0xaa, + 0xf0, 0x06, 0x8d, 0x5f, 0xaa, 0x4c, 0x80, 0xa1, + 0x60, 0x4c, 0xbf, 0x9d, 0x4c, 0x84, 0x9d, 0x4c, + 0xfd, 0xaa, 0x4c, 0xb5, 0xb7, 0xad, 0x0f, 0x9d, + 0xac, 0x0e, 0x9d, 0x60, 0xad, 0xc2, 0xaa, 0xac, + 0xc1, 0xaa, 0x60, 0x4c, 0x51, 0xa8, 0xea, 0xea, + 0x4c, 0x59, 0xfa, 0x4c, 0x65, 0xff, 0x4c, 0x58, + 0xff, 0x4c, 0x65, 0xff, 0x4c, 0x65, 0xff, 0x65, + 0xff, 0x20, 0xd1, 0x9e, 0xad, 0x51, 0xaa, 0xf0, + 0x15, 0x48, 0xad, 0x5c, 0xaa, 0x91, 0x28, 0x68, + 0x30, 0x03, 0x4c, 0x26, 0xa6, 0x20, 0xea, 0x9d, + 0xa4, 0x24, 0xa9, 0x60, 0x91, 0x28, 0xad, 0xb3, + 0xaa, 0xf0, 0x03, 0x20, 0x82, 0xa6, 0xa9, 0x03, + 0x8d, 0x52, 0xaa, 0x20, 0xba, 0x9f, 0x20, 0xba, + 0x9e, 0x8d, 0x5c, 0xaa, 0x8e, 0x5a, 0xaa, 0x4c, + 0xb3, 0x9f, 0x6c, 0x38, 0x00, 0x20, 0xd1, 0x9e, + 0xad, 0x52, 0xaa, 0x0a, 0xaa, 0xbd, 0x11, 0x9d, + 0x48, 0xbd, 0x10, 0x9d, 0x48, 0xad, 0x5c, 0xaa, + 0x60, 0x8d, 0x5c, 0xaa, 0x8e, 0x5a, 0xaa, 0x8c, + 0x5b, 0xaa, 0xba, 0xe8, 0xe8, 0x8e, 0x59, 0xaa, + 0xa2, 0x03, 0xbd, 0x53, 0xaa, 0x95, 0x36, 0xca, + 0x10, 0xf8, 0x60, 0xae, 0xb7, 0xaa, 0xf0, 0x03, + 0x4c, 0x78, 0x9f, 0xae, 0x51, 0xaa, 0xf0, 0x08, + 0xc9, 0xbf, 0xf0, 0x75, 0xc5, 0x33, 0xf0, 0x27, + + 0xa2, 0x02, 0x8e, 0x52, 0xaa, 0xcd, 0xb2, 0xaa, + 0xd0, 0x19, 0xca, 0x8e, 0x52, 0xaa, 0xca, 0x8e, + 0x5d, 0xaa, 0xae, 0x5d, 0xaa, 0x9d, 0x00, 0x02, + 0xe8, 0x8e, 0x5d, 0xaa, 0xc9, 0x8d, 0xd0, 0x75, + 0x4c, 0xcd, 0x9f, 0xc9, 0x8d, 0xd0, 0x7d, 0xa2, + 0x00, 0x8e, 0x52, 0xaa, 0x4c, 0xa4, 0x9f, 0xa2, + 0x00, 0x8e, 0x52, 0xaa, 0xc9, 0x8d, 0xf0, 0x07, + 0xad, 0xb3, 0xaa, 0xf0, 0x67, 0xd0, 0x5e, 0x48, + 0x38, 0xad, 0xb3, 0xaa, 0xd0, 0x03, 0x20, 0x5e, + 0xa6, 0x68, 0x90, 0xec, 0xae, 0x5a, 0xaa, 0x4c, + 0x15, 0x9f, 0xc9, 0x8d, 0xd0, 0x05, 0xa9, 0x05, + 0x8d, 0x52, 0xaa, 0x20, 0x0e, 0xa6, 0x4c, 0x99, + 0x9f, 0xcd, 0xb2, 0xaa, 0xf0, 0x85, 0xc9, 0x8a, + 0xf0, 0xf1, 0xa2, 0x04, 0x8e, 0x52, 0xaa, 0xd0, + 0xe1, 0xa9, 0x00, 0x8d, 0x52, 0xaa, 0xf0, 0x25, + 0xa9, 0x00, 0x8d, 0xb7, 0xaa, 0x20, 0x51, 0xa8, + 0x4c, 0xdc, 0xa4, 0xad, 0x00, 0x02, 0xcd, 0xb2, + 0xaa, 0xf0, 0x0a, 0xa9, 0x8d, 0x8d, 0x00, 0x02, + 0xa2, 0x00, 0x8e, 0x5a, 0xaa, 0xa9, 0x40, 0xd0, + 0x06, 0xa9, 0x10, 0xd0, 0x02, 0xa9, 0x20, 0x2d, + 0x5e, 0xaa, 0xf0, 0x0f, 0x20, 0xba, 0x9f, 0x20, + 0xc5, 0x9f, 0x8d, 0x5c, 0xaa, 0x8c, 0x5b, 0xaa, + 0x8e, 0x5a, 0xaa, 0x20, 0x51, 0xa8, 0xae, 0x59, + 0xaa, 0x9a, 0xad, 0x5c, 0xaa, 0xac, 0x5b, 0xaa, + 0xae, 0x5a, 0xaa, 0x38, 0x60, 0x6c, 0x36, 0x00, + 0xa9, 0x8d, 0x4c, 0xc5, 0x9f, 0xa0, 0xff, 0x8c, + 0x5f, 0xaa, 0xc8, 0x8c, 0x62, 0xaa, 0xee, 0x5f, + 0xaa, 0xa2, 0x00, 0x08, 0xbd, 0x00, 0x02, 0xcd, + 0xb2, 0xaa, 0xd0, 0x01, 0xe8, 0x8e, 0x5d, 0xaa, + 0x20, 0xa4, 0xa1, 0x29, 0x7f, 0x59, 0x84, 0xa8, + 0xc8, 0x0a, 0xf0, 0x02, 0x68, 0x08, 0x90, 0xf0, + 0x28, 0xf0, 0x20, 0xb9, 0x84, 0xa8, 0xd0, 0xd6, + + 0xad, 0x00, 0x02, 0xcd, 0xb2, 0xaa, 0xf0, 0x03, + 0x4c, 0xa4, 0x9f, 0xad, 0x01, 0x02, 0xc9, 0x8d, + 0xd0, 0x06, 0x20, 0x5b, 0xa7, 0x4c, 0x95, 0x9f, + 0x4c, 0xc4, 0xa6, 0x0e, 0x5f, 0xaa, 0xac, 0x5f, + 0xaa, 0x20, 0x5e, 0xa6, 0x90, 0x0c, 0xa9, 0x02, + 0x39, 0x09, 0xa9, 0xf0, 0x05, 0xa9, 0x0f, 0x4c, + 0xd2, 0xa6, 0xc0, 0x06, 0xd0, 0x02, 0x84, 0x33, + 0xa9, 0x20, 0x39, 0x09, 0xa9, 0xf0, 0x61, 0x20, + 0x95, 0xa0, 0x08, 0x20, 0xa4, 0xa1, 0xf0, 0x1e, + 0x0a, 0x90, 0x05, 0x30, 0x03, 0x4c, 0x00, 0xa0, + 0x6a, 0x4c, 0x59, 0xa0, 0x20, 0x93, 0xa1, 0xf0, + 0x0d, 0x99, 0x75, 0xaa, 0xc8, 0xc0, 0x3c, 0x90, + 0xf3, 0x20, 0x93, 0xa1, 0xd0, 0xfb, 0x28, 0xd0, + 0x0f, 0xac, 0x5f, 0xaa, 0xa9, 0x10, 0x39, 0x09, + 0xa9, 0xf0, 0x0c, 0xa0, 0x1e, 0x08, 0xd0, 0xcb, + 0xad, 0x93, 0xaa, 0xc9, 0xa0, 0xf0, 0x13, 0xad, + 0x75, 0xaa, 0xc9, 0xa0, 0xd0, 0x4b, 0xac, 0x5f, + 0xaa, 0xa9, 0xc0, 0x39, 0x09, 0xa9, 0xf0, 0x02, + 0x10, 0x3f, 0x4c, 0x00, 0xa0, 0xa0, 0x3c, 0xa9, + 0xa0, 0x99, 0x74, 0xaa, 0x88, 0xd0, 0xfa, 0x60, + 0x8d, 0x75, 0xaa, 0xa9, 0x0c, 0x39, 0x09, 0xa9, + 0xf0, 0x27, 0x20, 0xb9, 0xa1, 0xb0, 0x1f, 0xa8, + 0xd0, 0x17, 0xe0, 0x11, 0xb0, 0x13, 0xac, 0x5f, + 0xaa, 0xa9, 0x08, 0x39, 0x09, 0xa9, 0xf0, 0x06, + 0xe0, 0x08, 0xb0, 0xce, 0x90, 0x0b, 0x8a, 0xd0, + 0x08, 0xa9, 0x02, 0x4c, 0xd2, 0xa6, 0x4c, 0xc4, + 0xa6, 0xa9, 0x00, 0x8d, 0x65, 0xaa, 0x8d, 0x74, + 0xaa, 0x8d, 0x66, 0xaa, 0x8d, 0x6c, 0xaa, 0x8d, + 0x6d, 0xaa, 0x20, 0xdc, 0xbf, 0xad, 0x5d, 0xaa, + 0x20, 0xa4, 0xa1, 0xd0, 0x1f, 0xc9, 0x8d, 0xd0, + 0xf7, 0xae, 0x5f, 0xaa, 0xad, 0x65, 0xaa, 0x1d, + 0x0a, 0xa9, 0x5d, 0x0a, 0xa9, 0xd0, 0x93, 0xae, + + 0x63, 0xaa, 0xf0, 0x76, 0x8d, 0x63, 0xaa, 0x8e, + 0x5d, 0xaa, 0xd0, 0xdc, 0xa2, 0x0a, 0xdd, 0x40, + 0xa9, 0xf0, 0x05, 0xca, 0xd0, 0xf8, 0xf0, 0xb6, + 0xbd, 0x4a, 0xa9, 0x30, 0x47, 0x0d, 0x65, 0xaa, + 0x8d, 0x65, 0xaa, 0xca, 0x8e, 0x64, 0xaa, 0x20, + 0xb9, 0xa1, 0xb0, 0xa2, 0xad, 0x64, 0xaa, 0x0a, + 0x0a, 0xa8, 0xa5, 0x45, 0xd0, 0x09, 0xa5, 0x44, + 0xd9, 0x55, 0xa9, 0x90, 0x8c, 0xa5, 0x45, 0xd9, + 0x58, 0xa9, 0x90, 0x0b, 0xd0, 0x83, 0xa5, 0x44, + 0xd9, 0x57, 0xa9, 0x90, 0x02, 0xd0, 0xf5, 0xad, + 0x63, 0xaa, 0xd0, 0x94, 0x98, 0x4a, 0xa8, 0xa5, + 0x45, 0x99, 0x67, 0xaa, 0xa5, 0x44, 0x99, 0x66, + 0xaa, 0x4c, 0xe8, 0xa0, 0x48, 0xa9, 0x80, 0x0d, + 0x65, 0xaa, 0x8d, 0x65, 0xaa, 0x68, 0x29, 0x7f, + 0x0d, 0x74, 0xaa, 0x8d, 0x74, 0xaa, 0xd0, 0xe9, + 0xf0, 0x9c, 0x20, 0x80, 0xa1, 0x4c, 0x83, 0x9f, + 0x20, 0x5b, 0xa7, 0x20, 0xae, 0xa1, 0xad, 0x5f, + 0xaa, 0xaa, 0xbd, 0x1f, 0x9d, 0x48, 0xbd, 0x1e, + 0x9d, 0x48, 0x60, 0xae, 0x5d, 0xaa, 0xbd, 0x00, + 0x02, 0xc9, 0x8d, 0xf0, 0x06, 0xe8, 0x8e, 0x5d, + 0xaa, 0xc9, 0xac, 0x60, 0x20, 0x93, 0xa1, 0xf0, + 0xfa, 0xc9, 0xa0, 0xf0, 0xf7, 0x60, 0xa9, 0x00, + 0xa0, 0x16, 0x99, 0xba, 0xb5, 0x88, 0xd0, 0xfa, + 0x60, 0xa9, 0x00, 0x85, 0x44, 0x85, 0x45, 0x20, + 0xa4, 0xa1, 0x08, 0xc9, 0xa4, 0xf0, 0x3c, 0x28, + 0x4c, 0xce, 0xa1, 0x20, 0xa4, 0xa1, 0xd0, 0x06, + 0xa6, 0x44, 0xa5, 0x45, 0x18, 0x60, 0x38, 0xe9, + 0xb0, 0x30, 0x21, 0xc9, 0x0a, 0xb0, 0x1d, 0x20, + 0xfe, 0xa1, 0x65, 0x44, 0xaa, 0xa9, 0x00, 0x65, + 0x45, 0xa8, 0x20, 0xfe, 0xa1, 0x20, 0xfe, 0xa1, + 0x8a, 0x65, 0x44, 0x85, 0x44, 0x98, 0x65, 0x45, + 0x85, 0x45, 0x90, 0xcf, 0x38, 0x60, 0x06, 0x44, + + 0x26, 0x45, 0x60, 0x28, 0x20, 0xa4, 0xa1, 0xf0, + 0xc5, 0x38, 0xe9, 0xb0, 0x30, 0xee, 0xc9, 0x0a, + 0x90, 0x08, 0xe9, 0x07, 0x30, 0xe6, 0xc9, 0x10, + 0xb0, 0xe2, 0xa2, 0x04, 0x20, 0xfe, 0xa1, 0xca, + 0xd0, 0xfa, 0x05, 0x44, 0x85, 0x44, 0x4c, 0x04, + 0xa2, 0xa5, 0x44, 0x4c, 0x95, 0xfe, 0xa5, 0x44, + 0x4c, 0x8b, 0xfe, 0xad, 0x5e, 0xaa, 0x0d, 0x74, + 0xaa, 0x8d, 0x5e, 0xaa, 0x60, 0x2c, 0x74, 0xaa, + 0x50, 0x03, 0x20, 0xc8, 0x9f, 0xa9, 0x70, 0x4d, + 0x74, 0xaa, 0x2d, 0x5e, 0xaa, 0x8d, 0x5e, 0xaa, + 0x60, 0xa9, 0x00, 0x8d, 0xb3, 0xaa, 0xa5, 0x44, + 0x48, 0x20, 0x16, 0xa3, 0x68, 0x8d, 0x57, 0xaa, + 0x4c, 0xd4, 0xa7, 0xa9, 0x05, 0x20, 0xaa, 0xa2, + 0x20, 0x64, 0xa7, 0xa0, 0x00, 0x98, 0x91, 0x40, + 0x60, 0xa9, 0x07, 0xd0, 0x02, 0xa9, 0x08, 0x20, + 0xaa, 0xa2, 0x4c, 0xea, 0xa2, 0xa9, 0x0c, 0xd0, + 0xf6, 0xad, 0x08, 0x9d, 0x8d, 0xbd, 0xb5, 0xad, + 0x09, 0x9d, 0x8d, 0xbe, 0xb5, 0xa9, 0x09, 0x8d, + 0x63, 0xaa, 0x20, 0xc8, 0xa2, 0x4c, 0xea, 0xa2, + 0x20, 0xa3, 0xa2, 0x20, 0x8c, 0xa6, 0xd0, 0xfb, + 0x4c, 0x71, 0xb6, 0xa9, 0x00, 0x4c, 0xd5, 0xa3, + 0xa9, 0x01, 0x8d, 0x63, 0xaa, 0xad, 0x6c, 0xaa, + 0xd0, 0x0a, 0xad, 0x6d, 0xaa, 0xd0, 0x05, 0xa9, + 0x01, 0x8d, 0x6c, 0xaa, 0xad, 0x6c, 0xaa, 0x8d, + 0xbd, 0xb5, 0xad, 0x6d, 0xaa, 0x8d, 0xbe, 0xb5, + 0x20, 0xea, 0xa2, 0xa5, 0x45, 0xd0, 0x03, 0x4c, + 0xc8, 0xa6, 0x85, 0x41, 0xa5, 0x44, 0x85, 0x40, + 0x20, 0x43, 0xa7, 0x20, 0x4e, 0xa7, 0x20, 0x1a, + 0xa7, 0xad, 0x63, 0xaa, 0x8d, 0xbb, 0xb5, 0x4c, + 0xa8, 0xa6, 0xad, 0x75, 0xaa, 0xc9, 0xa0, 0xf0, + 0x25, 0x20, 0x64, 0xa7, 0xb0, 0x3a, 0x20, 0xfc, + 0xa2, 0x4c, 0xea, 0xa2, 0x20, 0xaf, 0xa7, 0xd0, + + 0x05, 0xa9, 0x00, 0x8d, 0xb3, 0xaa, 0xa0, 0x00, + 0x98, 0x91, 0x40, 0x20, 0x4e, 0xa7, 0xa9, 0x02, + 0x8d, 0xbb, 0xb5, 0x4c, 0xa8, 0xa6, 0x20, 0x92, + 0xa7, 0xd0, 0x05, 0x20, 0x9a, 0xa7, 0xf0, 0x10, + 0x20, 0xaf, 0xa7, 0xf0, 0xf6, 0x20, 0xaa, 0xa7, + 0xf0, 0xf1, 0x20, 0xfc, 0xa2, 0x4c, 0x16, 0xa3, + 0x60, 0xa9, 0x09, 0x2d, 0x65, 0xaa, 0xc9, 0x09, + 0xf0, 0x03, 0x4c, 0x00, 0xa0, 0xa9, 0x04, 0x20, + 0xd5, 0xa3, 0xad, 0x73, 0xaa, 0xac, 0x72, 0xaa, + 0x20, 0xe0, 0xa3, 0xad, 0x6d, 0xaa, 0xac, 0x6c, + 0xaa, 0x20, 0xe0, 0xa3, 0xad, 0x73, 0xaa, 0xac, + 0x72, 0xaa, 0x4c, 0xff, 0xa3, 0x20, 0xa8, 0xa2, + 0xa9, 0x7f, 0x2d, 0xc2, 0xb5, 0xc9, 0x04, 0xf0, + 0x03, 0x4c, 0xd0, 0xa6, 0xa9, 0x04, 0x20, 0xd5, + 0xa3, 0x20, 0x7a, 0xa4, 0xaa, 0xad, 0x65, 0xaa, + 0x29, 0x01, 0xd0, 0x06, 0x8e, 0x72, 0xaa, 0x8c, + 0x73, 0xaa, 0x20, 0x7a, 0xa4, 0xae, 0x72, 0xaa, + 0xac, 0x73, 0xaa, 0x4c, 0x71, 0xa4, 0x20, 0x5d, + 0xa3, 0x20, 0x51, 0xa8, 0x6c, 0x72, 0xaa, 0xad, + 0xb6, 0xaa, 0xf0, 0x20, 0xa5, 0xd6, 0x10, 0x03, + 0x4c, 0xcc, 0xa6, 0xa9, 0x02, 0x20, 0xd5, 0xa3, + 0x38, 0xa5, 0xaf, 0xe5, 0x67, 0xa8, 0xa5, 0xb0, + 0xe5, 0x68, 0x20, 0xe0, 0xa3, 0xa5, 0x68, 0xa4, + 0x67, 0x4c, 0xff, 0xa3, 0xa9, 0x01, 0x20, 0xd5, + 0xa3, 0x38, 0xa5, 0x4c, 0xe5, 0xca, 0xa8, 0xa5, + 0x4d, 0xe5, 0xcb, 0x20, 0xe0, 0xa3, 0xa5, 0xcb, + 0xa4, 0xca, 0x4c, 0xff, 0xa3, 0x8d, 0xc2, 0xb5, + 0x48, 0x20, 0xa8, 0xa2, 0x68, 0x4c, 0xc4, 0xa7, + 0x8c, 0xc1, 0xb5, 0x8c, 0xc3, 0xb5, 0x8d, 0xc2, + 0xb5, 0xa9, 0x04, 0x8d, 0xbb, 0xb5, 0xa9, 0x01, + 0x8d, 0xbc, 0xb5, 0x20, 0xa8, 0xa6, 0xad, 0xc2, + 0xb5, 0x8d, 0xc3, 0xb5, 0x4c, 0xa8, 0xa6, 0x8c, + + 0xc3, 0xb5, 0x8d, 0xc4, 0xb5, 0xa9, 0x02, 0x4c, + 0x86, 0xb6, 0x20, 0xa8, 0xa6, 0x4c, 0xea, 0xa2, + 0x4c, 0xd0, 0xa6, 0x20, 0x16, 0xa3, 0x20, 0xa8, + 0xa2, 0xa9, 0x23, 0x2d, 0xc2, 0xb5, 0xf0, 0xf0, + 0x8d, 0xc2, 0xb5, 0xad, 0xb6, 0xaa, 0xf0, 0x28, + 0xa9, 0x02, 0x20, 0xb1, 0xa4, 0x20, 0x7a, 0xa4, + 0x18, 0x65, 0x67, 0xaa, 0x98, 0x65, 0x68, 0xc5, + 0x74, 0xb0, 0x70, 0x85, 0xb0, 0x85, 0x6a, 0x86, + 0xaf, 0x86, 0x69, 0xa6, 0x67, 0xa4, 0x68, 0x20, + 0x71, 0xa4, 0x20, 0x51, 0xa8, 0x6c, 0x60, 0x9d, + 0xa9, 0x01, 0x20, 0xb1, 0xa4, 0x20, 0x7a, 0xa4, + 0x38, 0xa5, 0x4c, 0xed, 0x60, 0xaa, 0xaa, 0xa5, + 0x4d, 0xed, 0x61, 0xaa, 0x90, 0x45, 0xa8, 0xc4, + 0x4b, 0x90, 0x40, 0xf0, 0x3e, 0x84, 0xcb, 0x86, + 0xca, 0x8e, 0xc3, 0xb5, 0x8c, 0xc4, 0xb5, 0x4c, + 0x0a, 0xa4, 0xad, 0x0a, 0x9d, 0x8d, 0xc3, 0xb5, + 0xad, 0x0b, 0x9d, 0x8d, 0xc4, 0xb5, 0xa9, 0x00, + 0x8d, 0xc2, 0xb5, 0xa9, 0x02, 0x8d, 0xc1, 0xb5, + 0xa9, 0x03, 0x8d, 0xbb, 0xb5, 0xa9, 0x02, 0x8d, + 0xbc, 0xb5, 0x20, 0xa8, 0xa6, 0xad, 0x61, 0xaa, + 0x8d, 0xc2, 0xb5, 0xa8, 0xad, 0x60, 0xaa, 0x8d, + 0xc1, 0xb5, 0x60, 0x20, 0xea, 0xa2, 0x4c, 0xcc, + 0xa6, 0xcd, 0xc2, 0xb5, 0xf0, 0x1a, 0xae, 0x5f, + 0xaa, 0x8e, 0x62, 0xaa, 0x4a, 0xf0, 0x03, 0x4c, + 0x9e, 0xa5, 0xa2, 0x1d, 0xbd, 0x75, 0xaa, 0x9d, + 0x93, 0xaa, 0xca, 0x10, 0xf7, 0x4c, 0x7a, 0xa5, + 0x60, 0xad, 0xb6, 0xaa, 0xf0, 0x03, 0x8d, 0xb7, + 0xaa, 0x20, 0x13, 0xa4, 0x20, 0xc8, 0x9f, 0x20, + 0x51, 0xa8, 0x6c, 0x58, 0x9d, 0xa5, 0x4a, 0x85, + 0xcc, 0xa5, 0x4b, 0x85, 0xcd, 0x6c, 0x56, 0x9d, + 0x20, 0x16, 0xa4, 0x20, 0xc8, 0x9f, 0x20, 0x51, + 0xa8, 0x6c, 0x56, 0x9d, 0x20, 0x65, 0xd6, 0x85, + + 0x33, 0x85, 0xd8, 0x4c, 0xd2, 0xd7, 0x20, 0x65, + 0x0e, 0x85, 0x33, 0x85, 0xd8, 0x4c, 0xd4, 0x0f, + 0x20, 0x26, 0xa5, 0xa9, 0x05, 0x8d, 0x52, 0xaa, + 0x4c, 0x83, 0x9f, 0x20, 0x26, 0xa5, 0xa9, 0x01, + 0x8d, 0x51, 0xaa, 0x4c, 0x83, 0x9f, 0x20, 0x64, + 0xa7, 0x90, 0x06, 0x20, 0xa3, 0xa2, 0x4c, 0x34, + 0xa5, 0x20, 0x4e, 0xa7, 0xad, 0x65, 0xaa, 0x29, + 0x06, 0xf0, 0x13, 0xa2, 0x03, 0xbd, 0x6e, 0xaa, + 0x9d, 0xbd, 0xb5, 0xca, 0x10, 0xf7, 0xa9, 0x0a, + 0x8d, 0xbb, 0xb5, 0x20, 0xa8, 0xa6, 0x60, 0xa9, + 0x40, 0x2d, 0x65, 0xaa, 0xf0, 0x05, 0xad, 0x66, + 0xaa, 0xd0, 0x05, 0xa9, 0xfe, 0x8d, 0x66, 0xaa, + 0xad, 0x0d, 0x9d, 0x8d, 0xbc, 0xb5, 0xa9, 0x0b, + 0x20, 0xaa, 0xa2, 0x4c, 0x97, 0xa3, 0xa9, 0x06, + 0x20, 0xaa, 0xa2, 0xad, 0xbf, 0xb5, 0x8d, 0x66, + 0xaa, 0x60, 0xa9, 0x4c, 0x20, 0xb2, 0xa5, 0xf0, + 0x2e, 0xa9, 0x00, 0x8d, 0xb6, 0xaa, 0xa0, 0x1e, + 0x20, 0x97, 0xa0, 0xa2, 0x09, 0xbd, 0xb7, 0xaa, + 0x9d, 0x74, 0xaa, 0xca, 0xd0, 0xf7, 0xa9, 0xc0, + 0x8d, 0x51, 0xaa, 0x4c, 0xd1, 0xa4, 0xa9, 0x20, + 0x20, 0xb2, 0xa5, 0xf0, 0x05, 0xa9, 0x01, 0x4c, + 0xd2, 0xa6, 0xa9, 0x00, 0x8d, 0xb7, 0xaa, 0x4c, + 0x84, 0x9d, 0xcd, 0x00, 0xe0, 0xf0, 0x0e, 0x8d, + 0x80, 0xc0, 0xcd, 0x00, 0xe0, 0xf0, 0x06, 0x8d, + 0x81, 0xc0, 0xcd, 0x00, 0xe0, 0x60, 0x20, 0xa3, + 0xa2, 0xad, 0x4f, 0xaa, 0x8d, 0xb4, 0xaa, 0xad, + 0x50, 0xaa, 0x8d, 0xb5, 0xaa, 0xad, 0x75, 0xaa, + 0x8d, 0xb3, 0xaa, 0xd0, 0x0e, 0x20, 0x64, 0xa7, + 0x90, 0x06, 0x20, 0xa3, 0xa2, 0x4c, 0xeb, 0xa5, + 0x20, 0x4e, 0xa7, 0xad, 0x65, 0xaa, 0x29, 0x04, + 0xf0, 0x1b, 0xad, 0x6e, 0xaa, 0xd0, 0x08, 0xae, + 0x6f, 0xaa, 0xf0, 0x11, 0xce, 0x6f, 0xaa, 0xce, + + 0x6e, 0xaa, 0x20, 0x8c, 0xa6, 0xf0, 0x38, 0xc9, + 0x8d, 0xd0, 0xf7, 0xf0, 0xe5, 0x60, 0x20, 0x5e, + 0xa6, 0xb0, 0x66, 0xad, 0x5c, 0xaa, 0x8d, 0xc3, + 0xb5, 0xa9, 0x04, 0x8d, 0xbb, 0xb5, 0xa9, 0x01, + 0x8d, 0xbc, 0xb5, 0x4c, 0xa8, 0xa6, 0x20, 0x5e, + 0xa6, 0xb0, 0x4e, 0xa9, 0x06, 0x8d, 0x52, 0xaa, + 0x20, 0x8c, 0xa6, 0xd0, 0x0f, 0x20, 0xfc, 0xa2, + 0xa9, 0x03, 0xcd, 0x52, 0xaa, 0xf0, 0xce, 0xa9, + 0x05, 0x4c, 0xd2, 0xa6, 0xc9, 0xe0, 0x90, 0x02, + 0x29, 0x7f, 0x8d, 0x5c, 0xaa, 0xae, 0x5a, 0xaa, + 0xf0, 0x09, 0xca, 0xbd, 0x00, 0x02, 0x09, 0x80, + 0x9d, 0x00, 0x02, 0x4c, 0xb3, 0x9f, 0x48, 0xad, + 0xb6, 0xaa, 0xf0, 0x0e, 0xa6, 0x76, 0xe8, 0xf0, + 0x0d, 0xa6, 0x33, 0xe0, 0xdd, 0xf0, 0x07, 0x68, + 0x18, 0x60, 0xa5, 0xd9, 0x30, 0xf9, 0x68, 0x38, + 0x60, 0x20, 0xfc, 0xa2, 0x20, 0x5b, 0xa7, 0x4c, + 0xb3, 0x9f, 0x20, 0x9d, 0xa6, 0x20, 0x4e, 0xa7, + 0xa9, 0x03, 0xd0, 0xa1, 0xa9, 0x03, 0x8d, 0xbb, + 0xb5, 0xa9, 0x01, 0x8d, 0xbc, 0xb5, 0x20, 0xa8, + 0xa6, 0xad, 0xc3, 0xb5, 0x60, 0xad, 0xb5, 0xaa, + 0x85, 0x41, 0xad, 0xb4, 0xaa, 0x85, 0x40, 0x60, + 0x20, 0x06, 0xab, 0x90, 0x16, 0xad, 0xc5, 0xb5, + 0xc9, 0x05, 0xf0, 0x03, 0x4c, 0x5e, 0xb6, 0x4c, + 0x92, 0xb6, 0xea, 0xea, 0xea, 0xea, 0xa2, 0x00, + 0x8e, 0xc3, 0xb5, 0x60, 0xa9, 0x0b, 0xd0, 0x0a, + 0xa9, 0x0c, 0xd0, 0x06, 0xa9, 0x0e, 0xd0, 0x02, + 0xa9, 0x0d, 0x8d, 0x5c, 0xaa, 0x20, 0xe6, 0xbf, + 0xad, 0xb6, 0xaa, 0xf0, 0x04, 0xa5, 0xd8, 0x30, + 0x0e, 0xa2, 0x00, 0x20, 0x02, 0xa7, 0xae, 0x5c, + 0xaa, 0x20, 0x02, 0xa7, 0x20, 0xc8, 0x9f, 0x20, + 0x51, 0xa8, 0x20, 0x5e, 0xa6, 0xae, 0x5c, 0xaa, + 0xa9, 0x03, 0xb0, 0x03, 0x6c, 0x5a, 0x9d, 0x6c, + + 0x5e, 0x9d, 0xbd, 0x3f, 0xaa, 0xaa, 0x8e, 0x63, + 0xaa, 0xbd, 0x71, 0xa9, 0x48, 0x09, 0x80, 0x20, + 0xc5, 0x9f, 0xae, 0x63, 0xaa, 0xe8, 0x68, 0x10, + 0xed, 0x60, 0xad, 0x66, 0xaa, 0x8d, 0xbf, 0xb5, + 0xad, 0x68, 0xaa, 0x8d, 0xc0, 0xb5, 0xad, 0x6a, + 0xaa, 0x8d, 0xc1, 0xb5, 0xad, 0x06, 0x9d, 0x8d, + 0xc3, 0xb5, 0xad, 0x07, 0x9d, 0x8d, 0xc4, 0xb5, + 0xa5, 0x40, 0x8d, 0x4f, 0xaa, 0xa5, 0x41, 0x8d, + 0x50, 0xaa, 0x60, 0xa0, 0x1d, 0xb9, 0x75, 0xaa, + 0x91, 0x40, 0x88, 0x10, 0xf8, 0x60, 0xa0, 0x1e, + 0xb1, 0x40, 0x99, 0xa9, 0xb5, 0xc8, 0xc0, 0x26, + 0xd0, 0xf6, 0x60, 0xa0, 0x00, 0x8c, 0x51, 0xaa, + 0x8c, 0x52, 0xaa, 0x60, 0xa9, 0x00, 0x85, 0x45, + 0x20, 0x92, 0xa7, 0x4c, 0x73, 0xa7, 0x20, 0x9a, + 0xa7, 0xf0, 0x1d, 0x20, 0xaa, 0xa7, 0xd0, 0x0a, + 0xa5, 0x40, 0x85, 0x44, 0xa5, 0x41, 0x85, 0x45, + 0xd0, 0xec, 0xa0, 0x1d, 0xb1, 0x40, 0xd9, 0x75, + 0xaa, 0xd0, 0xe3, 0x88, 0x10, 0xf6, 0x18, 0x60, + 0x38, 0x60, 0xad, 0x00, 0x9d, 0xae, 0x01, 0x9d, + 0xd0, 0x0a, 0xa0, 0x25, 0xb1, 0x40, 0xf0, 0x09, + 0xaa, 0x88, 0xb1, 0x40, 0x86, 0x41, 0x85, 0x40, + 0x8a, 0x60, 0xa0, 0x00, 0xb1, 0x40, 0x60, 0xad, + 0xb3, 0xaa, 0xf0, 0x0e, 0xad, 0xb4, 0xaa, 0xc5, + 0x40, 0xd0, 0x08, 0xad, 0xb5, 0xaa, 0xc5, 0x41, + 0xf0, 0x01, 0xca, 0x60, 0x4d, 0xc2, 0xb5, 0xf0, + 0x0a, 0x29, 0x7f, 0xf0, 0x06, 0x20, 0xea, 0xa2, + 0x4c, 0xd0, 0xa6, 0x60, 0x38, 0xad, 0x00, 0x9d, + 0x85, 0x40, 0xad, 0x01, 0x9d, 0x85, 0x41, 0xad, + 0x57, 0xaa, 0x8d, 0x63, 0xaa, 0xa0, 0x00, 0x98, + 0x91, 0x40, 0xa0, 0x1e, 0x38, 0xa5, 0x40, 0xe9, + 0x2d, 0x91, 0x40, 0x48, 0xa5, 0x41, 0xe9, 0x00, + 0xc8, 0x91, 0x40, 0xaa, 0xca, 0x68, 0x48, 0xc8, + + 0x91, 0x40, 0x8a, 0xc8, 0x91, 0x40, 0xaa, 0xca, + 0x68, 0x48, 0xc8, 0x91, 0x40, 0xc8, 0x8a, 0x91, + 0x40, 0xce, 0x63, 0xaa, 0xf0, 0x17, 0xaa, 0x68, + 0x38, 0xe9, 0x26, 0xc8, 0x91, 0x40, 0x48, 0x8a, + 0xe9, 0x00, 0xc8, 0x91, 0x40, 0x85, 0x41, 0x68, + 0x85, 0x40, 0x4c, 0xe5, 0xa7, 0x48, 0xa9, 0x00, + 0xc8, 0x91, 0x40, 0xc8, 0x91, 0x40, 0xad, 0xb6, + 0xaa, 0xf0, 0x0b, 0x68, 0x85, 0x74, 0x85, 0x70, + 0x68, 0x85, 0x73, 0x85, 0x6f, 0x60, 0x68, 0x85, + 0x4d, 0x85, 0xcb, 0x68, 0x85, 0x4c, 0x85, 0xca, + 0x60, 0xa5, 0x39, 0xcd, 0x03, 0x9d, 0xf0, 0x12, + 0x8d, 0x56, 0xaa, 0xa5, 0x38, 0x8d, 0x55, 0xaa, + 0xad, 0x02, 0x9d, 0x85, 0x38, 0xad, 0x03, 0x9d, + 0x85, 0x39, 0xa5, 0x37, 0xcd, 0x05, 0x9d, 0xf0, + 0x12, 0x8d, 0x54, 0xaa, 0xa5, 0x36, 0x8d, 0x53, + 0xaa, 0xad, 0x04, 0x9d, 0x85, 0x36, 0xad, 0x05, + 0x9d, 0x85, 0x37, 0x60, 0x49, 0x4e, 0x49, 0xd4, + 0x4c, 0x4f, 0x41, 0xc4, 0x53, 0x41, 0x56, 0xc5, + 0x52, 0x55, 0xce, 0x43, 0x48, 0x41, 0x49, 0xce, + 0x44, 0x45, 0x4c, 0x45, 0x54, 0xc5, 0x4c, 0x4f, + 0x43, 0xcb, 0x55, 0x4e, 0x4c, 0x4f, 0x43, 0xcb, + 0x43, 0x4c, 0x4f, 0x53, 0xc5, 0x52, 0x45, 0x41, + 0xc4, 0x45, 0x58, 0x45, 0xc3, 0x57, 0x52, 0x49, + 0x54, 0xc5, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x49, + 0x4f, 0xce, 0x4f, 0x50, 0x45, 0xce, 0x41, 0x50, + 0x50, 0x45, 0x4e, 0xc4, 0x52, 0x45, 0x4e, 0x41, + 0x4d, 0xc5, 0x43, 0x41, 0x54, 0x41, 0x4c, 0x4f, + 0xc7, 0x4d, 0x4f, 0xce, 0x4e, 0x4f, 0x4d, 0x4f, + 0xce, 0x50, 0x52, 0xa3, 0x49, 0x4e, 0xa3, 0x4d, + 0x41, 0x58, 0x46, 0x49, 0x4c, 0x45, 0xd3, 0x46, + 0xd0, 0x49, 0x4e, 0xd4, 0x42, 0x53, 0x41, 0x56, + 0xc5, 0x42, 0x4c, 0x4f, 0x41, 0xc4, 0x42, 0x52, + + 0x55, 0xce, 0x56, 0x45, 0x52, 0x49, 0x46, 0xd9, + 0x00, 0x21, 0x70, 0xa0, 0x70, 0xa1, 0x70, 0xa0, + 0x70, 0x20, 0x70, 0x20, 0x70, 0x20, 0x70, 0x20, + 0x70, 0x60, 0x00, 0x22, 0x06, 0x20, 0x74, 0x22, + 0x06, 0x22, 0x04, 0x23, 0x78, 0x22, 0x70, 0x30, + 0x70, 0x40, 0x70, 0x40, 0x80, 0x40, 0x80, 0x08, + 0x00, 0x08, 0x00, 0x04, 0x00, 0x40, 0x70, 0x40, + 0x00, 0x21, 0x79, 0x20, 0x71, 0x20, 0x71, 0x20, + 0x70, 0xd6, 0xc4, 0xd3, 0xcc, 0xd2, 0xc2, 0xc1, + 0xc3, 0xc9, 0xcf, 0x40, 0x20, 0x10, 0x08, 0x04, + 0x02, 0x01, 0xc0, 0xa0, 0x90, 0x00, 0x00, 0xfe, + 0x00, 0x01, 0x00, 0x02, 0x00, 0x01, 0x00, 0x07, + 0x00, 0x01, 0x00, 0xff, 0x7f, 0x00, 0x00, 0xff, + 0x7f, 0x00, 0x00, 0xff, 0x7f, 0x00, 0x00, 0xff, + 0xff, 0x0d, 0x07, 0x8d, 0x4c, 0x41, 0x4e, 0x47, + 0x55, 0x41, 0x47, 0x45, 0x20, 0x4e, 0x4f, 0x54, + 0x20, 0x41, 0x56, 0x41, 0x49, 0x4c, 0x41, 0x42, + 0x4c, 0xc5, 0x52, 0x41, 0x4e, 0x47, 0x45, 0x20, + 0x45, 0x52, 0x52, 0x4f, 0xd2, 0x57, 0x52, 0x49, + 0x54, 0x45, 0x20, 0x50, 0x52, 0x4f, 0x54, 0x45, + 0x43, 0x54, 0x45, 0xc4, 0x45, 0x4e, 0x44, 0x20, + 0x4f, 0x46, 0x20, 0x44, 0x41, 0x54, 0xc1, 0x46, + 0x49, 0x4c, 0x45, 0x20, 0x4e, 0x4f, 0x54, 0x20, + 0x46, 0x4f, 0x55, 0x4e, 0xc4, 0x56, 0x4f, 0x4c, + 0x55, 0x4d, 0x45, 0x20, 0x4d, 0x49, 0x53, 0x4d, + 0x41, 0x54, 0x43, 0xc8, 0x49, 0x2f, 0x4f, 0x20, + 0x45, 0x52, 0x52, 0x4f, 0xd2, 0x44, 0x49, 0x53, + 0x4b, 0x20, 0x46, 0x55, 0x4c, 0xcc, 0x46, 0x49, + 0x4c, 0x45, 0x20, 0x4c, 0x4f, 0x43, 0x4b, 0x45, + 0xc4, 0x53, 0x59, 0x4e, 0x54, 0x41, 0x58, 0x20, + 0x45, 0x52, 0x52, 0x4f, 0xd2, 0x4e, 0x4f, 0x20, + 0x42, 0x55, 0x46, 0x46, 0x45, 0x52, 0x53, 0x20, + + 0x41, 0x56, 0x41, 0x49, 0x4c, 0x41, 0x42, 0x4c, + 0xc5, 0x46, 0x49, 0x4c, 0x45, 0x20, 0x54, 0x59, + 0x50, 0x45, 0x20, 0x4d, 0x49, 0x53, 0x4d, 0x41, + 0x54, 0x43, 0xc8, 0x50, 0x52, 0x4f, 0x47, 0x52, + 0x41, 0x4d, 0x20, 0x54, 0x4f, 0x4f, 0x20, 0x4c, + 0x41, 0x52, 0x47, 0xc5, 0x4e, 0x4f, 0x54, 0x20, + 0x44, 0x49, 0x52, 0x45, 0x43, 0x54, 0x20, 0x43, + 0x4f, 0x4d, 0x4d, 0x41, 0x4e, 0xc4, 0x8d, 0x00, + 0x03, 0x19, 0x19, 0x24, 0x33, 0x3e, 0x4c, 0x5b, + 0x64, 0x6d, 0x78, 0x84, 0x98, 0xaa, 0xbb, 0x2d, + 0x98, 0x00, 0x00, 0xf0, 0xfd, 0x1b, 0xfd, 0x03, + 0x03, 0xf4, 0x0d, 0x28, 0x8d, 0x0d, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x0b, 0x01, 0x20, 0xfe, 0x00, + 0x02, 0x00, 0x06, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xd0, 0x00, 0xc8, 0xc5, 0xcc, + 0xcc, 0xcf, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, + 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, + 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, + 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, + 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, + 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, + 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, + 0xa0, 0x03, 0x84, 0x00, 0x00, 0x00, 0x40, 0x00, + 0xc1, 0xd0, 0xd0, 0xcc, 0xc5, 0xd3, 0xcf, 0xc6, + 0xd4, 0xe8, 0xb7, 0xbb, 0xb3, 0xbb, 0xb4, 0x00, + 0xc0, 0x7e, 0xb3, 0x21, 0xab, 0x05, 0xac, 0x57, + 0xac, 0x6f, 0xac, 0x2a, 0xad, 0x97, 0xad, 0xee, + 0xac, 0xf5, 0xac, 0x39, 0xac, 0x11, 0xad, 0x8d, + 0xae, 0x17, 0xad, 0x7e, 0xb3, 0x7e, 0xb3, 0x89, + 0xac, 0x95, 0xac, 0x86, 0xac, 0x92, 0xac, 0x7e, + 0xb3, 0x7e, 0xb3, 0xbd, 0xac, 0xc9, 0xac, 0xba, + 0xac, 0xc6, 0xac, 0x7e, 0xb3, 0xe0, 0x00, 0xf0, + + 0x02, 0xa2, 0x02, 0x8e, 0x5f, 0xaa, 0xba, 0x8e, + 0x9b, 0xb3, 0x20, 0x6a, 0xae, 0xad, 0xbb, 0xb5, + 0xc9, 0x0d, 0xb0, 0x0b, 0x0a, 0xaa, 0xbd, 0xca, + 0xaa, 0x48, 0xbd, 0xc9, 0xaa, 0x48, 0x60, 0x4c, + 0x63, 0xb3, 0x20, 0x28, 0xab, 0x4c, 0x7f, 0xb3, + 0x20, 0xdc, 0xab, 0xa9, 0x01, 0x8d, 0xe3, 0xb5, + 0xae, 0xbe, 0xb5, 0xad, 0xbd, 0xb5, 0xd0, 0x05, + 0xe0, 0x00, 0xd0, 0x01, 0xe8, 0x8d, 0xe8, 0xb5, + 0x8e, 0xe9, 0xb5, 0x20, 0xc9, 0xb1, 0x90, 0x5e, + 0x8e, 0x9c, 0xb3, 0xae, 0x5f, 0xaa, 0xbd, 0x09, + 0xa9, 0xae, 0x9c, 0xb3, 0x4a, 0xb0, 0x0d, 0xad, + 0x51, 0xaa, 0xc9, 0xc0, 0xd0, 0x03, 0x4c, 0x5f, + 0xb3, 0x4c, 0x73, 0xb3, 0xa9, 0x00, 0x9d, 0xe8, + 0xb4, 0xa9, 0x01, 0x9d, 0xe7, 0xb4, 0x8e, 0x9c, + 0xb3, 0x20, 0x44, 0xb2, 0xae, 0x9c, 0xb3, 0x9d, + 0xc7, 0xb4, 0x8d, 0xd2, 0xb5, 0x8d, 0xd4, 0xb5, + 0xad, 0xf1, 0xb5, 0x9d, 0xc6, 0xb4, 0x8d, 0xd1, + 0xb5, 0x8d, 0xd3, 0xb5, 0xad, 0xc2, 0xb5, 0x9d, + 0xc8, 0xb4, 0x20, 0x37, 0xb0, 0x20, 0x0c, 0xaf, + 0x20, 0xd6, 0xb7, 0x20, 0x3a, 0xaf, 0xae, 0x9c, + 0xb3, 0xa9, 0x06, 0x8d, 0xc5, 0xb5, 0xbd, 0xc6, + 0xb4, 0x8d, 0xd1, 0xb5, 0xbd, 0xc7, 0xb4, 0x8d, + 0xd2, 0xb5, 0xbd, 0xc8, 0xb4, 0x8d, 0xc2, 0xb5, + 0x8d, 0xf6, 0xb5, 0xbd, 0xe7, 0xb4, 0x8d, 0xee, + 0xb5, 0xbd, 0xe8, 0xb4, 0x8d, 0xef, 0xb5, 0x8e, + 0xd9, 0xb5, 0xa9, 0xff, 0x8d, 0xe0, 0xb5, 0x8d, + 0xe1, 0xb5, 0xad, 0xe2, 0xb3, 0x8d, 0xda, 0xb5, + 0x18, 0x4c, 0x5e, 0xaf, 0xa9, 0x00, 0xaa, 0x9d, + 0xd1, 0xb5, 0xe8, 0xe0, 0x2d, 0xd0, 0xf8, 0xad, + 0xbf, 0xb5, 0x49, 0xff, 0x8d, 0xf9, 0xb5, 0xad, + 0xc0, 0xb5, 0x8d, 0xf8, 0xb5, 0xad, 0xc1, 0xb5, + 0x0a, 0x0a, 0x0a, 0x0a, 0xaa, 0x8e, 0xf7, 0xb5, + + 0xa9, 0x11, 0x8d, 0xfa, 0xb5, 0x60, 0x20, 0x1d, + 0xaf, 0x20, 0x34, 0xaf, 0x20, 0xc3, 0xb2, 0xa9, + 0x02, 0x2d, 0xd5, 0xb5, 0xf0, 0x21, 0x20, 0xf7, + 0xaf, 0xa9, 0x00, 0x18, 0x20, 0x11, 0xb0, 0x38, + 0xce, 0xd8, 0xb5, 0xd0, 0xf7, 0xae, 0xd9, 0xb5, + 0xad, 0xee, 0xb5, 0x9d, 0xe7, 0xb4, 0xad, 0xef, + 0xb5, 0x9d, 0xe8, 0xb4, 0x20, 0x37, 0xb0, 0x4c, + 0x7f, 0xb3, 0x20, 0x28, 0xab, 0xad, 0xf6, 0xb5, + 0x30, 0x2b, 0xad, 0xbd, 0xb5, 0x85, 0x42, 0xad, + 0xbe, 0xb5, 0x85, 0x43, 0xae, 0x9c, 0xb3, 0x20, + 0x1c, 0xb2, 0x20, 0x37, 0xb0, 0x4c, 0x7f, 0xb3, + 0xad, 0xbc, 0xb5, 0xc9, 0x05, 0xb0, 0x0b, 0x0a, + 0xaa, 0xbd, 0xe6, 0xaa, 0x48, 0xbd, 0xe5, 0xaa, + 0x48, 0x60, 0x4c, 0x67, 0xb3, 0x4c, 0x7b, 0xb3, + 0xad, 0xf6, 0xb5, 0x30, 0xf8, 0xad, 0xbc, 0xb5, + 0xc9, 0x05, 0xb0, 0xee, 0x0a, 0xaa, 0xbd, 0xf2, + 0xaa, 0x48, 0xbd, 0xf1, 0xaa, 0x48, 0x60, 0x20, + 0x00, 0xb3, 0x20, 0xa8, 0xac, 0x8d, 0xc3, 0xb5, + 0x4c, 0x7f, 0xb3, 0x20, 0x00, 0xb3, 0x20, 0xb5, + 0xb1, 0x20, 0xa8, 0xac, 0x48, 0x20, 0xa2, 0xb1, + 0xa0, 0x00, 0x68, 0x91, 0x42, 0x4c, 0x96, 0xac, + 0x20, 0xb6, 0xb0, 0xb0, 0x0b, 0xb1, 0x42, 0x48, + 0x20, 0x5b, 0xb1, 0x20, 0x94, 0xb1, 0x68, 0x60, + 0x4c, 0x6f, 0xb3, 0x20, 0x00, 0xb3, 0xad, 0xc3, + 0xb5, 0x20, 0xda, 0xac, 0x4c, 0x7f, 0xb3, 0x20, + 0x00, 0xb3, 0x20, 0xa2, 0xb1, 0xa0, 0x00, 0xb1, + 0x42, 0x20, 0xda, 0xac, 0x20, 0xb5, 0xb1, 0x4c, + 0xca, 0xac, 0x48, 0x20, 0xb6, 0xb0, 0x68, 0x91, + 0x42, 0xa9, 0x40, 0x0d, 0xd5, 0xb5, 0x8d, 0xd5, + 0xb5, 0x20, 0x5b, 0xb1, 0x4c, 0x94, 0xb1, 0xa9, + 0x80, 0x8d, 0x9e, 0xb3, 0xd0, 0x05, 0xa9, 0x00, + 0x8d, 0x9e, 0xb3, 0x20, 0x28, 0xab, 0xae, 0x9c, + + 0xb3, 0xbd, 0xc8, 0xb4, 0x29, 0x7f, 0x0d, 0x9e, + 0xb3, 0x9d, 0xc8, 0xb4, 0x20, 0x37, 0xb0, 0x4c, + 0x7f, 0xb3, 0x20, 0x00, 0xb3, 0x4c, 0x7f, 0xb3, + 0x20, 0x28, 0xab, 0x20, 0xb6, 0xb0, 0xb0, 0xef, + 0xee, 0xe4, 0xb5, 0xd0, 0xf6, 0xee, 0xe5, 0xb5, + 0x4c, 0x1b, 0xad, 0x20, 0x28, 0xab, 0xae, 0x9c, + 0xb3, 0xbd, 0xc8, 0xb4, 0x10, 0x03, 0x4c, 0x7b, + 0xb3, 0xae, 0x9c, 0xb3, 0xbd, 0xc6, 0xb4, 0x8d, + 0xd1, 0xb5, 0x9d, 0xe6, 0xb4, 0xa9, 0xff, 0x9d, + 0xc6, 0xb4, 0xbc, 0xc7, 0xb4, 0x8c, 0xd2, 0xb5, + 0x20, 0x37, 0xb0, 0x18, 0x20, 0x5e, 0xaf, 0xb0, + 0x2a, 0x20, 0x0c, 0xaf, 0xa0, 0x0c, 0x8c, 0x9c, + 0xb3, 0xb1, 0x42, 0x30, 0x0b, 0xf0, 0x09, 0x48, + 0xc8, 0xb1, 0x42, 0xa8, 0x68, 0x20, 0x89, 0xad, + 0xac, 0x9c, 0xb3, 0xc8, 0xc8, 0xd0, 0xe7, 0xad, + 0xd3, 0xb5, 0xac, 0xd4, 0xb5, 0x20, 0x89, 0xad, + 0x38, 0xb0, 0xd1, 0x20, 0xfb, 0xaf, 0x4c, 0x7f, + 0xb3, 0x38, 0x20, 0xdd, 0xb2, 0xa9, 0x00, 0xa2, + 0x05, 0x9d, 0xf0, 0xb5, 0xca, 0x10, 0xfa, 0x60, + 0x20, 0xdc, 0xab, 0xa9, 0xff, 0x8d, 0xf9, 0xb5, + 0x20, 0xf7, 0xaf, 0xa9, 0x16, 0x8d, 0x9d, 0xb3, + 0x20, 0x2f, 0xae, 0x20, 0x2f, 0xae, 0xa2, 0x0b, + 0xbd, 0xaf, 0xb3, 0x20, 0xed, 0xfd, 0xca, 0x10, + 0xf7, 0x86, 0x45, 0xad, 0xf6, 0xb7, 0x85, 0x44, + 0x20, 0x42, 0xae, 0x20, 0x2f, 0xae, 0x20, 0x2f, + 0xae, 0x18, 0x20, 0x11, 0xb0, 0xb0, 0x5d, 0xa2, + 0x00, 0x8e, 0x9c, 0xb3, 0xbd, 0xc6, 0xb4, 0xf0, + 0x53, 0x30, 0x4a, 0xa0, 0xa0, 0xbd, 0xc8, 0xb4, + 0x10, 0x02, 0xa0, 0xaa, 0x98, 0x20, 0xed, 0xfd, + 0xbd, 0xc8, 0xb4, 0x29, 0x7f, 0xa0, 0x07, 0x0a, + 0x0a, 0xb0, 0x03, 0x88, 0xd0, 0xfa, 0xb9, 0xa7, + 0xb3, 0x20, 0xed, 0xfd, 0xa9, 0xa0, 0x20, 0xed, + + 0xfd, 0xbd, 0xe7, 0xb4, 0x85, 0x44, 0xbd, 0xe8, + 0xb4, 0x85, 0x45, 0x20, 0x42, 0xae, 0xa9, 0xa0, + 0x20, 0xed, 0xfd, 0xe8, 0xe8, 0xe8, 0xa0, 0x1d, + 0xbd, 0xc6, 0xb4, 0x20, 0xed, 0xfd, 0xe8, 0x88, + 0x10, 0xf6, 0x20, 0x2f, 0xae, 0x20, 0x30, 0xb2, + 0x90, 0xa7, 0xb0, 0x9e, 0x4c, 0x7f, 0xb3, 0xa9, + 0x8d, 0x20, 0xed, 0xfd, 0xce, 0x9d, 0xb3, 0xd0, + 0x08, 0x20, 0x0c, 0xfd, 0xa9, 0x15, 0x8d, 0x9d, + 0xb3, 0x60, 0xa0, 0x02, 0xa9, 0x00, 0x48, 0xa5, + 0x44, 0xd9, 0xa4, 0xb3, 0x90, 0x12, 0xf9, 0xa4, + 0xb3, 0x85, 0x44, 0xa5, 0x45, 0xe9, 0x00, 0x85, + 0x45, 0x68, 0x69, 0x00, 0x48, 0x4c, 0x47, 0xae, + 0x68, 0x09, 0xb0, 0x20, 0xed, 0xfd, 0x88, 0x10, + 0xdb, 0x60, 0x20, 0x08, 0xaf, 0xa0, 0x00, 0x8c, + 0xc5, 0xb5, 0xb1, 0x42, 0x99, 0xd1, 0xb5, 0xc8, + 0xc0, 0x2d, 0xd0, 0xf6, 0x18, 0x60, 0x20, 0x08, + 0xaf, 0xa0, 0x00, 0xb9, 0xd1, 0xb5, 0x91, 0x42, + 0xc8, 0xc0, 0x2d, 0xd0, 0xf6, 0x60, 0x20, 0xdc, + 0xab, 0xa9, 0x04, 0x20, 0x58, 0xb0, 0xad, 0xf9, + 0xb5, 0x49, 0xff, 0x8d, 0xc1, 0xb3, 0xa9, 0x11, + 0x8d, 0xeb, 0xb3, 0xa9, 0x01, 0x8d, 0xec, 0xb3, + 0xa2, 0x38, 0xa9, 0x00, 0x9d, 0xbb, 0xb3, 0xe8, + 0xd0, 0xfa, 0xa2, 0x0c, 0xe0, 0x8c, 0xf0, 0x14, + 0xa0, 0x03, 0xb9, 0xa0, 0xb3, 0x9d, 0xf3, 0xb3, + 0xe8, 0x88, 0x10, 0xf6, 0xe0, 0x44, 0xd0, 0xec, + 0xa2, 0x48, 0xd0, 0xe8, 0x20, 0xfb, 0xaf, 0xa2, + 0x00, 0x8a, 0x9d, 0xbb, 0xb4, 0xe8, 0xd0, 0xfa, + 0x20, 0x45, 0xb0, 0xa9, 0x11, 0xac, 0xf0, 0xb3, + 0x88, 0x88, 0x8d, 0xec, 0xb7, 0x8d, 0xbc, 0xb4, + 0x8c, 0xbd, 0xb4, 0xc8, 0x8c, 0xed, 0xb7, 0xa9, + 0x02, 0x20, 0x58, 0xb0, 0xac, 0xbd, 0xb4, 0x88, + 0x30, 0x05, 0xd0, 0xec, 0x98, 0xf0, 0xe6, 0x20, + + 0xc2, 0xb7, 0x20, 0x4a, 0xb7, 0x4c, 0x7f, 0xb3, + 0xa2, 0x00, 0xf0, 0x06, 0xa2, 0x02, 0xd0, 0x02, + 0xa2, 0x04, 0xbd, 0xc7, 0xb5, 0x85, 0x42, 0xbd, + 0xc8, 0xb5, 0x85, 0x43, 0x60, 0x2c, 0xd5, 0xb5, + 0x70, 0x01, 0x60, 0x20, 0xe4, 0xaf, 0xa9, 0x02, + 0x20, 0x52, 0xb0, 0xa9, 0xbf, 0x2d, 0xd5, 0xb5, + 0x8d, 0xd5, 0xb5, 0x60, 0xad, 0xd5, 0xb5, 0x30, + 0x01, 0x60, 0x20, 0x4b, 0xaf, 0xa9, 0x02, 0x20, + 0x52, 0xb0, 0xa9, 0x7f, 0x2d, 0xd5, 0xb5, 0x8d, + 0xd5, 0xb5, 0x60, 0xad, 0xc9, 0xb5, 0x8d, 0xf0, + 0xb7, 0xad, 0xca, 0xb5, 0x8d, 0xf1, 0xb7, 0xae, + 0xd3, 0xb5, 0xac, 0xd4, 0xb5, 0x60, 0x08, 0x20, + 0x34, 0xaf, 0x20, 0x4b, 0xaf, 0x20, 0x0c, 0xaf, + 0x28, 0xb0, 0x09, 0xae, 0xd1, 0xb5, 0xac, 0xd2, + 0xb5, 0x4c, 0xb5, 0xaf, 0xa0, 0x01, 0xb1, 0x42, + 0xf0, 0x08, 0xaa, 0xc8, 0xb1, 0x42, 0xa8, 0x4c, + 0xb5, 0xaf, 0xad, 0xbb, 0xb5, 0xc9, 0x04, 0xf0, + 0x02, 0x38, 0x60, 0x20, 0x44, 0xb2, 0xa0, 0x02, + 0x91, 0x42, 0x48, 0x88, 0xad, 0xf1, 0xb5, 0x91, + 0x42, 0x48, 0x20, 0x3a, 0xaf, 0x20, 0xd6, 0xb7, + 0xa0, 0x05, 0xad, 0xde, 0xb5, 0x91, 0x42, 0xc8, + 0xad, 0xdf, 0xb5, 0x91, 0x42, 0x68, 0xaa, 0x68, + 0xa8, 0xa9, 0x02, 0xd0, 0x02, 0xa9, 0x01, 0x8e, + 0xd3, 0xb5, 0x8c, 0xd4, 0xb5, 0x20, 0x52, 0xb0, + 0xa0, 0x05, 0xb1, 0x42, 0x8d, 0xdc, 0xb5, 0x18, + 0x6d, 0xda, 0xb5, 0x8d, 0xde, 0xb5, 0xc8, 0xb1, + 0x42, 0x8d, 0xdd, 0xb5, 0x6d, 0xdb, 0xb5, 0x8d, + 0xdf, 0xb5, 0x18, 0x60, 0x20, 0xe4, 0xaf, 0xa9, + 0x01, 0x4c, 0x52, 0xb0, 0xac, 0xcb, 0xb5, 0xad, + 0xcc, 0xb5, 0x8c, 0xf0, 0xb7, 0x8d, 0xf1, 0xb7, + 0xae, 0xd6, 0xb5, 0xac, 0xd7, 0xb5, 0x60, 0xa9, + 0x01, 0xd0, 0x02, 0xa9, 0x02, 0xac, 0xc3, 0xaa, + + 0x8c, 0xf0, 0xb7, 0xac, 0xc4, 0xaa, 0x8c, 0xf1, + 0xb7, 0xae, 0xfa, 0xb5, 0xa0, 0x00, 0x4c, 0x52, + 0xb0, 0x08, 0x20, 0x45, 0xb0, 0x28, 0xb0, 0x08, + 0xac, 0xbd, 0xb3, 0xae, 0xbc, 0xb3, 0xd0, 0x0a, + 0xae, 0xbc, 0xb4, 0xd0, 0x02, 0x38, 0x60, 0xac, + 0xbd, 0xb4, 0x8e, 0x97, 0xb3, 0x8c, 0x98, 0xb3, + 0xa9, 0x01, 0x20, 0x52, 0xb0, 0x18, 0x60, 0x20, + 0x45, 0xb0, 0xae, 0x97, 0xb3, 0xac, 0x98, 0xb3, + 0xa9, 0x02, 0x4c, 0x52, 0xb0, 0xad, 0xc5, 0xaa, + 0x8d, 0xf0, 0xb7, 0xad, 0xc6, 0xaa, 0x8d, 0xf1, + 0xb7, 0x60, 0x8e, 0xec, 0xb7, 0x8c, 0xed, 0xb7, + 0x8d, 0xf4, 0xb7, 0xc9, 0x02, 0xd0, 0x06, 0x0d, + 0xd5, 0xb5, 0x8d, 0xd5, 0xb5, 0xad, 0xf9, 0xb5, + 0x49, 0xff, 0x8d, 0xeb, 0xb7, 0xad, 0xf7, 0xb5, + 0x8d, 0xe9, 0xb7, 0xad, 0xf8, 0xb5, 0x8d, 0xea, + 0xb7, 0xad, 0xe2, 0xb5, 0x8d, 0xf2, 0xb7, 0xad, + 0xe3, 0xb5, 0x8d, 0xf3, 0xb7, 0xa9, 0x01, 0x8d, + 0xe8, 0xb7, 0xac, 0xc1, 0xaa, 0xad, 0xc2, 0xaa, + 0x20, 0xb5, 0xb7, 0xad, 0xf6, 0xb7, 0x8d, 0xbf, + 0xb5, 0xa9, 0xff, 0x8d, 0xeb, 0xb7, 0xb0, 0x01, + 0x60, 0xad, 0xf5, 0xb7, 0xa0, 0x07, 0xc9, 0x20, + 0xf0, 0x08, 0xa0, 0x04, 0xc9, 0x10, 0xf0, 0x02, + 0xa0, 0x08, 0x98, 0x4c, 0x85, 0xb3, 0xad, 0xe4, + 0xb5, 0xcd, 0xe0, 0xb5, 0xd0, 0x08, 0xad, 0xe5, + 0xb5, 0xcd, 0xe1, 0xb5, 0xf0, 0x66, 0x20, 0x1d, + 0xaf, 0xad, 0xe5, 0xb5, 0xcd, 0xdd, 0xb5, 0x90, + 0x1c, 0xd0, 0x08, 0xad, 0xe4, 0xb5, 0xcd, 0xdc, + 0xb5, 0x90, 0x12, 0xad, 0xe5, 0xb5, 0xcd, 0xdf, + 0xb5, 0x90, 0x10, 0xd0, 0x08, 0xad, 0xe4, 0xb5, + 0xcd, 0xde, 0xb5, 0x90, 0x06, 0x20, 0x5e, 0xaf, + 0x90, 0xd7, 0x60, 0x38, 0xad, 0xe4, 0xb5, 0xed, + 0xdc, 0xb5, 0x0a, 0x69, 0x0c, 0xa8, 0x20, 0x0c, + + 0xaf, 0xb1, 0x42, 0xd0, 0x0f, 0xad, 0xbb, 0xb5, + 0xc9, 0x04, 0xf0, 0x02, 0x38, 0x60, 0x20, 0x34, + 0xb1, 0x4c, 0x20, 0xb1, 0x8d, 0xd6, 0xb5, 0xc8, + 0xb1, 0x42, 0x8d, 0xd7, 0xb5, 0x20, 0xdc, 0xaf, + 0xad, 0xe4, 0xb5, 0x8d, 0xe0, 0xb5, 0xad, 0xe5, + 0xb5, 0x8d, 0xe1, 0xb5, 0x20, 0x10, 0xaf, 0xac, + 0xe6, 0xb5, 0x18, 0x60, 0x8c, 0x9d, 0xb3, 0x20, + 0x44, 0xb2, 0xac, 0x9d, 0xb3, 0xc8, 0x91, 0x42, + 0x8d, 0xd7, 0xb5, 0x88, 0xad, 0xf1, 0xb5, 0x91, + 0x42, 0x8d, 0xd6, 0xb5, 0x20, 0x10, 0xaf, 0x20, + 0xd6, 0xb7, 0xa9, 0xc0, 0x0d, 0xd5, 0xb5, 0x8d, + 0xd5, 0xb5, 0x60, 0xae, 0xea, 0xb5, 0x8e, 0xbd, + 0xb5, 0xae, 0xeb, 0xb5, 0x8e, 0xbe, 0xb5, 0xae, + 0xec, 0xb5, 0xac, 0xed, 0xb5, 0x8e, 0xbf, 0xb5, + 0x8c, 0xc0, 0xb5, 0xe8, 0xd0, 0x01, 0xc8, 0xcc, + 0xe9, 0xb5, 0xd0, 0x11, 0xec, 0xe8, 0xb5, 0xd0, + 0x0c, 0xa2, 0x00, 0xa0, 0x00, 0xee, 0xea, 0xb5, + 0xd0, 0x03, 0xee, 0xeb, 0xb5, 0x8e, 0xec, 0xb5, + 0x8c, 0xed, 0xb5, 0x60, 0xee, 0xe6, 0xb5, 0xd0, + 0x08, 0xee, 0xe4, 0xb5, 0xd0, 0x03, 0xee, 0xe5, + 0xb5, 0x60, 0xac, 0xc3, 0xb5, 0xae, 0xc4, 0xb5, + 0x84, 0x42, 0x86, 0x43, 0xee, 0xc3, 0xb5, 0xd0, + 0x03, 0xee, 0xc4, 0xb5, 0x60, 0xac, 0xc1, 0xb5, + 0xd0, 0x08, 0xae, 0xc2, 0xb5, 0xf0, 0x07, 0xce, + 0xc2, 0xb5, 0xce, 0xc1, 0xb5, 0x60, 0x4c, 0x7f, + 0xb3, 0x20, 0xf7, 0xaf, 0xad, 0xc3, 0xb5, 0x85, + 0x42, 0xad, 0xc4, 0xb5, 0x85, 0x43, 0xa9, 0x01, + 0x8d, 0x9d, 0xb3, 0xa9, 0x00, 0x8d, 0xd8, 0xb5, + 0x18, 0xee, 0xd8, 0xb5, 0x20, 0x11, 0xb0, 0xb0, + 0x51, 0xa2, 0x00, 0x8e, 0x9c, 0xb3, 0xbd, 0xc6, + 0xb4, 0xf0, 0x1f, 0x30, 0x22, 0xa0, 0x00, 0xe8, + 0xe8, 0xe8, 0xb1, 0x42, 0xdd, 0xc6, 0xb4, 0xd0, + + 0x0a, 0xc8, 0xc0, 0x1e, 0xd0, 0xf3, 0xae, 0x9c, + 0xb3, 0x18, 0x60, 0x20, 0x30, 0xb2, 0x90, 0xdb, + 0xb0, 0xcf, 0xac, 0x9d, 0xb3, 0xd0, 0xc1, 0xac, + 0x9d, 0xb3, 0xd0, 0xef, 0xa0, 0x00, 0xe8, 0xe8, + 0xe8, 0xb1, 0x42, 0x9d, 0xc6, 0xb4, 0xc8, 0xc0, + 0x1e, 0xd0, 0xf5, 0xae, 0x9c, 0xb3, 0x38, 0x60, + 0x18, 0xad, 0x9c, 0xb3, 0x69, 0x23, 0xaa, 0xe0, + 0xf5, 0x60, 0xa9, 0x00, 0xac, 0x9d, 0xb3, 0xd0, + 0x97, 0x4c, 0x77, 0xb3, 0xad, 0xf1, 0xb5, 0xf0, + 0x21, 0xce, 0xf0, 0xb5, 0x30, 0x17, 0x18, 0xa2, + 0x04, 0x3e, 0xf1, 0xb5, 0xca, 0xd0, 0xfa, 0x90, + 0xf0, 0xee, 0xee, 0xb5, 0xd0, 0x03, 0xee, 0xef, + 0xb5, 0xad, 0xf0, 0xb5, 0x60, 0xa9, 0x00, 0x8d, + 0xf1, 0xb5, 0xa9, 0x00, 0x8d, 0x9e, 0xb3, 0x20, + 0xf7, 0xaf, 0x18, 0xad, 0xeb, 0xb3, 0x6d, 0xec, + 0xb3, 0xf0, 0x09, 0xcd, 0xef, 0xb3, 0x90, 0x14, + 0xa9, 0xff, 0xd0, 0x0a, 0xad, 0x9e, 0xb3, 0xd0, + 0x37, 0xa9, 0x01, 0x8d, 0x9e, 0xb3, 0x8d, 0xec, + 0xb3, 0x18, 0x69, 0x11, 0x8d, 0xeb, 0xb3, 0x8d, + 0xf1, 0xb5, 0xa8, 0x0a, 0x0a, 0xa8, 0xa2, 0x04, + 0x18, 0xb9, 0xf6, 0xb3, 0x9d, 0xf1, 0xb5, 0xf0, + 0x06, 0x38, 0xa9, 0x00, 0x99, 0xf6, 0xb3, 0x88, + 0xca, 0xd0, 0xee, 0x90, 0xbd, 0x20, 0xfb, 0xaf, + 0xad, 0xf0, 0xb3, 0x8d, 0xf0, 0xb5, 0xd0, 0x89, + 0x4c, 0x77, 0xb3, 0xad, 0xf1, 0xb5, 0xd0, 0x01, + 0x60, 0x48, 0x20, 0xf7, 0xaf, 0xac, 0xf0, 0xb5, + 0x68, 0x18, 0x20, 0xdd, 0xb2, 0xa9, 0x00, 0x8d, + 0xf1, 0xb5, 0x4c, 0xfb, 0xaf, 0xa2, 0xfc, 0x7e, + 0xf6, 0xb4, 0xe8, 0xd0, 0xfa, 0xc8, 0xcc, 0xf0, + 0xb3, 0xd0, 0xf2, 0x0a, 0x0a, 0xa8, 0xf0, 0x0f, + 0xa2, 0x04, 0xbd, 0xf1, 0xb5, 0x19, 0xf6, 0xb3, + 0x99, 0xf6, 0xb3, 0x88, 0xca, 0xd0, 0xf3, 0x60, + + 0xad, 0xbd, 0xb5, 0x8d, 0xe6, 0xb5, 0x8d, 0xea, + 0xb5, 0xad, 0xbe, 0xb5, 0x8d, 0xe4, 0xb5, 0x8d, + 0xeb, 0xb5, 0xa9, 0x00, 0x8d, 0xe5, 0xb5, 0xa0, + 0x10, 0xaa, 0xad, 0xe6, 0xb5, 0x4a, 0xb0, 0x03, + 0x8a, 0x90, 0x0e, 0x18, 0xad, 0xe5, 0xb5, 0x6d, + 0xe8, 0xb5, 0x8d, 0xe5, 0xb5, 0x8a, 0x6d, 0xe9, + 0xb5, 0x6a, 0x6e, 0xe5, 0xb5, 0x6e, 0xe4, 0xb5, + 0x6e, 0xe6, 0xb5, 0x88, 0xd0, 0xdb, 0xad, 0xbf, + 0xb5, 0x8d, 0xec, 0xb5, 0x6d, 0xe6, 0xb5, 0x8d, + 0xe6, 0xb5, 0xad, 0xc0, 0xb5, 0x8d, 0xed, 0xb5, + 0x6d, 0xe4, 0xb5, 0x8d, 0xe4, 0xb5, 0xa9, 0x00, + 0x6d, 0xe5, 0xb5, 0x8d, 0xe5, 0xb5, 0x60, 0xa9, + 0x01, 0xd0, 0x22, 0xa9, 0x02, 0xd0, 0x1e, 0xa9, + 0x03, 0xd0, 0x1a, 0xa9, 0x04, 0xd0, 0x16, 0xa9, + 0x05, 0xd0, 0x12, 0xa9, 0x06, 0xd0, 0x0e, 0x4c, + 0xed, 0xbf, 0xea, 0xa9, 0x0a, 0xd0, 0x06, 0xad, + 0xc5, 0xb5, 0x18, 0x90, 0x01, 0x38, 0x08, 0x8d, + 0xc5, 0xb5, 0xa9, 0x00, 0x85, 0x48, 0x20, 0x7e, + 0xae, 0x28, 0xae, 0x9b, 0xb3, 0x9a, 0x60, 0x11, + 0x0d, 0x00, 0x00, 0xee, 0x69, 0x01, 0x00, 0x00, + 0x00, 0x00, 0xff, 0xff, 0x01, 0x0a, 0x64, 0xd4, + 0xc9, 0xc1, 0xc2, 0xd3, 0xd2, 0xc1, 0xc2, 0xa0, + 0xc5, 0xcd, 0xd5, 0xcc, 0xcf, 0xd6, 0xa0, 0xcb, + 0xd3, 0xc9, 0xc4, 0x04, 0x11, 0x0f, 0x03, 0x00, + 0x00, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, 0x23, + 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, + + 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, + 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, + 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, + 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, + 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, + 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, + 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, + 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, + 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, + 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, + 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, + 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, + 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, + 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, + 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0b, 0x9d, 0x01, 0x00, 0xfe, + 0x02, 0x06, 0x00, 0x75, 0xaa, 0x00, 0x00, 0x00, + 0x98, 0x00, 0x97, 0x00, 0x96, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, + 0x02, 0x01, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, + + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + + +/* + * Three 13-sector tracks, in DOS order (i.e. track 0 sector 0 followed + * by track 0 sector 1). + * + * Obtained from the DOS 3.2.1 system master. The last two sectors of + * track 2 were unreadable, and have been zeroed out here. + */ +/*static*/ const unsigned char DiskFSDOS33::gDOS32Tracks[13 * 3 * 256] = { + 0xf0, 0x4a, 0x99, 0xff, 0xff, 0x03, 0x3c, 0xad, + 0xff, 0xff, 0xff, 0x26, 0xb3, 0xff, 0xff, 0x4d, + 0x4a, 0x10, 0xff, 0xff, 0x3d, 0x4a, 0xca, 0xff, + 0xff, 0xa5, 0x4a, 0xc8, 0xff, 0xff, 0x03, 0x4a, + 0x40, 0xff, 0xff, 0x46, 0x08, 0x91, 0xff, 0xff, + 0x20, 0x33, 0x09, 0xff, 0xff, 0x03, 0xbd, 0xcc, + 0xff, 0xff, 0x43, 0xc8, 0x1d, 0xff, 0xff, 0x20, + 0x40, 0x07, 0xff, 0xff, 0x3e, 0x91, 0x29, 0xff, + 0xff, 0x85, 0x09, 0x3c, 0xff, 0xff, 0x5d, 0x00, + 0xa5, 0xff, 0xff, 0xa9, 0x1d, 0xc8, 0xff, 0xff, + 0x3f, 0x4a, 0x40, 0xff, 0xff, 0x85, 0x2a, 0x91, + 0xff, 0xff, 0xc0, 0x85, 0x09, 0xff, 0xff, 0x09, + 0x4a, 0x99, 0xff, 0xff, 0x4a, 0x3c, 0x1d, 0xff, + 0xff, 0x4a, 0x85, 0x07, 0xff, 0xff, 0x4a, 0x4a, + 0x29, 0xff, 0xff, 0x4a, 0x4a, 0x2a, 0xff, 0xff, + 0x8a, 0x4a, 0xa5, 0xff, 0xff, 0x40, 0x08, 0xc8, + 0xff, 0xff, 0x84, 0x00, 0x40, 0xff, 0xff, 0x41, + 0xbd, 0x91, 0xff, 0xff, 0x85, 0x00, 0x09, 0xff, + 0xff, 0x03, 0xa0, 0x66, 0xff, 0xff, 0xcc, 0x32, + 0x1d, 0xff, 0xff, 0xad, 0xa2, 0x2a, 0xff, 0xff, + 0x27, 0x00, 0x26, 0xff, 0xff, 0x85, 0x3e, 0x4a, + 0xff, 0xff, 0x09, 0x6c, 0x3c, 0xff, 0xff, 0xa9, + 0x3f, 0x26, 0xff, 0xff, 0x2b, 0xe6, 0x4a, 0xff, + 0xff, 0xa6, 0x3f, 0x4a, 0xff, 0xff, 0xf4, 0x85, + 0x4a, 0xff, 0xff, 0xd0, 0x03, 0x4a, 0x60, 0xff, + 0xc8, 0xcc, 0x08, 0x2b, 0xff, 0x08, 0xad, 0x66, + 0xa6, 0xff, 0x00, 0x3e, 0xbd, 0x40, 0xff, 0x99, + 0x85, 0xc8, 0x91, 0xff, 0x0a, 0xed, 0x40, 0x09, + 0xff, 0x0a, 0xd0, 0x91, 0xff, 0xff, 0x0a, 0x3d, + 0x09, 0x0d, 0xff, 0x08, 0xe6, 0x33, 0x4a, 0xff, + 0x00, 0x41, 0x1d, 0x4a, 0xff, 0xb9, 0xe6, 0x2a, + 0x4a, 0xff, 0x99, 0x06, 0x26, 0x08, 0x36, 0x48, + + 0x8e, 0xe9, 0x37, 0x8e, 0xf7, 0x37, 0xa9, 0x01, + 0x8d, 0xf8, 0x37, 0x8d, 0xea, 0x37, 0xad, 0xe0, + 0x37, 0x8d, 0xe1, 0x37, 0xa9, 0x00, 0x8d, 0xec, + 0x37, 0xad, 0xe2, 0x37, 0x8d, 0xed, 0x37, 0xad, + 0xe3, 0x37, 0x8d, 0xf1, 0x37, 0xa9, 0x01, 0x8d, + 0xf4, 0x37, 0x8a, 0x4a, 0x4a, 0x4a, 0x4a, 0xaa, + 0xa9, 0x00, 0x9d, 0xf8, 0x04, 0x9d, 0x78, 0x04, + 0x20, 0x93, 0x37, 0xa2, 0xff, 0x9a, 0x8e, 0xeb, + 0x37, 0x20, 0x93, 0xfe, 0x20, 0x89, 0xfe, 0x4c, + 0x03, 0x1b, 0xad, 0xf1, 0x37, 0x8d, 0xe3, 0x37, + 0x38, 0xad, 0xe7, 0x37, 0xed, 0xe3, 0x37, 0x8d, + 0xe0, 0x37, 0xa9, 0x00, 0x8d, 0xec, 0x37, 0x8d, + 0xed, 0x37, 0x8d, 0xf0, 0x37, 0xad, 0xe7, 0x37, + 0x8d, 0xf1, 0x37, 0x8d, 0xfe, 0x36, 0xa9, 0x0a, + 0x8d, 0xe1, 0x37, 0x8d, 0xe2, 0x37, 0xa9, 0x48, + 0x8d, 0xff, 0x36, 0xa9, 0x02, 0x8d, 0xf4, 0x37, + 0x20, 0x93, 0x37, 0xad, 0xe3, 0x37, 0x8d, 0xf1, + 0x37, 0xad, 0xe0, 0x37, 0x8d, 0xe1, 0x37, 0x20, + 0x93, 0x37, 0x60, 0xad, 0xe5, 0x37, 0xac, 0xe4, + 0x37, 0x20, 0xb5, 0x37, 0xac, 0xed, 0x37, 0xc8, + 0xc0, 0x0d, 0xd0, 0x05, 0xa0, 0x00, 0xee, 0xec, + 0x37, 0x8c, 0xed, 0x37, 0xee, 0xf1, 0x37, 0xce, + 0xe1, 0x37, 0xd0, 0xdf, 0x60, 0x08, 0x78, 0x20, + 0x00, 0x3d, 0xb0, 0x03, 0x28, 0x18, 0x60, 0x28, + 0x38, 0x60, 0xad, 0xbc, 0x35, 0x8d, 0xf1, 0x37, + 0xa9, 0x00, 0x8d, 0xf0, 0x37, 0xad, 0xf9, 0x35, + 0x49, 0xff, 0x8d, 0xeb, 0x37, 0x60, 0xa9, 0x00, + 0xa8, 0x91, 0x42, 0xc8, 0xd0, 0xfb, 0x60, 0x00, + 0x1b, 0x09, 0x0a, 0x1b, 0xe8, 0x37, 0x00, 0x36, + 0x01, 0x60, 0x01, 0x00, 0x00, 0x01, 0xfb, 0x37, + 0x00, 0x37, 0x00, 0x00, 0x02, 0x07, 0xfe, 0x60, + 0x01, 0x00, 0x00, 0x00, 0x01, 0xef, 0xd8, 0x00, + + 0xa2, 0x32, 0xa0, 0x00, 0xb1, 0x3e, 0x85, 0x26, + 0x4a, 0x4a, 0x4a, 0x9d, 0x00, 0x3b, 0xc8, 0xb1, + 0x3e, 0x85, 0x27, 0x4a, 0x4a, 0x4a, 0x9d, 0x33, + 0x3b, 0xc8, 0xb1, 0x3e, 0x85, 0x2a, 0x4a, 0x4a, + 0x4a, 0x9d, 0x66, 0x3b, 0xc8, 0xb1, 0x3e, 0x4a, + 0x26, 0x2a, 0x4a, 0x26, 0x27, 0x4a, 0x26, 0x26, + 0x9d, 0x99, 0x3b, 0xc8, 0xb1, 0x3e, 0x4a, 0x26, + 0x2a, 0x4a, 0x26, 0x27, 0x4a, 0x9d, 0xcc, 0x3b, + 0xa5, 0x26, 0x2a, 0x29, 0x1f, 0x9d, 0x00, 0x3c, + 0xa5, 0x27, 0x29, 0x1f, 0x9d, 0x33, 0x3c, 0xa5, + 0x2a, 0x29, 0x1f, 0x9d, 0x66, 0x3c, 0xc8, 0xca, + 0x10, 0xaa, 0xb1, 0x3e, 0xaa, 0x29, 0x07, 0x8d, + 0x99, 0x3c, 0x8a, 0x4a, 0x4a, 0x4a, 0x8d, 0xff, + 0x3b, 0x60, 0x38, 0xbd, 0x8d, 0xc0, 0xbd, 0x8e, + 0xc0, 0x30, 0x7c, 0x86, 0x27, 0x8e, 0x78, 0x06, + 0xad, 0x00, 0x3c, 0x85, 0x26, 0xa9, 0xff, 0x9d, + 0x8f, 0xc0, 0x1d, 0x8c, 0xc0, 0x48, 0x68, 0xea, + 0xa0, 0x0a, 0x05, 0x26, 0x20, 0xf4, 0x38, 0x88, + 0xd0, 0xf8, 0xa9, 0xd5, 0x20, 0xf3, 0x38, 0xa9, + 0xaa, 0x20, 0xf3, 0x38, 0xa9, 0xad, 0x20, 0xf3, + 0x38, 0x98, 0xa0, 0x9a, 0xd0, 0x03, 0xb9, 0x00, + 0x3c, 0x59, 0xff, 0x3b, 0xaa, 0xbd, 0x9a, 0x3c, + 0xa6, 0x27, 0x9d, 0x8d, 0xc0, 0xbd, 0x8c, 0xc0, + 0x88, 0xd0, 0xeb, 0xa5, 0x26, 0xea, 0x59, 0x00, + 0x3b, 0xaa, 0xbd, 0x9a, 0x3c, 0xae, 0x78, 0x06, + 0x9d, 0x8d, 0xc0, 0xbd, 0x8c, 0xc0, 0xb9, 0x00, + 0x3b, 0xc8, 0xd0, 0xea, 0xaa, 0xbd, 0x9a, 0x3c, + 0xa6, 0x27, 0x20, 0xf6, 0x38, 0xa9, 0xde, 0x20, + 0xf3, 0x38, 0xa9, 0xaa, 0x20, 0xf3, 0x38, 0xa9, + 0xeb, 0x20, 0xf3, 0x38, 0xbd, 0x8e, 0xc0, 0xbd, + 0x8c, 0xc0, 0x60, 0x18, 0x48, 0x68, 0x9d, 0x8d, + 0xc0, 0x1d, 0x8c, 0xc0, 0x60, 0xa0, 0x20, 0x88, + + 0xf0, 0x61, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0x49, + 0xd5, 0xd0, 0xf4, 0xea, 0xbd, 0x8c, 0xc0, 0x10, + 0xfb, 0xc9, 0xaa, 0xd0, 0xf2, 0xa0, 0x9a, 0xbd, + 0x8c, 0xc0, 0x10, 0xfb, 0xc9, 0xad, 0xd0, 0xe7, + 0xa9, 0x00, 0x88, 0x84, 0x26, 0xbc, 0x8c, 0xc0, + 0x10, 0xfb, 0x59, 0x00, 0x3a, 0xa4, 0x26, 0x99, + 0x00, 0x3c, 0xd0, 0xee, 0x84, 0x26, 0xbc, 0x8c, + 0xc0, 0x10, 0xfb, 0x59, 0x00, 0x3a, 0xa4, 0x26, + 0x99, 0x00, 0x3b, 0xc8, 0xd0, 0xee, 0xbc, 0x8c, + 0xc0, 0x10, 0xfb, 0xd9, 0x00, 0x3a, 0xd0, 0x13, + 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0xc9, 0xde, 0xd0, + 0x0a, 0xea, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0xc9, + 0xaa, 0xf0, 0x5c, 0x38, 0x60, 0xa0, 0xf8, 0x84, + 0x26, 0xc8, 0xd0, 0x04, 0xe6, 0x26, 0xf0, 0xf3, + 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0xc9, 0xd5, 0xd0, + 0xf0, 0xea, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0xc9, + 0xaa, 0xd0, 0xf2, 0xa0, 0x03, 0xbd, 0x8c, 0xc0, + 0x10, 0xfb, 0xc9, 0xb5, 0xd0, 0xe7, 0xa9, 0x00, + 0x85, 0x27, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0x2a, + 0x85, 0x26, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0x25, + 0x26, 0x99, 0x2c, 0x00, 0x45, 0x27, 0x88, 0x10, + 0xe7, 0xa8, 0xd0, 0xb7, 0xbd, 0x8c, 0xc0, 0x10, + 0xfb, 0xc9, 0xde, 0xd0, 0xae, 0xea, 0xbd, 0x8c, + 0xc0, 0x10, 0xfb, 0xc9, 0xaa, 0xd0, 0xa4, 0x18, + 0x60, 0xa2, 0x32, 0xa0, 0x00, 0xbd, 0x00, 0x3c, + 0x4a, 0x4a, 0x4a, 0x85, 0x27, 0x4a, 0x85, 0x26, + 0x4a, 0x1d, 0x00, 0x3b, 0x91, 0x3e, 0xc8, 0xbd, + 0x33, 0x3c, 0x4a, 0x4a, 0x4a, 0x4a, 0x26, 0x27, + 0x4a, 0x26, 0x26, 0x1d, 0x33, 0x3b, 0x91, 0x3e, + 0xc8, 0xbd, 0x66, 0x3c, 0x4a, 0x4a, 0x4a, 0x4a, + 0x26, 0x27, 0x4a, 0x26, 0x26, 0x1d, 0x66, 0x3b, + 0x91, 0x3e, 0xc8, 0xa5, 0x26, 0x29, 0x07, 0x1d, + + 0x99, 0x3b, 0x91, 0x3e, 0xc8, 0xa5, 0x27, 0x29, + 0x07, 0x1d, 0xcc, 0x3b, 0x91, 0x3e, 0xc8, 0xca, + 0x10, 0xb3, 0xad, 0x99, 0x3c, 0x4a, 0x4a, 0x4a, + 0x0d, 0xff, 0x3b, 0x91, 0x3e, 0x60, 0x86, 0x2b, + 0x85, 0x2a, 0xcd, 0x78, 0x04, 0xf0, 0x53, 0xa9, + 0x00, 0x85, 0x26, 0xad, 0x78, 0x04, 0x85, 0x27, + 0x38, 0xe5, 0x2a, 0xf0, 0x33, 0xb0, 0x07, 0x49, + 0xff, 0xee, 0x78, 0x04, 0x90, 0x05, 0x69, 0xfe, + 0xce, 0x78, 0x04, 0xc5, 0x26, 0x90, 0x02, 0xa5, + 0x26, 0xc9, 0x0c, 0xb0, 0x01, 0xa8, 0x38, 0x20, + 0x6c, 0x3a, 0xb9, 0x8c, 0x3a, 0x20, 0x7b, 0x3a, + 0xa5, 0x27, 0x18, 0x20, 0x6f, 0x3a, 0xb9, 0x98, + 0x3a, 0x20, 0x7b, 0x3a, 0xe6, 0x26, 0xd0, 0xc3, + 0x20, 0x7b, 0x3a, 0x18, 0xad, 0x78, 0x04, 0x29, + 0x03, 0x2a, 0x05, 0x2b, 0xaa, 0xbd, 0x80, 0xc0, + 0xa6, 0x2b, 0x60, 0xa2, 0x11, 0xca, 0xd0, 0xfd, + 0xe6, 0x46, 0xd0, 0x02, 0xe6, 0x47, 0x38, 0xe9, + 0x01, 0xd0, 0xf0, 0x60, 0x01, 0x30, 0x28, 0x24, + 0x20, 0x1e, 0x1d, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, + 0x70, 0x2c, 0x26, 0x22, 0x1f, 0x1e, 0x1d, 0x1c, + 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x08, 0x10, 0x18, + 0x02, 0x03, 0x04, 0x05, 0x06, 0x20, 0x28, 0x30, + 0x07, 0x09, 0x38, 0x40, 0x0a, 0x48, 0x50, 0x58, + 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x11, 0x12, 0x13, + 0x14, 0x15, 0x16, 0x17, 0x19, 0x1a, 0x1b, 0x1c, + 0x1d, 0x1e, 0x21, 0x22, 0x23, 0x24, 0x60, 0x68, + 0x25, 0x26, 0x70, 0x78, 0x27, 0x80, 0x88, 0x90, + 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x31, + 0x32, 0x33, 0x98, 0xa0, 0x34, 0xa8, 0xb0, 0xb8, + 0x35, 0x36, 0x37, 0x39, 0x3a, 0xc0, 0xc8, 0xd0, + 0x3b, 0x3c, 0xd8, 0xe0, 0x3e, 0xe8, 0xf0, 0xf8, + + 0x1b, 0x18, 0x06, 0x14, 0x05, 0x05, 0x04, 0x0d, + 0x04, 0x03, 0x02, 0x01, 0x0a, 0x01, 0x00, 0x03, + 0x00, 0x03, 0x03, 0x03, 0x03, 0x06, 0x00, 0x00, + 0x1a, 0x05, 0x1f, 0x1c, 0x13, 0x15, 0x05, 0x04, + 0x10, 0x01, 0x15, 0x12, 0x00, 0x0f, 0x12, 0x09, + 0x05, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x03, 0x1c, 0x19, 0x06, 0x06, 0x06, + 0x05, 0x10, 0x04, 0x04, 0x03, 0x02, 0x01, 0x0b, + 0x07, 0x04, 0x00, 0x00, 0x03, 0x03, 0x0e, 0x03, + 0x05, 0x1a, 0x1c, 0x1f, 0x0c, 0x04, 0x04, 0x07, + 0x17, 0x00, 0x0f, 0x18, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x00, 0x03, 0x07, 0x1a, + 0x06, 0x15, 0x06, 0x05, 0x11, 0x04, 0x04, 0x03, + 0x02, 0x02, 0x01, 0x08, 0x05, 0x00, 0x00, 0x03, + 0x03, 0x05, 0x03, 0x04, 0x1e, 0x08, 0x1c, 0x14, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x02, 0x03, 0x00, + 0x00, 0x01, 0x02, 0x03, 0x00, 0x00, 0x01, 0x01, + 0x00, 0x1d, 0x07, 0x07, 0x16, 0x06, 0x05, 0x12, + 0x0e, 0x04, 0x03, 0x02, 0x02, 0x01, 0x01, 0x06, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x03, + 0x00, 0x00, 0x00, 0x03, 0x00, 0x03, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x02, 0x00, + 0x00, 0x00, 0x00, 0x02, 0x1e, 0x07, 0x07, 0x17, + 0x13, 0x05, 0x05, 0x0f, 0x0c, 0x00, 0x03, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x02, 0x02, 0x03, 0x00, 0x00, 0x00, 0x00, 0x1f, + + 0x0c, 0x00, 0x0c, 0x00, 0x0c, 0x00, 0x00, 0x00, + 0x14, 0x1d, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x18, 0x1e, 0x07, 0x04, 0x00, 0x00, + 0x00, 0x00, 0x0f, 0x00, 0x10, 0x15, 0x01, 0x00, + 0x00, 0x00, 0x1c, 0x1f, 0x19, 0x10, 0x0d, 0x1b, + 0x0f, 0x00, 0x14, 0x1e, 0x05, 0x01, 0x1c, 0x01, + 0x04, 0x03, 0x11, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x00, 0x02, 0x00, 0x1d, 0x1e, 0x03, 0x0a, 0x04, + 0x01, 0x02, 0x00, 0x02, 0x04, 0x13, 0x19, 0x05, + 0x08, 0x08, 0x04, 0x00, 0x10, 0x15, 0x15, 0x0e, + 0x18, 0x0f, 0x0c, 0x0c, 0x04, 0x1c, 0x01, 0x14, + 0x01, 0x0c, 0x00, 0x00, 0x11, 0x02, 0x05, 0x00, + 0x1c, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x1f, + 0x00, 0x00, 0x00, 0x0d, 0x0a, 0x00, 0x02, 0x19, + 0x0c, 0x1a, 0x03, 0x0c, 0x0c, 0x00, 0x00, 0x10, + 0x0c, 0x01, 0x00, 0x00, 0x00, 0x02, 0x05, 0x12, + 0x10, 0x16, 0x00, 0x09, 0x09, 0x09, 0x0c, 0x05, + 0x11, 0x03, 0x00, 0x01, 0x00, 0x10, 0x00, 0x01, + 0x10, 0x07, 0xab, 0xad, 0xae, 0xaf, 0xb5, 0xb6, + 0xb7, 0xba, 0xbb, 0xbd, 0xbe, 0xbf, 0xd6, 0xd7, + 0xda, 0xdb, 0xdd, 0xde, 0xdf, 0xea, 0xeb, 0xed, + 0xee, 0xef, 0xf5, 0xf6, 0xf7, 0xfa, 0xfb, 0xfd, + 0xfe, 0xff, 0x1c, 0x1c, 0x1c, 0x00, 0x00, 0x00, + 0xa4, 0x2d, 0xb9, 0xd0, 0x3c, 0xa0, 0x05, 0x4c, + 0x0a, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x05, 0x0a, 0x02, 0x07, 0x0c, 0x04, 0x09, + 0x01, 0x06, 0x0b, 0x03, 0x08, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + 0x84, 0x48, 0x85, 0x49, 0xa0, 0x02, 0x8c, 0xf8, + 0x06, 0xa0, 0x04, 0x8c, 0xf8, 0x04, 0xa0, 0x01, + 0xb1, 0x48, 0xaa, 0xa0, 0x0f, 0xd1, 0x48, 0xf0, + 0x1b, 0x8a, 0x48, 0xb1, 0x48, 0xaa, 0x68, 0x48, + 0x91, 0x48, 0xbd, 0x8e, 0xc0, 0xa0, 0x08, 0xbd, + 0x8c, 0xc0, 0xdd, 0x8c, 0xc0, 0xd0, 0xf6, 0x88, + 0xd0, 0xf8, 0x68, 0xaa, 0xbd, 0x8e, 0xc0, 0xbd, + 0x8c, 0xc0, 0xbd, 0x8c, 0xc0, 0x48, 0x68, 0x8e, + 0xf8, 0x05, 0xdd, 0x8c, 0xc0, 0x08, 0xbd, 0x89, + 0xc0, 0xa0, 0x06, 0xb1, 0x48, 0x99, 0x36, 0x00, + 0xc8, 0xc0, 0x0a, 0xd0, 0xf6, 0xa0, 0x03, 0xb1, + 0x3c, 0x85, 0x47, 0xa0, 0x02, 0xb1, 0x48, 0xa0, + 0x10, 0xd1, 0x48, 0xf0, 0x06, 0x91, 0x48, 0x28, + 0xa0, 0x00, 0x08, 0x6a, 0x90, 0x05, 0xbd, 0x8a, + 0xc0, 0xb0, 0x03, 0xbd, 0x8b, 0xc0, 0x66, 0x35, + 0x28, 0x08, 0xd0, 0x0b, 0xa0, 0x07, 0x20, 0x7f, + 0x3a, 0x88, 0xd0, 0xfa, 0xae, 0xf8, 0x05, 0xa0, + 0x04, 0xb1, 0x48, 0x20, 0x4b, 0x3e, 0x28, 0xd0, + 0x0d, 0xa0, 0x12, 0x88, 0xd0, 0xfd, 0xe6, 0x46, + 0xd0, 0xf7, 0xe6, 0x47, 0xd0, 0xf3, 0xa0, 0x0c, + 0xb1, 0x48, 0xf0, 0x5a, 0xc9, 0x04, 0xf0, 0x58, + 0x6a, 0x08, 0xb0, 0x03, 0x20, 0x00, 0x38, 0xa0, + 0x30, 0x8c, 0x78, 0x05, 0xae, 0xf8, 0x05, 0x20, + 0x65, 0x39, 0x90, 0x24, 0xce, 0x78, 0x05, 0x10, + 0xf3, 0xad, 0x78, 0x04, 0x48, 0xa9, 0x60, 0x20, + 0x86, 0x3e, 0xce, 0xf8, 0x06, 0xf0, 0x28, 0xa9, + 0x04, 0x8d, 0xf8, 0x04, 0xa9, 0x00, 0x20, 0x4b, + 0x3e, 0x68, 0x20, 0x4b, 0x3e, 0x4c, 0xaf, 0x3d, + 0xa4, 0x2e, 0xcc, 0x78, 0x04, 0xf0, 0x22, 0xad, + 0x78, 0x04, 0x48, 0x98, 0x20, 0x86, 0x3e, 0x68, + 0xce, 0xf8, 0x04, 0xd0, 0xe5, 0xf0, 0xca, 0x68, + 0xa9, 0x40, 0x28, 0x4c, 0x39, 0x3e, 0xf0, 0x37, + + 0xa0, 0x03, 0xb1, 0x48, 0x85, 0x2f, 0x4c, 0xa0, + 0x3e, 0xa0, 0x03, 0xb1, 0x48, 0x48, 0xa5, 0x2f, + 0xa0, 0x0e, 0x91, 0x48, 0x68, 0xf0, 0x08, 0xc5, + 0x2f, 0xf0, 0x04, 0xa9, 0x20, 0xd0, 0xdb, 0xa0, + 0x05, 0xa5, 0x2d, 0xd1, 0x48, 0xd0, 0x95, 0x28, + 0x90, 0x18, 0x20, 0xfd, 0x38, 0x08, 0xb0, 0x8c, + 0x28, 0x20, 0xc1, 0x39, 0xae, 0xf8, 0x05, 0x18, + 0x24, 0x38, 0xa0, 0x0d, 0x91, 0x48, 0xbd, 0x88, + 0xc0, 0x60, 0x20, 0x6a, 0x38, 0x90, 0xf0, 0xa9, + 0x10, 0xb0, 0xee, 0x48, 0xa0, 0x01, 0xb1, 0x3c, + 0x6a, 0x68, 0x90, 0x08, 0x0a, 0x20, 0x5c, 0x3e, + 0x4e, 0x78, 0x04, 0x60, 0x85, 0x2e, 0x20, 0x7f, + 0x3e, 0xb9, 0x78, 0x04, 0x24, 0x35, 0x30, 0x03, + 0xb9, 0xf8, 0x04, 0x8d, 0x78, 0x04, 0xa5, 0x2e, + 0x24, 0x35, 0x30, 0x05, 0x99, 0xf8, 0x04, 0x10, + 0x03, 0x99, 0x78, 0x04, 0x4c, 0x1e, 0x3a, 0x8a, + 0x4a, 0x4a, 0x4a, 0x4a, 0xa8, 0x60, 0x48, 0xa0, + 0x02, 0xb1, 0x48, 0x6a, 0x66, 0x35, 0x20, 0x7f, + 0x3e, 0x68, 0x0a, 0x24, 0x35, 0x30, 0x05, 0x99, + 0xf8, 0x04, 0x10, 0x03, 0x99, 0x78, 0x04, 0x60, + 0xa9, 0x80, 0x8d, 0x78, 0x04, 0xa9, 0x00, 0x85, + 0x41, 0x20, 0x1e, 0x3a, 0xa9, 0xaa, 0x85, 0x4a, + 0xa0, 0x50, 0x84, 0x47, 0xa9, 0x27, 0x85, 0x4b, + 0xbd, 0x8d, 0xc0, 0xbd, 0x8e, 0xc0, 0xa9, 0xff, + 0x9d, 0x8f, 0xc0, 0xdd, 0x8c, 0xc0, 0x24, 0x00, + 0x88, 0xf0, 0x0f, 0x48, 0x68, 0xea, 0x48, 0x68, + 0xea, 0xea, 0x9d, 0x8d, 0xc0, 0xdd, 0x8c, 0xc0, + 0xb0, 0xee, 0xc6, 0x4b, 0xd0, 0xf0, 0xa4, 0x47, + 0xea, 0xea, 0xd0, 0x06, 0x48, 0x68, 0x48, 0x68, + 0xc1, 0x00, 0xea, 0x9d, 0x8d, 0xc0, 0xdd, 0x8c, + 0xc0, 0x88, 0xd0, 0xf0, 0xa9, 0xd5, 0x20, 0xcc, + 0x3f, 0xa9, 0xaa, 0x20, 0xcd, 0x3f, 0xa9, 0xb5, + + 0x20, 0xcd, 0x3f, 0xa5, 0x2f, 0x20, 0xbd, 0x3f, + 0xa5, 0x41, 0x20, 0xbd, 0x3f, 0xa5, 0x4b, 0x20, + 0xbd, 0x3f, 0xa5, 0x2f, 0x45, 0x41, 0x45, 0x4b, + 0x48, 0x4a, 0x05, 0x4a, 0x9d, 0x8d, 0xc0, 0xdd, + 0x8c, 0xc0, 0x68, 0x09, 0xaa, 0x20, 0xcc, 0x3f, + 0xa9, 0xde, 0x20, 0xcd, 0x3f, 0xa9, 0xaa, 0x20, + 0xcd, 0x3f, 0xa9, 0xeb, 0x20, 0xcd, 0x3f, 0xa9, + 0xff, 0x20, 0xcd, 0x3f, 0xa0, 0x02, 0x84, 0x46, + 0xa0, 0xad, 0xd0, 0x06, 0x88, 0xf0, 0x0d, 0x48, + 0x68, 0xea, 0x48, 0x68, 0x9d, 0x8d, 0xc0, 0xdd, + 0x8c, 0xc0, 0xb0, 0xf0, 0xc6, 0x46, 0xd0, 0xf2, + 0xa4, 0x47, 0x18, 0x24, 0x00, 0x9d, 0x8d, 0xc0, + 0xbd, 0x8c, 0xc0, 0xa5, 0x4b, 0x69, 0x0a, 0x85, + 0x4b, 0xe9, 0x0c, 0xf0, 0x0a, 0xb0, 0x01, 0x2c, + 0x85, 0x4b, 0xa9, 0xff, 0x4c, 0xeb, 0x3e, 0x48, + 0x68, 0xa4, 0x47, 0xbd, 0x8d, 0xc0, 0xbd, 0x8e, + 0xc0, 0x30, 0x32, 0x88, 0x48, 0x68, 0x48, 0x68, + 0x48, 0x68, 0x88, 0xd0, 0xf7, 0x20, 0x65, 0x39, + 0xb0, 0x04, 0xa5, 0x2d, 0xf0, 0x0a, 0xa4, 0x47, + 0x88, 0xc0, 0x10, 0x90, 0x18, 0x4c, 0xb2, 0x3e, + 0xe6, 0x41, 0xa5, 0x41, 0xc9, 0x23, 0xb0, 0x12, + 0x0a, 0x20, 0x1e, 0x3a, 0xa4, 0x47, 0xc8, 0xc8, + 0x84, 0x47, 0x4c, 0xb2, 0x3e, 0xa9, 0x40, 0x4c, + 0x39, 0x3e, 0x4c, 0x37, 0x3e, 0x48, 0x4a, 0x05, + 0x4a, 0x9d, 0x8d, 0xc0, 0xdd, 0x8c, 0xc0, 0x68, + 0xc1, 0x00, 0x09, 0xaa, 0xea, 0x48, 0x68, 0xea, + 0x9d, 0x8d, 0xc0, 0xdd, 0x8c, 0xc0, 0x60, 0x01, + 0x60, 0x4c, 0xdd, 0x25, 0x8d, 0x63, 0x2a, 0x8d, + 0x70, 0x2a, 0x8d, 0x71, 0x2a, 0x60, 0x20, 0x5b, + 0x27, 0x8c, 0xb7, 0x2a, 0x60, 0x20, 0x7e, 0x2e, + 0xae, 0x9b, 0x33, 0x9a, 0x20, 0x16, 0x23, 0xba, + 0x8e, 0x9b, 0x33, 0xa9, 0x09, 0x4c, 0x85, 0x33, + + 0x4c, 0x84, 0x1d, 0xa9, 0xbf, 0x85, 0x41, 0xa2, + 0x00, 0x86, 0x40, 0xa0, 0x00, 0xa1, 0x40, 0x85, + 0x26, 0x98, 0x45, 0x26, 0x85, 0x26, 0x98, 0x41, + 0x40, 0x81, 0x40, 0xc5, 0x26, 0xd0, 0x05, 0xc8, + 0xd0, 0xef, 0xf0, 0x04, 0xc6, 0x41, 0xd0, 0xe3, + 0xa5, 0x41, 0x29, 0xdf, 0x85, 0x43, 0x86, 0x42, + 0xa1, 0x42, 0x48, 0x85, 0x26, 0x98, 0x45, 0x26, + 0x85, 0x26, 0x98, 0x41, 0x40, 0x81, 0x42, 0xc5, + 0x26, 0xd0, 0x09, 0xc8, 0xd0, 0xef, 0xa4, 0x43, + 0x68, 0x4c, 0x51, 0x1b, 0x68, 0x81, 0x42, 0xa4, + 0x41, 0xc8, 0x8c, 0x79, 0x1c, 0x38, 0x98, 0xed, + 0x7a, 0x1c, 0x8d, 0x78, 0x1c, 0x38, 0xed, 0x76, + 0x1c, 0xf0, 0x9d, 0x8d, 0x7b, 0x1c, 0xad, 0x76, + 0x1c, 0x8d, 0x0d, 0x1d, 0xa9, 0x1d, 0x8d, 0x49, + 0x37, 0xa9, 0x84, 0x8d, 0x48, 0x37, 0xa2, 0x00, + 0x86, 0x40, 0xbd, 0x29, 0x1c, 0xa8, 0xbd, 0x2a, + 0x1c, 0x85, 0x41, 0x4c, 0x93, 0x1b, 0x18, 0xb1, + 0x40, 0x6d, 0x7b, 0x1c, 0x91, 0x40, 0xc8, 0xd0, + 0x02, 0xe6, 0x41, 0xc8, 0xd0, 0x02, 0xe6, 0x41, + 0xa5, 0x41, 0xdd, 0x2c, 0x1c, 0x90, 0xe7, 0x98, + 0xdd, 0x2b, 0x1c, 0x90, 0xe1, 0x8a, 0x18, 0x69, + 0x04, 0xaa, 0xec, 0x28, 0x1c, 0x90, 0xcb, 0xa2, + 0x00, 0x8e, 0x9c, 0x33, 0xbd, 0x5a, 0x1c, 0x85, + 0x40, 0xbd, 0x5b, 0x1c, 0x85, 0x41, 0xa2, 0x00, + 0xa1, 0x40, 0x20, 0x8e, 0xf8, 0xa4, 0x2f, 0xc0, + 0x02, 0xd0, 0x11, 0xb1, 0x40, 0xcd, 0x76, 0x1c, + 0x90, 0x0a, 0xcd, 0x77, 0x1c, 0xb0, 0x05, 0x6d, + 0x7b, 0x1c, 0x91, 0x40, 0x38, 0xa5, 0x2f, 0x65, + 0x40, 0x85, 0x40, 0xa9, 0x00, 0x65, 0x41, 0x85, + 0x41, 0xae, 0x9c, 0x33, 0xdd, 0x5d, 0x1c, 0x90, + 0xcd, 0xa5, 0x40, 0xdd, 0x5c, 0x1c, 0x90, 0xc6, + 0x8a, 0x18, 0x69, 0x04, 0xaa, 0xec, 0x59, 0x1c, + + 0x90, 0xaf, 0xa9, 0x3f, 0x85, 0x41, 0xac, 0x79, + 0x1c, 0x88, 0x84, 0x43, 0xa9, 0x00, 0x85, 0x40, + 0x85, 0x42, 0xa8, 0xb1, 0x40, 0x91, 0x42, 0xc8, + 0xd0, 0xf9, 0xce, 0x7c, 0x1c, 0xf0, 0x06, 0xc6, + 0x41, 0xc6, 0x43, 0xd0, 0xee, 0x4c, 0x54, 0x1e, + 0x24, 0x00, 0x1d, 0x56, 0x1d, 0x58, 0x1d, 0x5a, + 0x1d, 0x64, 0x1d, 0x66, 0x1d, 0x6c, 0x1d, 0x70, + 0x1d, 0x78, 0x1d, 0x7c, 0x1d, 0x7e, 0x1d, 0x80, + 0x1d, 0xc1, 0x2a, 0xfd, 0x2a, 0xe4, 0x37, 0xe8, + 0x37, 0xee, 0x37, 0xf0, 0x37, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x18, 0x84, 0x1d, 0x84, 0x28, 0xfd, 0x2a, + 0x97, 0x33, 0x00, 0x37, 0xe0, 0x37, 0xfe, 0x35, + 0xfe, 0x35, 0x00, 0x38, 0x8f, 0x3a, 0x00, 0x3d, + 0xff, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x1d, 0x40, + 0x00, 0x00, 0x23, 0x00, 0x23, 0xa5, 0x74, 0x86, + 0x6f, 0x85, 0x70, 0xa0, 0x00, 0x84, 0x8b, 0xa5, + 0x6d, 0xa6, 0x6e, 0x85, 0x9b, 0x86, 0x9c, 0xa9, + 0x55, 0xa2, 0x00, 0x85, 0x5e, 0x86, 0x5f, 0xc5, + 0x52, 0xf0, 0x05, 0x20, 0x1a, 0x1d, 0xf0, 0xf7, + 0xa9, 0x07, 0x85, 0x8f, 0xa5, 0x69, 0xa6, 0x6a, + 0x85, 0x5e, 0x86, 0x5f, 0xe4, 0x6c, 0xd0, 0x04, + 0xc5, 0x6b, 0xf0, 0x05, 0x20, 0x10, 0x1d, 0xf0, + 0xf3, 0x85, 0x94, 0x86, 0x95, 0xa9, 0x03, 0x85, + 0x8f, 0xa5, 0x94, 0xa6, 0x95, 0xe4, 0x6e, 0xd0, + 0x07, 0xc5, 0x6d, 0xd0, 0x03, 0x4c, 0x59, 0x1d, + 0x85, 0x5e, 0x86, 0x00, 0xa0, 0x00, 0xb1, 0x5e, + 0xaa, 0xc8, 0xb1, 0x5e, 0x08, 0xc8, 0xb1, 0x5e, + 0x65, 0x94, 0x85, 0x94, 0xc8, 0xb1, 0x5e, 0x65, + 0x95, 0x85, 0x95, 0x28, 0x10, 0xd3, 0x8a, 0x30, + 0xd0, 0xa6, 0x1c, 0xa6, 0x1b, 0xa6, 0x1a, 0x80, + 0x1a, 0x65, 0x5e, 0x85, 0x5e, 0x90, 0x02, 0xe6, + + 0xd3, 0x1c, 0x81, 0x1e, 0xbd, 0x1e, 0x75, 0x2a, + 0x93, 0x2a, 0x60, 0x2a, 0x00, 0x1b, 0xbb, 0x35, + 0xea, 0x1e, 0x11, 0x1f, 0x22, 0x1f, 0x2e, 0x1f, + 0x51, 0x1f, 0x60, 0x1f, 0x70, 0x1f, 0x4e, 0x25, + 0x12, 0x24, 0x96, 0x23, 0xd0, 0x24, 0xef, 0x24, + 0x62, 0x22, 0x70, 0x22, 0x74, 0x22, 0xe9, 0x22, + 0x1a, 0x25, 0xc5, 0x25, 0x0f, 0x25, 0xdc, 0x25, + 0xa2, 0x22, 0x97, 0x22, 0x80, 0x22, 0x6d, 0x25, + 0x32, 0x22, 0x3c, 0x22, 0x28, 0x22, 0x2d, 0x22, + 0x50, 0x22, 0x79, 0x25, 0x9d, 0x25, 0x30, 0x23, + 0x5c, 0x23, 0x8d, 0x23, 0x7c, 0x22, 0x36, 0xe8, + 0xe5, 0x24, 0xe3, 0xe3, 0x00, 0xe0, 0x03, 0xe0, + 0x00, 0x00, 0x36, 0xe8, 0xe5, 0x24, 0xe3, 0xe3, + 0x00, 0xe0, 0x03, 0xe0, 0xfc, 0x24, 0xfc, 0x24, + 0x65, 0xd8, 0x00, 0xe0, 0x3c, 0xd4, 0xf2, 0xd4, + 0x06, 0x25, 0x06, 0x25, 0x67, 0x10, 0x84, 0x1d, + 0x3c, 0x0c, 0xf2, 0x0c, 0xad, 0xe9, 0x37, 0x4a, + 0x4a, 0x4a, 0x4a, 0x8d, 0x6a, 0x2a, 0xad, 0xea, + 0x37, 0x8d, 0x68, 0x2a, 0xad, 0x00, 0xe0, 0x49, + 0x20, 0xd0, 0x11, 0x8d, 0xb6, 0x2a, 0xa2, 0x0a, + 0xbd, 0x61, 0x1d, 0x9d, 0x55, 0x1d, 0xca, 0xd0, + 0xf7, 0x4c, 0xbc, 0x1d, 0xa9, 0x40, 0x8d, 0xb6, + 0x2a, 0xa2, 0x0c, 0xbd, 0x6b, 0x1d, 0x9d, 0x55, + 0x1d, 0xca, 0xd0, 0xf7, 0x38, 0xb0, 0x12, 0xad, + 0xb6, 0x2a, 0xd0, 0x04, 0xa9, 0x20, 0xd0, 0x05, + 0x0a, 0x10, 0x05, 0xa9, 0x4c, 0x20, 0xb2, 0x25, + 0x18, 0x08, 0x20, 0x51, 0x28, 0xa9, 0x00, 0x8d, + 0x5e, 0x2a, 0x8d, 0x52, 0x2a, 0x28, 0x6a, 0x8d, + 0x51, 0x2a, 0x30, 0x03, 0x6c, 0x5e, 0x1d, 0x6c, + 0x5c, 0x1d, 0x0a, 0x10, 0x19, 0x8d, 0xb6, 0x2a, + 0xa2, 0x0c, 0xbd, 0x77, 0x1d, 0x9d, 0x55, 0x1d, + 0xca, 0xd0, 0xf7, 0xa2, 0x1d, 0xbd, 0x93, 0x2a, + + 0x9d, 0x75, 0x2a, 0xca, 0x10, 0xf7, 0xad, 0xb1, + 0x2a, 0x8d, 0x57, 0x2a, 0x20, 0xd4, 0x27, 0xad, + 0xb3, 0x2a, 0xf0, 0x09, 0x48, 0x20, 0x9d, 0x26, + 0x68, 0xa0, 0x00, 0x91, 0x40, 0x20, 0x5b, 0x27, + 0xad, 0x5f, 0x2a, 0xd0, 0x20, 0xa2, 0x2f, 0xbd, + 0x51, 0x1e, 0x9d, 0xd0, 0x03, 0xca, 0x10, 0xf7, + 0xad, 0x53, 0x1e, 0x8d, 0xf3, 0x03, 0x49, 0xa5, + 0x8d, 0xf4, 0x03, 0xad, 0x52, 0x1e, 0x8d, 0xf2, + 0x03, 0xa9, 0x06, 0xd0, 0x05, 0xad, 0x62, 0x2a, + 0xf0, 0x06, 0x8d, 0x5f, 0x2a, 0x4c, 0x80, 0x21, + 0x60, 0x4c, 0xbf, 0x1d, 0x4c, 0x84, 0x1d, 0x4c, + 0xfd, 0x2a, 0x4c, 0xb5, 0x37, 0xad, 0x0f, 0x1d, + 0xac, 0x0e, 0x1d, 0x60, 0xad, 0xc2, 0x2a, 0xac, + 0xc1, 0x2a, 0x60, 0x4c, 0x51, 0x28, 0xea, 0xea, + 0x4c, 0x59, 0xfa, 0x4c, 0x65, 0xff, 0x4c, 0x58, + 0xff, 0x4c, 0x65, 0xff, 0x4c, 0x65, 0xff, 0x65, + 0xff, 0x20, 0xd1, 0x1e, 0xad, 0x51, 0x2a, 0xf0, + 0x15, 0x48, 0xad, 0x5c, 0x2a, 0x91, 0x28, 0x68, + 0x30, 0x03, 0x4c, 0x26, 0x26, 0x20, 0xea, 0x1d, + 0xa4, 0x24, 0xa9, 0x60, 0x91, 0x28, 0xad, 0xb3, + 0x2a, 0xf0, 0x03, 0x20, 0x82, 0x26, 0xa9, 0x03, + 0x8d, 0x52, 0x2a, 0x20, 0xba, 0x1f, 0x20, 0xba, + 0x1e, 0x8d, 0x5c, 0x2a, 0x8e, 0x5a, 0x2a, 0x4c, + 0xb3, 0x1f, 0x6c, 0x38, 0x00, 0x20, 0xd1, 0x1e, + 0xad, 0x52, 0x2a, 0x0a, 0xaa, 0xbd, 0x11, 0x1d, + 0x48, 0xbd, 0x10, 0x1d, 0x48, 0xad, 0x5c, 0x2a, + 0x60, 0x8d, 0x5c, 0x2a, 0x8e, 0x5a, 0x2a, 0x8c, + 0x5b, 0x2a, 0xba, 0xe8, 0xe8, 0x8e, 0x59, 0x2a, + 0xa2, 0x03, 0xbd, 0x53, 0x2a, 0x95, 0x36, 0xca, + 0x10, 0xf8, 0x60, 0xae, 0xb7, 0x2a, 0xf0, 0x03, + 0x4c, 0x78, 0x1f, 0xae, 0x51, 0x2a, 0xf0, 0x08, + 0xc9, 0xbf, 0xf0, 0x75, 0xc5, 0x33, 0xf0, 0x27, + + 0xa2, 0x02, 0x8e, 0x52, 0x2a, 0xcd, 0xb2, 0x2a, + 0xd0, 0x19, 0xca, 0x8e, 0x52, 0x2a, 0xca, 0x8e, + 0x5d, 0x2a, 0xae, 0x5d, 0x2a, 0x9d, 0x00, 0x02, + 0xe8, 0x8e, 0x5d, 0x2a, 0xc9, 0x8d, 0xd0, 0x75, + 0x4c, 0xcd, 0x1f, 0xc9, 0x8d, 0xd0, 0x7d, 0xa2, + 0x00, 0x8e, 0x52, 0x2a, 0x4c, 0xa4, 0x1f, 0xa2, + 0x00, 0x8e, 0x52, 0x2a, 0xc9, 0x8d, 0xf0, 0x07, + 0xad, 0xb3, 0x2a, 0xf0, 0x67, 0xd0, 0x5e, 0x48, + 0x38, 0xad, 0xb3, 0x2a, 0xd0, 0x03, 0x20, 0x5e, + 0x26, 0x68, 0x90, 0xec, 0xae, 0x5a, 0x2a, 0x4c, + 0x15, 0x1f, 0xc9, 0x8d, 0xd0, 0x05, 0xa9, 0x05, + 0x8d, 0x52, 0x2a, 0x20, 0x0e, 0x26, 0x4c, 0x99, + 0x1f, 0xcd, 0xb2, 0x2a, 0xf0, 0x85, 0xc9, 0x8a, + 0xf0, 0xf1, 0xa2, 0x04, 0x8e, 0x52, 0x2a, 0xd0, + 0xe1, 0xa9, 0x00, 0x8d, 0x52, 0x2a, 0xf0, 0x25, + 0xa9, 0x00, 0x8d, 0xb7, 0x2a, 0x20, 0x51, 0x28, + 0x4c, 0xdc, 0x24, 0xad, 0x00, 0x02, 0xcd, 0xb2, + 0x2a, 0xf0, 0x0a, 0xa9, 0x8d, 0x8d, 0x00, 0x02, + 0xa2, 0x00, 0x8e, 0x5a, 0x2a, 0xa9, 0x40, 0xd0, + 0x06, 0xa9, 0x10, 0xd0, 0x02, 0xa9, 0x20, 0x2d, + 0x5e, 0x2a, 0xf0, 0x0f, 0x20, 0xba, 0x1f, 0x20, + 0xc5, 0x1f, 0x8d, 0x5c, 0x2a, 0x8c, 0x5b, 0x2a, + 0x8e, 0x5a, 0x2a, 0x20, 0x51, 0x28, 0xae, 0x59, + 0x2a, 0x9a, 0xad, 0x5c, 0x2a, 0xac, 0x5b, 0x2a, + 0xae, 0x5a, 0x2a, 0x38, 0x60, 0x6c, 0x36, 0x00, + 0xa9, 0x8d, 0x4c, 0xc5, 0x1f, 0xa0, 0xff, 0x8c, + 0x5f, 0x2a, 0xc8, 0x8c, 0x62, 0x2a, 0xee, 0x5f, + 0x2a, 0xa2, 0x00, 0x08, 0xbd, 0x00, 0x02, 0xcd, + 0xb2, 0x2a, 0xd0, 0x01, 0xe8, 0x8e, 0x5d, 0x2a, + 0x20, 0xa4, 0x21, 0x29, 0x7f, 0x59, 0x84, 0x28, + 0xc8, 0x0a, 0xf0, 0x02, 0x68, 0x08, 0x90, 0xf0, + 0x28, 0xf0, 0x20, 0xb9, 0x84, 0x28, 0xd0, 0xd6, + + 0xad, 0x00, 0x02, 0xcd, 0xb2, 0x2a, 0xf0, 0x03, + 0x4c, 0xa4, 0x1f, 0xad, 0x01, 0x02, 0xc9, 0x8d, + 0xd0, 0x06, 0x20, 0x5b, 0x27, 0x4c, 0x95, 0x1f, + 0x4c, 0xc4, 0x26, 0x0e, 0x5f, 0x2a, 0xac, 0x5f, + 0x2a, 0x20, 0x5e, 0x26, 0x90, 0x0c, 0xa9, 0x02, + 0x39, 0x09, 0x29, 0xf0, 0x05, 0xa9, 0x0f, 0x4c, + 0xd2, 0x26, 0xc0, 0x06, 0xd0, 0x02, 0x84, 0x33, + 0xa9, 0x20, 0x39, 0x09, 0x29, 0xf0, 0x61, 0x20, + 0x95, 0x20, 0x08, 0x20, 0xa4, 0x21, 0xf0, 0x1e, + 0x0a, 0x90, 0x05, 0x30, 0x03, 0x4c, 0x00, 0x20, + 0x6a, 0x4c, 0x59, 0x20, 0x20, 0x93, 0x21, 0xf0, + 0x0d, 0x99, 0x75, 0x2a, 0xc8, 0xc0, 0x3c, 0x90, + 0xf3, 0x20, 0x93, 0x21, 0xd0, 0xfb, 0x28, 0xd0, + 0x0f, 0xac, 0x5f, 0x2a, 0xa9, 0x10, 0x39, 0x09, + 0x29, 0xf0, 0x0c, 0xa0, 0x1e, 0x08, 0xd0, 0xcb, + 0xad, 0x93, 0x2a, 0xc9, 0xa0, 0xf0, 0x13, 0xad, + 0x75, 0x2a, 0xc9, 0xa0, 0xd0, 0x4b, 0xac, 0x5f, + 0x2a, 0xa9, 0xc0, 0x39, 0x09, 0x29, 0xf0, 0x02, + 0x10, 0x3f, 0x4c, 0x00, 0x20, 0xa0, 0x3c, 0xa9, + 0xa0, 0x99, 0x74, 0x2a, 0x88, 0xd0, 0xfa, 0x60, + 0x8d, 0x75, 0x2a, 0xa9, 0x0c, 0x39, 0x09, 0x29, + 0xf0, 0x27, 0x20, 0xb9, 0x21, 0xb0, 0x1f, 0xa8, + 0xd0, 0x17, 0xe0, 0x11, 0xb0, 0x13, 0xac, 0x5f, + 0x2a, 0xa9, 0x08, 0x39, 0x09, 0x29, 0xf0, 0x06, + 0xe0, 0x08, 0xb0, 0xce, 0x90, 0x0b, 0x8a, 0xd0, + 0x08, 0xa9, 0x02, 0x4c, 0xd2, 0x26, 0x4c, 0xc4, + 0x26, 0xa9, 0x00, 0x8d, 0x65, 0x2a, 0x8d, 0x74, + 0x2a, 0x8d, 0x66, 0x2a, 0x8d, 0x6c, 0x2a, 0x8d, + 0x6d, 0x2a, 0x20, 0xdc, 0x3f, 0xad, 0x5d, 0x2a, + 0x20, 0xa4, 0x21, 0xd0, 0x1f, 0xc9, 0x8d, 0xd0, + 0xf7, 0xae, 0x5f, 0x2a, 0xad, 0x65, 0x2a, 0x1d, + 0x0a, 0x29, 0x5d, 0x0a, 0x29, 0xd0, 0x93, 0xae, + + 0x63, 0x2a, 0xf0, 0x76, 0x8d, 0x63, 0x2a, 0x8e, + 0x5d, 0x2a, 0xd0, 0xdc, 0xa2, 0x0a, 0xdd, 0x40, + 0x29, 0xf0, 0x05, 0xca, 0xd0, 0xf8, 0xf0, 0xb6, + 0xbd, 0x4a, 0x29, 0x30, 0x47, 0x0d, 0x65, 0x2a, + 0x8d, 0x65, 0x2a, 0xca, 0x8e, 0x64, 0x2a, 0x20, + 0xb9, 0x21, 0xb0, 0xa2, 0xad, 0x64, 0x2a, 0x0a, + 0x0a, 0xa8, 0xa5, 0x45, 0xd0, 0x09, 0xa5, 0x44, + 0xd9, 0x55, 0x29, 0x90, 0x8c, 0xa5, 0x45, 0xd9, + 0x58, 0x29, 0x90, 0x0b, 0xd0, 0x83, 0xa5, 0x44, + 0xd9, 0x57, 0x29, 0x90, 0x02, 0xd0, 0xf5, 0xad, + 0x63, 0x2a, 0xd0, 0x94, 0x98, 0x4a, 0xa8, 0xa5, + 0x45, 0x99, 0x67, 0x2a, 0xa5, 0x44, 0x99, 0x66, + 0x2a, 0x4c, 0xe8, 0x20, 0x48, 0xa9, 0x80, 0x0d, + 0x65, 0x2a, 0x8d, 0x65, 0x2a, 0x68, 0x29, 0x7f, + 0x0d, 0x74, 0x2a, 0x8d, 0x74, 0x2a, 0xd0, 0xe9, + 0xf0, 0x9c, 0x20, 0x80, 0x21, 0x4c, 0x83, 0x1f, + 0x20, 0x5b, 0x27, 0x20, 0xae, 0x21, 0xad, 0x5f, + 0x2a, 0xaa, 0xbd, 0x1f, 0x1d, 0x48, 0xbd, 0x1e, + 0x1d, 0x48, 0x60, 0xae, 0x5d, 0x2a, 0xbd, 0x00, + 0x02, 0xc9, 0x8d, 0xf0, 0x06, 0xe8, 0x8e, 0x5d, + 0x2a, 0xc9, 0xac, 0x60, 0x20, 0x93, 0x21, 0xf0, + 0xfa, 0xc9, 0xa0, 0xf0, 0xf7, 0x60, 0xa9, 0x00, + 0xa0, 0x16, 0x99, 0xba, 0x35, 0x88, 0xd0, 0xfa, + 0x60, 0xa9, 0x00, 0x85, 0x44, 0x85, 0x45, 0x20, + 0xa4, 0x21, 0x08, 0xc9, 0xa4, 0xf0, 0x3c, 0x28, + 0x4c, 0xce, 0x21, 0x20, 0xa4, 0x21, 0xd0, 0x06, + 0xa6, 0x44, 0xa5, 0x45, 0x18, 0x60, 0x38, 0xe9, + 0xb0, 0x30, 0x21, 0xc9, 0x0a, 0xb0, 0x1d, 0x20, + 0xfe, 0x21, 0x65, 0x44, 0xaa, 0xa9, 0x00, 0x65, + 0x45, 0xa8, 0x20, 0xfe, 0x21, 0x20, 0xfe, 0x21, + 0x8a, 0x65, 0x44, 0x85, 0x44, 0x98, 0x65, 0x45, + 0x85, 0x45, 0x90, 0xcf, 0x38, 0x60, 0x06, 0x44, + + 0x26, 0x45, 0x60, 0x28, 0x20, 0xa4, 0x21, 0xf0, + 0xc5, 0x38, 0xe9, 0xb0, 0x30, 0xee, 0xc9, 0x0a, + 0x90, 0x08, 0xe9, 0x07, 0x30, 0xe6, 0xc9, 0x10, + 0xb0, 0xe2, 0xa2, 0x04, 0x20, 0xfe, 0x21, 0xca, + 0xd0, 0xfa, 0x05, 0x44, 0x85, 0x44, 0x4c, 0x04, + 0x22, 0xa5, 0x44, 0x4c, 0x95, 0xfe, 0xa5, 0x44, + 0x4c, 0x8b, 0xfe, 0xad, 0x5e, 0x2a, 0x0d, 0x74, + 0x2a, 0x8d, 0x5e, 0x2a, 0x60, 0x2c, 0x74, 0x2a, + 0x50, 0x03, 0x20, 0xc8, 0x1f, 0xa9, 0x70, 0x4d, + 0x74, 0x2a, 0x2d, 0x5e, 0x2a, 0x8d, 0x5e, 0x2a, + 0x60, 0xa9, 0x00, 0x8d, 0xb3, 0x2a, 0xa5, 0x44, + 0x48, 0x20, 0x16, 0x23, 0x68, 0x8d, 0x57, 0x2a, + 0x4c, 0xd4, 0x27, 0xa9, 0x05, 0x20, 0xaa, 0x22, + 0x20, 0x64, 0x27, 0xa0, 0x00, 0x98, 0x91, 0x40, + 0x60, 0xa9, 0x07, 0xd0, 0x02, 0xa9, 0x08, 0x20, + 0xaa, 0x22, 0x4c, 0xea, 0x22, 0xa9, 0x0c, 0xd0, + 0xf6, 0xad, 0x08, 0x1d, 0x8d, 0xbd, 0x35, 0xad, + 0x09, 0x1d, 0x8d, 0xbe, 0x35, 0xa9, 0x09, 0x8d, + 0x63, 0x2a, 0x20, 0xc8, 0x22, 0x4c, 0xea, 0x22, + 0x20, 0xa3, 0x22, 0x20, 0x8c, 0x26, 0xd0, 0xfb, + 0x4c, 0x46, 0x25, 0xa9, 0x00, 0x4c, 0xd5, 0x23, + 0xa9, 0x01, 0x8d, 0x63, 0x2a, 0xad, 0x6c, 0x2a, + 0xd0, 0x0a, 0xad, 0x6d, 0x2a, 0xd0, 0x05, 0xa9, + 0x01, 0x8d, 0x6c, 0x2a, 0xad, 0x6c, 0x2a, 0x8d, + 0xbd, 0x35, 0xad, 0x6d, 0x2a, 0x8d, 0xbe, 0x35, + 0x20, 0xea, 0x22, 0xa5, 0x45, 0xd0, 0x03, 0x4c, + 0xc8, 0x26, 0x85, 0x41, 0xa5, 0x44, 0x85, 0x40, + 0x20, 0x43, 0x27, 0x20, 0x4e, 0x27, 0x20, 0x1a, + 0x27, 0xad, 0x63, 0x2a, 0x8d, 0xbb, 0x35, 0x4c, + 0xa8, 0x26, 0xad, 0x75, 0x2a, 0xc9, 0xa0, 0xf0, + 0x25, 0x20, 0x64, 0x27, 0xb0, 0x3a, 0x20, 0xfc, + 0x22, 0x4c, 0xea, 0x22, 0x20, 0xaf, 0x27, 0xd0, + + 0x05, 0xa9, 0x00, 0x8d, 0xb3, 0x2a, 0xa0, 0x00, + 0x98, 0x91, 0x40, 0x20, 0x4e, 0x27, 0xa9, 0x02, + 0x8d, 0xbb, 0x35, 0x4c, 0xa8, 0x26, 0x20, 0x92, + 0x27, 0xd0, 0x05, 0x20, 0x9a, 0x27, 0xf0, 0x10, + 0x20, 0xaf, 0x27, 0xf0, 0xf6, 0x20, 0xaa, 0x27, + 0xf0, 0xf1, 0x20, 0xfc, 0x22, 0x4c, 0x16, 0x23, + 0x60, 0xa9, 0x09, 0x2d, 0x65, 0x2a, 0xc9, 0x09, + 0xf0, 0x03, 0x4c, 0x00, 0x20, 0xa9, 0x04, 0x20, + 0xd5, 0x23, 0xad, 0x73, 0x2a, 0xac, 0x72, 0x2a, + 0x20, 0xe0, 0x23, 0xad, 0x6d, 0x2a, 0xac, 0x6c, + 0x2a, 0x20, 0xe0, 0x23, 0xad, 0x73, 0x2a, 0xac, + 0x72, 0x2a, 0x4c, 0xff, 0x23, 0x20, 0xa8, 0x22, + 0xa9, 0x7f, 0x2d, 0xc2, 0x35, 0xc9, 0x04, 0xf0, + 0x03, 0x4c, 0xd0, 0x26, 0xa9, 0x04, 0x20, 0xd5, + 0x23, 0x20, 0x7a, 0x24, 0xaa, 0xad, 0x65, 0x2a, + 0x29, 0x01, 0xd0, 0x06, 0x8e, 0x72, 0x2a, 0x8c, + 0x73, 0x2a, 0x20, 0x7a, 0x24, 0xae, 0x72, 0x2a, + 0xac, 0x73, 0x2a, 0x4c, 0x71, 0x24, 0x20, 0x5d, + 0x23, 0x20, 0x51, 0x28, 0x6c, 0x72, 0x2a, 0xad, + 0xb6, 0x2a, 0xf0, 0x20, 0xa5, 0xd6, 0x10, 0x03, + 0x4c, 0xcc, 0x26, 0xa9, 0x02, 0x20, 0xd5, 0x23, + 0x38, 0xa5, 0xaf, 0xe5, 0x67, 0xa8, 0xa5, 0xb0, + 0xe5, 0x68, 0x20, 0xe0, 0x23, 0xa5, 0x68, 0xa4, + 0x67, 0x4c, 0xff, 0x23, 0xa9, 0x01, 0x20, 0xd5, + 0x23, 0x38, 0xa5, 0x4c, 0xe5, 0xca, 0xa8, 0xa5, + 0x4d, 0xe5, 0xcb, 0x20, 0xe0, 0x23, 0xa5, 0xcb, + 0xa4, 0xca, 0x4c, 0xff, 0x23, 0x8d, 0xc2, 0x35, + 0x48, 0x20, 0xa8, 0x22, 0x68, 0x4c, 0xc4, 0x27, + 0x8c, 0xc1, 0x35, 0x8c, 0xc3, 0x35, 0x8d, 0xc2, + 0x35, 0xa9, 0x04, 0x8d, 0xbb, 0x35, 0xa9, 0x01, + 0x8d, 0xbc, 0x35, 0x20, 0xa8, 0x26, 0xad, 0xc2, + 0x35, 0x8d, 0xc3, 0x35, 0x4c, 0xa8, 0x26, 0x8c, + + 0xc3, 0x35, 0x8d, 0xc4, 0x35, 0xa9, 0x02, 0x8d, + 0xbc, 0x35, 0x20, 0xa8, 0x26, 0x4c, 0xea, 0x22, + 0x4c, 0xd0, 0x26, 0x20, 0x16, 0x23, 0x20, 0xa8, + 0x22, 0xa9, 0x23, 0x2d, 0xc2, 0x35, 0xf0, 0xf0, + 0x8d, 0xc2, 0x35, 0xad, 0xb6, 0x2a, 0xf0, 0x28, + 0xa9, 0x02, 0x20, 0xb1, 0x24, 0x20, 0x7a, 0x24, + 0x18, 0x65, 0x67, 0xaa, 0x98, 0x65, 0x68, 0xc5, + 0x74, 0xb0, 0x70, 0x85, 0xb0, 0x85, 0x6a, 0x86, + 0xaf, 0x86, 0x69, 0xa6, 0x67, 0xa4, 0x68, 0x20, + 0x71, 0x24, 0x20, 0x51, 0x28, 0x6c, 0x60, 0x1d, + 0xa9, 0x01, 0x20, 0xb1, 0x24, 0x20, 0x7a, 0x24, + 0x38, 0xa5, 0x4c, 0xed, 0x60, 0x2a, 0xaa, 0xa5, + 0x4d, 0xed, 0x61, 0x2a, 0x90, 0x45, 0xa8, 0xc4, + 0x4b, 0x90, 0x40, 0xf0, 0x3e, 0x84, 0xcb, 0x86, + 0xca, 0x8e, 0xc3, 0x35, 0x8c, 0xc4, 0x35, 0x4c, + 0x0a, 0x24, 0xad, 0x0a, 0x1d, 0x8d, 0xc3, 0x35, + 0xad, 0x0b, 0x1d, 0x8d, 0xc4, 0x35, 0xa9, 0x00, + 0x8d, 0xc2, 0x35, 0xa9, 0x02, 0x8d, 0xc1, 0x35, + 0xa9, 0x03, 0x8d, 0xbb, 0x35, 0xa9, 0x02, 0x8d, + 0xbc, 0x35, 0x20, 0xa8, 0x26, 0xad, 0x61, 0x2a, + 0x8d, 0xc2, 0x35, 0xa8, 0xad, 0x60, 0x2a, 0x8d, + 0xc1, 0x35, 0x60, 0x20, 0xea, 0x22, 0x4c, 0xcc, + 0x26, 0xcd, 0xc2, 0x35, 0xf0, 0x1a, 0xae, 0x5f, + 0x2a, 0x8e, 0x62, 0x2a, 0x4a, 0xf0, 0x03, 0x4c, + 0x9e, 0x25, 0xa2, 0x1d, 0xbd, 0x75, 0x2a, 0x9d, + 0x93, 0x2a, 0xca, 0x10, 0xf7, 0x4c, 0x7a, 0x25, + 0x60, 0xad, 0xb6, 0x2a, 0xf0, 0x03, 0x8d, 0xb7, + 0x2a, 0x20, 0x13, 0x24, 0x20, 0xc8, 0x1f, 0x20, + 0x51, 0x28, 0x6c, 0x58, 0x1d, 0xa5, 0x4a, 0x85, + 0xcc, 0xa5, 0x4b, 0x85, 0xcd, 0x6c, 0x56, 0x1d, + 0x20, 0x16, 0x24, 0x20, 0xc8, 0x1f, 0x20, 0x51, + 0x28, 0x6c, 0x56, 0x1d, 0x20, 0x65, 0xd6, 0x85, + + 0x33, 0x85, 0xd8, 0x4c, 0xd2, 0xd7, 0x20, 0x65, + 0x0e, 0x85, 0x33, 0x85, 0xd8, 0x4c, 0xd4, 0x0f, + 0x20, 0x26, 0x25, 0xa9, 0x05, 0x8d, 0x52, 0x2a, + 0x4c, 0x83, 0x1f, 0x20, 0x26, 0x25, 0xa9, 0x01, + 0x8d, 0x51, 0x2a, 0x4c, 0x83, 0x1f, 0x20, 0x64, + 0x27, 0x90, 0x06, 0x20, 0xa3, 0x22, 0x4c, 0x34, + 0x25, 0x20, 0x4e, 0x27, 0xad, 0x65, 0x2a, 0x29, + 0x06, 0xf0, 0x13, 0xa2, 0x03, 0xbd, 0x6e, 0x2a, + 0x9d, 0xbd, 0x35, 0xca, 0x10, 0xf7, 0xa9, 0x0a, + 0x8d, 0xbb, 0x35, 0x20, 0xa8, 0x26, 0x60, 0xa9, + 0x40, 0x2d, 0x65, 0x2a, 0xf0, 0x05, 0xad, 0x66, + 0x2a, 0xd0, 0x05, 0xa9, 0xfe, 0x8d, 0x66, 0x2a, + 0xad, 0x0d, 0x1d, 0x8d, 0xbc, 0x35, 0xa9, 0x0b, + 0x20, 0xaa, 0x22, 0x4c, 0x97, 0x23, 0xa9, 0x06, + 0x20, 0xaa, 0x22, 0xad, 0xbf, 0x35, 0x8d, 0x66, + 0x2a, 0x60, 0xa9, 0x4c, 0x20, 0xb2, 0x25, 0xf0, + 0x2e, 0xa9, 0x00, 0x8d, 0xb6, 0x2a, 0xa0, 0x1e, + 0x20, 0x97, 0x20, 0xa2, 0x09, 0xbd, 0xb7, 0x2a, + 0x9d, 0x74, 0x2a, 0xca, 0xd0, 0xf7, 0xa9, 0xc0, + 0x8d, 0x51, 0x2a, 0x4c, 0xd1, 0x24, 0xa9, 0x20, + 0x20, 0xb2, 0x25, 0xf0, 0x05, 0xa9, 0x01, 0x4c, + 0xd2, 0x26, 0xa9, 0x00, 0x8d, 0xb7, 0x2a, 0x4c, + 0x84, 0x1d, 0xcd, 0x00, 0xe0, 0xf0, 0x0e, 0x8d, + 0x80, 0xc0, 0xcd, 0x00, 0xe0, 0xf0, 0x06, 0x8d, + 0x81, 0xc0, 0xcd, 0x00, 0xe0, 0x60, 0x20, 0xa3, + 0x22, 0xad, 0x4f, 0x2a, 0x8d, 0xb4, 0x2a, 0xad, + 0x50, 0x2a, 0x8d, 0xb5, 0x2a, 0xad, 0x75, 0x2a, + 0x8d, 0xb3, 0x2a, 0xd0, 0x0e, 0x20, 0x64, 0x27, + 0x90, 0x06, 0x20, 0xa3, 0x22, 0x4c, 0xeb, 0x25, + 0x20, 0x4e, 0x27, 0xad, 0x65, 0x2a, 0x29, 0x04, + 0xf0, 0x1b, 0xad, 0x6e, 0x2a, 0xd0, 0x08, 0xae, + 0x6f, 0x2a, 0xf0, 0x11, 0xce, 0x6f, 0x2a, 0xce, + + 0x6e, 0x2a, 0x20, 0x8c, 0x26, 0xf0, 0x38, 0xc9, + 0x8d, 0xd0, 0xf7, 0xf0, 0xe5, 0x60, 0x20, 0x5e, + 0x26, 0xb0, 0x66, 0xad, 0x5c, 0x2a, 0x8d, 0xc3, + 0x35, 0xa9, 0x04, 0x8d, 0xbb, 0x35, 0xa9, 0x01, + 0x8d, 0xbc, 0x35, 0x4c, 0xa8, 0x26, 0x20, 0x5e, + 0x26, 0xb0, 0x4e, 0xa9, 0x06, 0x8d, 0x52, 0x2a, + 0x20, 0x8c, 0x26, 0xd0, 0x0f, 0x20, 0xfc, 0x22, + 0xa9, 0x03, 0xcd, 0x52, 0x2a, 0xf0, 0xce, 0xa9, + 0x05, 0x4c, 0xd2, 0x26, 0xc9, 0xe0, 0x90, 0x02, + 0x29, 0x7f, 0x8d, 0x5c, 0x2a, 0xae, 0x5a, 0x2a, + 0xf0, 0x09, 0xca, 0xbd, 0x00, 0x02, 0x09, 0x80, + 0x9d, 0x00, 0x02, 0x4c, 0xb3, 0x1f, 0x48, 0xad, + 0xb6, 0x2a, 0xf0, 0x0e, 0xa6, 0x76, 0xe8, 0xf0, + 0x0d, 0xa6, 0x33, 0xe0, 0xdd, 0xf0, 0x07, 0x68, + 0x18, 0x60, 0xa5, 0xd9, 0x30, 0xf9, 0x68, 0x38, + 0x60, 0x20, 0xfc, 0x22, 0x20, 0x5b, 0x27, 0x4c, + 0xb3, 0x1f, 0x20, 0x9d, 0x26, 0x20, 0x4e, 0x27, + 0xa9, 0x03, 0xd0, 0xa1, 0xa9, 0x03, 0x8d, 0xbb, + 0x35, 0xa9, 0x01, 0x8d, 0xbc, 0x35, 0x20, 0xa8, + 0x26, 0xad, 0xc3, 0x35, 0x60, 0xad, 0xb5, 0x2a, + 0x85, 0x41, 0xad, 0xb4, 0x2a, 0x85, 0x40, 0x60, + 0x20, 0x06, 0x2b, 0x90, 0x16, 0x20, 0x64, 0x27, + 0xb0, 0x05, 0xa9, 0x00, 0xa8, 0x91, 0x40, 0xad, + 0xc5, 0x35, 0xc9, 0x05, 0xd0, 0x14, 0xa2, 0x00, + 0x8e, 0xc3, 0x35, 0x60, 0xa9, 0x0b, 0xd0, 0x0a, + 0xa9, 0x0c, 0xd0, 0x06, 0xa9, 0x0e, 0xd0, 0x02, + 0xa9, 0x0d, 0x8d, 0x5c, 0x2a, 0x20, 0xe6, 0x3f, + 0xad, 0xb6, 0x2a, 0xf0, 0x04, 0xa5, 0xd8, 0x30, + 0x0e, 0xa2, 0x00, 0x20, 0x02, 0x27, 0xae, 0x5c, + 0x2a, 0x20, 0x02, 0x27, 0x20, 0xc8, 0x1f, 0x20, + 0x51, 0x28, 0x20, 0x5e, 0x26, 0xae, 0x5c, 0x2a, + 0xa9, 0x03, 0xb0, 0x03, 0x6c, 0x5a, 0x1d, 0x6c, + + 0x5e, 0x1d, 0xbd, 0x3f, 0x2a, 0xaa, 0x8e, 0x63, + 0x2a, 0xbd, 0x71, 0x29, 0x48, 0x09, 0x80, 0x20, + 0xc5, 0x1f, 0xae, 0x63, 0x2a, 0xe8, 0x68, 0x10, + 0xed, 0x60, 0xad, 0x66, 0x2a, 0x8d, 0xbf, 0x35, + 0xad, 0x68, 0x2a, 0x8d, 0xc0, 0x35, 0xad, 0x6a, + 0x2a, 0x8d, 0xc1, 0x35, 0xad, 0x06, 0x1d, 0x8d, + 0xc3, 0x35, 0xad, 0x07, 0x1d, 0x8d, 0xc4, 0x35, + 0xa5, 0x40, 0x8d, 0x4f, 0x2a, 0xa5, 0x41, 0x8d, + 0x50, 0x2a, 0x60, 0xa0, 0x1d, 0xb9, 0x75, 0x2a, + 0x91, 0x40, 0x88, 0x10, 0xf8, 0x60, 0xa0, 0x1e, + 0xb1, 0x40, 0x99, 0xa9, 0x35, 0xc8, 0xc0, 0x26, + 0xd0, 0xf6, 0x60, 0xa0, 0x00, 0x8c, 0x51, 0x2a, + 0x8c, 0x52, 0x2a, 0x60, 0xa9, 0x00, 0x85, 0x45, + 0x20, 0x92, 0x27, 0x4c, 0x73, 0x27, 0x20, 0x9a, + 0x27, 0xf0, 0x1d, 0x20, 0xaa, 0x27, 0xd0, 0x0a, + 0xa5, 0x40, 0x85, 0x44, 0xa5, 0x41, 0x85, 0x45, + 0xd0, 0xec, 0xa0, 0x1d, 0xb1, 0x40, 0xd9, 0x75, + 0x2a, 0xd0, 0xe3, 0x88, 0x10, 0xf6, 0x18, 0x60, + 0x38, 0x60, 0xad, 0x00, 0x1d, 0xae, 0x01, 0x1d, + 0xd0, 0x0a, 0xa0, 0x25, 0xb1, 0x40, 0xf0, 0x09, + 0xaa, 0x88, 0xb1, 0x40, 0x86, 0x41, 0x85, 0x40, + 0x8a, 0x60, 0xa0, 0x00, 0xb1, 0x40, 0x60, 0xad, + 0xb3, 0x2a, 0xf0, 0x0e, 0xad, 0xb4, 0x2a, 0xc5, + 0x40, 0xd0, 0x08, 0xad, 0xb5, 0x2a, 0xc5, 0x41, + 0xf0, 0x01, 0xca, 0x60, 0x4d, 0xc2, 0x35, 0xf0, + 0x0a, 0x29, 0x7f, 0xf0, 0x06, 0x20, 0xea, 0x22, + 0x4c, 0xd0, 0x26, 0x60, 0x38, 0xad, 0x00, 0x1d, + 0x85, 0x40, 0xad, 0x01, 0x1d, 0x85, 0x41, 0xad, + 0x57, 0x2a, 0x8d, 0x63, 0x2a, 0xa0, 0x00, 0x98, + 0x91, 0x40, 0xa0, 0x1e, 0x38, 0xa5, 0x40, 0xe9, + 0x2d, 0x91, 0x40, 0x48, 0xa5, 0x41, 0xe9, 0x00, + 0xc8, 0x91, 0x40, 0xaa, 0xca, 0x68, 0x48, 0xc8, + + 0x91, 0x40, 0x8a, 0xc8, 0x91, 0x40, 0xaa, 0xca, + 0x68, 0x48, 0xc8, 0x91, 0x40, 0xc8, 0x8a, 0x91, + 0x40, 0xce, 0x63, 0x2a, 0xf0, 0x17, 0xaa, 0x68, + 0x38, 0xe9, 0x26, 0xc8, 0x91, 0x40, 0x48, 0x8a, + 0xe9, 0x00, 0xc8, 0x91, 0x40, 0x85, 0x41, 0x68, + 0x85, 0x40, 0x4c, 0xe5, 0x27, 0x48, 0xa9, 0x00, + 0xc8, 0x91, 0x40, 0xc8, 0x91, 0x40, 0xad, 0xb6, + 0x2a, 0xf0, 0x0b, 0x68, 0x85, 0x74, 0x85, 0x70, + 0x68, 0x85, 0x73, 0x85, 0x6f, 0x60, 0x68, 0x85, + 0x4d, 0x85, 0xcb, 0x68, 0x85, 0x4c, 0x85, 0xca, + 0x60, 0xa5, 0x39, 0xcd, 0x03, 0x1d, 0xf0, 0x12, + 0x8d, 0x56, 0x2a, 0xa5, 0x38, 0x8d, 0x55, 0x2a, + 0xad, 0x02, 0x1d, 0x85, 0x38, 0xad, 0x03, 0x1d, + 0x85, 0x39, 0xa5, 0x37, 0xcd, 0x05, 0x1d, 0xf0, + 0x12, 0x8d, 0x54, 0x2a, 0xa5, 0x36, 0x8d, 0x53, + 0x2a, 0xad, 0x04, 0x1d, 0x85, 0x36, 0xad, 0x05, + 0x1d, 0x85, 0x37, 0x60, 0x49, 0x4e, 0x49, 0xd4, + 0x4c, 0x4f, 0x41, 0xc4, 0x53, 0x41, 0x56, 0xc5, + 0x52, 0x55, 0xce, 0x43, 0x48, 0x41, 0x49, 0xce, + 0x44, 0x45, 0x4c, 0x45, 0x54, 0xc5, 0x4c, 0x4f, + 0x43, 0xcb, 0x55, 0x4e, 0x4c, 0x4f, 0x43, 0xcb, + 0x43, 0x4c, 0x4f, 0x53, 0xc5, 0x52, 0x45, 0x41, + 0xc4, 0x45, 0x58, 0x45, 0xc3, 0x57, 0x52, 0x49, + 0x54, 0xc5, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x49, + 0x4f, 0xce, 0x4f, 0x50, 0x45, 0xce, 0x41, 0x50, + 0x50, 0x45, 0x4e, 0xc4, 0x52, 0x45, 0x4e, 0x41, + 0x4d, 0xc5, 0x43, 0x41, 0x54, 0x41, 0x4c, 0x4f, + 0xc7, 0x4d, 0x4f, 0xce, 0x4e, 0x4f, 0x4d, 0x4f, + 0xce, 0x50, 0x52, 0xa3, 0x49, 0x4e, 0xa3, 0x4d, + 0x41, 0x58, 0x46, 0x49, 0x4c, 0x45, 0xd3, 0x46, + 0xd0, 0x49, 0x4e, 0xd4, 0x42, 0x53, 0x41, 0x56, + 0xc5, 0x42, 0x4c, 0x4f, 0x41, 0xc4, 0x42, 0x52, + + 0x55, 0xce, 0x56, 0x45, 0x52, 0x49, 0x46, 0xd9, + 0x00, 0x21, 0x70, 0xa0, 0x70, 0xa1, 0x70, 0xa0, + 0x70, 0x20, 0x70, 0x20, 0x70, 0x20, 0x70, 0x20, + 0x70, 0x60, 0x00, 0x22, 0x06, 0x20, 0x74, 0x22, + 0x06, 0x22, 0x04, 0x23, 0x78, 0x22, 0x70, 0x30, + 0x70, 0x40, 0x70, 0x40, 0x80, 0x40, 0x80, 0x08, + 0x00, 0x08, 0x00, 0x04, 0x00, 0x40, 0x70, 0x40, + 0x00, 0x21, 0x79, 0x20, 0x71, 0x20, 0x71, 0x20, + 0x70, 0xd6, 0xc4, 0xd3, 0xcc, 0xd2, 0xc2, 0xc1, + 0xc3, 0xc9, 0xcf, 0x40, 0x20, 0x10, 0x08, 0x04, + 0x02, 0x01, 0xc0, 0xa0, 0x90, 0x00, 0x00, 0xfe, + 0x00, 0x01, 0x00, 0x02, 0x00, 0x01, 0x00, 0x07, + 0x00, 0x01, 0x00, 0xff, 0x7f, 0x00, 0x00, 0xff, + 0x7f, 0x00, 0x00, 0xff, 0x7f, 0x00, 0x00, 0xff, + 0xff, 0x0d, 0x07, 0x8d, 0x4c, 0x41, 0x4e, 0x47, + 0x55, 0x41, 0x47, 0x45, 0x20, 0x4e, 0x4f, 0x54, + 0x20, 0x41, 0x56, 0x41, 0x49, 0x4c, 0x41, 0x42, + 0x4c, 0xc5, 0x52, 0x41, 0x4e, 0x47, 0x45, 0x20, + 0x45, 0x52, 0x52, 0x4f, 0xd2, 0x57, 0x52, 0x49, + 0x54, 0x45, 0x20, 0x50, 0x52, 0x4f, 0x54, 0x45, + 0x43, 0x54, 0x45, 0xc4, 0x45, 0x4e, 0x44, 0x20, + 0x4f, 0x46, 0x20, 0x44, 0x41, 0x54, 0xc1, 0x46, + 0x49, 0x4c, 0x45, 0x20, 0x4e, 0x4f, 0x54, 0x20, + 0x46, 0x4f, 0x55, 0x4e, 0xc4, 0x56, 0x4f, 0x4c, + 0x55, 0x4d, 0x45, 0x20, 0x4d, 0x49, 0x53, 0x4d, + 0x41, 0x54, 0x43, 0xc8, 0x49, 0x2f, 0x4f, 0x20, + 0x45, 0x52, 0x52, 0x4f, 0xd2, 0x44, 0x49, 0x53, + 0x4b, 0x20, 0x46, 0x55, 0x4c, 0xcc, 0x46, 0x49, + 0x4c, 0x45, 0x20, 0x4c, 0x4f, 0x43, 0x4b, 0x45, + 0xc4, 0x53, 0x59, 0x4e, 0x54, 0x41, 0x58, 0x20, + 0x45, 0x52, 0x52, 0x4f, 0xd2, 0x4e, 0x4f, 0x20, + 0x42, 0x55, 0x46, 0x46, 0x45, 0x52, 0x53, 0x20, + + 0x41, 0x56, 0x41, 0x49, 0x4c, 0x41, 0x42, 0x4c, + 0xc5, 0x46, 0x49, 0x4c, 0x45, 0x20, 0x54, 0x59, + 0x50, 0x45, 0x20, 0x4d, 0x49, 0x53, 0x4d, 0x41, + 0x54, 0x43, 0xc8, 0x50, 0x52, 0x4f, 0x47, 0x52, + 0x41, 0x4d, 0x20, 0x54, 0x4f, 0x4f, 0x20, 0x4c, + 0x41, 0x52, 0x47, 0xc5, 0x4e, 0x4f, 0x54, 0x20, + 0x44, 0x49, 0x52, 0x45, 0x43, 0x54, 0x20, 0x43, + 0x4f, 0x4d, 0x4d, 0x41, 0x4e, 0xc4, 0x8d, 0x00, + 0x03, 0x19, 0x19, 0x24, 0x33, 0x3e, 0x4c, 0x5b, + 0x64, 0x6d, 0x78, 0x84, 0x98, 0xaa, 0xbb, 0x2d, + 0x18, 0x00, 0x00, 0xf0, 0xfd, 0x1b, 0xfd, 0x03, + 0x03, 0xfb, 0x0a, 0x28, 0x8d, 0x0a, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x06, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xc8, 0xc5, 0xcc, + 0xcc, 0xcf, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, + 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, + 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, + 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, + 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, + 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, + 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, 0xa0, + 0xa0, 0x03, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xc1, 0xd0, 0xd0, 0xcc, 0xc5, 0xd3, 0xcf, 0xc6, + 0xd4, 0xe8, 0x37, 0xbb, 0x33, 0xbb, 0x34, 0x00, + 0x40, 0x7e, 0x33, 0x21, 0x2b, 0x05, 0x2c, 0x57, + 0x2c, 0x6f, 0x2c, 0x2a, 0x2d, 0x97, 0x2d, 0xee, + 0x2c, 0xf5, 0x2c, 0x39, 0x2c, 0x11, 0x2d, 0x8d, + 0x2e, 0x17, 0x2d, 0x7e, 0x33, 0x7e, 0x33, 0x89, + 0x2c, 0x95, 0x2c, 0x86, 0x2c, 0x92, 0x2c, 0x7e, + 0x33, 0x7e, 0x33, 0xbd, 0x2c, 0xc9, 0x2c, 0xba, + 0x2c, 0xc6, 0x2c, 0x7e, 0x33, 0xe0, 0x00, 0xf0, + + 0x02, 0xa2, 0x02, 0x8e, 0x5f, 0x2a, 0xba, 0x8e, + 0x9b, 0x33, 0x20, 0x6a, 0x2e, 0xad, 0xbb, 0x35, + 0xc9, 0x0d, 0xb0, 0x0b, 0x0a, 0xaa, 0xbd, 0xca, + 0x2a, 0x48, 0xbd, 0xc9, 0x2a, 0x48, 0x60, 0x4c, + 0x63, 0x33, 0x20, 0x28, 0x2b, 0x4c, 0x7f, 0x33, + 0x20, 0xdc, 0x2b, 0xa9, 0x01, 0x8d, 0xe3, 0x35, + 0xae, 0xbe, 0x35, 0xad, 0xbd, 0x35, 0xd0, 0x05, + 0xe0, 0x00, 0xd0, 0x01, 0xe8, 0x8d, 0xe8, 0x35, + 0x8e, 0xe9, 0x35, 0x20, 0xc9, 0x31, 0x90, 0x5e, + 0x8e, 0x9c, 0x33, 0xae, 0x5f, 0x2a, 0xbd, 0x09, + 0x29, 0xae, 0x9c, 0x33, 0x4a, 0xb0, 0x0d, 0xad, + 0x51, 0x2a, 0xc9, 0xc0, 0xd0, 0x03, 0x4c, 0x5f, + 0x33, 0x4c, 0x73, 0x33, 0xa9, 0x00, 0x9d, 0xe8, + 0x34, 0xa9, 0x01, 0x9d, 0xe7, 0x34, 0x8e, 0x9c, + 0x33, 0x20, 0x44, 0x32, 0xae, 0x9c, 0x33, 0x9d, + 0xc7, 0x34, 0x8d, 0xd2, 0x35, 0x8d, 0xd4, 0x35, + 0xad, 0xf1, 0x35, 0x9d, 0xc6, 0x34, 0x8d, 0xd1, + 0x35, 0x8d, 0xd3, 0x35, 0xad, 0xc2, 0x35, 0x9d, + 0xc8, 0x34, 0x20, 0x37, 0x30, 0x20, 0x0c, 0x2f, + 0x20, 0xd6, 0x37, 0x20, 0x3a, 0x2f, 0xae, 0x9c, + 0x33, 0xa9, 0x06, 0x8d, 0xc5, 0x35, 0xbd, 0xc6, + 0x34, 0x8d, 0xd1, 0x35, 0xbd, 0xc7, 0x34, 0x8d, + 0xd2, 0x35, 0xbd, 0xc8, 0x34, 0x8d, 0xc2, 0x35, + 0x8d, 0xf6, 0x35, 0xbd, 0xe7, 0x34, 0x8d, 0xee, + 0x35, 0xbd, 0xe8, 0x34, 0x8d, 0xef, 0x35, 0x8e, + 0xd9, 0x35, 0xa9, 0xff, 0x8d, 0xe0, 0x35, 0x8d, + 0xe1, 0x35, 0xad, 0xe2, 0x33, 0x8d, 0xda, 0x35, + 0x18, 0x4c, 0x5e, 0x2f, 0xa9, 0x00, 0xaa, 0x9d, + 0xd1, 0x35, 0xe8, 0xe0, 0x2d, 0xd0, 0xf8, 0xad, + 0xbf, 0x35, 0x49, 0xff, 0x8d, 0xf9, 0x35, 0xad, + 0xc0, 0x35, 0x8d, 0xf8, 0x35, 0xad, 0xc1, 0x35, + 0x0a, 0x0a, 0x0a, 0x0a, 0xaa, 0x8e, 0xf7, 0x35, + + 0xa9, 0x11, 0x8d, 0xfa, 0x35, 0x60, 0x20, 0x1d, + 0x2f, 0x20, 0x34, 0x2f, 0x20, 0xc3, 0x32, 0xa9, + 0x02, 0x2d, 0xd5, 0x35, 0xf0, 0x21, 0x20, 0xf7, + 0x2f, 0xa9, 0x00, 0x18, 0x20, 0x11, 0x30, 0x38, + 0xce, 0xd8, 0x35, 0xd0, 0xf7, 0xae, 0xd9, 0x35, + 0xad, 0xee, 0x35, 0x9d, 0xe7, 0x34, 0xad, 0xef, + 0x35, 0x9d, 0xe8, 0x34, 0x20, 0x37, 0x30, 0x4c, + 0x7f, 0x33, 0x20, 0x28, 0x2b, 0xad, 0xf6, 0x35, + 0x30, 0x2b, 0xad, 0xbd, 0x35, 0x85, 0x42, 0xad, + 0xbe, 0x35, 0x85, 0x43, 0xae, 0x9c, 0x33, 0x20, + 0x1c, 0x32, 0x20, 0x37, 0x30, 0x4c, 0x7f, 0x33, + 0xad, 0xbc, 0x35, 0xc9, 0x05, 0xb0, 0x0b, 0x0a, + 0xaa, 0xbd, 0xe6, 0x2a, 0x48, 0xbd, 0xe5, 0x2a, + 0x48, 0x60, 0x4c, 0x67, 0x33, 0x4c, 0x7b, 0x33, + 0xad, 0xf6, 0x35, 0x30, 0xf8, 0xad, 0xbc, 0x35, + 0xc9, 0x05, 0xb0, 0xee, 0x0a, 0xaa, 0xbd, 0xf2, + 0x2a, 0x48, 0xbd, 0xf1, 0x2a, 0x48, 0x60, 0x20, + 0x00, 0x33, 0x20, 0xa8, 0x2c, 0x8d, 0xc3, 0x35, + 0x4c, 0x7f, 0x33, 0x20, 0x00, 0x33, 0x20, 0xb5, + 0x31, 0x20, 0xa8, 0x2c, 0x48, 0x20, 0xa2, 0x31, + 0xa0, 0x00, 0x68, 0x91, 0x42, 0x4c, 0x96, 0x2c, + 0x20, 0xb6, 0x30, 0xb0, 0x0b, 0xb1, 0x42, 0x48, + 0x20, 0x5b, 0x31, 0x20, 0x94, 0x31, 0x68, 0x60, + 0x4c, 0x6f, 0x33, 0x20, 0x00, 0x33, 0xad, 0xc3, + 0x35, 0x20, 0xda, 0x2c, 0x4c, 0x7f, 0x33, 0x20, + 0x00, 0x33, 0x20, 0xa2, 0x31, 0xa0, 0x00, 0xb1, + 0x42, 0x20, 0xda, 0x2c, 0x20, 0xb5, 0x31, 0x4c, + 0xca, 0x2c, 0x48, 0x20, 0xb6, 0x30, 0x68, 0x91, + 0x42, 0xa9, 0x40, 0x0d, 0xd5, 0x35, 0x8d, 0xd5, + 0x35, 0x20, 0x5b, 0x31, 0x4c, 0x94, 0x31, 0xa9, + 0x80, 0x8d, 0x9e, 0x33, 0xd0, 0x05, 0xa9, 0x00, + 0x8d, 0x9e, 0x33, 0x20, 0x28, 0x2b, 0xae, 0x9c, + + 0x33, 0xbd, 0xc8, 0x34, 0x29, 0x7f, 0x0d, 0x9e, + 0x33, 0x9d, 0xc8, 0x34, 0x20, 0x37, 0x30, 0x4c, + 0x7f, 0x33, 0x20, 0x00, 0x33, 0x4c, 0x7f, 0x33, + 0x20, 0x28, 0x2b, 0x20, 0xb6, 0x30, 0xb0, 0xef, + 0xee, 0xe4, 0x35, 0xd0, 0xf6, 0xee, 0xe5, 0x35, + 0x4c, 0x1b, 0x2d, 0x20, 0x28, 0x2b, 0xae, 0x9c, + 0x33, 0xbd, 0xc8, 0x34, 0x10, 0x03, 0x4c, 0x7b, + 0x33, 0xae, 0x9c, 0x33, 0xbd, 0xc6, 0x34, 0x8d, + 0xd1, 0x35, 0x9d, 0xe6, 0x34, 0xa9, 0xff, 0x9d, + 0xc6, 0x34, 0xbc, 0xc7, 0x34, 0x8c, 0xd2, 0x35, + 0x20, 0x37, 0x30, 0x18, 0x20, 0x5e, 0x2f, 0xb0, + 0x2a, 0x20, 0x0c, 0x2f, 0xa0, 0x0c, 0x8c, 0x9c, + 0x33, 0xb1, 0x42, 0x30, 0x0b, 0xf0, 0x09, 0x48, + 0xc8, 0xb1, 0x42, 0xa8, 0x68, 0x20, 0x89, 0x2d, + 0xac, 0x9c, 0x33, 0xc8, 0xc8, 0xd0, 0xe7, 0xad, + 0xd3, 0x35, 0xac, 0xd4, 0x35, 0x20, 0x89, 0x2d, + 0x38, 0xb0, 0xd1, 0x20, 0xfb, 0x2f, 0x4c, 0x7f, + 0x33, 0x38, 0x20, 0xdd, 0x32, 0xa9, 0x00, 0xa2, + 0x03, 0x9d, 0xf0, 0x35, 0xca, 0x10, 0xfa, 0x60, + 0x20, 0xdc, 0x2b, 0xa9, 0xff, 0x8d, 0xf9, 0x35, + 0x20, 0xf7, 0x2f, 0xa9, 0x16, 0x8d, 0x9d, 0x33, + 0x20, 0x2f, 0x2e, 0x20, 0x2f, 0x2e, 0xa2, 0x0b, + 0xbd, 0xaf, 0x33, 0x20, 0xed, 0xfd, 0xca, 0x10, + 0xf7, 0x86, 0x45, 0xad, 0xf6, 0x37, 0x85, 0x44, + 0x20, 0x42, 0x2e, 0x20, 0x2f, 0x2e, 0x20, 0x2f, + 0x2e, 0x18, 0x20, 0x11, 0x30, 0xb0, 0x5d, 0xa2, + 0x00, 0x8e, 0x9c, 0x33, 0xbd, 0xc6, 0x34, 0xf0, + 0x53, 0x30, 0x4a, 0xa0, 0xa0, 0xbd, 0xc8, 0x34, + 0x10, 0x02, 0xa0, 0xaa, 0x98, 0x20, 0xed, 0xfd, + 0xbd, 0xc8, 0x34, 0x29, 0x7f, 0xa0, 0x07, 0x0a, + 0x0a, 0xb0, 0x03, 0x88, 0xd0, 0xfa, 0xb9, 0xa7, + 0x33, 0x20, 0xed, 0xfd, 0xa9, 0xa0, 0x20, 0xed, + + 0xfd, 0xbd, 0xe7, 0x34, 0x85, 0x44, 0xbd, 0xe8, + 0x34, 0x85, 0x45, 0x20, 0x42, 0x2e, 0xa9, 0xa0, + 0x20, 0xed, 0xfd, 0xe8, 0xe8, 0xe8, 0xa0, 0x1d, + 0xbd, 0xc6, 0x34, 0x20, 0xed, 0xfd, 0xe8, 0x88, + 0x10, 0xf6, 0x20, 0x2f, 0x2e, 0x20, 0x30, 0x32, + 0x90, 0xa7, 0xb0, 0x9e, 0x4c, 0x7f, 0x33, 0xa9, + 0x8d, 0x20, 0xed, 0xfd, 0xce, 0x9d, 0x33, 0xd0, + 0x08, 0x20, 0x0c, 0xfd, 0xa9, 0x15, 0x8d, 0x9d, + 0x33, 0x60, 0xa0, 0x02, 0xa9, 0x00, 0x48, 0xa5, + 0x44, 0xd9, 0xa4, 0x33, 0x90, 0x12, 0xf9, 0xa4, + 0x33, 0x85, 0x44, 0xa5, 0x45, 0xe9, 0x00, 0x85, + 0x45, 0x68, 0x69, 0x00, 0x48, 0x4c, 0x47, 0x2e, + 0x68, 0x09, 0xb0, 0x20, 0xed, 0xfd, 0x88, 0x10, + 0xdb, 0x60, 0x20, 0x08, 0x2f, 0xa0, 0x00, 0x8c, + 0xc5, 0x35, 0xb1, 0x42, 0x99, 0xd1, 0x35, 0xc8, + 0xc0, 0x2d, 0xd0, 0xf6, 0x18, 0x60, 0x20, 0x08, + 0x2f, 0xa0, 0x00, 0xb9, 0xd1, 0x35, 0x91, 0x42, + 0xc8, 0xc0, 0x2d, 0xd0, 0xf6, 0x60, 0x20, 0xdc, + 0x2b, 0xa9, 0x04, 0x20, 0x58, 0x30, 0xad, 0xf9, + 0x35, 0x49, 0xff, 0x8d, 0xc1, 0x33, 0xa9, 0x11, + 0x8d, 0xeb, 0x33, 0xa9, 0x01, 0x8d, 0xec, 0x33, + 0xa2, 0x38, 0xa9, 0x00, 0x9d, 0xbb, 0x33, 0xe8, + 0xd0, 0xfa, 0xa2, 0x0c, 0xe0, 0x8c, 0xf0, 0x14, + 0xa0, 0x03, 0xb9, 0xa0, 0x33, 0x9d, 0xf3, 0x33, + 0xe8, 0x88, 0x10, 0xf6, 0xe0, 0x44, 0xd0, 0xec, + 0xa2, 0x48, 0xd0, 0xe8, 0x20, 0xfb, 0x2f, 0xa2, + 0x00, 0x8a, 0x9d, 0xbb, 0x34, 0xe8, 0xd0, 0xfa, + 0x20, 0x45, 0x30, 0xa9, 0x11, 0xac, 0xf0, 0x33, + 0x88, 0x88, 0x8d, 0xec, 0x37, 0x8d, 0xbc, 0x34, + 0x8c, 0xbd, 0x34, 0xc8, 0x8c, 0xed, 0x37, 0xa9, + 0x02, 0x20, 0x58, 0x30, 0xac, 0xbd, 0x34, 0x88, + 0x30, 0x05, 0xd0, 0xec, 0x98, 0xf0, 0xe6, 0x20, + + 0xc2, 0x37, 0x20, 0x4a, 0x37, 0x4c, 0x7f, 0x33, + 0xa2, 0x00, 0xf0, 0x06, 0xa2, 0x02, 0xd0, 0x02, + 0xa2, 0x04, 0xbd, 0xc7, 0x35, 0x85, 0x42, 0xbd, + 0xc8, 0x35, 0x85, 0x43, 0x60, 0x2c, 0xd5, 0x35, + 0x70, 0x01, 0x60, 0x20, 0xe4, 0x2f, 0xa9, 0x02, + 0x20, 0x52, 0x30, 0xa9, 0xbf, 0x2d, 0xd5, 0x35, + 0x8d, 0xd5, 0x35, 0x60, 0xad, 0xd5, 0x35, 0x30, + 0x01, 0x60, 0x20, 0x4b, 0x2f, 0xa9, 0x02, 0x20, + 0x52, 0x30, 0xa9, 0x7f, 0x2d, 0xd5, 0x35, 0x8d, + 0xd5, 0x35, 0x60, 0xad, 0xc9, 0x35, 0x8d, 0xf0, + 0x37, 0xad, 0xca, 0x35, 0x8d, 0xf1, 0x37, 0xae, + 0xd3, 0x35, 0xac, 0xd4, 0x35, 0x60, 0x08, 0x20, + 0x34, 0x2f, 0x20, 0x4b, 0x2f, 0x20, 0x0c, 0x2f, + 0x28, 0xb0, 0x09, 0xae, 0xd1, 0x35, 0xac, 0xd2, + 0x35, 0x4c, 0xb5, 0x2f, 0xa0, 0x01, 0xb1, 0x42, + 0xf0, 0x08, 0xaa, 0xc8, 0xb1, 0x42, 0xa8, 0x4c, + 0xb5, 0x2f, 0xad, 0xbb, 0x35, 0xc9, 0x04, 0xf0, + 0x02, 0x38, 0x60, 0x20, 0x44, 0x32, 0xa0, 0x02, + 0x91, 0x42, 0x48, 0x88, 0xad, 0xf1, 0x35, 0x91, + 0x42, 0x48, 0x20, 0x3a, 0x2f, 0x20, 0xd6, 0x37, + 0xa0, 0x05, 0xad, 0xde, 0x35, 0x91, 0x42, 0xc8, + 0xad, 0xdf, 0x35, 0x91, 0x42, 0x68, 0xaa, 0x68, + 0xa8, 0xa9, 0x02, 0xd0, 0x02, 0xa9, 0x01, 0x8e, + 0xd3, 0x35, 0x8c, 0xd4, 0x35, 0x20, 0x52, 0x30, + 0xa0, 0x05, 0xb1, 0x42, 0x8d, 0xdc, 0x35, 0x18, + 0x6d, 0xda, 0x35, 0x8d, 0xde, 0x35, 0xc8, 0xb1, + 0x42, 0x8d, 0xdd, 0x35, 0x6d, 0xdb, 0x35, 0x8d, + 0xdf, 0x35, 0x18, 0x60, 0x20, 0xe4, 0x2f, 0xa9, + 0x01, 0x4c, 0x52, 0x30, 0xac, 0xcb, 0x35, 0xad, + 0xcc, 0x35, 0x8c, 0xf0, 0x37, 0x8d, 0xf1, 0x37, + 0xae, 0xd6, 0x35, 0xac, 0xd7, 0x35, 0x60, 0xa9, + 0x01, 0xd0, 0x02, 0xa9, 0x02, 0xac, 0xc3, 0x2a, + + 0x8c, 0xf0, 0x37, 0xac, 0xc4, 0x2a, 0x8c, 0xf1, + 0x37, 0xae, 0xfa, 0x35, 0xa0, 0x00, 0x4c, 0x52, + 0x30, 0x08, 0x20, 0x45, 0x30, 0x28, 0xb0, 0x08, + 0xac, 0xbd, 0x33, 0xae, 0xbc, 0x33, 0xd0, 0x0a, + 0xae, 0xbc, 0x34, 0xd0, 0x02, 0x38, 0x60, 0xac, + 0xbd, 0x34, 0x8e, 0x97, 0x33, 0x8c, 0x98, 0x33, + 0xa9, 0x01, 0x20, 0x52, 0x30, 0x18, 0x60, 0x20, + 0x45, 0x30, 0xae, 0x97, 0x33, 0xac, 0x98, 0x33, + 0xa9, 0x02, 0x4c, 0x52, 0x30, 0xad, 0xc5, 0x2a, + 0x8d, 0xf0, 0x37, 0xad, 0xc6, 0x2a, 0x8d, 0xf1, + 0x37, 0x60, 0x8e, 0xec, 0x37, 0x8c, 0xed, 0x37, + 0x8d, 0xf4, 0x37, 0xc9, 0x02, 0xd0, 0x06, 0x0d, + 0xd5, 0x35, 0x8d, 0xd5, 0x35, 0xad, 0xf9, 0x35, + 0x49, 0xff, 0x8d, 0xeb, 0x37, 0xad, 0xf7, 0x35, + 0x8d, 0xe9, 0x37, 0xad, 0xf8, 0x35, 0x8d, 0xea, + 0x37, 0xad, 0xe2, 0x35, 0x8d, 0xf2, 0x37, 0xad, + 0xe3, 0x35, 0x8d, 0xf3, 0x37, 0xa9, 0x01, 0x8d, + 0xe8, 0x37, 0xac, 0xc1, 0x2a, 0xad, 0xc2, 0x2a, + 0x20, 0xb5, 0x37, 0xad, 0xf6, 0x37, 0x8d, 0xbf, + 0x35, 0xa9, 0xff, 0x8d, 0xeb, 0x37, 0xb0, 0x01, + 0x60, 0xad, 0xf5, 0x37, 0xa0, 0x07, 0xc9, 0x20, + 0xf0, 0x08, 0xa0, 0x04, 0xc9, 0x10, 0xf0, 0x02, + 0xa0, 0x08, 0x98, 0x4c, 0x85, 0x33, 0xad, 0xe4, + 0x35, 0xcd, 0xe0, 0x35, 0xd0, 0x08, 0xad, 0xe5, + 0x35, 0xcd, 0xe1, 0x35, 0xf0, 0x66, 0x20, 0x1d, + 0x2f, 0xad, 0xe5, 0x35, 0xcd, 0xdd, 0x35, 0x90, + 0x1c, 0xd0, 0x08, 0xad, 0xe4, 0x35, 0xcd, 0xdc, + 0x35, 0x90, 0x12, 0xad, 0xe5, 0x35, 0xcd, 0xdf, + 0x35, 0x90, 0x10, 0xd0, 0x08, 0xad, 0xe4, 0x35, + 0xcd, 0xde, 0x35, 0x90, 0x06, 0x20, 0x5e, 0x2f, + 0x90, 0xd7, 0x60, 0x38, 0xad, 0xe4, 0x35, 0xed, + 0xdc, 0x35, 0x0a, 0x69, 0x0c, 0xa8, 0x20, 0x0c, + + 0x2f, 0xb1, 0x42, 0xd0, 0x0f, 0xad, 0xbb, 0x35, + 0xc9, 0x04, 0xf0, 0x02, 0x38, 0x60, 0x20, 0x34, + 0x31, 0x4c, 0x20, 0x31, 0x8d, 0xd6, 0x35, 0xc8, + 0xb1, 0x42, 0x8d, 0xd7, 0x35, 0x20, 0xdc, 0x2f, + 0xad, 0xe4, 0x35, 0x8d, 0xe0, 0x35, 0xad, 0xe5, + 0x35, 0x8d, 0xe1, 0x35, 0x20, 0x10, 0x2f, 0xac, + 0xe6, 0x35, 0x18, 0x60, 0x8c, 0x9d, 0x33, 0x20, + 0x44, 0x32, 0xac, 0x9d, 0x33, 0xc8, 0x91, 0x42, + 0x8d, 0xd7, 0x35, 0x88, 0xad, 0xf1, 0x35, 0x91, + 0x42, 0x8d, 0xd6, 0x35, 0x20, 0x10, 0x2f, 0x20, + 0xd6, 0x37, 0xa9, 0xc0, 0x0d, 0xd5, 0x35, 0x8d, + 0xd5, 0x35, 0x60, 0xae, 0xea, 0x35, 0x8e, 0xbd, + 0x35, 0xae, 0xeb, 0x35, 0x8e, 0xbe, 0x35, 0xae, + 0xec, 0x35, 0xac, 0xed, 0x35, 0x8e, 0xbf, 0x35, + 0x8c, 0xc0, 0x35, 0xe8, 0xd0, 0x01, 0xc8, 0xcc, + 0xe9, 0x35, 0xd0, 0x11, 0xec, 0xe8, 0x35, 0xd0, + 0x0c, 0xa2, 0x00, 0xa0, 0x00, 0xee, 0xea, 0x35, + 0xd0, 0x03, 0xee, 0xeb, 0x35, 0x8e, 0xec, 0x35, + 0x8c, 0xed, 0x35, 0x60, 0xee, 0xe6, 0x35, 0xd0, + 0x08, 0xee, 0xe4, 0x35, 0xd0, 0x03, 0xee, 0xe5, + 0x35, 0x60, 0xac, 0xc3, 0x35, 0xae, 0xc4, 0x35, + 0x84, 0x42, 0x86, 0x43, 0xee, 0xc3, 0x35, 0xd0, + 0x03, 0xee, 0xc4, 0x35, 0x60, 0xac, 0xc1, 0x35, + 0xd0, 0x08, 0xae, 0xc2, 0x35, 0xf0, 0x07, 0xce, + 0xc2, 0x35, 0xce, 0xc1, 0x35, 0x60, 0x4c, 0x7f, + 0x33, 0x20, 0xf7, 0x2f, 0xad, 0xc3, 0x35, 0x85, + 0x42, 0xad, 0xc4, 0x35, 0x85, 0x43, 0xa9, 0x01, + 0x8d, 0x9d, 0x33, 0xa9, 0x00, 0x8d, 0xd8, 0x35, + 0x18, 0xee, 0xd8, 0x35, 0x20, 0x11, 0x30, 0xb0, + 0x51, 0xa2, 0x00, 0x8e, 0x9c, 0x33, 0xbd, 0xc6, + 0x34, 0xf0, 0x1f, 0x30, 0x22, 0xa0, 0x00, 0xe8, + 0xe8, 0xe8, 0xb1, 0x42, 0xdd, 0xc6, 0x34, 0xd0, + + 0x0a, 0xc8, 0xc0, 0x1e, 0xd0, 0xf3, 0xae, 0x9c, + 0x33, 0x18, 0x60, 0x20, 0x30, 0x32, 0x90, 0xdb, + 0xb0, 0xcf, 0xac, 0x9d, 0x33, 0xd0, 0xc1, 0xac, + 0x9d, 0x33, 0xd0, 0xef, 0xa0, 0x00, 0xe8, 0xe8, + 0xe8, 0xb1, 0x42, 0x9d, 0xc6, 0x34, 0xc8, 0xc0, + 0x1e, 0xd0, 0xf5, 0xae, 0x9c, 0x33, 0x38, 0x60, + 0x18, 0xad, 0x9c, 0x33, 0x69, 0x23, 0xaa, 0xe0, + 0xf5, 0x60, 0xa9, 0x00, 0xac, 0x9d, 0x33, 0xd0, + 0x97, 0x4c, 0x77, 0x33, 0xad, 0xf1, 0x35, 0xf0, + 0x21, 0xce, 0xf0, 0x35, 0x30, 0x17, 0x18, 0xa2, + 0x04, 0x3e, 0xf1, 0x35, 0xca, 0xd0, 0xfa, 0x90, + 0xf0, 0xee, 0xee, 0x35, 0xd0, 0x03, 0xee, 0xef, + 0x35, 0xad, 0xf0, 0x35, 0x60, 0xa9, 0x00, 0x8d, + 0xf1, 0x35, 0xa9, 0x00, 0x8d, 0x9e, 0x33, 0x20, + 0xf7, 0x2f, 0x18, 0xad, 0xeb, 0x33, 0x6d, 0xec, + 0x33, 0xf0, 0x09, 0xcd, 0xef, 0x33, 0x90, 0x14, + 0xa9, 0xff, 0xd0, 0x0a, 0xad, 0x9e, 0x33, 0xd0, + 0x37, 0xa9, 0x01, 0x8d, 0x9e, 0x33, 0x8d, 0xec, + 0x33, 0x18, 0x69, 0x11, 0x8d, 0xeb, 0x33, 0x8d, + 0xf1, 0x35, 0xa8, 0x0a, 0x0a, 0xa8, 0xa2, 0x04, + 0x18, 0xb9, 0xf6, 0x33, 0x9d, 0xf1, 0x35, 0xf0, + 0x06, 0x38, 0xa9, 0x00, 0x99, 0xf6, 0x33, 0x88, + 0xca, 0xd0, 0xee, 0x90, 0xbd, 0x20, 0xfb, 0x2f, + 0xad, 0xf0, 0x33, 0x8d, 0xf0, 0x35, 0xd0, 0x89, + 0x4c, 0x77, 0x33, 0xad, 0xf1, 0x35, 0xd0, 0x01, + 0x60, 0x48, 0x20, 0xf7, 0x2f, 0xac, 0xf0, 0x35, + 0x68, 0x18, 0x20, 0xdd, 0x32, 0xa9, 0x00, 0x8d, + 0xf1, 0x35, 0x4c, 0xfb, 0x2f, 0xa2, 0xfc, 0x7e, + 0xf6, 0x34, 0xe8, 0xd0, 0xfa, 0xc8, 0xcc, 0xf0, + 0x33, 0xd0, 0xf2, 0x0a, 0x0a, 0xa8, 0xf0, 0x0f, + 0xa2, 0x04, 0xbd, 0xf1, 0x35, 0x19, 0xf6, 0x33, + 0x99, 0xf6, 0x33, 0x88, 0xca, 0xd0, 0xf3, 0x60, + + 0xad, 0xbd, 0x35, 0x8d, 0xe6, 0x35, 0x8d, 0xea, + 0x35, 0xad, 0xbe, 0x35, 0x8d, 0xe4, 0x35, 0x8d, + 0xeb, 0x35, 0xa9, 0x00, 0x8d, 0xe5, 0x35, 0xa0, + 0x10, 0xaa, 0xad, 0xe6, 0x35, 0x4a, 0xb0, 0x03, + 0x8a, 0x90, 0x0e, 0x18, 0xad, 0xe5, 0x35, 0x6d, + 0xe8, 0x35, 0x8d, 0xe5, 0x35, 0x8a, 0x6d, 0xe9, + 0x35, 0x6a, 0x6e, 0xe5, 0x35, 0x6e, 0xe4, 0x35, + 0x6e, 0xe6, 0x35, 0x88, 0xd0, 0xdb, 0xad, 0xbf, + 0x35, 0x8d, 0xec, 0x35, 0x6d, 0xe6, 0x35, 0x8d, + 0xe6, 0x35, 0xad, 0xc0, 0x35, 0x8d, 0xed, 0x35, + 0x6d, 0xe4, 0x35, 0x8d, 0xe4, 0x35, 0xa9, 0x00, + 0x6d, 0xe5, 0x35, 0x8d, 0xe5, 0x35, 0x60, 0xa9, + 0x01, 0xd0, 0x22, 0xa9, 0x02, 0xd0, 0x1e, 0xa9, + 0x03, 0xd0, 0x1a, 0xa9, 0x04, 0xd0, 0x16, 0xa9, + 0x05, 0xd0, 0x12, 0xa9, 0x06, 0xd0, 0x0e, 0x4c, + 0xed, 0x3f, 0xea, 0xa9, 0x0a, 0xd0, 0x06, 0xad, + 0xc5, 0x35, 0x18, 0x90, 0x01, 0x38, 0x08, 0x8d, + 0xc5, 0x35, 0xa9, 0x00, 0x85, 0x48, 0x20, 0x7e, + 0x2e, 0x28, 0xae, 0x9b, 0x33, 0x9a, 0x60, 0x00, + 0x00, 0x00, 0x00, 0xf5, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xf8, 0xff, 0x01, 0x0a, 0x64, 0xd4, + 0xc9, 0xc1, 0xc2, 0xd3, 0xd2, 0xc1, 0xc2, 0xa0, + 0xc5, 0xcd, 0xd5, 0xcc, 0xcf, 0xd6, 0xa0, 0xcb, + 0xd3, 0xc9, 0xc4, 0x02, 0x11, 0x0c, 0x03, 0x00, + 0x00, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x7a, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, 0x23, + 0x0d, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, + + 0xf8, 0x00, 0x00, 0xff, 0xf8, 0x00, 0x00, 0xff, + 0xf8, 0x00, 0x00, 0xff, 0xf8, 0x00, 0x00, 0xff, + 0xf8, 0x00, 0x00, 0xff, 0xf8, 0x00, 0x00, 0xff, + 0xf8, 0x00, 0x00, 0xff, 0xf8, 0x00, 0x00, 0xff, + 0xf8, 0x00, 0x00, 0xff, 0xf8, 0x00, 0x00, 0xff, + 0xf8, 0x00, 0x00, 0xff, 0xf8, 0x00, 0x00, 0xff, + 0xf8, 0x00, 0x00, 0xff, 0xf8, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xff, 0xf8, 0x00, 0x00, 0xff, + 0xf8, 0x00, 0x00, 0xff, 0xf8, 0x00, 0x00, 0xff, + 0xf8, 0x00, 0x00, 0xff, 0xf8, 0x00, 0x00, 0xff, + 0xf8, 0x00, 0x00, 0xff, 0xf8, 0x00, 0x00, 0xff, + 0xf8, 0x00, 0x00, 0xff, 0xf8, 0x00, 0x00, 0xff, + 0xf8, 0x00, 0x00, 0xff, 0xf8, 0x00, 0x00, 0xff, + 0xf8, 0x00, 0x00, 0xff, 0xf8, 0x00, 0x00, 0xff, + 0xf8, 0x00, 0x00, 0xff, 0xf8, 0x00, 0x00, 0xff, + 0xf8, 0x00, 0x00, 0xff, 0xf8, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0b, 0x1b, 0x01, 0x00, 0xfe, + 0x01, 0x06, 0x00, 0x75, 0x2a, 0x00, 0x00, 0x00, + 0x18, 0x00, 0x17, 0x00, 0x16, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, + 0x01, 0x01, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, + + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + + +}; // namespace DiskImgLib diff --git a/diskimg/DiskFS.cpp b/diskimg/DiskFS.cpp new file mode 100644 index 0000000..201429c --- /dev/null +++ b/diskimg/DiskFS.cpp @@ -0,0 +1,558 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * DiskFS base class. + */ +#include "StdAfx.h" +#include "DiskImgPriv.h" + + +/* + * =========================================================================== + * A2File + * =========================================================================== + */ + +/* + * Set the quality level (a/k/a damage level) of a file. + * + * Refuse to "improve" the quality level of a file. + */ +void +A2File::SetQuality(FileQuality quality) +{ + if (quality == kQualityGood && + (fFileQuality == kQualitySuspicious || fFileQuality == kQualityDamaged)) + { + assert(false); + return; + } + if (quality == kQualitySuspicious && fFileQuality == kQualityDamaged) { + //assert(false); + return; + } + + fFileQuality = quality; +} + +/* + * Reset the quality level after making repairs. + */ +void +A2File::ResetQuality(void) +{ + fFileQuality = kQualityGood; +} + + +/* + * =========================================================================== + * DiskFS + * =========================================================================== + */ + +/* + * Set the DiskImg pointer. We add or subtract from the DiskImg's ref count + * so that it can be sure there are no DiskFS objects left dangling when the + * DiskImg is deleted. + */ +void +DiskFS::SetDiskImg(DiskImg* pImg) +{ + if (pImg == nil && fpImg == nil) { + WMSG0("SetDiskImg: no-op (both nil)\n"); + return; + } else if (fpImg == pImg) { + WMSG0("SetDiskImg: no-op (old == new)\n"); + return; + } + + if (fpImg != nil) + fpImg->RemoveDiskFS(this); + if (pImg != nil) + pImg->AddDiskFS(this); + fpImg = pImg; +} + +/* + * Flush changes to disk. + */ +DIError +DiskFS::Flush(DiskImg::FlushMode mode) +{ + SubVolume* pSubVol = GetNextSubVolume(nil); + DIError dierr; + + while (pSubVol != nil) { + + // quick sanity check + assert(pSubVol->GetDiskFS()->GetDiskImg() == pSubVol->GetDiskImg()); + + dierr = pSubVol->GetDiskFS()->Flush(mode); // recurse + if (dierr != kDIErrNone) + return dierr; + + pSubVol = GetNextSubVolume(pSubVol); + } + + assert(fpImg != nil); + + return fpImg->FlushImage(mode); +} + +/* + * Set the "read only" flag on our DiskImg and those of our sub-volumes. + */ +void +DiskFS::SetAllReadOnly(bool val) +{ + SubVolume* pSubVol = GetNextSubVolume(nil); + + /* put current volume in read-only mode */ + if (fpImg != nil) + fpImg->SetReadOnly(val); + + /* handle our kids */ + while (pSubVol != nil) { + // quick sanity check + assert(pSubVol->GetDiskFS()->GetDiskImg() == pSubVol->GetDiskImg()); + + //pSubVol->GetDiskImg()->SetReadOnly(val); + pSubVol->GetDiskFS()->SetAllReadOnly(val); // recurse + + pSubVol = GetNextSubVolume(pSubVol); + } +} + + +/* + * The file list looks something like this: + * + * volume-dir + * file1 + * file2 + * subdir1 + * subdir1:file1 + * subdir1:file2 + * subdir1:subsub1 + * subdir1:subsub1:file1 + * subdir1:subsub2 + * subdir1:subsub2:file1 + * subdir1:subsub2:file2 + * subdir1:file3 + * file3 + * + * Everything contained within a subdir comes after the subdir entry and + * before any entries from later subdirs at the same level. + * + * It's unclear whether a linear list or a hierarchical tree structure is + * the most appropriate way to hold the data. The tree is easier to update, + * but the linear list corresponds to the primary view in CiderPress, and + * lists are simpler and easier to manage. For now I'm sticking with a list. + * + * The files MUST be in the order in which they came from the disk. This + * doesn't matter most of the time, but for Pascal volumes it's essential + * for ensuring that the Write command doesn't run over the next file. + */ + +/* + * Add a file to the end of our list. + */ +void +DiskFS::AddFileToList(A2File* pFile) +{ + assert(pFile->GetNext() == nil); + + if (fpA2Head == nil) { + assert(fpA2Tail == nil); + fpA2Head = fpA2Tail = pFile; + } else { + pFile->SetPrev(fpA2Tail); + fpA2Tail->SetNext(pFile); + fpA2Tail = pFile; + } +} + +/* + * Insert a file into its appropriate place in the list, based on a file + * hierarchy. + * + * Pass in the thing to be added ("pFile") and the previous entry ("pPrev"). + * An empty hierarchic filesystem will have an entry for the volume dir, so + * we should never have an empty list or a NULL pPrev. + * + * The part where things go pear-shaped happens if "pPrev" is a subdirectory. + * If so, we need to come after all of the subdir's entries, including any + * entries for sub-subdirs. There's no graceful way to go about this in a + * linear list. + * + * (We'd love to be able to find the *next* entry and then back up one, + * but odds are that there isn't a "next" entry if we're busily creating + * files.) + */ +void +DiskFS::InsertFileInList(A2File* pFile, A2File* pPrev) +{ + assert(pFile->GetNext() == nil); + + if (fpA2Head == nil) { + assert(pPrev == nil); + fpA2Head = fpA2Tail = pFile; + return; + } else if (pPrev == nil) { + // create two entries on DOS disk, delete first, add new file + pFile->SetNext(fpA2Head); + fpA2Head = pFile; + return; + } + + /* + * If we're inserting after the parent (i.e. we're the very first thing + * in a subdir) or after a plain file, just drop it in. + * + * If we're inserting after a subdir, go fish. + */ + if (pPrev->IsDirectory() && pFile->GetParent() != pPrev) { + pPrev = SkipSubdir(pPrev); + } + + pFile->SetNext(pPrev->GetNext()); + pPrev->SetNext(pFile); +} + +/* + * Skip over all entries in the subdir we're pointing to. + * + * The return value is the very last entry in the subdir. + */ +A2File* +DiskFS::SkipSubdir(A2File* pSubdir) +{ + if (pSubdir->GetNext() == nil) + return pSubdir; // end of list reached -- subdir is empty + + A2File* pCur = pSubdir; + A2File* pNext = nil; + + assert(pCur != nil); // at least one time through the loop + + while (pCur != nil) { + pNext = pCur->GetNext(); + if (pNext == nil) // end of list reached + return pCur; + + if (pNext->GetParent() != pSubdir) // end of dir reached + return pCur; + if (pNext->IsDirectory()) + pCur = SkipSubdir(pNext); // get last entry in dir + else + pCur = pNext; // advance forward one + } + + /* should never get here */ + assert(false); + return pNext; +} + +/* + * Delete a member from the list. + * + * We're currently singly-linked, making this rather expensive. + */ +void +DiskFS::DeleteFileFromList(A2File* pFile) +{ + if (fpA2Head == pFile) { + /* delete the head of the list */ + fpA2Head = fpA2Head->GetNext(); + delete pFile; + } else { + A2File* pCur = fpA2Head; + while (pCur != nil) { + if (pCur->GetNext() == pFile) { + /* found it */ + A2File* pNextNext = pCur->GetNext()->GetNext(); + delete pCur->GetNext(); + pCur->SetNext(pNextNext); + break; + } + pCur = pCur->GetNext(); + } + + if (pCur == nil) { + WMSG0("GLITCH: couldn't find element to delete!\n"); + assert(false); + } + } +} + + +/* + * Access the "next" pointer. + * + * Because we apparently can't declare an anonymous class as a friend + * in MSVC++6.0, this can't be an inline function. + */ +A2File* +DiskFS::GetNextFile(A2File* pFile) const +{ + if (pFile == NULL) + return fpA2Head; + else + return pFile->GetNext(); +} + +/* + * Return the #of elements in the linear file list. + * + * Right now the only code that calls this is the disk info panel in + * CiderPress, so we don't need it to be efficient. + */ +long +DiskFS::GetFileCount(void) const +{ + long count = 0; + + A2File* pFile = fpA2Head; + while (pFile != nil) { + count++; + pFile = pFile->GetNext(); + } + + return count; +} + +/* + * Delete all entries in the list. + */ +void +DiskFS::DeleteFileList(void) +{ + A2File* pFile; + A2File* pNext; + + pFile = fpA2Head; + while (pFile != nil) { + pNext = pFile->GetNext(); + delete pFile; + pFile = pNext; + } +} + +/* + * Dump file list. + */ +void +DiskFS::DumpFileList(void) +{ + A2File* pFile; + + WMSG0("DiskFS file list contents:\n"); + + pFile = GetNextFile(nil); + while (pFile != nil) { + WMSG1(" %s\n", pFile->GetPathName()); + pFile = GetNextFile(pFile); + } +} + +/* + * Run through the list of files and find one that matches (case-insensitive). + * + * This does not attempt to open files in sub-volumes. We could, but it's + * likely that the application has "decorated" the name in some fashion, + * e.g. by prepending the sub-volume's volume name to the filename. May + * be best to let the application dig for the sub-volume. + */ +A2File* +DiskFS::GetFileByName(const char* fileName, StringCompareFunc func) +{ + A2File* pFile; + + if (func == nil) + func = ::strcasecmp; + + pFile = GetNextFile(nil); + while (pFile != nil) { + if ((*func)(pFile->GetPathName(), fileName) == 0) + return pFile; + + pFile = GetNextFile(pFile); + } + + return nil; +} + + +/* + * Add a sub-volume to the end of our list. + * + * Copies some parameters from "this" into pDiskFS, such as whether to + * scan for sub-volumes and the various DiskFS parameters. + * + * Note this happens AFTER the disk has been scanned. + */ +void +DiskFS::AddSubVolumeToList(DiskImg* pDiskImg, DiskFS* pDiskFS) +{ + SubVolume* pSubVol; + + /* + * Check the arguments. + */ + if (pDiskImg == nil || pDiskFS == nil) { + WMSG2(" DiskFS bogus sub volume ptrs %08lx %08lx\n", + (long) pDiskImg, (long) pDiskFS); + assert(false); + return; + } + if (pDiskImg == fpImg || pDiskFS == this) { + WMSG0(" DiskFS attempt to add self to sub-vol list\n"); + assert(false); + return; + } + if (pDiskFS->GetDiskImg() == nil) { + WMSG0(" DiskFS lacks a DiskImg pointer\n"); + assert(false); + return; + } + pSubVol = fpSubVolumeHead; + while (pSubVol != nil) { + if (pSubVol->GetDiskImg() == pDiskImg || + pSubVol->GetDiskFS() == pDiskFS) + { + WMSG0(" DiskFS multiple adds on diskimg or diskfs\n"); + assert(false); + return; + } + pSubVol = pSubVol->GetNext(); + } + + assert(pDiskFS->GetDiskImg() == pDiskImg); + + /* + * Looks good. Add it. + */ + pSubVol = new SubVolume; + if (pSubVol == nil) + return; + + pSubVol->Create(pDiskImg, pDiskFS); + + if (fpSubVolumeHead == nil) { + assert(fpSubVolumeTail == nil); + fpSubVolumeHead = fpSubVolumeTail = pSubVol; + } else { + pSubVol->SetPrev(fpSubVolumeTail); + fpSubVolumeTail->SetNext(pSubVol); + fpSubVolumeTail = pSubVol; + } + + /* make sure inheritable stuff gets copied */ + CopyInheritables(pDiskFS); +} + +/* + * Copy parameters to a sub-volume. + */ +void +DiskFS::CopyInheritables(DiskFS* pNewFS) +{ + for (int i = 0; i < (int) NELEM(fParmTable); i++) + pNewFS->fParmTable[i] = fParmTable[i]; + + pNewFS->fScanForSubVolumes = fScanForSubVolumes; + +#if 0 + /* copy scan progress update stuff */ + pNewFS->fpScanProgressCallback = fpScanProgressCallback; + pNewFS->fpScanProgressCookie = fpScanProgressCookie; + pNewFS->fpScanCount = -1; + strcpy(pNewFS->fpScanMsg, "HEY"); +#endif +} + +/* + * Access the "next" pointer. + * + * Because we apparently can't declare an anonymous class as a friend + * in MSVC++6.0, this can't be an inline function. + */ +DiskFS::SubVolume* +DiskFS::GetNextSubVolume(const SubVolume* pSubVol) const +{ + if (pSubVol == NULL) + return fpSubVolumeHead; + else + return pSubVol->GetNext(); +} + +/* + * Delete all entries in the list. + */ +void +DiskFS::DeleteSubVolumeList(void) +{ + SubVolume* pSubVol; + SubVolume* pNext; + + pSubVol = fpSubVolumeHead; + while (pSubVol != nil) { + pNext = pSubVol->GetNext(); + delete pSubVol; + pSubVol = pNext; + } +} + + +/* + * Get a parameter. + */ +long +DiskFS::GetParameter(DiskFSParameter parm) +{ + assert(parm > kParmUnknown && parm < kParmMax); + return fParmTable[parm]; +} + +/* + * Set a parameter. + * + * The setting propagates to all sub-volumes. + */ +void +DiskFS::SetParameter(DiskFSParameter parm, long val) +{ + assert(parm > kParmUnknown && parm < kParmMax); + fParmTable[parm] = val; + + SubVolume* pSubVol = GetNextSubVolume(nil); + while (pSubVol != nil) { + pSubVol->GetDiskFS()->SetParameter(parm, val); + pSubVol = GetNextSubVolume(pSubVol); + } +} + + +/* + * Scan for damaged or suspicious files. + */ +void +DiskFS::ScanForDamagedFiles(bool* pDamaged, bool* pSuspicious) +{ + A2File* pFile; + + *pDamaged = *pSuspicious = false; + + pFile = GetNextFile(nil); + while (pFile != nil) { + if (pFile->GetQuality() == A2File::kQualityDamaged) + *pDamaged = true; + if (pFile->GetQuality() != A2File::kQualityGood) + *pSuspicious = true; + pFile = GetNextFile(pFile); + } +} diff --git a/diskimg/DiskImg.cpp b/diskimg/DiskImg.cpp new file mode 100644 index 0000000..d164e83 --- /dev/null +++ b/diskimg/DiskImg.cpp @@ -0,0 +1,3515 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Implementation of the DiskImg class. + */ +#include "StdAfx.h" +#include "DiskImgPriv.h" +#include "TwoImg.h" + + +/* + * =========================================================================== + * DiskImg + * =========================================================================== + */ + +/* + * Standard NibbleDescr profiles. + * + * These will be tried in the order in which they appear here. + * + * IMPORTANT: if you add or remove an entry, update the StdNibbleDescr enum + * in DiskImg.h. + * + * Formats that allow the data checksum to be ignored should NOT be written. + * It's possible that the DOS on the disk is ignoring the checksums, but + * it's more likely that they're using a non-standard seed, and the newly- + * written sectors will have the wrong checksum value. + * + * Non-standard headers are usually okay, because we don't rewrite the + * headers, just the sector contents. + */ +/*static*/ const DiskImg::NibbleDescr DiskImg::kStdNibbleDescrs[] = { + { + "DOS 3.3 Standard", + 16, + { 0xd5, 0xaa, 0x96 }, { 0xde, 0xaa, 0xeb }, + 0x00, // checksum seed + true, // verify checksum + true, // verify track + 2, // epilog verify count + { 0xd5, 0xaa, 0xad }, { 0xde, 0xaa, 0xeb }, + 0x00, // checksum seed + true, // verify checksum + 2, // epilog verify count + kNibbleEnc62, + kNibbleSpecialNone, + }, + { + "DOS 3.3 Patched", + 16, + { 0xd5, 0xaa, 0x96 }, { 0xde, 0xaa, 0xeb }, + 0x00, // checksum seed + false, // verify checksum + false, // verify track + 0, // epilog verify count + { 0xd5, 0xaa, 0xad }, { 0xde, 0xaa, 0xeb }, + 0x00, // checksum seed + true, // verify checksum + 0, // epilog verify count + kNibbleEnc62, + kNibbleSpecialNone, + }, + { + "DOS 3.3 Ignore Checksum", + 16, + { 0xd5, 0xaa, 0x96 }, { 0xde, 0xaa, 0xeb }, + 0x00, // checksum seed + false, // verify checksum + false, // verify track + 0, // epilog verify count + { 0xd5, 0xaa, 0xad }, { 0xde, 0xaa, 0xeb }, + 0x00, // checksum seed + false, // verify checksum + 0, // epilog verify count + kNibbleEnc62, + kNibbleSpecialNone, + }, + { + "DOS 3.2 Standard", + 13, + { 0xd5, 0xaa, 0xb5 }, { 0xde, 0xaa, 0xeb }, + 0x00, + true, + true, + 2, + { 0xd5, 0xaa, 0xad }, { 0xde, 0xaa, 0xeb }, + 0x00, + true, + 2, + kNibbleEnc53, + kNibbleSpecialNone, + }, + { + "DOS 3.2 Patched", + 13, + { 0xd5, 0xaa, 0xb5 }, { 0xde, 0xaa, 0xeb }, + 0x00, + false, + false, + 0, + { 0xd5, 0xaa, 0xad }, { 0xde, 0xaa, 0xeb }, + 0x00, + true, + 0, + kNibbleEnc53, + kNibbleSpecialNone, + }, + { + "Muse DOS 3.2", // standard DOS 3.2 with doubled sectors + 13, + { 0xd5, 0xaa, 0xb5 }, { 0xde, 0xaa, 0xeb }, + 0x00, + true, + true, + 2, + { 0xd5, 0xaa, 0xad }, { 0xde, 0xaa, 0xeb }, + 0x00, + true, + 2, + kNibbleEnc53, + kNibbleSpecialMuse, + }, + { + "RDOS 3.3", // SSI 16-sector RDOS, with altered headers + 16, + { 0xd4, 0xaa, 0x96 }, { 0xde, 0xaa, 0xeb }, + 0x00, + true, + true, + 0, // epilog verify count + { 0xd5, 0xaa, 0xad }, { 0xde, 0xaa, 0xeb }, + 0x00, + true, + 2, + kNibbleEnc62, + kNibbleSpecialSkipFirstAddrByte, + /* odd tracks use d4aa96, even tracks use d5aa96 */ + }, + { + "RDOS 3.2", // SSI 13-sector RDOS, with altered headers + 13, + { 0xd4, 0xaa, 0xb7 }, { 0xde, 0xaa, 0xeb }, + 0x00, + true, + true, + 2, + { 0xd5, 0xaa, 0xad }, { 0xde, 0xaa, 0xeb }, + 0x00, + true, + 2, + kNibbleEnc53, + kNibbleSpecialNone, + }, + { + "Custom", // reserve space for empty slot + 0, + }, +}; +/*static*/ const DiskImg::NibbleDescr* +DiskImg::GetStdNibbleDescr(StdNibbleDescr idx) +{ + if ((int)idx < 0 || (int)idx >= (int) NELEM(kStdNibbleDescrs)) + return nil; + return &kStdNibbleDescrs[(int)idx]; +} + + +/* + * Initialize the members during construction. + */ +DiskImg::DiskImg(void) +{ + assert(Global::GetAppInitCalled()); + + fOuterFormat = kOuterFormatUnknown; + fFileFormat = kFileFormatUnknown; + fPhysical = kPhysicalFormatUnknown; + fpNibbleDescr = nil; + fOrder = kSectorOrderUnknown; + fFormat = kFormatUnknown; + + fFileSysOrder = kSectorOrderUnknown; + fSectorPairing = false; + fSectorPairOffset = -1; + + fpOuterGFD = nil; + fpWrapperGFD = nil; + fpDataGFD = nil; + fpOuterWrapper = nil; + fpImageWrapper = nil; + fpParentImg = nil; + fDOSVolumeNum = kVolumeNumNotSet; + fOuterLength = -1; + fWrappedLength = -1; + fLength = -1; + fExpandable = false; + fReadOnly = true; + fDirty = false; + + fHasSectors = false; + fHasBlocks = false; + fHasNibbles = false; + + fNumTracks = -1; + fNumSectPerTrack = -1; + fNumBlocks = -1; + + fpScanProgressCallback = NULL; + + /* + * Create a working copy of the nibble descr table. We want to leave + * open the possibility of applications editing or discarding entries, + * so we work off of a copy. + * + * Ideally we'd allow these to be set per-track, so that certain odd + * formats could be handled transparently (e.g. Muse tweaked DOS 3.2) + * for formatting as well as reading. + */ + assert(kStdNibbleDescrs[kNibbleDescrCustom].numSectors == 0); + assert(kNibbleDescrCustom == NELEM(kStdNibbleDescrs)-1); + fpNibbleDescrTable = new NibbleDescr[NELEM(kStdNibbleDescrs)]; + fNumNibbleDescrEntries = NELEM(kStdNibbleDescrs); + memcpy(fpNibbleDescrTable, kStdNibbleDescrs, sizeof(kStdNibbleDescrs)); + + fNibbleTrackBuf = nil; + fNibbleTrackLoaded = -1; + + fNuFXCompressType = kNuThreadFormatLZW2; + + fNotes = nil; + fpBadBlockMap = nil; + fDiskFSRefCnt = 0; +} + +/* + * Throw away local storage. + */ +DiskImg::~DiskImg(void) +{ + if (fpDataGFD != nil) { + WMSG0("~DiskImg closing GenericFD(s)\n"); + } + (void) CloseImage(); + delete[] fpNibbleDescrTable; + delete[] fNibbleTrackBuf; + delete[] fNotes; + delete fpBadBlockMap; + + /* normally these will be closed, but perhaps not if something failed */ + if (fpOuterGFD != nil) + delete fpOuterGFD; + if (fpWrapperGFD != nil) + delete fpWrapperGFD; + if (fpDataGFD != nil) + delete fpDataGFD; + if (fpOuterWrapper != nil) + delete fpOuterWrapper; + if (fpImageWrapper != nil) + delete fpImageWrapper; + + fDiskFSRefCnt = 100; // flag as freed +} + + +/* + * Set the nibble descr pointer. + */ +void +DiskImg::SetNibbleDescr(int idx) +{ + assert(idx >= 0 && idx < kNibbleDescrMAX); + fpNibbleDescr = &fpNibbleDescrTable[idx]; +} + +/* + * Set up a custom nibble descriptor. + */ +void +DiskImg::SetCustomNibbleDescr(const NibbleDescr* pDescr) +{ + if (pDescr == NULL) { + fpNibbleDescr = NULL; + } else { + assert(fpNibbleDescrTable != NULL); + //WMSG2("Overwriting entry %d with new value (special=%d)\n", + // kNibbleDescrCustom, pDescr->special); + fpNibbleDescrTable[kNibbleDescrCustom] = *pDescr; + fpNibbleDescr = &fpNibbleDescrTable[kNibbleDescrCustom]; + } +} + + +/* + * Open a volume or a file on disk. + * + * For Windows, we need to handle logical/physical volumes specially. If + * the filename matches the appropriate pattern, use a different GFD. + */ +DIError +DiskImg::OpenImage(const char* pathName, char fssep, bool readOnly) +{ + DIError dierr = kDIErrNone; + bool isWinDevice = false; + + if (fpDataGFD != nil) { + WMSG0(" DI already open!\n"); + return kDIErrAlreadyOpen; + } + WMSG3(" DI OpenImage '%s' '%.1s' ro=%d\n", pathName, &fssep, readOnly); + + fReadOnly = readOnly; + +#ifdef _WIN32 + if ((fssep == '\0' || fssep == '\\') && + pathName[0] >= 'A' && pathName[0] <= 'Z' && + pathName[1] == ':' && pathName[2] == '\\' && + pathName[3] == '\0') + { + isWinDevice = true; // logical volume ("A:\") + } + if ((fssep == '\0' || fssep == '\\') && + isdigit(pathName[0]) && isdigit(pathName[1]) && + pathName[2] == ':' && pathName[3] == '\\' && + pathName[4] == '\0') + { + isWinDevice = true; // physical volume ("80:\") + } + if ((fssep == '\0' || fssep == '\\') && + strncmp(pathName, kASPIDev, strlen(kASPIDev)) == 0 && + pathName[strlen(pathName)-1] == '\\') + { + isWinDevice = true; // ASPI volume ("ASPI:x:y:z\") + } +#endif + + if (isWinDevice) { +#ifdef _WIN32 + GFDWinVolume* pGFDWinVolume = new GFDWinVolume; + + dierr = pGFDWinVolume->Open(pathName, fReadOnly); + if (dierr != kDIErrNone) { + delete pGFDWinVolume; + goto bail; + } + + fpWrapperGFD = pGFDWinVolume; + // Use a unique extension to skip some of the probing. + dierr = AnalyzeImageFile("CPDevice.cp-win-vol", '\0'); + if (dierr != kDIErrNone) + goto bail; +#endif + } else { + GFDFile* pGFDFile = new GFDFile; + + dierr = pGFDFile->Open(pathName, fReadOnly); + if (dierr != kDIErrNone) { + delete pGFDFile; + goto bail; + } + + //fImageFileName = new char[strlen(pathName) + 1]; + //strcpy(fImageFileName, pathName); + + fpWrapperGFD = pGFDFile; + pGFDFile = nil; + + dierr = AnalyzeImageFile(pathName, fssep); + if (dierr != kDIErrNone) + goto bail; + } + + + assert(fpDataGFD != nil); + +bail: + return dierr; +} + +/* + * Open from a buffer, which could point to unadorned ready-to-go content + * or to a preloaded image file. + */ +DIError +DiskImg::OpenImage(const void* buffer, long length, bool readOnly) +{ + if (fpDataGFD != nil) { + WMSG0(" DI already open!\n"); + return kDIErrAlreadyOpen; + } + WMSG3(" DI OpenImage %08lx %ld ro=%d\n", (long) buffer, length, readOnly); + + DIError dierr; + GFDBuffer* pGFDBuffer; + + fReadOnly = readOnly; + pGFDBuffer = new GFDBuffer; + + dierr = pGFDBuffer->Open(const_cast(buffer), length, false, false, + readOnly); + if (dierr != kDIErrNone) { + delete pGFDBuffer; + return dierr; + } + + fpWrapperGFD = pGFDBuffer; + pGFDBuffer = nil; + + dierr = AnalyzeImageFile("", '\0'); + if (dierr != kDIErrNone) + return dierr; + + assert(fpDataGFD != nil); + return kDIErrNone; +} + +/* + * Open a range of blocks from an already-open disk image. This is only + * useful for things like UNIDOS volumes, which don't have an associated + * file in the image and are linear. + * + * The "read only" flag is inherited from the parent. + * + * For embedded images with visible file structure, we should be using + * an EmbeddedFD instead. [Note these were never implemented.] + * + * NOTE: there is an implicit ProDOS block ordering imposed on the parent + * image. It turns out that all of our current embedded parents use + * ProDOS-ordered blocks, so it works out okay, but the "linear" requirement + * above goes beyond just having contiguous blocks. + */ +DIError +DiskImg::OpenImage(DiskImg* pParent, long firstBlock, long numBlocks) +{ + WMSG3(" DI OpenImage parent=0x%08lx %ld %ld\n", (long) pParent, firstBlock, + numBlocks); + if (fpDataGFD != nil) { + WMSG0(" DI already open!\n"); + return kDIErrAlreadyOpen; + } + + if (pParent == nil || firstBlock < 0 || numBlocks <= 0 || + firstBlock + numBlocks > pParent->GetNumBlocks()) + { + assert(false); + return kDIErrInvalidArg; + } + + fReadOnly = pParent->GetReadOnly(); // very important + + DIError dierr; + GFDGFD* pGFDGFD; + + pGFDGFD = new GFDGFD; + dierr = pGFDGFD->Open(pParent->fpDataGFD, firstBlock * kBlockSize, fReadOnly); + if (dierr != kDIErrNone) { + delete pGFDGFD; + return dierr; + } + + fpDataGFD = pGFDGFD; + assert(fpWrapperGFD == nil); + + /* + * This replaces the call to "analyze image file" because we know we + * already have an open file with specific characteristics. + */ + //fOffset = pParent->fOffset + kBlockSize * firstBlock; + fLength = numBlocks * kBlockSize; + fOuterLength = fWrappedLength = fLength; + fFileFormat = kFileFormatUnadorned; + fPhysical = pParent->fPhysical; + fOrder = pParent->fOrder; + + fpParentImg = pParent; + + return dierr; +} +DIError +DiskImg::OpenImage(DiskImg* pParent, long firstTrack, long firstSector, + long numSectors) +{ + WMSG4(" DI OpenImage parent=0x%08lx %ld %ld %ld\n", (long) pParent, + firstTrack, firstSector, numSectors); + if (fpDataGFD != nil) { + WMSG0(" DI already open!\n"); + return kDIErrAlreadyOpen; + } + + if (pParent == nil) + return kDIErrInvalidArg; + + int prntSectPerTrack = pParent->GetNumSectPerTrack(); + int lastTrack = firstTrack + + (numSectors + prntSectPerTrack-1) / prntSectPerTrack; + if (firstTrack < 0 || numSectors <= 0 || + lastTrack > pParent->GetNumTracks()) + { + return kDIErrInvalidArg; + } + + fReadOnly = pParent->GetReadOnly(); // very important + + DIError dierr; + GFDGFD* pGFDGFD; + + pGFDGFD = new GFDGFD; + dierr = pGFDGFD->Open(pParent->fpDataGFD, + kSectorSize * firstTrack * prntSectPerTrack, fReadOnly); + if (dierr != kDIErrNone) { + delete pGFDGFD; + return dierr; + } + + fpDataGFD = pGFDGFD; + assert(fpWrapperGFD == nil); + + /* + * This replaces the call to "analyze image file" because we know we + * already have an open file with specific characteristics. + */ + assert(firstSector == 0); // else fOffset calculation breaks + //fOffset = pParent->fOffset + kSectorSize * firstTrack * prntSectPerTrack; + fLength = numSectors * kSectorSize; + fOuterLength = fWrappedLength = fLength; + fFileFormat = kFileFormatUnadorned; + fPhysical = pParent->fPhysical; + fOrder = pParent->fOrder; + + fpParentImg = pParent; + + return dierr; +} + + +/* + * Enable sector pairing. Useful for OzDOS. + */ +void +DiskImg::SetPairedSectors(bool enable, int idx) +{ + fSectorPairing = enable; + fSectorPairOffset = idx; + + if (enable) { + assert(idx == 0 || idx == 1); + } +} + +/* + * Close the image, freeing resources. + * + * If we write to a child DiskImg, it's responsible for setting the "dirty" + * flag in its parent (and so on up the chain). That's necessary so that, + * when we close the file, changes made to a child DiskImg cause the parent + * to do any necessary recompression. + * + * [ This is getting called even when image creation failed with an error. + * This is probably the correct behavior, but we may want to be aborting the + * image creation instead of completing it. That's a higher-level decision + * though. ++ATM 20040506 ] + */ +DIError +DiskImg::CloseImage(void) +{ + DIError dierr; + + WMSG1("CloseImage %p\n", this); + + /* check for DiskFS objects that still point to us */ + if (fDiskFSRefCnt != 0) { + WMSG1("ERROR: CloseImage: fDiskFSRefCnt=%d\n", fDiskFSRefCnt); + assert(false); //DebugBreak(); + } + + /* + * Flush any changes. + */ + dierr = FlushImage(kFlushAll); + if (dierr != kDIErrNone) + return dierr; + + /* + * Clean up. Close GFD, OrigGFD, and OuterGFD. Delete ImageWrapper + * and OuterWrapper. + * + * In some cases we will have the file open more than once (e.g. a + * NuFX archive, which must be opened on disk). + */ + if (fpDataGFD != nil) { + fpDataGFD->Close(); + delete fpDataGFD; + fpDataGFD = nil; + } + if (fpWrapperGFD != nil) { + fpWrapperGFD->Close(); + delete fpWrapperGFD; + fpWrapperGFD = nil; + } + if (fpOuterGFD != nil) { + fpOuterGFD->Close(); + delete fpOuterGFD; + fpOuterGFD = nil; + } + delete fpImageWrapper; + fpImageWrapper = nil; + delete fpOuterWrapper; + fpOuterWrapper = nil; + + return dierr; +} + + +/* + * Flush data to disk. + * + * The only time this really needs to do anything on a disk image file is + * when we have compressed data (NuFX, DDD, .gz, .zip). The uncompressed + * wrappers either don't do anything ("unadorned") or just update some + * header fields (DiskCopy42). + * + * If "mode" is kFlushFastOnly, we only flush the formats that don't really + * need flushing. This is part of a scheme to keep the disk contents in a + * reasonable state on the off chance we crash with a modified file open. + * It also helps the user understand when changes are being made immediately + * vs. when they're written to memory and compressed later. We could just + * refuse to raise the "dirty" flag when modifying "simple" file formats, + * but that would change the meaning of the flag from "something has been + * changed" to "what's in the file and what's in memory differ". I want it + * to be a "dirty" flag. + */ +DIError +DiskImg::FlushImage(FlushMode mode) +{ + DIError dierr = kDIErrNone; + + WMSG2(" DI FlushImage (dirty=%d mode=%d)\n", fDirty, mode); + if (!fDirty) + return kDIErrNone; + if (fpDataGFD == nil) { + /* + * This can happen if we tried to create a disk image but failed, e.g. + * couldn't create the output file because of access denied on the + * directory. There's no data, therefore nothing to flush, but the + * "dirty" flag is set because CreateImageCommon sets it almost + * immediately. + */ + WMSG0(" (disk must've failed during creation)\n"); + fDirty = false; + return kDIErrNone; + } + + if (mode == kFlushFastOnly && + ((fpImageWrapper != nil && !fpImageWrapper->HasFastFlush()) || + (fpOuterWrapper != nil && !fpOuterWrapper->HasFastFlush()) )) + { + WMSG0("DI fast flush requested, but one or both wrappers are slow\n"); + return kDIErrNone; + } + + /* + * Step 1: make sure any local caches have been flushed. + */ + /* (none) */ + + /* + * Step 2: push changes from fpDataGFD to fpWrapperGFD. This will + * cause ImageWrapper to rebuild itself (SHK, DDD, whatever). In + * some cases this amounts to copying the data on top of itself, + * which we can avoid easily. + * + * Embedded volumes don't have wrappers; when you write to an + * embedded volume, it passes straight through to the parent. + * + * (Note to self: formats like NuFX that write to a temp file and then + * rename over the old will close fpWrapperGFD and just access it + * directly. This is bad, because it doesn't allow them to have an + * "outer" format, but it's the way life is. The point is that it's + * okay for fpWrapperGFD to be non-nil but represent a closed file, + * so long as the "Flush" function has it figured out.) + */ + if (fpWrapperGFD != nil) { + WMSG2(" DI flushing data changes to wrapper (fLen=%ld fWrapLen=%ld)\n", + (long) fLength, (long) fWrappedLength); + dierr = fpImageWrapper->Flush(fpWrapperGFD, fpDataGFD, fLength, + &fWrappedLength); + if (dierr != kDIErrNone) { + WMSG1(" ERROR: wrapper flush failed (err=%d)\n", dierr); + return dierr; + } + /* flush the GFD in case it's a Win32 volume with block caching */ + dierr = fpWrapperGFD->Flush(); + } else { + assert(fpParentImg != nil); + } + + /* + * Step 3: if we have an fpOuterGFD, rebuild the file with the data + * in fpWrapperGFD. + */ + if (fpOuterWrapper != nil) { + WMSG1(" DI saving wrapper to outer, fWrapLen=%ld\n", + (long) fWrappedLength); + assert(fpOuterGFD != nil); + dierr = fpOuterWrapper->Save(fpOuterGFD, fpWrapperGFD, + fWrappedLength); + if (dierr != kDIErrNone) { + WMSG1(" ERROR: outer save failed (err=%d)\n", dierr); + return dierr; + } + } + + fDirty = false; + return kDIErrNone; +} + + + +/* + * Given the filename extension and a GFD, figure out what's inside. + * + * The filename extension should give us some idea what to expect: + * SHK, SDK, BXY - ShrinkIt compressed disk image + * GZ - gzip-compressed file (with something else inside) + * ZIP - ZIP archive with a single disk image inside + * DDD - DDD, DDD Pro, or DDD5.0 compressed image + * DSK - DiskCopy 4.2 or DO/PO + * DC - DiskCopy 4.2 (or 6?) + * DC6 - DiskCopy 6 (usually just raw sectors) + * DO, PO, D13, RAW? - DOS-order or ProDOS-order uncompressed + * IMG - Copy ][+ image (unadorned, physical sector order) + * HDV - virtual hard drive image + * NIB, RAW? - nibblized image + * (no extension) uncompressed + * cp-win-vol - our "magic" extension to indicate a Windows logical volume + * + * We can also examine the file length to see if it's a standard size + * (140K, 800K) and look for magic values in the header. + * + * If we can access the contents directly from disk, we do so. It's + * possibly more efficient to load the whole thing into memory, but if + * we have that much memory then the OS should cache it for us. (I have + * some 20MB disk images from my hard drive that shouldn't be loaded + * in their entirety. Certainly don't want to load a 512MB CFFA image.) + * + * On input, the following fields must be set: + * fpWrapperGFD - GenericFD for the file pointed to by "pathname" (or for a + * memory buffer if this is a sub-volume) + * + * On success, the following fields will be set: + * fWrappedLength, fOuterLength - set appropriately + * fpDataGFD - GFD for the raw data, possibly just a GFDGFD with an offset + * fLength - length of unadorned data in the file, or the length of + * data stored in fBuffer (test for fBuffer!=nil) + * fFileFormat - set to the overall file format, mostly interesting + * for identification of the file "wrapper" + * fPhysicalFormat - set to the type of data this holds + * (maybe) fOrder - set when the file format or extension dictates, e.g. + * 2MG or *.po; not always reliable + * (maybe) fDOSVolumeNum - set to DOS volume number from wrapper + * + * This may set fReadOnly if one of the wrappers looks okay but is reporting + * a bad checksum. + */ +DIError +DiskImg::AnalyzeImageFile(const char* pathName, char fssep) +{ + DIError dierr = kDIErrNone; + FileFormat probableFormat; + bool reliableExt; + const char* ext = FindExtension(pathName, fssep); + char* extBuf = nil; // uses malloc/free + bool needExtFromOuter = false; + + if (ext != nil) { + assert(*ext == '.'); + ext++; + } else + ext = ""; + + WMSG1(" DI AnalyzeImageFile ext='%s'\n", ext); + + /* sanity check: nobody should have configured these yet */ + assert(fOuterFormat == kOuterFormatUnknown); + assert(fFileFormat == kFileFormatUnknown); + assert(fOrder == kSectorOrderUnknown); + assert(fFormat == kFormatUnknown); + fLength = -1; + dierr = fpWrapperGFD->Seek(0, kSeekEnd); + if (dierr != kDIErrNone) { + WMSG0(" DI Couldn't seek to end of wrapperGFD\n"); + goto bail; + } + fWrappedLength = fOuterLength = fpWrapperGFD->Tell(); + + /* quick test for zero-length files */ + if (fWrappedLength == 0) + return kDIErrUnrecognizedFileFmt; + + /* + * Start by checking for a zip/gzip "wrapper wrapper". We want to strip + * that away before we do anything else. Because web sites tend to + * gzip everything in sight whether it needs it or not, we treat this + * as a special case and assume that anything could be inside. + * + * Some cases are difficult to handle, e.g. ".SDK", since NufxLib + * doesn't let us open an archive that is sitting in memory. + * + * We could also handle disk images stored as ordinary files stored + * inside SHK. Not much point in handling multiple files down at + * this level though. + */ + if (strcasecmp(ext, "gz") == 0 && + OuterGzip::Test(fpWrapperGFD, fOuterLength) == kDIErrNone) + { + WMSG0(" DI found gz outer wrapper\n"); + + fpOuterWrapper = new OuterGzip(); + if (fpOuterWrapper == nil) { + dierr = kDIErrMalloc; + goto bail; + } + fOuterFormat = kOuterFormatGzip; + + /* drop the ".gz" and get down to the next extension */ + ext = ""; + extBuf = strdup(pathName); + if (extBuf != nil) { + char* localExt; + + localExt = (char*) FindExtension(extBuf, fssep); + if (localExt != nil) + *localExt = '\0'; + localExt = (char*) FindExtension(extBuf, fssep); + if (localExt != nil) { + ext = localExt; + assert(*ext == '.'); + ext++; + } + } + WMSG1(" DI after gz, ext='%s'\n", ext == nil ? "(nil)" : ext); + + } else if (strcasecmp(ext, "zip") == 0) { + dierr = OuterZip::Test(fpWrapperGFD, fOuterLength); + if (dierr != kDIErrNone) + goto bail; + + WMSG0(" DI found ZIP outer wrapper\n"); + + fpOuterWrapper = new OuterZip(); + if (fpOuterWrapper == nil) { + dierr = kDIErrMalloc; + goto bail; + } + fOuterFormat = kOuterFormatZip; + + needExtFromOuter = true; + + } else { + fOuterFormat = kOuterFormatNone; + } + + /* finish up outer wrapper stuff */ + if (fOuterFormat != kOuterFormatNone) { + GenericFD* pNewGFD = nil; + dierr = fpOuterWrapper->Load(fpWrapperGFD, fOuterLength, fReadOnly, + &fWrappedLength, &pNewGFD); + if (dierr != kDIErrNone) { + WMSG0(" DI outer prep failed\n"); + /* extensions are "reliable", so failure is unavoidable */ + goto bail; + } + + /* Load() sets this */ + if (fpOuterWrapper->IsDamaged()) { + AddNote(kNoteWarning, "The zip/gzip wrapper appears to be damaged."); + fReadOnly = true; + } + + /* shift GFDs */ + fpOuterGFD = fpWrapperGFD; + fpWrapperGFD = pNewGFD; + + if (needExtFromOuter) { + ext = fpOuterWrapper->GetExtension(); + if (ext == nil) + ext = ""; + } + } + + /* + * Try to figure out what format the file is in. + * + * First pass, try only what the filename says it is. This way, if + * two file formats look alike, we have a good chance of getting it + * right. + * + * The "Test" functions have the complete file at their disposal. The + * file's length is stored in "fWrappedLength" for convenience. + */ + reliableExt = false; + probableFormat = kFileFormatUnknown; + if (strcasecmp(ext, "2mg") == 0 || strcasecmp(ext, "2img") == 0) { + reliableExt = true; + if (Wrapper2MG::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) + probableFormat = kFileFormat2MG; + } else if (strcasecmp(ext, "shk") == 0 || strcasecmp(ext, "sdk") == 0 || + strcasecmp(ext, "bxy") == 0) + { + DIError dierr2; + reliableExt = true; + dierr2 = WrapperNuFX::Test(fpWrapperGFD, fWrappedLength); + if (dierr2 == kDIErrNone) + probableFormat = kFileFormatNuFX; + else if (dierr2 == kDIErrFileArchive) { + WMSG0(" AnalyzeImageFile thinks it found a NuFX file archive\n"); + dierr = dierr2; + goto bail; + } + } else if (strcasecmp(ext, "hdv") == 0) { + /* usually just a "raw" disk, but check for Sim //e */ + if (WrapperSim2eHDV::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) + probableFormat = kFileFormatSim2eHDV; + + /* ProDOS .hdv volumes can expand */ + fExpandable = true; + } else if (strcasecmp(ext, "dsk") == 0 || strcasecmp(ext, "dc") == 0) { + /* might be DiskCopy */ + if (WrapperDiskCopy42::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) + probableFormat = kFileFormatDiskCopy42; + } else if (strcasecmp(ext, "ddd") == 0) { + /* do this after compressed formats but before unadorned */ + reliableExt = true; + if (WrapperDDD::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) + probableFormat = kFileFormatDDD; + } else if (strcasecmp(ext, "app") == 0) { + reliableExt = true; + if (WrapperTrackStar::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) + probableFormat = kFileFormatTrackStar; + } else if (strcasecmp(ext, "fdi") == 0) { + reliableExt = true; + if (WrapperFDI::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) + probableFormat = kFileFormatFDI; + } else if (strcasecmp(ext, "img") == 0) { + if (WrapperUnadornedSector::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) + { + probableFormat = kFileFormatUnadorned; + fPhysical = kPhysicalFormatSectors; + fOrder = kSectorOrderPhysical; + } + } else if (strcasecmp(ext, "nib") == 0 || strcasecmp(ext, "raw") == 0) { + if (WrapperUnadornedNibble::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) + { + probableFormat = kFileFormatUnadorned; + fPhysical = kPhysicalFormatNib525_6656; + /* figure out NibbleFormat later */ + } + } else if (strcasecmp(ext, "do") == 0 || strcasecmp(ext, "po") == 0 || + strcasecmp(ext, "d13") == 0 || strcasecmp(ext, "dc6") == 0) + { + if (WrapperUnadornedSector::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) + { + probableFormat = kFileFormatUnadorned; + fPhysical = kPhysicalFormatSectors; + if (strcasecmp(ext, "do") == 0 || strcasecmp(ext, "d13") == 0) + fOrder = kSectorOrderDOS; + else + fOrder = kSectorOrderProDOS; // po, dc6 + WMSG1(" DI guessing order is %d by extension\n", fOrder); + } + } else if (strcasecmp(ext, "cp-win-vol") == 0) { + /* this is a Windows logical volume */ + reliableExt = true; + probableFormat = kFileFormatUnadorned; + fPhysical = kPhysicalFormatSectors; + fOrder = kSectorOrderProDOS; + } else { + /* no match on the filename extension; start guessing */ + } + + if (probableFormat != kFileFormatUnknown) { + /* + * Found a match. Use "probableFormat" to open the file. + */ + WMSG1(" DI scored hit on extension '%s'\n", ext); + } else { + /* + * Didn't work. If the file extension was marked "reliable", then + * either we have the wrong extension on the file, or the contents + * are damaged. + * + * If the extension isn't reliable, or simply absent, then we have + * to probe through the formats we know and just hope for the best. + * + * If the "test" function returns with a checksum failure, we take + * it to mean that the format was positively identified, but the + * data inside is corrupted. This results in an immediate return + * with the checksum failure noted. Only a few wrapper formats + * have checksums embedded. (The "test" functions should only + * be looking at header checksums.) + */ + if (reliableExt) { + WMSG1(" DI file extension '%s' did not match contents\n", ext); + dierr = kDIErrBadFileFormat; + goto bail; + } else { + WMSG1(" DI extension '%s' not useful, probing formats\n", ext); + dierr = WrapperNuFX::Test(fpWrapperGFD, fWrappedLength); + if (dierr == kDIErrNone) { + probableFormat = kFileFormatNuFX; + goto gotit; + } else if (dierr == kDIErrFileArchive) + goto bail; // we know it's NuFX, we know we can't use it + else if (dierr == kDIErrBadChecksum) + goto bail; // right file type, bad data + + dierr = WrapperDiskCopy42::Test(fpWrapperGFD, fWrappedLength); + if (dierr == kDIErrNone) { + probableFormat = kFileFormatDiskCopy42; + goto gotit; + } else if (dierr == kDIErrBadChecksum) + goto bail; // right file type, bad data + + if (Wrapper2MG::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) { + probableFormat = kFileFormat2MG; + } else if (WrapperDDD::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) { + probableFormat = kFileFormatDDD; + } else if (WrapperSim2eHDV::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) + { + probableFormat = kFileFormatSim2eHDV; + } else if (WrapperTrackStar::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) + { + probableFormat = kFileFormatTrackStar; + } else if (WrapperFDI::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) + { + probableFormat = kFileFormatFDI; + } else if (WrapperUnadornedNibble::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) { + probableFormat = kFileFormatUnadorned; + fPhysical = kPhysicalFormatNib525_6656; // placeholder + } else if (WrapperUnadornedSector::Test(fpWrapperGFD, fWrappedLength) == kDIErrNone) { + probableFormat = kFileFormatUnadorned; + fPhysical = kPhysicalFormatSectors; + } +gotit: ; + } + } + + /* + * Either we recognize it or we don't. Finish opening the file by + * setting up "fLength" and "fPhysical" values, extracting data + * into a memory buffer if necessary. fpDataGFD is set up by the + * "prep" function. + * + * If we're lucky, this will also configure "fOrder" for us, which is + * important when we can't recognize the filesystem format (for correct + * operation of disk tools). + */ + switch (probableFormat) { + case kFileFormat2MG: + fpImageWrapper = new Wrapper2MG(); + break; + case kFileFormatDiskCopy42: + fpImageWrapper = new WrapperDiskCopy42(); + break; + case kFileFormatSim2eHDV: + fpImageWrapper = new WrapperSim2eHDV(); + break; + case kFileFormatTrackStar: + fpImageWrapper = new WrapperTrackStar(); + break; + case kFileFormatFDI: + fpImageWrapper = new WrapperFDI(); + fReadOnly = true; // writing to FDI not yet supported + break; + case kFileFormatNuFX: + fpImageWrapper = new WrapperNuFX(); + ((WrapperNuFX*)fpImageWrapper)->SetCompressType( + (NuThreadFormat) fNuFXCompressType); + break; + case kFileFormatDDD: + fpImageWrapper = new WrapperDDD(); + break; + case kFileFormatUnadorned: + if (IsSectorFormat(fPhysical)) + fpImageWrapper = new WrapperUnadornedSector(); + else if (IsNibbleFormat(fPhysical)) + fpImageWrapper = new WrapperUnadornedNibble(); + else { + assert(false); + } + break; + default: + WMSG0(" DI couldn't figure out the file format\n"); + dierr = kDIErrUnrecognizedFileFmt; + break; + } + if (fpImageWrapper != nil) { + assert(fpDataGFD == nil); + dierr = fpImageWrapper->Prep(fpWrapperGFD, fWrappedLength, fReadOnly, + &fLength, &fPhysical, &fOrder, &fDOSVolumeNum, + &fpBadBlockMap, &fpDataGFD); + } else { + /* could be a mem alloc failure that didn't set dierr */ + if (dierr == kDIErrNone) + dierr = kDIErrGeneric; + } + + if (dierr != kDIErrNone) { + WMSG1(" DI wrapper prep failed (err=%d)\n", dierr); + goto bail; + } + + /* check for non-fatal checksum failures, e.g. DiskCopy42 */ + if (fpImageWrapper->IsDamaged()) { + AddNote(kNoteWarning, "File checksum didn't match."); + fReadOnly = true; + } + + fFileFormat = probableFormat; + + assert(fLength >= 0); + assert(fpDataGFD != nil); + assert(fOuterFormat != kOuterFormatUnknown); + assert(fFileFormat != kFileFormatUnknown); + assert(fPhysical != kPhysicalFormatUnknown); + +bail: + free(extBuf); + return dierr; +} + + +/* + * Try to figure out what we're looking at. + * + * Returns an error if we don't think this is even a disk image. If we + * just can't figure it out, we return success but with the format value + * set to "unknown". This gives the caller a chance to use "override" + * to help us find our way. + * + * On entry: + * fpDataGFD, fLength, and fFileFormat are defined + * fSectorPairing is specified + * fOrder has a semi-reliable guess at sector ordering + * On exit: + * fOrder and fFormat are set to the best of our ability + * fNumTracks, fNumSectPerTrack, and fNumBlocks are set + * fHasSectors, fHasTracks, and fHasNibbles are set + * fFileSysOrder is set + * fpNibbleDescr will be set for nibble images + */ +DIError +DiskImg::AnalyzeImage(void) +{ + assert(fLength >= 0); + assert(fpDataGFD != nil); + assert(fFileFormat != kFileFormatUnknown); + assert(fPhysical != kPhysicalFormatUnknown); + assert(fFormat == kFormatUnknown); + assert(fFileSysOrder == kSectorOrderUnknown); + assert(fNumTracks == -1); + assert(fNumSectPerTrack == -1); + assert(fNumBlocks == -1); + if (fpDataGFD == nil) + return kDIErrInternal; + + /* + * Figure out how many tracks and sectors the image has. + * + * For an odd-sized ProDOS image, there will be no tracks and sectors. + */ + if (IsSectorFormat(fPhysical)) { + if (!fLength) { + WMSG0(" DI zero-length disk images not allowed\n"); + return kDIErrOddLength; + } + + if (fLength == kD13Length) { + /* 13-sector .d13 image */ + fHasSectors = true; + fNumSectPerTrack = 13; + fNumTracks = kTrackCount525; + assert(!fHasBlocks); + } else if (fLength % (16 * kSectorSize) == 0) { + /* looks like a collection of 16-sector tracks */ + fHasSectors = true; + + fNumSectPerTrack = 16; + fNumTracks = (int) (fLength / (fNumSectPerTrack * kSectorSize)); + + /* sector pairing effectively cuts #of tracks in half */ + if (fSectorPairing) { + if ((fNumTracks & 0x01) != 0) { + WMSG0(" DI error: bad attempt at sector pairing\n"); + assert(false); + fSectorPairing = false; + } + } + + if (fSectorPairing) + fNumTracks /= 2; + } else { + if (fSectorPairing) { + WMSG1("GLITCH: sector pairing enabled, but fLength=%ld\n", + (long) fLength); + return kDIErrOddLength; + } + + assert(fNumTracks == -1); + assert(fNumSectPerTrack == -1); + assert((fLength % kBlockSize) == 0); + + fHasBlocks = true; + fNumBlocks = (long) (fLength / kBlockSize); + } + } else if (IsNibbleFormat(fPhysical)) { + fHasNibbles = fHasSectors = true; + + /* + * Figure out if it's 13-sector or 16-sector (or garbage). We + * have to make an assessment of the entire disk so we can declare + * it to be 13-sector or 16-sector, which is useful for DiskFS + * which will want to scan for DOS VTOCs and other goodies. We + * also want to provide a default NibbleDescr. + * + * Failing that, we still allow it to be opened for raw track access. + * + * This also sets fNumTracks, which could be more than 35 if we're + * working with a TrackStar or FDI image. + */ + DIError dierr; + dierr = AnalyzeNibbleData(); // sets nibbleDescr and DOS vol num + if (dierr == kDIErrNone) { + assert(fpNibbleDescr != nil); + fNumSectPerTrack = fpNibbleDescr->numSectors; + fOrder = kSectorOrderPhysical; + + if (!fReadOnly && !fpNibbleDescr->dataVerifyChecksum) { + WMSG0("DI nibbleDescr does not verify data checksum, disabling writes\n"); + AddNote(kNoteInfo, + "Sectors use non-standard data checksums; writing disabled."); + fReadOnly = true; + } + } else { + //assert(fpNibbleDescr == nil); + fNumSectPerTrack = -1; + fOrder = kSectorOrderPhysical; + fHasSectors = false; + } + } else { + WMSG1("Unsupported physical %d\n", fPhysical); + assert(false); + return kDIErrGeneric; + } + + /* + * Compute the number of blocks. For a 13-sector disk, block access + * is not possible. + * + * For nibble formats, we have to base the block count on the number + * of sectors rather than the file length. + */ + if (fHasSectors) { + assert(fNumSectPerTrack > 0); + if ((fNumSectPerTrack & 0x01) == 0) { + /* not a 13-sector disk, so define blocks in terms of sectors */ + /* (effects of sector pairing are already taken into account) */ + fHasBlocks = true; + fNumBlocks = (fNumTracks * fNumSectPerTrack) / 2; + } + } else if (fHasBlocks) { + if ((fLength % kBlockSize) == 0) { + /* not sector-oriented, so define blocks based on length */ + fHasBlocks = true; + fNumBlocks = (long) (fLength / kBlockSize); + + if (fSectorPairing) { + if ((fNumBlocks & 0x01) != 0) { + WMSG0(" DI error: bad attempt at sector pairing (blk)\n"); + assert(false); + fSectorPairing = false; + } else + fNumBlocks /= 2; + } + + } else { + assert(false); + return kDIErrGeneric; + } + } else if (fHasNibbles) { + assert(fNumBlocks == -1); + } else { + WMSG0(" DI none of fHasSectors/fHasBlocks/fHasNibbles are set\n"); + assert(false); + return kDIErrInternal; + } + + /* + * We've got the track/sector/block layout sorted out; now figure out + * what kind of filesystem we're dealing with. + */ + AnalyzeImageFS(); + + WMSG4(" DI AnalyzeImage tracks=%ld sectors=%d blocks=%ld fileSysOrder=%d\n", + fNumTracks, fNumSectPerTrack, fNumBlocks, fFileSysOrder); + WMSG3(" hasBlocks=%d hasSectors=%d hasNibbles=%d\n", + fHasBlocks, fHasSectors, fHasNibbles); + + return kDIErrNone; +} + +/* + * Try to figure out what filesystem exists on this disk image. + * + * We want to test for DOS before ProDOS, because sometimes they overlap (e.g. + * 800K ProDOS disk with five 160K DOS volumes on it). + * + * Sets fFormat, fOrder, and fFileSysOrder. + */ +void +DiskImg::AnalyzeImageFS(void) +{ + /* + * In some circumstances it would be useful to have a set describing + * what filesystems we might expect to find, e.g. we're not likely to + * encounter RDOS embedded in a CF card. + */ + if (DiskFSMacPart::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) + { + assert(fFormat == kFormatMacPart); + WMSG1(" DI found MacPart, order=%d\n", fOrder); + } else if (DiskFSMicroDrive::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) + { + assert(fFormat == kFormatMicroDrive); + WMSG1(" DI found MicroDrive, order=%d\n", fOrder); + } else if (DiskFSFocusDrive::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) + { + assert(fFormat == kFormatFocusDrive); + WMSG1(" DI found FocusDrive, order=%d\n", fOrder); + } else if (DiskFSCFFA::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) + { + // The CFFA format doesn't have a partition map, but we do insist + // on finding multiple volumes. It needs to come after MicroDrive, + // because a disk formatted for CFFA then subsequently partitioned + // for MicroDrive will still look like valid CFFA unless you zero + // out the blocks. + assert(fFormat == kFormatCFFA4 || fFormat == kFormatCFFA8); + WMSG1(" DI found CFFA, order=%d\n", fOrder); + } else if (DiskFSFAT::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) + { + // This is really just a trap to catch CFFA cards that were formatted + // for ProDOS and then re-formatted for MSDOS. As such it needs to + // come before the ProDOS test. It only works on larger volumes, + // and can be overridden, so it's pretty safe. + assert(fFormat == kFormatMSDOS); + WMSG1(" DI found MSDOS, order=%d\n", fOrder); + } else if (DiskFSDOS33::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) + { + assert(fFormat == kFormatDOS32 || fFormat == kFormatDOS33); + WMSG1(" DI found DOS3.x, order=%d\n", fOrder); + if (fNumSectPerTrack == 13) + fFormat = kFormatDOS32; + } else if (DiskFSUNIDOS::TestWideFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) + { + // Should only succeed on 400K embedded chunks. + assert(fFormat == kFormatDOS33); + fNumSectPerTrack = 32; + fNumTracks /= 2; + WMSG1(" DI found 'wide' DOS3.3, order=%d\n", fOrder); + } else if (DiskFSUNIDOS::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) + { + assert(fFormat == kFormatUNIDOS); + fNumSectPerTrack = 32; + fNumTracks /= 2; + WMSG1(" DI found UNIDOS, order=%d\n", fOrder); + } else if (DiskFSOzDOS::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) + { + assert(fFormat == kFormatOzDOS); + fNumSectPerTrack = 32; + fNumTracks /= 2; + WMSG1(" DI found OzDOS, order=%d\n", fOrder); + } else if (DiskFSProDOS::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) + { + assert(fFormat == kFormatProDOS); + WMSG1(" DI found ProDOS, order=%d\n", fOrder); + } else if (DiskFSPascal::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) + { + assert(fFormat == kFormatPascal); + WMSG1(" DI found Pascal, order=%d\n", fOrder); + } else if (DiskFSCPM::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) + { + assert(fFormat == kFormatCPM); + WMSG1(" DI found CP/M, order=%d\n", fOrder); + } else if (DiskFSRDOS::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) + { + assert(fFormat == kFormatRDOS33 || + fFormat == kFormatRDOS32 || + fFormat == kFormatRDOS3); + WMSG1(" DI found RDOS 3.3, order=%d\n", fOrder); + } else if (DiskFSHFS::TestFS(this, &fOrder, &fFormat, DiskFS::kLeniencyNot) == kDIErrNone) + { + assert(fFormat == kFormatMacHFS); + WMSG1(" DI found HFS, order=%d\n", fOrder); + } else { + fFormat = kFormatUnknown; + WMSG1(" DI no recognizeable filesystem found (fOrder=%d)\n", + fOrder); + } + + fFileSysOrder = CalcFSSectorOrder(); +} + + +/* + * Override the format determined by the analyzer. + * + * If they insist on the presence of a valid filesystem, check to make sure + * that filesystem actually exists. + * + * Note that this does not allow overriding the file structure, which must + * be clearly identifiable to be at all useful. If the file has no "wrapper" + * structure, the "unadorned" format should be specified, and the contents + * identified by the PhysicalFormat. + */ +DIError +DiskImg::OverrideFormat(PhysicalFormat physical, FSFormat format, + SectorOrder order) +{ + DIError dierr = kDIErrNone; + SectorOrder newOrder; + FSFormat newFormat; + + WMSG3(" DI override: physical=%d format=%d order=%d\n", + physical, format, order); + + if (!IsSectorFormat(physical) && !IsNibbleFormat(physical)) + return kDIErrUnsupportedPhysicalFmt; + + /* don't allow forcing physical format change */ + if (physical != fPhysical) + return kDIErrInvalidArg; + + /* optimization */ + if (physical == fPhysical && format == fFormat && order == fOrder) { + WMSG0(" DI override matches existing, ignoring\n"); + return kDIErrNone; + } + + newOrder = order; + newFormat = format; + + switch (format) { + case kFormatDOS33: + case kFormatDOS32: + dierr = DiskFSDOS33::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); + // Go ahead and allow the override even if the DOS version is wrong. + // So long as the sector count is correct, it's okay. + break; + case kFormatProDOS: + dierr = DiskFSProDOS::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); + break; + case kFormatPascal: + dierr = DiskFSPascal::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); + break; + case kFormatMacHFS: + dierr = DiskFSHFS::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); + break; + case kFormatUNIDOS: + dierr = DiskFSUNIDOS::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); + break; + case kFormatOzDOS: + dierr = DiskFSOzDOS::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); + break; + case kFormatCFFA4: + case kFormatCFFA8: + dierr = DiskFSCFFA::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); + // So long as it's CFFA, we allow the user to force it to be 4-mode + // or 8-mode. Don't require newFormat==format. + break; + case kFormatMacPart: + dierr = DiskFSMacPart::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); + break; + case kFormatMicroDrive: + dierr = DiskFSMicroDrive::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); + break; + case kFormatFocusDrive: + dierr = DiskFSFocusDrive::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); + break; + case kFormatCPM: + dierr = DiskFSCPM::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); + break; + case kFormatMSDOS: + dierr = DiskFSFAT::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); + break; + case kFormatRDOS33: + case kFormatRDOS32: + case kFormatRDOS3: + dierr = DiskFSRDOS::TestFS(this, &newOrder, &newFormat, DiskFS::kLeniencyVery); + if (newFormat != format) + dierr = kDIErrFilesystemNotFound; // found RDOS, but wrong flavor + break; + case kFormatGenericPhysicalOrd: + case kFormatGenericProDOSOrd: + case kFormatGenericDOSOrd: + case kFormatGenericCPMOrd: + /* no discussion possible, since there's no FS to validate */ + newFormat = format; + newOrder = order; + break; + case kFormatUnknown: + /* only valid in rare situations, e.g. CFFA CreatePlaceholder */ + newFormat = format; + newOrder = order; + break; + default: + dierr = kDIErrUnsupportedFSFmt; + break; + } + + if (dierr != kDIErrNone) { + WMSG0(" DI override failed\n"); + goto bail; + } + + /* + * We passed in "order" to TestFS. If it came back with something + * different, it means that it didn't like the new order value even + * when "leniency" was granted. + */ + if (newOrder != order) { + dierr = kDIErrBadOrdering; + goto bail; + } + + fFormat = format; + fOrder = newOrder; + fFileSysOrder = CalcFSSectorOrder(); + + WMSG0(" DI override accepted\n"); + +bail: + return dierr; +} + +/* + * Figure out the sector ordering for this filesystem, so we can decide + * how the sectors need to be re-arranged when we're reading them. + * + * If the value returned by this function matches fOrder, then no swapping + * will be done. + * + * NOTE: this table is redundant with some knowledge embedded in the + * individual "TestFS" functions. + */ +DiskImg::SectorOrder +DiskImg::CalcFSSectorOrder(void) const +{ + /* in the absence of information, just leave it alone */ + if (fFormat == kFormatUnknown || fOrder == kSectorOrderUnknown) { + WMSG0(" DI WARNING: FindSectorOrder but format not known\n"); + return fOrder; + } + + assert(fOrder == kSectorOrderPhysical || fOrder == kSectorOrderCPM || + fOrder == kSectorOrderProDOS || fOrder == kSectorOrderDOS); + + switch (fFormat) { + case kFormatGenericPhysicalOrd: + case kFormatRDOS32: + case kFormatRDOS3: + return kSectorOrderPhysical; + + case kFormatGenericDOSOrd: + case kFormatDOS33: + case kFormatDOS32: + case kFormatUNIDOS: + case kFormatOzDOS: + return kSectorOrderDOS; + + case kFormatGenericCPMOrd: + case kFormatCPM: + return kSectorOrderCPM; + + case kFormatGenericProDOSOrd: + case kFormatProDOS: + case kFormatRDOS33: + case kFormatPascal: + case kFormatMacHFS: + case kFormatMacMFS: + case kFormatLisa: + case kFormatMSDOS: + case kFormatISO9660: + case kFormatCFFA4: + case kFormatCFFA8: + case kFormatMacPart: + case kFormatMicroDrive: + case kFormatFocusDrive: + return kSectorOrderProDOS; + + default: + assert(false); + return fOrder; + } +} + +/* + * Based on the disk format, figure out if we should prefer blocks or + * sectors when examining disk contents. + */ +bool +DiskImg::ShowAsBlocks(void) const +{ + if (!fHasBlocks) + return false; + + /* in the absence of information, assume sectors */ + if (fFormat == kFormatUnknown) { + if (fOrder == kSectorOrderProDOS) + return true; + else + return false; + } + + switch (fFormat) { + case kFormatGenericPhysicalOrd: + case kFormatGenericDOSOrd: + case kFormatDOS33: + case kFormatDOS32: + case kFormatRDOS3: + case kFormatRDOS33: + case kFormatUNIDOS: + case kFormatOzDOS: + return false; + + case kFormatGenericProDOSOrd: + case kFormatGenericCPMOrd: + case kFormatProDOS: + case kFormatPascal: + case kFormatMacHFS: + case kFormatMacMFS: + case kFormatLisa: + case kFormatCPM: + case kFormatMSDOS: + case kFormatISO9660: + case kFormatCFFA4: + case kFormatCFFA8: + case kFormatMacPart: + case kFormatMicroDrive: + case kFormatFocusDrive: + return true; + + default: + assert(false); + return false; + } +} + + +/* + * Format an image with the requested fileystem format. This only works if + * the matching DiskFS supports formatting of disks. + */ +DIError +DiskImg::FormatImage(FSFormat format, const char* volName) +{ + DIError dierr = kDIErrNone; + DiskFS* pDiskFS = nil; + FSFormat savedFormat; + + WMSG1(" DI FormatImage '%s'\n", volName); + + /* + * Open a temporary DiskFS for the requested format. We do this via the + * standard OpenAppropriate call, so we temporarily switch our format + * out. (We will eventually replace it, but we want to make sure that + * local error handling works correctly, so we restore it for now.) + */ + savedFormat = fFormat; + fFormat = format; + pDiskFS = OpenAppropriateDiskFS(false); + fFormat = savedFormat; + + if (pDiskFS == nil) { + dierr = kDIErrUnsupportedFSFmt; + goto bail; + } + + dierr = pDiskFS->Format(this, volName); + if (dierr != kDIErrNone) + goto bail; + + WMSG0("DI format successful\n"); + fFormat = format; + +bail: + delete pDiskFS; + return dierr; +} + +/* + * Clear an image to zeros, usually done as a prelude to a higher-level format. + * + * BUG: this should also handle the track/sector case. + * + * HEY: this is awfully slow on large disks... should have some sort of + * optimized path that just writes to the GFD or something. Maybe even just + * a "ZeroBlock" instead of "WriteBlock" so we can memset instead of memcpy? + */ +DIError +DiskImg::ZeroImage(void) +{ + DIError dierr = kDIErrNone; + unsigned char blkBuf[kBlockSize]; + long block; + + WMSG1(" DI ZeroImage (%ld blocks)\n", GetNumBlocks()); + memset(blkBuf, 0, sizeof(blkBuf)); + + for (block = 0; block < GetNumBlocks(); block++) { + dierr = WriteBlock(block, blkBuf); + if (dierr != kDIErrNone) + break; + } + + return dierr; +} + + +/* + * Set the "scan progress" function. + * + * We want to use the same function for our sub-volumes too. + */ +void +DiskImg::SetScanProgressCallback(ScanProgressCallback func, void* cookie) +{ + if (fpParentImg != nil) { + /* unexpected, but perfectly okay */ + DebugBreak(); + } + + fpScanProgressCallback = func; + fScanProgressCookie = cookie; + fScanCount = 0; + fScanMsg[0] = '\0'; + fScanLastMsgWhen = time(nil); +} + +/* + * Update the progress. Call with a string at the start of a volume, then + * call with a NULL pointer every time we add a file. + */ +bool +DiskImg::UpdateScanProgress(const char* newStr) +{ + ScanProgressCallback func = fpScanProgressCallback; + DiskImg* pImg = this; + bool result = true; + + /* search up the tree to find a progress updater */ + while (func == nil) { + pImg = pImg->fpParentImg; + if (pImg == nil) + return result; // none defined, bail out + func = pImg->fpScanProgressCallback; + } + + time_t now = time(nil); + + if (newStr == NULL) { + fScanCount++; + //if ((fScanCount % 100) == 0) + if (fScanLastMsgWhen != now) { + result = (*func)(fScanProgressCookie, + fScanMsg, fScanCount); + fScanLastMsgWhen = now; + } + } else { + fScanCount = 0; + strncpy(fScanMsg, newStr, sizeof(fScanMsg)); + fScanMsg[sizeof(fScanMsg)-1] = '\0'; + result = (*func)(fScanProgressCookie, fScanMsg, + fScanCount); + fScanLastMsgWhen = now; + } + + return result; +} + + +/* + * ========================================================================== + * Block/track/sector I/O + * ========================================================================== + */ + +/* + * Handle sector order conversions. + */ +DIError +DiskImg::CalcSectorAndOffset(long track, int sector, SectorOrder imageOrder, + SectorOrder fsOrder, di_off_t* pOffset, int* pNewSector) +{ + if (!fHasSectors) + return kDIErrUnsupportedAccess; + + /* + * Sector order conversions. No table is needed for Copy ][+ format, + * which is equivalent to "physical". + */ + static const int raw2dos[16] = { + 0, 7, 14, 6, 13, 5, 12, 4, 11, 3, 10, 2, 9, 1, 8, 15 + }; + static const int dos2raw[16] = { + 0, 13, 11, 9, 7, 5, 3, 1, 14, 12, 10, 8, 6, 4, 2, 15 + }; + static const int raw2prodos[16] = { + 0, 8, 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15 + }; + static const int prodos2raw[16] = { + 0, 2, 4, 6, 8, 10, 12, 14, 1, 3, 5, 7, 9, 11, 13, 15 + }; + static const int raw2cpm[16] = { + 0, 11, 6, 1, 12, 7, 2, 13, 8, 3, 14, 9, 4, 15, 10, 5 + }; + static const int cpm2raw[16] = { + 0, 3, 6, 9, 12, 15, 2, 5, 8, 11, 14, 1, 4, 7, 10, 13 + }; + + if (track < 0 || track >= fNumTracks) { + WMSG1(" DI read invalid track %ld\n", track); + return kDIErrInvalidTrack; + } + if (sector < 0 || sector >= fNumSectPerTrack) { + WMSG1(" DI read invalid sector %d\n", sector); + return kDIErrInvalidSector; + } + + di_off_t offset; + int newSector = -1; + + /* + * 16-sector disks write sectors in ascending order and then remap + * them with a translation table. + */ + if (fNumSectPerTrack == 16 || fNumSectPerTrack == 32) { + if (fSectorPairing) { + assert(fSectorPairOffset == 0 || fSectorPairOffset == 1); + // this pushes "track" beyond fNumTracks + track *= 2; + if (sector >= 16) { + track++; + sector -= 16; + } + offset = track * fNumSectPerTrack * kSectorSize; + + sector = sector * 2 + fSectorPairOffset; + if (sector >= 16) { + offset += 16*kSectorSize; + sector -= 16; + } + } else { + offset = track * fNumSectPerTrack * kSectorSize; + if (sector >= 16) { + offset += 16*kSectorSize; + sector -= 16; + } + } + assert(sector >= 0 && sector < 16); + + /* convert request to "raw" sector number */ + switch (fsOrder) { + case kSectorOrderProDOS: + newSector = prodos2raw[sector]; + break; + case kSectorOrderDOS: + newSector = dos2raw[sector]; + break; + case kSectorOrderCPM: + newSector = cpm2raw[sector]; + break; + case kSectorOrderPhysical: // used for Copy ][+ + newSector = sector; + break; + case kSectorOrderUnknown: + // should never happen; fall through to "default" + default: + assert(false); + newSector = sector; + break; + } + + /* convert "raw" request to the image's ordering */ + switch (imageOrder) { + case kSectorOrderProDOS: + newSector = raw2prodos[newSector]; + break; + case kSectorOrderDOS: + newSector = raw2dos[newSector]; + break; + case kSectorOrderCPM: + newSector = raw2cpm[newSector]; + break; + case kSectorOrderPhysical: + //newSector = newSector; + break; + case kSectorOrderUnknown: + // should never happen; fall through to "default" + default: + assert(false); + //newSector = newSector; + break; + } + + if (imageOrder == fsOrder) { + assert(sector == newSector); + } + + offset += newSector * kSectorSize; + } else if (fNumSectPerTrack == 13) { + /* sector skew has no meaning, so assume no translation */ + offset = track * fNumSectPerTrack * kSectorSize; + newSector = sector; + offset += newSector * kSectorSize; + if (imageOrder != fsOrder) { + /* translation expected */ + WMSG2("NOTE: CalcSectorAndOffset for nspt=13 with img=%d fs=%d\n", + imageOrder, fsOrder); + } + } else { + assert(false); // should not be here + + /* try to do something reasonable */ + assert(imageOrder == fsOrder); + offset = (di_off_t)track * fNumSectPerTrack * kSectorSize; + offset += sector * kSectorSize; + } + + *pOffset = offset; + *pNewSector = newSector; + return kDIErrNone; +} + +/* + * Determine whether an image uses a linear mapping. This allows us to + * optimize block reads & writes, very useful when dealing with logical + * volumes under Windows (which also use 512-byte blocks). + * + * The "imageOrder" argument usually comes from fOrder, and "fsOrder" + * comes from "fFileSysOrder". + */ +inline bool +DiskImg::IsLinearBlocks(SectorOrder imageOrder, SectorOrder fsOrder) +{ + /* + * Any time fOrder==fFileSysOrder, we know that we have a linear + * mapping. This holds true for reading ProDOS blocks from a ".po" + * file or reading DOS sectors from a ".do" file. + */ + return (IsSectorFormat(fPhysical) && fHasBlocks && + imageOrder == fsOrder); +} + +/* + * Read the specified track and sector, adjusting for sector ordering as + * appropriate. + * + * Copies 256 bytes into "*buf". + * + * Returns 0 on success, nonzero on failure. + */ +DIError +DiskImg::ReadTrackSectorSwapped(long track, int sector, void* buf, + SectorOrder imageOrder, SectorOrder fsOrder) +{ + DIError dierr; + di_off_t offset; + int newSector = -1; + + if (buf == nil) + return kDIErrInvalidArg; + +#if 0 // Pre-d13 + if (fNumSectPerTrack == 13) { + /* no sector skewing possible for 13-sector disks */ + assert(fHasNibbles); + + return ReadNibbleSector(track, sector, buf, fpNibbleDescr); + } +#endif + + dierr = CalcSectorAndOffset(track, sector, imageOrder, fsOrder, + &offset, &newSector); + if (dierr != kDIErrNone) + return dierr; + + if (IsSectorFormat(fPhysical)) { + assert(offset+kSectorSize <= fLength); + + //WMSG2(" DI t=%d s=%d\n", track, + // (offset - track * fNumSectPerTrack * kSectorSize) / kSectorSize); + + dierr = CopyBytesOut(buf, offset, kSectorSize); + } else if (IsNibbleFormat(fPhysical)) { + if (imageOrder != kSectorOrderPhysical) { + WMSG2(" NOTE: nibble imageOrder is %d (expected %d)\n", + imageOrder, kSectorOrderPhysical); + } + dierr = ReadNibbleSector(track, newSector, buf, fpNibbleDescr); + } else { + assert(false); + dierr = kDIErrInternal; + } + + return dierr; +} + +/* + * Write the specified track and sector, adjusting for sector ordering as + * appropriate. + * + * Copies 256 bytes out of "buf". + * + * Returns 0 on success, nonzero on failure. + */ +DIError +DiskImg::WriteTrackSector(long track, int sector, const void* buf) +{ + DIError dierr; + di_off_t offset; + int newSector = -1; + + if (buf == nil) + return kDIErrInvalidArg; + if (fReadOnly) + return kDIErrAccessDenied; + +#if 0 // Pre-d13 + if (fNumSectPerTrack == 13) { + /* no sector skewing possible for 13-sector disks */ + assert(fHasNibbles); + + return WriteNibbleSector(track, sector, buf, fpNibbleDescr); + } +#endif + + dierr = CalcSectorAndOffset(track, sector, fOrder, fFileSysOrder, + &offset, &newSector); + if (dierr != kDIErrNone) + return dierr; + + if (IsSectorFormat(fPhysical)) { + assert(offset+kSectorSize <= fLength); + + //WMSG2(" DI t=%d s=%d\n", track, + // (offset - track * fNumSectPerTrack * kSectorSize) / kSectorSize); + + dierr = CopyBytesIn(buf, offset, kSectorSize); + } else if (IsNibbleFormat(fPhysical)) { + if (fOrder != kSectorOrderPhysical) { + WMSG2(" NOTE: nibble fOrder is %d (expected %d)\n", + fOrder, kSectorOrderPhysical); + } + dierr = WriteNibbleSector(track, newSector, buf, fpNibbleDescr); + } else { + assert(false); + dierr = kDIErrInternal; + } + + return dierr; +} + +/* + * Read a 512-byte block. + * + * Copies 512 bytes into "*buf". + */ +DIError +DiskImg::ReadBlockSwapped(long block, void* buf, SectorOrder imageOrder, + SectorOrder fsOrder) +{ + if (!fHasBlocks) + return kDIErrUnsupportedAccess; + if (block < 0 || block >= fNumBlocks) + return kDIErrInvalidBlock; + if (buf == nil) + return kDIErrInvalidArg; + + DIError dierr; + long track, blkInTrk; + + /* if we have a bad block map, check it */ + if (CheckForBadBlocks(block, 1)) { + dierr = kDIErrReadFailed; + goto bail; + } + + if (fHasSectors && !IsLinearBlocks(imageOrder, fsOrder)) { + /* run it through the t/s call so we handle DOS ordering */ + track = block / (fNumSectPerTrack/2); + blkInTrk = block - (track * (fNumSectPerTrack/2)); + dierr = ReadTrackSectorSwapped(track, blkInTrk*2, buf, + imageOrder, fsOrder); + if (dierr != kDIErrNone) + return dierr; + dierr = ReadTrackSectorSwapped(track, blkInTrk*2+1, + (char*)buf+kSectorSize, imageOrder, fsOrder); + } else if (fHasBlocks) { + /* no sectors, so no swapping; must be linear blocks */ + if (imageOrder != fsOrder) { + WMSG2(" DI NOTE: ReadBlockSwapped on non-sector (%d/%d)\n", + imageOrder, fsOrder); + } + dierr = CopyBytesOut(buf, (di_off_t) block * kBlockSize, kBlockSize); + } else { + assert(false); + dierr = kDIErrInternal; + } + +bail: + return dierr; +} + +/* + * Read multiple blocks. + * + * IMPORTANT: this returns immediately when a read fails. The buffer will + * probably not contain data from all readable sectors. The application is + * expected to retry the blocks individually. + */ +DIError +DiskImg::ReadBlocks(long startBlock, int numBlocks, void* buf) +{ + DIError dierr = kDIErrNone; + + assert(fHasBlocks); + assert(startBlock >= 0); + assert(numBlocks > 0); + assert(buf != nil); + + if (startBlock < 0 || numBlocks + startBlock > GetNumBlocks()) { + assert(false); + return kDIErrInvalidArg; + } + + /* if we have a bad block map, check it */ + if (CheckForBadBlocks(startBlock, numBlocks)) { + dierr = kDIErrReadFailed; + goto bail; + } + + if (!IsLinearBlocks(fOrder, fFileSysOrder)) { + /* + * This isn't a collection of linear blocks, so we need to read it one + * block at a time with sector swapping. This almost certainly means + * that we're not reading from physical media, so performance shouldn't + * be an issue. + */ + if (startBlock == 0) { + WMSG0(" ReadBlocks: nonlinear, not trying\n"); + } + while (numBlocks--) { + dierr = ReadBlock(startBlock, buf); + if (dierr != kDIErrNone) + goto bail; + startBlock++; + buf = (unsigned char*)buf + kBlockSize; + } + } else { + if (startBlock == 0) { + WMSG0(" ReadBlocks: doing big linear reads\n"); + } + dierr = CopyBytesOut(buf, + (di_off_t) startBlock * kBlockSize, numBlocks * kBlockSize); + } + +bail: + return dierr; +} + +/* + * Check to see if any blocks in a range of blocks show up in the bad + * block map. This is primarily useful for 3.5" disk images converted + * from nibble images, because we convert them directly to "cooked" + * 512-byte blocks. + * + * Returns "true" if we found bad blocks, "false" if not. + */ +bool +DiskImg::CheckForBadBlocks(long startBlock, int numBlocks) +{ + int i; + + if (fpBadBlockMap == nil) + return false; + + for (i = startBlock; i < startBlock+numBlocks; i++) { + if (fpBadBlockMap->IsSet(i)) + return true; + } + return false; +} + +/* + * Write a block of data to a DiskImg. + * + * Returns immediately when a block write fails. Does not try to write all + * blocks before returning failure. + */ +DIError +DiskImg::WriteBlock(long block, const void* buf) +{ + if (!fHasBlocks) + return kDIErrUnsupportedAccess; + if (block < 0 || block >= fNumBlocks) + return kDIErrInvalidBlock; + if (buf == nil) + return kDIErrInvalidArg; + if (fReadOnly) + return kDIErrAccessDenied; + + DIError dierr; + long track, blkInTrk; + + if (fHasSectors && !IsLinearBlocks(fOrder, fFileSysOrder)) { + /* run it through the t/s call so we handle DOS ordering */ + track = block / (fNumSectPerTrack/2); + blkInTrk = block - (track * (fNumSectPerTrack/2)); + dierr = WriteTrackSector(track, blkInTrk*2, buf); + if (dierr != kDIErrNone) + return dierr; + dierr = WriteTrackSector(track, blkInTrk*2+1, (char*)buf+kSectorSize); + } else if (fHasBlocks) { + /* no sectors, so no swapping; must be linear blocks */ + if (fOrder != fFileSysOrder) { + WMSG2(" DI NOTE: WriteBlock on non-sector (%d/%d)\n", + fOrder, fFileSysOrder); + } + dierr = CopyBytesIn(buf, (di_off_t)block * kBlockSize, kBlockSize); + } else { + assert(false); + dierr = kDIErrInternal; + } + return dierr; +} + +/* + * Write multiple blocks. + */ +DIError +DiskImg::WriteBlocks(long startBlock, int numBlocks, const void* buf) +{ + DIError dierr = kDIErrNone; + + assert(fHasBlocks); + assert(startBlock >= 0); + assert(numBlocks > 0); + assert(buf != nil); + + if (startBlock < 0 || numBlocks + startBlock > GetNumBlocks()) { + assert(false); + return kDIErrInvalidArg; + } + + if (!IsLinearBlocks(fOrder, fFileSysOrder)) { + /* + * This isn't a collection of linear blocks, so we need to write it + * one block at a time with sector swapping. This almost certainly + * means that we're not reading from physical media, so performance + * shouldn't be an issue. + */ + if (startBlock == 0) { + WMSG0(" WriteBlocks: nonlinear, not trying\n"); + } + while (numBlocks--) { + dierr = WriteBlock(startBlock, buf); + if (dierr != kDIErrNone) + goto bail; + startBlock++; + buf = (unsigned char*)buf + kBlockSize; + } + } else { + if (startBlock == 0) { + WMSG0(" WriteBlocks: doing big linear writes\n"); + } + dierr = CopyBytesIn(buf, + (di_off_t) startBlock * kBlockSize, numBlocks * kBlockSize); + } + +bail: + return dierr; +} + + +/* + * Copy a chunk of bytes out of the disk image. + * + * (This is the lowest-level read routine in this class.) + */ +DIError +DiskImg::CopyBytesOut(void* buf, di_off_t offset, int size) const +{ + DIError dierr; + + dierr = fpDataGFD->Seek(offset, kSeekSet); + if (dierr != kDIErrNone) { + WMSG2(" DI seek off=%ld failed (err=%d)\n", (long) offset, dierr); + return dierr; + } + + dierr = fpDataGFD->Read(buf, size); + if (dierr != kDIErrNone) { + WMSG3(" DI read off=%ld size=%d failed (err=%d)\n", + (long) offset, size, dierr); + return dierr; + } + + return kDIErrNone; +} + +/* + * Copy a chunk of bytes into the disk image. + * + * Sets the "dirty" flag. + * + * (This is the lowest-level write routine in DiskImg.) + */ +DIError +DiskImg::CopyBytesIn(const void* buf, di_off_t offset, int size) +{ + DIError dierr; + + if (fReadOnly) { + DebugBreak(); + return kDIErrAccessDenied; + } + assert(fpDataGFD != nil); // somebody closed the image? + + dierr = fpDataGFD->Seek(offset, kSeekSet); + if (dierr != kDIErrNone) { + WMSG2(" DI seek off=%ld failed (err=%d)\n", (long) offset, dierr); + return dierr; + } + + dierr = fpDataGFD->Write(buf, size); + if (dierr != kDIErrNone) { + WMSG3(" DI write off=%ld size=%d failed (err=%d)\n", + (long) offset, size, dierr); + return dierr; + } + + /* set the dirty flag here and everywhere above */ + DiskImg* pImg = this; + while (pImg != nil) { + pImg->fDirty = true; + pImg = pImg->fpParentImg; + } + + return kDIErrNone; +} + + +/* + * =========================================================================== + * Image creation + * =========================================================================== + */ + +/* + * Create a disk image with the specified parameters. + * + * "storageName" and "pNibbleDescr" may be nil. + */ +DIError +DiskImg::CreateImage(const char* pathName, const char* storageName, + OuterFormat outerFormat, FileFormat fileFormat, PhysicalFormat physical, + const NibbleDescr* pNibbleDescr, SectorOrder order, + FSFormat format, long numBlocks, bool skipFormat) +{ + assert(fpDataGFD == nil); // should not be open already! + + if (numBlocks <= 0) { + WMSG1("ERROR: bad numBlocks %ld\n", numBlocks); + assert(false); + return kDIErrInvalidCreateReq; + } + + fOuterFormat = outerFormat; + fFileFormat = fileFormat; + fPhysical = physical; + SetCustomNibbleDescr(pNibbleDescr); + fOrder = order; + fFormat = format; + + fNumBlocks = numBlocks; + fHasBlocks = true; + + return CreateImageCommon(pathName, storageName, skipFormat); +} +DIError +DiskImg::CreateImage(const char* pathName, const char* storageName, + OuterFormat outerFormat, FileFormat fileFormat, PhysicalFormat physical, + const NibbleDescr* pNibbleDescr, SectorOrder order, + FSFormat format, long numTracks, long numSectPerTrack, bool skipFormat) +{ + assert(fpDataGFD == nil); // should not be open already! + + if (numTracks <= 0 || numSectPerTrack == 0) { + WMSG2("ERROR: bad tracks/sectors %ld/%ld\n", numTracks, numSectPerTrack); + assert(false); + return kDIErrInvalidCreateReq; + } + + fOuterFormat = outerFormat; + fFileFormat = fileFormat; + fPhysical = physical; + SetCustomNibbleDescr(pNibbleDescr); + fOrder = order; + fFormat = format; + + fNumTracks = numTracks; + fNumSectPerTrack = numSectPerTrack; + fHasSectors = true; + if (numSectPerTrack < 0) { + /* nibble image with non-standard formatting */ + if (!IsNibbleFormat(fPhysical)) { + WMSG0("Whoa: expected nibble format here\n"); + assert(false); + return kDIErrInvalidCreateReq; + } + WMSG0("Sector image w/o sectors, switching to nibble mode\n"); + fHasNibbles = true; + fHasSectors = false; + fpNibbleDescr = nil; + } + + return CreateImageCommon(pathName, storageName, skipFormat); +} + +/* + * Do the actual disk image creation. + */ +DIError +DiskImg::CreateImageCommon(const char* pathName, const char* storageName, + bool skipFormat) +{ + DIError dierr; + + /* + * Step 1: figure out fHasBlocks/fHasSectors/fHasNibbles and any + * other misc fields. + * + * If the disk is a nibble image expected to have a particular + * volume number, it should have already been set by the application. + */ + if (fHasBlocks) { + if ((fNumBlocks % 8) == 0) { + fHasSectors = true; + fNumSectPerTrack = 16; + fNumTracks = fNumBlocks / 8; + } else { + WMSG0("NOTE: sector access to new image not possible\n"); + } + } else if (fHasSectors) { + if ((fNumSectPerTrack & 0x01) == 0) { + fHasBlocks = true; + fNumBlocks = (fNumTracks * fNumSectPerTrack) / 2; + } else { + WMSG0("NOTE: block access to new image not possible\n"); + } + } + if (fHasSectors && fPhysical != kPhysicalFormatSectors) + fHasNibbles = true; + assert(fHasBlocks || fHasSectors || fHasNibbles); + + fFileSysOrder = CalcFSSectorOrder(); + fReadOnly = false; + fDirty = true; + + /* + * Step 2: check for invalid arguments and bad combinations. + */ + dierr = ValidateCreateFormat(); + if (dierr != kDIErrNone) { + WMSG0("ERROR: CIC arg validation failed, bailing\n"); + goto bail; + } + + /* + * Step 3: create the destination file. Put this into fpWrapperGFD + * or fpOuterGFD. + * + * The file must not already exist. + * + * THOUGHT: should allow creation of an in-memory disk image. This won't + * work for NuFX, but will work for pretty much everything else. + */ + WMSG1(" CIC: creating '%s'\n", pathName); + int fd; + fd = open(pathName, O_CREAT | O_EXCL, 0644); + if (fd < 0) { + dierr = (DIError) errno; + WMSG2("ERROR: unable to create file '%s' (errno=%d)\n", + pathName, dierr); + goto bail; + } + close(fd); + + GFDFile* pGFDFile; + pGFDFile = new GFDFile; + + dierr = pGFDFile->Open(pathName, false); + if (dierr != kDIErrNone) { + delete pGFDFile; + goto bail; + } + + if (fOuterFormat == kOuterFormatNone) + fpWrapperGFD = pGFDFile; + else + fpOuterGFD = pGFDFile; + pGFDFile = nil; + + /* + * Step 4: if we have an outer GFD and therefore don't currently have + * an fpWrapperGFD, create an expandable memory buffer to use. + * + * We want to take a guess at how big the image will be, so compute + * fLength now. + * + * Create an OuterWrapper as needed. + */ + if (IsSectorFormat(fPhysical)) { + if (fHasBlocks) + fLength = (di_off_t) GetNumBlocks() * kBlockSize; + else + fLength = (di_off_t) GetNumTracks() * GetNumSectPerTrack() * kSectorSize; + } else { + assert(IsNibbleFormat(fPhysical)); + fLength = GetNumTracks() * GetNibbleTrackAllocLength(); + } + assert(fLength > 0); + + if (fpWrapperGFD == nil) { + /* shift GFDs and create a new memory GFD, pre-sized */ + GFDBuffer* pGFDBuffer = new GFDBuffer; + + /* use fLength as a starting point for buffer size; this may expand */ + dierr = pGFDBuffer->Open(nil, fLength, true, true, false); + if (dierr != kDIErrNone) { + delete pGFDBuffer; + goto bail; + } + + fpWrapperGFD = pGFDBuffer; + pGFDBuffer = nil; + } + + /* create an fpOuterWrapper struct */ + switch (fOuterFormat) { + case kOuterFormatNone: + break; + case kOuterFormatGzip: + fpOuterWrapper = new OuterGzip; + if (fpOuterWrapper == nil) { + dierr = kDIErrMalloc; + goto bail; + } + break; + case kOuterFormatZip: + fpOuterWrapper = new OuterZip; + if (fpOuterWrapper == nil) { + dierr = kDIErrMalloc; + goto bail; + } + break; + default: + assert(false); + dierr = kDIErrInternal; + goto bail; + } + + /* + * Step 5: tell the ImageWrapper to write itself into the GFD, passing + * in the blank memory buffer. + * + * - Unadorned formats copy from memory buffer to fpWrapperGFD on disk. + * (With gz, fpWrapperGFD is actually a memory buffer.) fpDataGFD + * becomes an offset into the file. + * - 2MG writes header into GFD and follows it with all data; DC42 + * and Sim2e do similar things. + * - NuFX reopens pathName as SHK file (fpWrapperGFD must point to a + * file) and accesses the archive through an fpArchive. fpDataGFD + * is created as a memory buffer and the blank image is copied in. + * - DDD leaves fpWrapperGFD alone and copies the blank image into a + * new buffer for fpDataGFD. + * + * Sets fWrappedLength when possible, determined from fPhysical and + * either fNumBlocks or fNumTracks. Creates fpDataGFD, often as a + * GFDGFD offset into fpWrapperGFD. + */ + switch (fFileFormat) { + case kFileFormat2MG: + fpImageWrapper = new Wrapper2MG(); + break; + case kFileFormatDiskCopy42: + fpImageWrapper = new WrapperDiskCopy42(); + fpImageWrapper->SetStorageName(storageName); + break; + case kFileFormatSim2eHDV: + fpImageWrapper = new WrapperSim2eHDV(); + break; + case kFileFormatTrackStar: + fpImageWrapper = new WrapperTrackStar(); + fpImageWrapper->SetStorageName(storageName); + break; + case kFileFormatFDI: + fpImageWrapper = new WrapperFDI(); + break; + case kFileFormatNuFX: + fpImageWrapper = new WrapperNuFX(); + fpImageWrapper->SetStorageName(storageName); + ((WrapperNuFX*)fpImageWrapper)->SetCompressType( + (NuThreadFormat) fNuFXCompressType); + break; + case kFileFormatDDD: + fpImageWrapper = new WrapperDDD(); + break; + case kFileFormatUnadorned: + if (IsSectorFormat(fPhysical)) + fpImageWrapper = new WrapperUnadornedSector(); + else if (IsNibbleFormat(fPhysical)) + fpImageWrapper = new WrapperUnadornedNibble(); + else { + assert(false); + } + break; + default: + assert(fpImageWrapper == nil); + break; + } + + if (fpImageWrapper == nil) { + WMSG0(" DI couldn't figure out the file format\n"); + dierr = kDIErrUnrecognizedFileFmt; + goto bail; + } + + /* create the wrapper, write the header, and create fpDataGFD */ + assert(fpDataGFD == nil); + dierr = fpImageWrapper->Create(fLength, fPhysical, fOrder, + fDOSVolumeNum, fpWrapperGFD, &fWrappedLength, &fpDataGFD); + if (dierr != kDIErrNone) { + WMSG1("ImageWrapper Create failed, err=%d\n", dierr); + goto bail; + } + assert(fpDataGFD != nil); + + /* + * Step 6: "format" fpDataGFD. + * + * Note we don't specify an ordering to the "create blank" functions. + * Either it's sectors, in which case it's all zeroes, or it's nibbles, + * in which case it's always in physical order. + * + * If we're formatting for nibbles, and the application hasn't specified + * a disk volume number, use the default (254). + */ + if (fPhysical == kPhysicalFormatSectors) + dierr = FormatSectors(fpDataGFD, skipFormat); // zero out the image + else { + assert(!skipFormat); // don't skip low-level nibble formatting! + if (fDOSVolumeNum == kVolumeNumNotSet) { + fDOSVolumeNum = kDefaultNibbleVolumeNum; + WMSG0(" Using default nibble volume num\n"); + } + + dierr = FormatNibbles(fpDataGFD); // write basic nibble stuff + } + + + /* + * We're done! + * + * Quick sanity check... + */ + if (fOuterFormat != kOuterFormatNone) { + assert(fpOuterGFD != nil); + assert(fpWrapperGFD != nil); + assert(fpDataGFD != nil); + } + +bail: + return dierr; +} + +/* + * Check that the requested format is one we can create. + * + * We don't allow .SDK.GZ or 6384-byte nibble 2MG. 2MG sector images + * must be in DOS or ProDOS order. + * + * Only "generic" FS formats may be used. The application may choose + * to call AnalyzeImage later on to set the actual FS once data has + * been written. + */ +DIError +DiskImg::ValidateCreateFormat(void) const +{ + /* + * Check for invalid arguments. + */ + if (fHasBlocks && fNumBlocks >= 4194304) { // 2GB or larger? + if (fFileFormat != kFileFormatUnadorned) { + WMSG0("CreateImage: images >= 2GB can only be unadorned\n"); + return kDIErrInvalidCreateReq; + } + } + if (fOuterFormat == kOuterFormatUnknown || + fFileFormat == kFileFormatUnknown || + fPhysical == kPhysicalFormatUnknown || + fOrder == kSectorOrderUnknown || + fFormat == kFormatUnknown) + { + WMSG0("CreateImage: ambiguous format\n"); + return kDIErrInvalidCreateReq; + } + if (fOuterFormat != kOuterFormatNone && + fOuterFormat != kOuterFormatGzip && + fOuterFormat != kOuterFormatZip) + { + WMSG1("CreateImage: unsupported outer format %d\n", fOuterFormat); + return kDIErrInvalidCreateReq; + } + if (fFileFormat != kFileFormatUnadorned && + fFileFormat != kFileFormat2MG && + fFileFormat != kFileFormatDiskCopy42 && + fFileFormat != kFileFormatSim2eHDV && + fFileFormat != kFileFormatTrackStar && + fFileFormat != kFileFormatFDI && + fFileFormat != kFileFormatNuFX && + fFileFormat != kFileFormatDDD) + { + WMSG1("CreateImage: unsupported file format %d\n", fFileFormat); + return kDIErrInvalidCreateReq; + } + if (fFormat != kFormatGenericPhysicalOrd && + fFormat != kFormatGenericProDOSOrd && + fFormat != kFormatGenericDOSOrd && + fFormat != kFormatGenericCPMOrd) + { + WMSG0("CreateImage: may only use 'generic' formats\n"); + return kDIErrInvalidCreateReq; + } + + /* + * Check for invalid combinations. + */ + if (fPhysical != kPhysicalFormatSectors) { + if (fOrder != kSectorOrderPhysical) { + WMSG0("CreateImage: nibble images are always 'physical' order\n"); + return kDIErrInvalidCreateReq; + } + + if (GetHasSectors() == false && GetHasNibbles() == false) { + WMSG2("CreateImage: must set hasSectors(%d) or hasNibbles(%d)\n", + GetHasSectors(), GetHasNibbles()); + return kDIErrInvalidCreateReq; + } + + if (fpNibbleDescr == nil && GetNumSectPerTrack() > 0) { + WMSG0("CreateImage: must provide NibbleDescr for non-sector\n"); + return kDIErrInvalidCreateReq; + } + + if (fpNibbleDescr != nil && + fpNibbleDescr->numSectors != GetNumSectPerTrack()) + { + WMSG2("CreateImage: ?? nd->numSectors=%d, GetNumSectPerTrack=%d\n", + fpNibbleDescr->numSectors, GetNumSectPerTrack()); + return kDIErrInvalidCreateReq; + } + + if (fpNibbleDescr != nil && ( + (fpNibbleDescr->numSectors == 13 && + fpNibbleDescr->encoding != kNibbleEnc53) || + (fpNibbleDescr->numSectors == 16 && + fpNibbleDescr->encoding != kNibbleEnc62)) + ) + { + WMSG0("CreateImage: sector count/encoding mismatch\n"); + return kDIErrInvalidCreateReq; + } + + if (GetNumTracks() != kTrackCount525 && + !(GetNumTracks() == 40 && fFileFormat == kFileFormatTrackStar)) + { + WMSG1("CreateImage: unexpected track count %ld\n", GetNumTracks()); + return kDIErrInvalidCreateReq; + } + } + if (fFileFormat == kFileFormat2MG) { + if (fPhysical != kPhysicalFormatSectors && + fPhysical != kPhysicalFormatNib525_6656) + { + WMSG1("CreateImage: 2MG can't handle physical %d\n", fPhysical); + return kDIErrInvalidCreateReq; + } + + if (fPhysical == kPhysicalFormatSectors && + (fOrder != kSectorOrderProDOS && + fOrder != kSectorOrderDOS)) + { + WMSG0("CreateImage: 2MG requires DOS or ProDOS ordering\n"); + return kDIErrInvalidCreateReq; + } + } + if (fFileFormat == kFileFormatNuFX) { + if (fOuterFormat != kOuterFormatNone) { + WMSG0("CreateImage: can't mix NuFX and outer wrapper\n"); + return kDIErrInvalidCreateReq; + } + if (fPhysical != kPhysicalFormatSectors) { + WMSG0("CreateImage: NuFX physical must be sectors\n"); + return kDIErrInvalidCreateReq; + } + if (fOrder != kSectorOrderProDOS) { + WMSG0("CreateImage: NuFX is always ProDOS-order\n"); + return kDIErrInvalidCreateReq; + } + } + if (fFileFormat == kFileFormatDiskCopy42) { + if (fPhysical != kPhysicalFormatSectors) { + WMSG0("CreateImage: DC42 physical must be sectors\n"); + return kDIErrInvalidCreateReq; + } + if ((GetHasBlocks() && GetNumBlocks() != 1600) || + GetHasSectors() && + (GetNumTracks() != 200 || GetNumSectPerTrack() != 16)) + { + WMSG0("CreateImage: DC42 only for 800K disks\n"); + return kDIErrInvalidCreateReq; + } + if (fOrder != kSectorOrderProDOS && + fOrder != kSectorOrderDOS) // used for UNIDOS disks?? + { + WMSG0("CreateImage: DC42 is always ProDOS or DOS\n"); + return kDIErrInvalidCreateReq; + } + } + if (fFileFormat == kFileFormatSim2eHDV) { + if (fPhysical != kPhysicalFormatSectors) { + WMSG0("CreateImage: Sim2eHDV physical must be sectors\n"); + return kDIErrInvalidCreateReq; + } + if (fOrder != kSectorOrderProDOS) { + WMSG0("CreateImage: Sim2eHDV is always ProDOS-order\n"); + return kDIErrInvalidCreateReq; + } + } + if (fFileFormat == kFileFormatTrackStar) { + if (fPhysical != kPhysicalFormatNib525_Var) { + WMSG0("CreateImage: TrackStar physical must be var-nibbles\n"); + return kDIErrInvalidCreateReq; + } + } + if (fFileFormat == kFileFormatFDI) { + if (fPhysical != kPhysicalFormatNib525_Var) { + WMSG0("CreateImage: FDI physical must be var-nibbles\n"); + return kDIErrInvalidCreateReq; + } + } + if (fFileFormat == kFileFormatDDD) { + if (fPhysical != kPhysicalFormatSectors) { + WMSG0("CreateImage: DDD physical must be sectors\n"); + return kDIErrInvalidCreateReq; + } + if (fOrder != kSectorOrderDOS) { + WMSG0("CreateImage: DDD is always DOS-order\n"); + return kDIErrInvalidCreateReq; + } + if (!GetHasSectors() || GetNumTracks() != 35 || + GetNumSectPerTrack() != 16) + { + WMSG0("CreateImage: DDD is only for 16-sector 35-track disks\n"); + return kDIErrInvalidCreateReq; + } + } + + return kDIErrNone; +} + +/* + * Create a blank image for physical=="sectors". + * + * fLength must be a multiple of 256. + * + * If "quickFormat" is set, only the very last sector is written (to set + * the EOF on the file). + */ +DIError +DiskImg::FormatSectors(GenericFD* pGFD, bool quickFormat) const +{ + DIError dierr = kDIErrNone; + char sctBuf[kSectorSize]; + di_off_t length; + + assert(fLength > 0 && (fLength & 0xff) == 0); + + //if (!(fLength & 0x01)) + // return FormatBlocks(pGFD); + + memset(sctBuf, 0, sizeof(sctBuf)); + pGFD->Rewind(); + + if (quickFormat) { + dierr = pGFD->Seek(fLength - sizeof(sctBuf), kSeekSet); + if (dierr != kDIErrNone) { + WMSG2(" FormatSectors: GFD seek %ld failed (err=%d)\n", + (long) fLength - sizeof(sctBuf), dierr); + goto bail; + } + dierr = pGFD->Write(sctBuf, sizeof(sctBuf), nil); + if (dierr != kDIErrNone) { + WMSG1(" FormatSectors: GFD quick write failed (err=%d)\n", dierr); + goto bail; + } + } else { + for (length = fLength ; length > 0; length -= sizeof(sctBuf)) { + dierr = pGFD->Write(sctBuf, sizeof(sctBuf), nil); + if (dierr != kDIErrNone) { + WMSG1(" FormatSectors: GFD write failed (err=%d)\n", dierr); + goto bail; + } + } + assert(length == 0); + } + + +bail: + return dierr; +} + +#if 0 // didn't help +/* + * Create a blank image for physical=="sectors". This is called from + * FormatSectors when it looks like we're formatting entire blocks. + */ +DIError +DiskImg::FormatBlocks(GenericFD* pGFD) const +{ + DIError dierr; + char blkBuf[kBlockSize]; + long length; + time_t start, end; + + assert(fLength > 0 && (fLength & 0x1ff) == 0); + + start = time(nil); + + memset(blkBuf, 0, sizeof(blkBuf)); + pGFD->Rewind(); + + for (length = fLength ; length > 0; length -= sizeof(blkBuf)) { + dierr = pGFD->Write(blkBuf, sizeof(blkBuf), nil); + if (dierr != kDIErrNone) { + WMSG1(" FormatBlocks: GFD write failed (err=%d)\n", dierr); + return dierr; + } + } + assert(length == 0); + + end = time(nil); + WMSG1("FormatBlocks complete, time=%ld\n", end - start); + + return kDIErrNone; +} +#endif + + +/* + * =========================================================================== + * Utility functions + * =========================================================================== + */ + +/* + * Add a note to this disk image. + * + * This is how we communicate cautions and warnings to the user. Use + * linefeeds ('\n') to indicate line breaks. + * + * The maximum length of a single note is set by the size of "buf". + */ +void +DiskImg::AddNote(NoteType type, const char* fmt, ...) +{ + char buf[512]; + char* cp = buf; + int maxLen = sizeof(buf); + va_list args; + int len; + + /* + * Prepend a string that highlights the note. + */ + switch (type) { + case kNoteWarning: + strcpy(cp, "- WARNING: "); + break; + default: + strcpy(cp, "- "); + break; + } + len = strlen(cp); + cp += len; + maxLen -= len; + + /* + * Add the note. + */ + va_start(args, fmt); +#if defined(HAVE_VSNPRINTF) + (void) vsnprintf(cp, maxLen, fmt, args); +#elif defined(HAVE__VSNPRINTF) + (void) _vsnprintf(cp, maxLen, fmt, args); +#else +# error "hosed" +#endif + va_end(args); + + buf[sizeof(buf)-2] = '\0'; // leave room for additional '\n' + len = strlen(buf); + if (len > 0 && buf[len-1] != '\n') { + buf[len] = '\n'; + buf[len+1] = '\0'; + len++; + } + + WMSG1("+++ adding note '%s'\n", buf); + + if (fNotes == nil) { + fNotes = new char[len +1]; + if (fNotes == nil) { + WMSG1("Unable to create notes[%d]\n", len+1); + assert(false); + return; + } + strcpy(fNotes, buf); + } else { + int existingLen = strlen(fNotes); + char* newNotes = new char[existingLen + len +1]; + if (newNotes == nil) { + WMSG1("Unable to create newNotes[%d]\n", existingLen+len+1); + assert(false); + return; + } + strcpy(newNotes, fNotes); + strcpy(newNotes + existingLen, buf); + delete[] fNotes; + fNotes = newNotes; + } +} + +/* + * Return a string with the notes in it. + */ +const char* +DiskImg::GetNotes(void) const +{ + if (fNotes == nil) + return ""; + else + return fNotes; +} + + +/* + * Get length and offset of tracks in a nibble image. This is necessary + * because of formats with variable-length tracks (e.g. TrackStar). + */ +int +DiskImg::GetNibbleTrackLength(long track) const +{ + assert(fpImageWrapper != NULL); + return fpImageWrapper->GetNibbleTrackLength(fPhysical, track); +} +int +DiskImg::GetNibbleTrackOffset(long track) const +{ + assert(fpImageWrapper != NULL); + return fpImageWrapper->GetNibbleTrackOffset(fPhysical, track); +} + + +/* + * Return a new object with the appropriate DiskFS sub-class. + * + * If the image hasn't been analyzed, or was analyzed to no avail, "nil" + * is returned unless "allowUnknown" is set to "true". In that case, a + * DiskFSUnknown is returned. + * + * This doesn't inspire the DiskFS to do any processing, just creates the + * new object. + */ +DiskFS* +DiskImg::OpenAppropriateDiskFS(bool allowUnknown) +{ + DiskFS* pDiskFS = nil; + + /* + * Create an appropriate DiskFS object. + */ + switch (GetFSFormat()) { + case DiskImg::kFormatDOS33: + case DiskImg::kFormatDOS32: + pDiskFS = new DiskFSDOS33(); + break; + case DiskImg::kFormatProDOS: + pDiskFS = new DiskFSProDOS(); + break; + case DiskImg::kFormatPascal: + pDiskFS = new DiskFSPascal(); + break; + case DiskImg::kFormatMacHFS: + pDiskFS = new DiskFSHFS(); + break; + case DiskImg::kFormatUNIDOS: + pDiskFS = new DiskFSUNIDOS(); + break; + case DiskImg::kFormatOzDOS: + pDiskFS = new DiskFSOzDOS(); + break; + case DiskImg::kFormatCFFA4: + case DiskImg::kFormatCFFA8: + pDiskFS = new DiskFSCFFA(); + break; + case DiskImg::kFormatMacPart: + pDiskFS = new DiskFSMacPart(); + break; + case DiskImg::kFormatMicroDrive: + pDiskFS = new DiskFSMicroDrive(); + break; + case DiskImg::kFormatFocusDrive: + pDiskFS = new DiskFSFocusDrive(); + break; + case DiskImg::kFormatCPM: + pDiskFS = new DiskFSCPM(); + break; + case DiskImg::kFormatMSDOS: + pDiskFS = new DiskFSFAT(); + break; + case DiskImg::kFormatRDOS33: + case DiskImg::kFormatRDOS32: + case DiskImg::kFormatRDOS3: + pDiskFS = new DiskFSRDOS(); + break; + + default: + WMSG1("WARNING: unhandled DiskFS case %d\n", GetFSFormat()); + assert(false); + /* fall through */ + case DiskImg::kFormatGenericPhysicalOrd: + case DiskImg::kFormatGenericProDOSOrd: + case DiskImg::kFormatGenericDOSOrd: + case DiskImg::kFormatGenericCPMOrd: + case DiskImg::kFormatUnknown: + if (allowUnknown) { + pDiskFS = new DiskFSUnknown(); + break; + } + } + + return pDiskFS; +} + + +/* + * Fill an array with SectorOrder values. The ordering specified by "first" + * will come first. Unused entries will be set to "unknown" and should be + * ignored. + * + * "orderArray" must have kSectorOrderMax elements. + */ +/*static*/ void +DiskImg::GetSectorOrderArray(SectorOrder* orderArray, SectorOrder first) +{ + // init array + for (int i = 0; i < kSectorOrderMax; i++) + orderArray[i] = (SectorOrder) i; + + // pull the best-guess ordering to the front + assert(orderArray[0] == kSectorOrderUnknown); + + orderArray[0] = first; + orderArray[(int) first] = kSectorOrderUnknown; + + // don't bother checking CP/M sector order + orderArray[kSectorOrderCPM] = kSectorOrderUnknown; +} + + +/* + * Return a short string describing "format". + * + * These are semi-duplicated in ImageFormatDialog.cpp in CiderPress. + */ +/*static*/ const char* +DiskImg::ToStringCommon(int format, const ToStringLookup* pTable, + int tableSize) +{ + for (int i = 0; i < tableSize; i++) { + if (pTable[i].format == format) + return pTable[i].str; + } + + assert(false); + return "(unknown)"; +} + +/*static*/ const char* +DiskImg::ToString(OuterFormat format) +{ + static const ToStringLookup kOuterFormats[] = { + { DiskImg::kOuterFormatUnknown, "Unknown format" }, + { DiskImg::kOuterFormatNone, "(none)" }, + { DiskImg::kOuterFormatCompress, "UNIX compress" }, + { DiskImg::kOuterFormatGzip, "gzip" }, + { DiskImg::kOuterFormatBzip2, "bzip2" }, + { DiskImg::kOuterFormatZip, "Zip archive" }, + }; + + return ToStringCommon(format, kOuterFormats, NELEM(kOuterFormats)); +} +/*static*/ const char* +DiskImg::ToString(FileFormat format) +{ + static const ToStringLookup kFileFormats[] = { + { DiskImg::kFileFormatUnknown, "Unknown format" }, + { DiskImg::kFileFormatUnadorned, "Unadorned raw data" }, + { DiskImg::kFileFormat2MG, "2MG" }, + { DiskImg::kFileFormatNuFX, "NuFX (ShrinkIt)" }, + { DiskImg::kFileFormatDiskCopy42, "DiskCopy 4.2" }, + { DiskImg::kFileFormatDiskCopy60, "DiskCopy 6.0" }, + { DiskImg::kFileFormatDavex, "Davex volume image" }, + { DiskImg::kFileFormatSim2eHDV, "Sim //e HDV" }, + { DiskImg::kFileFormatTrackStar, "TrackStar image" }, + { DiskImg::kFileFormatFDI, "FDI image" }, + { DiskImg::kFileFormatDDD, "DDD" }, + { DiskImg::kFileFormatDDDDeluxe, "DDDDeluxe" }, + }; + + return ToStringCommon(format, kFileFormats, NELEM(kFileFormats)); +}; +/*static*/ const char* +DiskImg::ToString(PhysicalFormat format) +{ + static const ToStringLookup kPhysicalFormats[] = { + { DiskImg::kPhysicalFormatUnknown, "Unknown format" }, + { DiskImg::kPhysicalFormatSectors, "Sectors" }, + { DiskImg::kPhysicalFormatNib525_6656, "Raw nibbles (6656-byte)" }, + { DiskImg::kPhysicalFormatNib525_6384, "Raw nibbles (6384-byte)" }, + { DiskImg::kPhysicalFormatNib525_Var, "Raw nibbles (variable len)" }, + }; + + return ToStringCommon(format, kPhysicalFormats, NELEM(kPhysicalFormats)); +}; +/*static*/ const char* +DiskImg::ToString(SectorOrder format) +{ + static const ToStringLookup kSectorOrders[] = { + { DiskImg::kSectorOrderUnknown, "Unknown ordering" }, + { DiskImg::kSectorOrderProDOS, "ProDOS block ordering" }, + { DiskImg::kSectorOrderDOS, "DOS sector ordering" }, + { DiskImg::kSectorOrderCPM, "CP/M block ordering" }, + { DiskImg::kSectorOrderPhysical, "Physical sector ordering" }, + }; + + return ToStringCommon(format, kSectorOrders, NELEM(kSectorOrders)); +}; +/*static*/ const char* +DiskImg::ToString(FSFormat format) +{ + static const ToStringLookup kFSFormats[] = { + { DiskImg::kFormatUnknown, "Unknown" }, + { DiskImg::kFormatProDOS, "ProDOS" }, + { DiskImg::kFormatDOS33, "DOS 3.3" }, + { DiskImg::kFormatDOS32, "DOS 3.2" }, + { DiskImg::kFormatPascal, "Pascal" }, + { DiskImg::kFormatMacHFS, "HFS" }, + { DiskImg::kFormatMacMFS, "MFS" }, + { DiskImg::kFormatLisa, "Lisa" }, + { DiskImg::kFormatCPM, "CP/M" }, + { DiskImg::kFormatMSDOS, "MS-DOS FAT" }, + { DiskImg::kFormatISO9660, "ISO-9660" }, + { DiskImg::kFormatRDOS33, "RDOS 3.3 (16-sector)" }, + { DiskImg::kFormatRDOS32, "RDOS 3.2 (13-sector)" }, + { DiskImg::kFormatRDOS3, "RDOS 3 (cracked 13-sector)" }, + { DiskImg::kFormatGenericDOSOrd, "Generic DOS sectors" }, + { DiskImg::kFormatGenericProDOSOrd, "Generic ProDOS blocks" }, + { DiskImg::kFormatGenericPhysicalOrd, "Generic raw sectors" }, + { DiskImg::kFormatGenericCPMOrd, "Generic CP/M blocks" }, + { DiskImg::kFormatUNIDOS, "UNIDOS (400K DOS x2)" }, + { DiskImg::kFormatOzDOS, "OzDOS (400K DOS x2)" }, + { DiskImg::kFormatCFFA4, "CFFA (4 or 6 partitions)" }, + { DiskImg::kFormatCFFA8, "CFFA (8 partitions)" }, + { DiskImg::kFormatMacPart, "Macintosh partitioned disk" }, + { DiskImg::kFormatMicroDrive, "MicroDrive partitioned disk" }, + { DiskImg::kFormatFocusDrive, "FocusDrive partitioned disk" }, + }; + + return ToStringCommon(format, kFSFormats, NELEM(kFSFormats)); +}; + + +/* + * strerror() equivalent for DiskImg errors. + */ +const char* +DiskImgLib::DIStrError(DIError dierr) +{ + if (dierr > 0) { + const char* msg; + msg = strerror(dierr); + if (msg != nil) + return msg; + } + + /* + * BUG: this should be set up as per-thread storage in an MT environment. + * I would be more inclined to worry about this if I was expecting + * to hit "default:". So long as valid values are passed in, and the + * switch statement is kept up to date, we should never have cause + * to return this. + * + * An easier solution, should this present a problem for someone, would + * be to have the function return nil or "unknown error" when the + * error value isn't recognized. I'd recommend leaving it as-is for + * debug builds, though, as it's helpful to know *which* error is not + * recognized. + */ + static char defaultMsg[32]; + + switch (dierr) { + case kDIErrNone: + return "(no error)"; + + case kDIErrAccessDenied: + return "access denied"; + case kDIErrVWAccessForbidden: + return "for safety, write access to this volume is forbidden"; + case kDIErrSharingViolation: + return "file is already open and cannot be shared"; + case kDIErrNoExclusiveAccess: + return "couldn't get exclusive access"; + case kDIErrWriteProtected: + return "write protected"; + case kDIErrCDROMNotSupported: + return "access to CD-ROM drives is not supported"; + case kDIErrASPIFailure: + return "an ASPI request failed"; + case kDIErrSPTIFailure: + return "an SPTI request failed"; + case kDIErrSCSIFailure: + return "a SCSI request failed"; + case kDIErrDeviceNotReady: + return "device not ready"; + + case kDIErrFileNotFound: + return "file not found"; + case kDIErrForkNotFound: + return "fork not found"; + case kDIErrAlreadyOpen: + return "an image is already open"; + case kDIErrFileOpen: + return "file is open"; + case kDIErrNotReady: + return "object not ready"; + case kDIErrFileExists: + return "file already exists"; + case kDIErrDirectoryExists: + return "directory already exists"; + + case kDIErrEOF: + return "end of file reached"; + case kDIErrReadFailed: + return "read failed"; + case kDIErrWriteFailed: + return "write failed"; + case kDIErrDataUnderrun: + return "tried to read past end of file"; + case kDIErrDataOverrun: + return "tried to write past end of file"; + case kDIErrGenericIO: + return "I/O error"; + + case kDIErrOddLength: + return "image size is wrong"; + case kDIErrUnrecognizedFileFmt: + return "not a recognized disk image format"; + case kDIErrBadFileFormat: + return "image file contents aren't in expected format"; + case kDIErrUnsupportedFileFmt: + return "file format not supported"; + case kDIErrUnsupportedPhysicalFmt: + return "physical format not supported"; + case kDIErrUnsupportedFSFmt: + return "filesystem type not supported"; + case kDIErrBadOrdering: + return "bad sector ordering"; + case kDIErrFilesystemNotFound: + return "specified filesystem not found"; + case kDIErrUnsupportedAccess: + return "the method of access used isn't supported for this image"; + case kDIErrUnsupportedImageFeature: + return "image file uses features that CiderPress doesn't support"; + + case kDIErrInvalidTrack: + return "invalid track number"; + case kDIErrInvalidSector: + return "invalid sector number"; + case kDIErrInvalidBlock: + return "invalid block number"; + case kDIErrInvalidIndex: + return "invalid index number"; + + case kDIErrDirectoryLoop: + return "disk directory structure has an infinite loop"; + case kDIErrFileLoop: + return "file structure has an infinite loop"; + case kDIErrBadDiskImage: + return "the filesystem on this image appears damaged"; + case kDIErrBadFile: + return "file structure appears damaged"; + case kDIErrBadDirectory: + return "a directory appears damaged"; + case kDIErrBadPartition: + return "bad partition"; + + case kDIErrFileArchive: + return "this looks like a file archive, not a disk archive"; + case kDIErrUnsupportedCompression: + return "compression method not supported"; + case kDIErrBadChecksum: + return "checksum doesn't match, data may be corrupted"; + case kDIErrBadCompressedData: + return "the compressed data is corrupted"; + case kDIErrBadArchiveStruct: + return "archive may be damaged"; + + case kDIErrBadNibbleSectors: + return "couldn't read sectors from this image"; + case kDIErrSectorUnreadable: + return "sector not readable"; + case kDIErrInvalidDiskByte: + return "found invalid nibble image disk byte"; + case kDIErrBadRawData: + return "couldn't convert raw data to nibble data"; + + case kDIErrInvalidFileName: + return "invalid file name"; + case kDIErrDiskFull: + return "disk full"; + case kDIErrVolumeDirFull: + return "volume directory is full"; + case kDIErrInvalidCreateReq: + return "invalid disk image create request"; + case kDIErrTooBig: + return "size is larger than we can handle"; + + case kDIErrGeneric: + return "DiskImg generic error"; + case kDIErrInternal: + return "DiskImg internal error"; + case kDIErrMalloc: + return "memory allocation failure"; + case kDIErrInvalidArg: + return "invalid argument"; + case kDIErrNotSupported: + return "feature not supported"; + case kDIErrCancelled: + return "cancelled by user"; + + case kDIErrNufxLibInitFailed: + return "NufxLib initialization failed"; + + default: + sprintf(defaultMsg, "(error=%d)", dierr); + return defaultMsg; + } +} + +/* + * High ASCII conversion table, from Technical Note PT515, + * "Apple File Exchange Q&As". The table is available in a hopelessly + * blurry PDF or a pair of GIFs created with small fonts, but I think I + * have mostly captured it. + */ +/*static*/ const unsigned char DiskImg::kMacHighASCII[128+1] = + "AACENOUaaaaaaceeeeiiiinooooouuuu" // 0x80 - 0x9f + "tocL$oPBrct'.=AO%+<>YudsPpSaoOao" // 0xa0 - 0xbf + "?!-vf=d<>. AAOOo--\"\"''/oyY/o<> f" // 0xc0 - 0xdf + "|*,,%AEAEEIIIIOOaOUUUi^~-,**,\"? "; // 0xe0 - 0xff + + +/* + * Hack for Win32 systems. See Win32BlockIO.cpp for commentary. + */ +bool DiskImgLib::gAllowWritePhys0 = false; +/*static*/ void DiskImg::SetAllowWritePhys0(bool val) { + DiskImgLib::gAllowWritePhys0 = val; +} diff --git a/diskimg/DiskImg.h b/diskimg/DiskImg.h new file mode 100644 index 0000000..462a93e --- /dev/null +++ b/diskimg/DiskImg.h @@ -0,0 +1,1659 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Public declarations for the DiskImg library. + * + * Everything is wrapped in the "DiskImgLib" namespace. Either prefix + * all references with "DiskImgLib::", or add "using namespace DiskImgLib" + * to all C++ source files that make use of it. + * + * Under Linux, this should be compiled with -D_FILE_OFFSET_BITS=64. + * + * These classes are not thread-safe with respect to access to a single + * disk image. Writing to the same disk image from multiple threads + * simultaneously is bound to end in disaster. Simultaneous access to + * different objects will work, though modifying the same disk image + * file from multiple objects will lead to unpredictable results. + */ +#ifndef __DISK_IMG__ +#define __DISK_IMG__ + +#include +#include +#include + +//#define EXCISE_GPL_CODE + +/* Windows DLL stuff */ +#ifdef _WIN32 +# ifdef DISKIMG_EXPORTS +# define DISKIMG_API __declspec(dllexport) +# else +# define DISKIMG_API __declspec(dllimport) +# endif +#else +# define DISKIMG_API +#endif + +namespace DiskImgLib { + +/* compiled-against versions; call DiskImg::GetVersion for linked-against */ +#define kDiskImgVersionMajor 4 +#define kDiskImgVersionMinor 5 +#define kDiskImgVersionBug 2 + + +/* + * Errors from the various disk image classes. + */ +typedef enum DIError { + kDIErrNone = 0, + + /* I/O request errors (should renumber/rename to match GS/OS errors?) */ + kDIErrAccessDenied = -10, + kDIErrVWAccessForbidden = -11, // write access to volume forbidden + kDIErrSharingViolation = -12, // file is in use and not shareable + kDIErrNoExclusiveAccess = -13, // couldn't get exclusive access + kDIErrWriteProtected = -14, // disk is write protected + kDIErrCDROMNotSupported = -15, // access to CD-ROM drives not supptd + kDIErrASPIFailure = -16, // generic ASPI failure result + kDIErrSPTIFailure = -17, // generic SPTI failure result + kDIErrSCSIFailure = -18, // generic SCSI failure result + kDIErrDeviceNotReady = -19, // floppy or CD-ROM drive has no media + + kDIErrFileNotFound = -20, + kDIErrForkNotFound = -21, // requested fork does not exist + kDIErrAlreadyOpen = -22, // already open, can't open a 2nd time + kDIErrFileOpen = -23, // file is open, can't delete it + kDIErrNotReady = -24, + kDIErrFileExists = -25, // file already exists + kDIErrDirectoryExists = -26, // directory already exists + + kDIErrEOF = -30, // end-of-file reached + kDIErrReadFailed = -31, + kDIErrWriteFailed = -32, + kDIErrDataUnderrun = -33, // tried to read off end of the image + kDIErrDataOverrun = -34, // tried to write off end of the image + kDIErrGenericIO = -35, // generic I/O error + + kDIErrOddLength = -40, // image size not multiple of sectors + kDIErrUnrecognizedFileFmt = -41, // file format just not recognized + kDIErrBadFileFormat = -42, // filename ext doesn't match contents + kDIErrUnsupportedFileFmt = -43, // recognized but not supported + kDIErrUnsupportedPhysicalFmt = -44, // (same) + kDIErrUnsupportedFSFmt = -45, // (and again) + kDIErrBadOrdering = -46, // requested sector ordering no good + kDIErrFilesystemNotFound = -47, // requested filesystem isn't there + kDIErrUnsupportedAccess = -48, // e.g. read sectors from blocks-only + kDIErrUnsupportedImageFeature = -49, // e.g. FDI image w/Amiga sectors + + kDIErrInvalidTrack = -50, // request for invalid track number + kDIErrInvalidSector = -51, // request for invalid sector number + kDIErrInvalidBlock = -52, // request for invalid block number + kDIErrInvalidIndex = -53, // request with an invalid index + + kDIErrDirectoryLoop = -60, // directory chain points into itself + kDIErrFileLoop = -61, // file sector or block alloc loops + kDIErrBadDiskImage = -62, // the FS on the disk image is damaged + kDIErrBadFile = -63, // bad file on disk image + kDIErrBadDirectory = -64, // bad dir on disk image + kDIErrBadPartition = -65, // bad partition on multi-part format + + kDIErrFileArchive = -70, // file archive, not disk archive + kDIErrUnsupportedCompression = -71, // compression method is not supported + kDIErrBadChecksum = -72, // image file's checksum is bad + kDIErrBadCompressedData = -73, // data can't even be unpacked + kDIErrBadArchiveStruct = -74, // bad archive structure + + kDIErrBadNibbleSectors = -80, // can't read sectors from this image + kDIErrSectorUnreadable = -81, // requested sector not readable + kDIErrInvalidDiskByte = -82, // invalid byte for encoding type + kDIErrBadRawData = -83, // couldn't get correct nibbles + + kDIErrInvalidFileName = -90, // tried to create file with bad name + kDIErrDiskFull = -91, // no space left on disk + kDIErrVolumeDirFull = -92, // no more entries in volume dir + kDIErrInvalidCreateReq = -93, // CreateImage request was flawed + kDIErrTooBig = -94, // larger than we want to handle + + /* higher-level errors */ + kDIErrGeneric = -101, + kDIErrInternal = -102, + kDIErrMalloc = -103, + kDIErrInvalidArg = -104, + kDIErrNotSupported = -105, // feature not currently supported + kDIErrCancelled = -106, // an operation was cancelled by user + + kDIErrNufxLibInitFailed = -110, +} DIError; + +/* return a string describing the error */ +DISKIMG_API const char* DIStrError(DIError dierr); + + +/* exact definition of off_t varies, so just define our own */ +#ifdef _ULONGLONG_ + typedef LONGLONG di_off_t; +#else + typedef off_t di_off_t; +#endif + +/* common definition of "whence" for seeks */ +typedef enum DIWhence { + kSeekSet = SEEK_SET, + kSeekCur = SEEK_CUR, + kSeekEnd = SEEK_END +}; + +/* try to load ASPI under Win2K; if successful, SPTI should be disabled */ +const bool kAlwaysTryASPI = false; +/* ASPI device "filenames" look like "ASPI:x:y:z\" */ +DISKIMG_API extern const char* kASPIDev; + +/* some nibble-encoding constants */ +const int kTrackLenNib525 = 6656; +const int kTrackLenNb2525 = 6384; +const int kTrackLenTrackStar525 = 6525; // max len of data in TS image +const int kTrackAllocSize = 6656; // max 5.25 nibble track len; for buffers +const int kTrackCount525 = 35; // expected #of tracks on 5.25 img +const int kMaxNibbleTracks525 = 40; // max #of tracks on 5.25 nibble img +const int kDefaultNibbleVolumeNum = 254; +const int kBlockSize = 512; // block size for DiskImg interfaces +const int kSectorSize = 256; // sector size (1/2 block) +const int kD13Length = 256 * 13 * 35; // length of a .d13 image + +/* largest expanse we allow access to on a volume (8GB in 512-byte blocks) */ +const long kVolumeMaxBlocks = 8*1024*(1024*1024 / kBlockSize); + +/* largest .gz file we'll open (uncompressed size) */ +const long kGzipMax = 32*1024*1024; + +/* forward and external class definitions */ +class DiskFS; +class A2File; +class A2FileDescr; +class GenericFD; +class OuterWrapper; +class ImageWrapper; +class CircularBufferAccess; +class ASPI; +class LinearBitmap; + + +/* + * Library-global data functions. + * + * This class is just a namespace clumper. Do not instantiate. + */ +class DISKIMG_API Global { +public: + // one-time DLL initialization; use SetDebugMsgHandler first + static DIError AppInit(void); + // one-time DLL cleanup + static DIError AppCleanup(void); + + // return the DiskImg version number + static void GetVersion(long* pMajor, long* pMinor, long* pBug); + + static bool GetAppInitCalled(void) { return fAppInitCalled; } + static bool GetHasSPTI(void); + static bool GetHasASPI(void); + + // return a pointer to our global ASPI instance + static ASPI* GetASPI(void) { return fpASPI; } + // shortcut for fpASPI->GetVersion() + static unsigned long GetASPIVersion(void); + + // pointer to the debug message handler + typedef void (*DebugMsgHandler)(const char* file, int line, const char* msg); + static DebugMsgHandler gDebugMsgHandler; + + static DebugMsgHandler SetDebugMsgHandler(DebugMsgHandler handler); + static void PrintDebugMsg(const char* file, int line, const char* fmt, ...) + #if defined(__GNUC__) + __attribute__ ((format(printf, 3, 4))) + #endif + ; + +private: + // no instantiation allowed + Global(void) {} + ~Global(void) {} + + // make sure app calls AppInit + static bool fAppInitCalled; + + static ASPI* fpASPI; +}; + +extern bool gAllowWritePhys0; // ugh -- see Win32BlockIO.cpp + + +/* + * Disk I/O class, roughly equivalent to a GS/OS disk device driver. + * + * Abstracts away the file's source (file on disk, file in memory) and + * storage format (DOS order, ProDOS order, nibble). Will also cope + * with common disk compression and wrapper formats (Mac DiskCopy, 2MG, + * ShrinkIt, etc) if handed a file on disk. + * + * Images may be embedded within other images, e.g. UNIDOS and storage + * type $04 pascal volumes. + * + * THOUGHT: we need a list(?) of pointers from here back to the DiskFS + * so that any modifications here will "wake" the DiskFS and sub-volumes. + * We also need a "dirty" flag so things like CloseNufx can know not to + * re-do work when Closing after a Flush. Also DiskFS can alert us to + * any locally cached stuff, and we can tell them to flush everything. + * (Possibly useful when doing disk updates, so stuff can be trivially + * un-done. Currently CiderPress checks the filename manually after + * each write, but that's generally less reliable than having the knowledge + * contained in the DiskImg.) + * + * THOUGHT: need a ReadRawTrack that gets raw nibblized data. For a + * nibblized image it returns the data, for a sector image it generates + * the raw data. + * + * THOUGHT: we could reduce the risk of problems and increase performance + * for physical media with a "copy on write" scheme. We'd create a shadow + * array of modified blocks, and write them at Flush time. This would + * provide an instantaneous "revert" feature, and prevent formats like + * DiskCopy42 (which has a CRC in its header) from being inconsistent for + * long stretches. + */ +class DISKIMG_API DiskImg { +public: + // create DiskImg object + DiskImg(void); + virtual ~DiskImg(void); + + /* + * Types describing an image file. + * + * The file itself is described by an external parameter ("file source") + * that is either the name of the file, a memory buffer, or an EFD + * (EmbeddedFileDescriptor). + */ + typedef enum { // format of the "wrapper wrapper" + kOuterFormatUnknown = 0, + kOuterFormatNone = 1, // (plain) + kOuterFormatCompress = 2, // .xx.Z + kOuterFormatGzip = 3, // .xx.gz + kOuterFormatBzip2 = 4, // .xx.bz2 + kOuterFormatZip = 10, // .zip + } OuterFormat; + typedef enum { // format of the image "wrapper" + kFileFormatUnknown = 0, + kFileFormatUnadorned = 1, // .po, .do, ,nib, .raw, .d13 + kFileFormat2MG = 2, // .2mg, .2img, $e0/0130 + kFileFormatDiskCopy42 = 3, // .dsk/.disk, maybe .dc + kFileFormatDiskCopy60 = 4, // .dc6 (often just raw format) + kFileFormatDavex = 5, // $e0/8004 + kFileFormatSim2eHDV = 6, // .hdv + kFileFormatTrackStar = 7, // .app (40-track or 80-track) + kFileFormatFDI = 8, // .fdi (5.25" or 3.5") + kFileFormatNuFX = 20, // .shk, .sdk, .bxy + kFileFormatDDD = 21, // .ddd + kFileFormatDDDDeluxe = 22, // $DD, .ddd + } FileFormat; + typedef enum { // format of the image data stream + kPhysicalFormatUnknown = 0, + kPhysicalFormatSectors = 1, // sequential 256-byte sectors (13/16/32) + kPhysicalFormatNib525_6656 = 2, // 5.25" disk ".nib" (6656 bytes/track) + kPhysicalFormatNib525_6384 = 3, // 5.25" disk ".nb2" (6384 bytes/track) + kPhysicalFormatNib525_Var = 4, // 5.25" disk (variable len, e.g. ".app") + } PhysicalFormat; + typedef enum { // sector ordering for "sector" format images + kSectorOrderUnknown = 0, + kSectorOrderProDOS = 1, // written as series of ProDOS blocks + kSectorOrderDOS = 2, // written as series of DOS sectors + kSectorOrderCPM = 3, // written as series of 1K CP/M blocks + kSectorOrderPhysical = 4, // written as un-interleaved sectors + kSectorOrderMax, // (used for array sizing) + } SectorOrder; + typedef enum { // main filesystem format (based on NuFX enum) + kFormatUnknown = 0, + kFormatProDOS = 1, + kFormatDOS33 = 2, + kFormatDOS32 = 3, + kFormatPascal = 4, + kFormatMacHFS = 5, + kFormatMacMFS = 6, + kFormatLisa = 7, + kFormatCPM = 8, + //kFormatCharFST + kFormatMSDOS = 10, // any FAT filesystem + //kFormatHighSierra + kFormatISO9660 = 12, + //kFormatAppleShare + kFormatRDOS33 = 20, // 16-sector RDOS disk + kFormatRDOS32 = 21, // 13-sector RDOS disk + kFormatRDOS3 = 22, // 13-sector RDOS disk converted to 16 + // "generic" formats *must* be in their own "decade" + kFormatGenericPhysicalOrd = 30, // unknown, but physical-sector-ordered + kFormatGenericProDOSOrd = 31, // unknown, but ProDOS-block-ordered + kFormatGenericDOSOrd = 32, // unknown, but DOS-sector-ordered + kFormatGenericCPMOrd = 33, // unknown, but CP/M-block-ordered + kFormatUNIDOS = 40, // two 400K DOS 3.3 volumes + kFormatOzDOS = 41, // two 400K DOS 3.3 volumes, weird order + kFormatCFFA4 = 42, // CFFA image with 4 or 6 partitions + kFormatCFFA8 = 43, // CFFA image with 8 partitions + kFormatMacPart = 44, // Macintosh-style partitioned disk + kFormatMicroDrive = 45, // ///SHH Systeme's MicroDrive format + kFormatFocusDrive = 46, // Parsons Engineering FocusDrive format + + // try to keep this in an unsigned char, e.g. for CP clipboard + } FSFormat; + + /* + * Nibble encode/decode description. Use no pointers here, so we + * store as an array and resize at will. + * + * Should we define an enum to describe whether address and data + * headers are standard or some wacky variant? + */ + typedef enum { + kNibbleAddrPrologLen = 3, // d5 aa 96 + kNibbleAddrEpilogLen = 3, // de aa eb + kNibbleDataPrologLen = 3, // d5 aa ad + kNibbleDataEpilogLen = 3, // de aa eb + }; + typedef enum { + kNibbleEncUnknown = 0, + kNibbleEnc44, + kNibbleEnc53, + kNibbleEnc62, + } NibbleEnc; + typedef enum { + kNibbleSpecialNone = 0, + kNibbleSpecialMuse, // doubled sector numbers on tracks > 2 + kNibbleSpecialSkipFirstAddrByte, + } NibbleSpecial; + typedef struct { + char description[32]; + short numSectors; // 13 or 16 (or 18?) + + unsigned char addrProlog[kNibbleAddrPrologLen]; + unsigned char addrEpilog[kNibbleAddrEpilogLen]; + unsigned char addrChecksumSeed; + bool addrVerifyChecksum; + bool addrVerifyTrack; + int addrEpilogVerifyCount; + + unsigned char dataProlog[kNibbleDataPrologLen]; + unsigned char dataEpilog[kNibbleDataEpilogLen]; + unsigned char dataChecksumSeed; + bool dataVerifyChecksum; + int dataEpilogVerifyCount; + + NibbleEnc encoding; + NibbleSpecial special; + } NibbleDescr; + + + static inline bool IsSectorFormat(PhysicalFormat fmt) { + return (fmt == kPhysicalFormatSectors); + } + static inline bool IsNibbleFormat(PhysicalFormat fmt) { + return (fmt == kPhysicalFormatNib525_6656 || + fmt == kPhysicalFormatNib525_6384 || + fmt == kPhysicalFormatNib525_Var); + } + + // file is on disk; stuff like 2MG headers will be identified and stripped + DIError OpenImage(const char* filename, char fssep, bool readOnly); + // file is in memory; provide a pointer to the data start and buffer size + DIError OpenImage(const void* buffer, long length, bool readOnly); + // file is a range of blocks on an open block-oriented disk image + DIError OpenImage(DiskImg* pParent, long firstBlock, long numBlocks); + // file is a range of tracks/sectors on an open sector-oriented disk image + DIError OpenImage(DiskImg* pParent, long firstTrack, long firstSector, + long numSectors); + + // create a new, blank image file + DIError CreateImage(const char* pathName, const char* storageName, + OuterFormat outerFormat, FileFormat fileFormat, + PhysicalFormat physical, const NibbleDescr* pNibbleDescr, + SectorOrder order, FSFormat format, + long numBlocks, bool skipFormat); + DIError CreateImage(const char* pathName, const char* storageName, + OuterFormat outerFormat, FileFormat fileFormat, + PhysicalFormat physical, const NibbleDescr* pNibbleDescr, + SectorOrder order, FSFormat format, + long numTracks, long numSectPerTrack, bool skipFormat); + + // flush any changes to disk; slow recompress only for "kFlushAll" + typedef enum { kFlushUnknown=0, kFlushFastOnly=1, kFlushAll=2 } FlushMode; + DIError FlushImage(FlushMode mode); + // close the image, freeing up any resources in use + DIError CloseImage(void); + // raise/lower refCnt (may want to track pointers someday) + void AddDiskFS(DiskFS* pDiskFS) { fDiskFSRefCnt++; } + void RemoveDiskFS(DiskFS* pDiskFS) { + assert(fDiskFSRefCnt > 0); + fDiskFSRefCnt--; + } + + // (re-)format this image in the specified FS format + DIError FormatImage(FSFormat format, const char* volName); + // reset all blocks/sectors to zeroes + DIError ZeroImage(void); + + // configure for paired sectors (OzDOS) + void SetPairedSectors(bool enable, int idx); + + // identify sector ordering and disk format + // (may want a version that takes "hints" for special disks?) + DIError AnalyzeImage(void); + // figure out what FS and sector ordering is on the disk image + void AnalyzeImageFS(void); + bool ShowAsBlocks(void) const; + // overrule the analyzer (generally not recommended) -- does not + // override FileFormat, which is very reliable + DIError OverrideFormat(PhysicalFormat physical, FSFormat format, + SectorOrder order); + + // Create a DiskFS that matches this DiskImg. Must be called after + // AnalayzeImage, or you will always get a DiskFSUnknown. The DiskFS + // must be freed with "delete" when no longer needed. + DiskFS* OpenAppropriateDiskFS(bool allowUnknown = false); + + // Add information or a warning to the list of notes. Use linefeeds to + // indicate line breaks. This is currently append-only. + typedef enum { kNoteInfo, kNoteWarning } NoteType; + void AddNote(NoteType type, const char* fmt, ...) + #if defined(__GNUC__) + __attribute__ ((format(printf, 3, 4))) + #endif + ; + const char* GetNotes(void) const; + + // simple accessors + OuterFormat GetOuterFormat(void) const { return fOuterFormat; } + FileFormat GetFileFormat(void) const { return fFileFormat; } + PhysicalFormat GetPhysicalFormat(void) const { return fPhysical; } + SectorOrder GetSectorOrder(void) const { return fOrder; } + FSFormat GetFSFormat(void) const { return fFormat; } + long GetNumTracks(void) const { return fNumTracks; } + int GetNumSectPerTrack(void) const { return fNumSectPerTrack; } + long GetNumBlocks(void) const { return fNumBlocks; } + bool GetReadOnly(void) const { return fReadOnly; } + bool GetDirtyFlag(void) const { return fDirty; } + + // set read-only flag; don't use this (open with correct setting; + // this was added as safety hack for the volume copier) + void SetReadOnly(bool val) { fReadOnly = val; } + + // read a 256-byte sector + // NOTE to self: this function should not be available for odd-sized + // volumes, e.g. a ProDOS /RAM or /RAM5 stored with Davex. Need some way + // to communicate that to disk editor so it knows to grey-out the + // selection checkbox and/or not use "show as sectors" as default. + virtual DIError ReadTrackSector(long track, int sector, void* buf) { + return ReadTrackSectorSwapped(track, sector, buf, fOrder, + fFileSysOrder); + } + DIError ReadTrackSectorSwapped(long track, int sector, + void* buf, SectorOrder imageOrder, SectorOrder fsOrder); + // write a 256-byte sector + virtual DIError WriteTrackSector(long track, int sector, const void* buf); + + // read a 512-byte block + virtual DIError ReadBlock(long block, void* buf) { + return ReadBlockSwapped(block, buf, fOrder, fFileSysOrder); + } + DIError ReadBlockSwapped(long block, void* buf, SectorOrder imageOrder, + SectorOrder fsOrder); + // read multiple blocks + virtual DIError ReadBlocks(long startBlock, int numBlocks, void* buf); + // check our virtual bad block map + bool CheckForBadBlocks(long startBlock, int numBlocks); + // write a 512-byte block + virtual DIError WriteBlock(long block, const void* buf); + // write multiple blocks + virtual DIError WriteBlocks(long startBlock, int numBlocks, const void* buf); + + // read an entire nibblized track + virtual DIError ReadNibbleTrack(long track, unsigned char* buf, + long* pTrackLen); + // write a track; trackLen must be <= those in image + virtual DIError WriteNibbleTrack(long track, const unsigned char* buf, + long trackLen); + + // save the current image as a 2MG file + //DIError Write2MG(const char* filename); + + // need to treat the DOS volume number as meta-data for some disks + short GetDOSVolumeNum(void) const { return fDOSVolumeNum; } + void SetDOSVolumeNum(short val) { fDOSVolumeNum = val; } + enum { kVolumeNumNotSet = -1 }; + + // some simple getters + bool GetHasSectors(void) const { return fHasSectors; } + bool GetHasBlocks(void) const { return fHasBlocks; } + bool GetHasNibbles(void) const { return fHasNibbles; } + bool GetIsEmbedded(void) const { return fpParentImg != NULL; } + + // return the current NibbleDescr + const NibbleDescr* GetNibbleDescr(void) const { return fpNibbleDescr; } + // set the NibbleDescr; we do this by copying the entry into our table + // (could improve by doing memcmp on available entries?) + void SetNibbleDescr(int idx); + void SetCustomNibbleDescr(const NibbleDescr* pDescr); + const NibbleDescr* GetNibbleDescrTable(int* pCount) const { + *pCount = fNumNibbleDescrEntries; + return fpNibbleDescrTable; + } + + // set the NuFX compression type, used when compressing or re-compressing; + // must be set before image is opened or created + void SetNuFXCompressionType(int val) { fNuFXCompressType = val; } + + /* + * Set up a progress callback to use when scanning a disk volume. Pass + * nil for "func" to disable. + * + * The callback function is expected to return "true" if all is well. + * If it returns false, kDIErrCancelled will eventually come back. + */ + typedef bool (*ScanProgressCallback)(void* cookie, const char* str, + int count); + void SetScanProgressCallback(ScanProgressCallback func, void* cookie); + /* update status dialog during disk scan; called from DiskFS code */ + bool UpdateScanProgress(const char* newStr); + + /* + * Static utility functions. + */ + // returns "true" if the files on this image have DOS structure, i.e. + // simple file types and high-ASCII text files + static bool UsesDOSFileStructure(FSFormat format) { + return (format == kFormatDOS33 || + format == kFormatDOS32 || + format == kFormatUNIDOS || + format == kFormatOzDOS || + format == kFormatRDOS33 || + format == kFormatRDOS32 || + format == kFormatRDOS3); + } + // returns "true" if we can open files on the specified filesystem + static bool CanOpenFiles(FSFormat format) { + return (format == kFormatProDOS || + format == kFormatDOS33 || + format == kFormatDOS32 || + format == kFormatPascal || + format == kFormatCPM || + format == kFormatRDOS33 || + format == kFormatRDOS32 || + format == kFormatRDOS3); + } + // returns "true" if we can create subdirectories on this filesystem + static bool IsHierarchical(FSFormat format) { + return (format == kFormatProDOS || + format == kFormatMacHFS || + format == kFormatMSDOS); + } + // returns "true" if we can create resource forks on this filesystem + static bool HasResourceForks(FSFormat format) { + return (format == kFormatProDOS || + format == kFormatMacHFS); + } + // returns "true" if the format is one of the "generics" + static bool IsGenericFormat(FSFormat format) { + return (format / 10 == DiskImg::kFormatGenericDOSOrd / 10); + } + + /* this must match DiskImg::kStdNibbleDescrs table */ + typedef enum StdNibbleDescr { + kNibbleDescrDOS33Std = 0, + kNibbleDescrDOS33Patched, + kNibbleDescrDOS33IgnoreChecksum, + kNibbleDescrDOS32Std, + kNibbleDescrDOS32Patched, + kNibbleDescrMuse32, + kNibbleDescrRDOS33, + kNibbleDescrRDOS32, + kNibbleDescrCustom, // slot for custom entry + + kNibbleDescrMAX // must be last + }; + static const NibbleDescr* GetStdNibbleDescr(StdNibbleDescr idx); + // call this once, at DLL initialization time + static void CalcNibbleInvTables(void); + // calculate block number from cyl/head/sect on 3.5" disk + static int CylHeadSect35ToBlock(int cyl, int head, int sect); + // unpack nibble data from a 3.5" disk track + static DIError UnpackNibbleTrack35(const unsigned char* nibbleBuf, + long nibbleLen, unsigned char* outputBuf, int cyl, int head, + LinearBitmap* pBadBlockMap); + // compute the #of sectors per track for cylinder N (0-79) + static int SectorsPerTrack35(int cylinder); + + // get the order in which we test for sector ordering + static void GetSectorOrderArray(SectorOrder* orderArray, SectorOrder first); + + // utility function used by HFS filename normalizer; available to apps + static inline unsigned char MacToASCII(unsigned char uch) { + if (uch < 0x20) + return '?'; + else if (uch < 0x80) + return uch; + else + return kMacHighASCII[uch - 0x80]; + } + + // Allow write access to physical disk 0. This is usually the boot disk, + // but with some BIOS the first IDE drive is always physical 0 even if + // you're booting from SATA. This only has meaning under Win32. + static void SetAllowWritePhys0(bool val); + + /* + * Get string constants for enumerated values. + */ + typedef struct { int format; const char* str; } ToStringLookup; + static const char* ToStringCommon(int format, const ToStringLookup* pTable, + int tableSize); + static const char* ToString(OuterFormat format); + static const char* ToString(FileFormat format); + static const char* ToString(PhysicalFormat format); + static const char* ToString(SectorOrder format); + static const char* ToString(FSFormat format); + +private: + /* + * Fundamental disk image identification. + */ + OuterFormat fOuterFormat; // e.g. gzip + FileFormat fFileFormat; + PhysicalFormat fPhysical; + const NibbleDescr* fpNibbleDescr; // only used for "nibble" images + SectorOrder fOrder; // only used for "sector" images + FSFormat fFormat; + + /* + * This affects how the DiskImg responds to requests for reading + * a track or sector. + * + * "fFileSysOrder", together with with "fOrder", determines how + * sector numbers are translated. It describes the order that the + * DiskFS filesystem expects things to be in. If the image isn't + * sector-addressable, then it is assumed to be in linear block order. + * + * If "fSectorPairing" is set, the DiskImg treats the disk as if + * it were in OzDOS format, with one sector from two different + * volumes in a single 512-byte block. + */ + SectorOrder fFileSysOrder; + bool fSectorPairing; + int fSectorPairOffset; // which image (should be 0 or 1) + + /* + * Internal state. + */ + GenericFD* fpOuterGFD; // outer wrapper, if any (.gz only) + GenericFD* fpWrapperGFD; // Apple II image file + GenericFD* fpDataGFD; // raw Apple II data + OuterWrapper* fpOuterWrapper; // needed for outer .gz wrapper + ImageWrapper* fpImageWrapper; // disk image wrapper (2MG, SHK, etc) + DiskImg* fpParentImg; // set for embedded volumes + short fDOSVolumeNum; // specified by some wrapper formats + di_off_t fOuterLength; // total len of file + di_off_t fWrappedLength; // len of file after Outer wrapper removed + di_off_t fLength; // len of disk image (w/o wrappers) + bool fExpandable; // ProDOS .hdv can expand + bool fReadOnly; // allow writes to this image? + bool fDirty; // have we modified this image? + //bool fIsEmbedded; // is this image embedded in another? + + bool fHasSectors; // image is sector-addressable + bool fHasBlocks; // image is block-addressable + bool fHasNibbles; // image is nibble-addressable + + long fNumTracks; // for sector-addressable images + int fNumSectPerTrack; // (ditto) + long fNumBlocks; // for 512-byte block-addressable images + + unsigned char* fNibbleTrackBuf; // allocated on heap + int fNibbleTrackLoaded; // track currently in buffer + + int fNuFXCompressType; // used when compressing a NuFX image + + char* fNotes; // warnings and FYIs about DiskImg/DiskFS + + LinearBitmap* fpBadBlockMap; // used for 3.5" nibble images + + int fDiskFSRefCnt; // #of DiskFS objects pointing at us + + /* + * NibbleDescr entries. There are several standard ones, and we want + * to allow applications to define additional ones. + */ + NibbleDescr* fpNibbleDescrTable; + int fNumNibbleDescrEntries; + + /* static table of default values */ + static const NibbleDescr kStdNibbleDescrs[]; + + DIError CreateImageCommon(const char* pathName, const char* storageName, + bool skipFormat); + DIError ValidateCreateFormat(void) const; + DIError FormatSectors(GenericFD* pGFD, bool quickFormat) const; + //DIError FormatBlocks(GenericFD* pGFD) const; + + DIError CopyBytesOut(void* buf, di_off_t offset, int size) const; + DIError CopyBytesIn(const void* buf, di_off_t offset, int size); + DIError AnalyzeImageFile(const char* pathName, char fssep); + // Figure out the sector ordering for this filesystem, so we can decide + // how the sectors need to be re-arranged when we're reading them. + SectorOrder CalcFSSectorOrder(void) const; + // Handle sector order calculations. + DIError CalcSectorAndOffset(long track, int sector, SectorOrder ImageOrder, + SectorOrder fsOrder, di_off_t* pOffset, int* pNewSector); + inline bool IsLinearBlocks(SectorOrder imageOrder, SectorOrder fsOrder); + + /* + * Progress update during the filesystem scan. This only exists in the + * topmost DiskImg. (This is arguably more appropriate in DiskFS, but + * DiskFS objects don't have a notion of "parent" and are somewhat more + * ephemeral.) + */ + ScanProgressCallback fpScanProgressCallback; + void* fScanProgressCookie; + int fScanCount; + char fScanMsg[128]; + time_t fScanLastMsgWhen; + + /* + * 5.25" nibble image access. + */ + enum { + kDataSize62 = 343, // 342 bytes + checksum byte + kChunkSize62 = 86, // (0x56) + + kDataSize53 = 411, // 410 bytes + checksum byte + kChunkSize53 = 51, // (0x33) + kThreeSize = (kChunkSize53 * 3) + 1, // same as 410 - 256 + }; + DIError ReadNibbleSector(long track, int sector, void* buf, + const NibbleDescr* pNibbleDescr); + DIError WriteNibbleSector(long track, int sector, const void* buf, + const NibbleDescr* pNibbleDescr); + void DumpNibbleDescr(const NibbleDescr* pNibDescr) const; + int GetNibbleTrackLength(long track) const; + int GetNibbleTrackOffset(long track) const; + int GetNibbleTrackFormatLength(void) const { + /* return length to use when formatting for 16 sectors */ + if (fPhysical == kPhysicalFormatNib525_6656) + return kTrackLenNib525; + else if (fPhysical == kPhysicalFormatNib525_6384) + return kTrackLenNb2525; + else if (fPhysical == kPhysicalFormatNib525_Var) { + if (fFileFormat == kFileFormatTrackStar || + fFileFormat == kFileFormatFDI) + { + return kTrackLenNb2525; // use minimum possible + } + } + assert(false); + return -1; + } + int GetNibbleTrackAllocLength(void) const { + /* return length to allocate when creating an image */ + if (fPhysical == kPhysicalFormatNib525_Var && + (fFileFormat == kFileFormatTrackStar || + fFileFormat == kFileFormatFDI)) + { + // use maximum possible + return kTrackLenTrackStar525; + } + return GetNibbleTrackFormatLength(); + } + DIError LoadNibbleTrack(long track, long* pTrackLen); + DIError SaveNibbleTrack(void); + int DiskImg::FindNibbleSectorStart(const CircularBufferAccess& buffer, + int track, int sector, const NibbleDescr* pNibbleDescr, int* pVol); + void DecodeAddr(const CircularBufferAccess& buffer, int offset, + short* pVol, short* pTrack, short* pSector, short* pChksum); + inline unsigned int ConvFrom44(unsigned char val1, unsigned char val2) { + return ((val1 << 1) | 0x01) & val2; + } + DIError DecodeNibbleData(const CircularBufferAccess& buffer, int idx, + unsigned char* sctBuf, const NibbleDescr* pNibbleDescr); + void EncodeNibbleData(const CircularBufferAccess& buffer, int idx, + const unsigned char* sctBuf, const NibbleDescr* pNibbleDescr) const; + DIError DecodeNibble62(const CircularBufferAccess& buffer, int idx, + unsigned char* sctBuf, const NibbleDescr* pNibbleDescr); + void EncodeNibble62(const CircularBufferAccess& buffer, int idx, + const unsigned char* sctBuf, const NibbleDescr* pNibbleDescr) const; + DIError DecodeNibble53(const CircularBufferAccess& buffer, int idx, + unsigned char* sctBuf, const NibbleDescr* pNibbleDescr); + void EncodeNibble53(const CircularBufferAccess& buffer, int idx, + const unsigned char* sctBuf, const NibbleDescr* pNibbleDescr) const; + int TestNibbleTrack(int track, const NibbleDescr* pNibbleDescr, int* pVol); + DIError AnalyzeNibbleData(void); + inline unsigned char Conv44(unsigned short val, bool first) const { + if (first) + return (val >> 1) | 0xaa; + else + return val | 0xaa; + } + DIError FormatNibbles(GenericFD* pGFD) const; + + static const unsigned char kMacHighASCII[]; + + /* + * 3.5" nibble access + */ + static int FindNextSector35(const CircularBufferAccess& buffer, int start, + int cyl, int head, int* pSector); + static bool DecodeNibbleSector35(const CircularBufferAccess& buffer, + int start, unsigned char* sectorBuf, unsigned char* readChecksum, + unsigned char* calcChecksum); + static bool UnpackChecksum35(const CircularBufferAccess& buffer, + int offset, unsigned char* checksumBuf); + static void EncodeNibbleSector35(const unsigned char* sectorData, + unsigned char* outBuf); + + /* static data tables */ + static unsigned char kDiskBytes53[32]; + static unsigned char kDiskBytes62[64]; + static unsigned char kInvDiskBytes53[256]; + static unsigned char kInvDiskBytes62[256]; + enum { kInvInvalidValue = 0xff }; + +private: // some C++ stuff to block behavior we don't support + DiskImg& operator=(const DiskImg&); + DiskImg(const DiskImg&); +}; + + + +/* + * Disk filesystem class, roughly equivalent to a GS/OS FST. This is an + * abstract base class. + * + * Static functions know how to access a DiskImg and figure out what kind + * of image we have. Once known, the appropriate sub-class can be + * instantiated. + * + * We maintain a linear list of files to make it easy for applications to + * traverse the full set of files. Sometimes this makes it hard for us to + * update internally (especially HFS). With some minor cleverness it + * should be possible to switch to a tree structure while retaining the + * linear "get next" API. This would be a big help for ProDOS and HFS. + * + * NEED: some notification mechanism for changes to files and/or block + * editing of the disk (especially with regard to open sub-volumes). If + * a disk volume open for file-by-file viewing is modified with the disk + * editor, we should close the file when the disk editor exits. + * + * NEED? disk utilities, such as "disk crunch", for Pascal volumes. Could + * be written externally, but might as well keep fs knowledge embedded. + * + * MISSING: there is no way to override the image analyzer when working + * with sub-volumes. Actually, there is; it just has to happen *after* + * the DiskFS has been created. We should provide an approach that either + * stifles the DiskFS creation, or allows us to override and replace the + * internal DiskFS so we can pop up a sub-volume list, show what we *think* + * is there, and then let the user pick a volume and pick overrides (mainly + * for use in the disk editor). Not sure if we want the changes to "stick"; + * we probably do. Q: does the "scan for sub-volumes" attribute propagate + * recursively to each sub-sub-volume? Probably. + * + * NOTE to self: should make "test" functions more lax when called + * from here, on the assumption that the caller is knowledgeable. Perhaps + * an independent "strictness" variable that can be cranked down through + * multiple calls to AnalyzeImage?? + */ +class DISKIMG_API DiskFS { +public: + /* + * Information about volume usage. + * + * Each "chunk" is a track/sector on a DOS disk or a block on a ProDOS + * or Pascal disk. CP/M really ought to use 1K blocks, but for + * convenience we're just using 512-byte blocks (it's up to the CP/M + * code to set two "chunks" per block). + * + * NOTE: the current DOS/ProDOS/Pascal code is sloppy when it comes to + * keeping this structure up to date. HFS doesn't use it at all. This + * has always been a low-priority feature. + */ + class DISKIMG_API VolumeUsage { + public: + VolumeUsage(void) { + fByBlocks = false; + fTotalChunks = -1; + fNumSectors = -1; + //fFreeChunks = -1; + fList = NULL; + fListSize = -1; + } + ~VolumeUsage(void) { + delete[] fList; + } + + /* + * These values MUST fit in five bits. + * + * Suggested disk map colors: + * 0 = unknown (color-key pink) + * 1 = conflict (medium-red) + * 2 = boot loader (dark-blue) + * 3 = volume dir (light-blue) + * 4 = subdir (medium-blue) + * 5 = user data (medium-green) + * 6 = user index blocks (light-green) + * 7 = embedded filesys (yellow) + * + * THOUGHT: Add flag for I/O error (nibble images) -- requires + * automatic disk verify pass. (Or maybe could be done manually + * on request?) + * + * unused --> black + * marked-used-but-not-used --> dark-red + * used-but-not-marked-used --> orange + */ + typedef enum { + kChunkPurposeUnknown = 0, + kChunkPurposeConflict = 1, // two or more different things + kChunkPurposeSystem = 2, // boot blocks, volume bitmap + kChunkPurposeVolumeDir = 3, // volume dir (or only dir) + kChunkPurposeSubdir = 4, // ProDOS sub-directory + kChunkPurposeUserData = 5, // file on this filesystem + kChunkPurposeFileStruct = 6, // index blocks, T/S lists + kChunkPurposeEmbedded = 7, // embedded filesystem + // how about: outside range claimed by disk, e.g. fTotalBlocks on + // 800K ProDOS disk in a 32MB CFFA volume? + } ChunkPurpose; + + typedef struct ChunkState { + bool isUsed; + bool isMarkedUsed; + ChunkPurpose purpose; // only valid if isUsed is set + } ChunkState; + + // initialize, configuring for either blocks or sectors + DIError Create(long numBlocks); + DIError Create(long numTracks, long numSectors); + bool GetInitialized(void) const { return (fList != NULL); } + + // return the number of chunks on this disk + long GetNumChunks(void) const { return fTotalChunks; } + + // return the number of unallocated chunks, taking into account + // both the free-chunk bitmap (if any) and the actual usage + long GetActualFreeChunks(void) const; + + // return the state of the specified chunk + DIError GetChunkState(long block, ChunkState* pState) const; + DIError GetChunkState(long track, long sector, + ChunkState* pState) const; + + // set the state of a particular chunk (should only be done by + // the DiskFS sub-classes) + DIError SetChunkState(long block, const ChunkState* pState); + DIError SetChunkState(long track, long sector, + const ChunkState* pState); + + void Dump(void) const; // debugging + + private: + DIError GetChunkStateIdx(int idx, ChunkState* pState) const; + DIError SetChunkStateIdx(int idx, const ChunkState* pState); + inline char StateToChar(ChunkState* pState) const; + + /* + * Chunk state is stored as a set of bits in one byte: + * + * 0-4: how is block used (only has meaning if bit 6 is set) + * 5: for nibble images, indicates the block or sector is unreadable + * 6: is block used by something (0=no, 1=yes) + * 7: is block marked "in use" by system map (0=no, 1=yes) + * + * [ Consider reducing "purpose" to 0-3 and adding bad block bit for + * nibble images and physical media.] + */ + enum { + kChunkPurposeMask = 0x1f, // ChunkPurpose enum + kChunkDamagedFlag = 0x20, + kChunkUsedFlag = 0x40, + kChunkMarkedUsedFlag = 0x80, + }; + + bool fByBlocks; + long fTotalChunks; + long fNumSectors; // only valid if !fByBlocks + //long fFreeChunks; + unsigned char* fList; + int fListSize; + }; // end of VolumeUsage class + + /* + * List of sub-volumes. The SubVolume owns the DiskImg and DiskFS + * that are handed to it, so they can be deleted when the SubVolume + * is deleted as part of destroying the parent. + */ + class SubVolume { + public: + SubVolume(void) : fpDiskImg(NULL), fpDiskFS(NULL), + fpPrev(NULL), fpNext(NULL) {} + ~SubVolume(void) { + delete fpDiskFS; // must close first; may need flush to DiskImg + delete fpDiskImg; + } + + void Create(DiskImg* pDiskImg, DiskFS* pDiskFS) { + assert(pDiskImg != NULL); + assert(pDiskFS != NULL); + fpDiskImg = pDiskImg; + fpDiskFS = pDiskFS; + } + + DiskImg* GetDiskImg(void) const { return fpDiskImg; } + DiskFS* GetDiskFS(void) const { return fpDiskFS; } + + SubVolume* GetPrev(void) const { return fpPrev; } + void SetPrev(SubVolume* pSubVol) { fpPrev = pSubVol; } + SubVolume* GetNext(void) const { return fpNext; } + void SetNext(SubVolume* pSubVol) { fpNext = pSubVol; } + + private: + DiskImg* fpDiskImg; + DiskFS* fpDiskFS; + + SubVolume* fpPrev; + SubVolume* fpNext; + }; // end of SubVolume class + + + + /* + * Start of DiskFS declarations. + */ +public: + typedef enum SubScanMode { + kScanSubUnknown = 0, + kScanSubDisabled, + kScanSubEnabled, + kScanSubContainerOnly, + } SubScanMode; + + + DiskFS(void) { + fpA2Head = fpA2Tail = NULL; + fpSubVolumeHead = fpSubVolumeTail = NULL; + fpImg = NULL; + fScanForSubVolumes = kScanSubDisabled; + + fParmTable[kParm_CreateUnique] = 0; + fParmTable[kParmProDOS_AllowLowerCase] = 1; + fParmTable[kParmProDOS_AllocSparse] = 1; + } + virtual ~DiskFS(void) { + DeleteSubVolumeList(); + DeleteFileList(); + SetDiskImg(NULL); + } + + /* + * Static FSFormat-analysis functions, called by the DiskImg AnalyzeImage + * function. Capable of determining with a high degree of accuracy + * what format the disk is in, yet remaining flexible enough to + * function correctly with variations (like DOS3.3 disks with + * truncated TOCs and ProDOS images from hard drives). + * + * The "pOrder" and "pFormat" arguments are in/out. Set them to the + * appropriate "unknown" enum value on entry. If something is known + * of the order or format, put that in instead; in some cases this will + * bias the proceedings, which is useful for efficiency and for making + * overrides work correctly. + * + * On success, these return kDIErrNone and set "*pOrder". On failure, + * they return nonzero and leave "*pOrder" unmodified. + * + * Each DiskFS sub-class should declare a TestFS function. It's not + * virtual void here because, since it's called before the DiskFS is + * created, it must be static. + */ + typedef enum FSLeniency { kLeniencyNot, kLeniencyVery } FSLeniency; + //static DIError TestFS(const DiskImg* pImg, DiskImg::SectorOrder* pOrder, + // DiskImg::FSFormat* pFormat, FSLeniency leniency); + + /* + * Load the disk contents and (if enabled) scan for sub-volumes. + * + * If "headerOnly" is set, we just do a quick scan of the volume header + * to get basic information. The deep file scan is skipped (but can + * be done later). (Sub-classes can choose to ignore the flag and + * always do the full scan; this is an optimization.) Guaranteed to + * set the volume name and volume block/sector count. + * + * If a progress callback is set up, this can return with a "cancelled" + * result, which should not be treated as a failure. + */ + typedef enum { kInitUnknown = 0, kInitHeaderOnly, kInitFull } InitMode; + virtual DIError Initialize(DiskImg* pImg, InitMode initMode) = 0; + + /* + * Format the disk with the appropriate filesystem, creating all filesystem + * structures and (when appropriate) boot blocks. + */ + virtual DIError Format(DiskImg* pDiskImg, const char* volName) + { return kDIErrNotSupported; } + + /* + * Pass in a full path to normalize, and a buffer to copy the output + * into. On entry "pNormalizedBufLen" holds the length of the buffer. + * On exit, it holds the size of the buffer required to hold the + * normalized string. If the buffer is nil or isn't big enough, no part + * of the normalized path will be copied into the buffer, and a specific + * error (kDIErrDataOverrun) will be returned. + */ + virtual DIError NormalizePath(const char* path, char fssep, + char* normalizedBuf, int* pNormalizedBufLen) + { return kDIErrNotSupported; } + + + /* + * Create a file. The CreateParms struct specifies the full set of file + * details. To remain FS-agnostic, use the NufxLib constants + * (kNuStorageDirectory, kNuAccessUnlocked, etc). They match up with + * their ProDOS equivalents, and I promise to make them work right. + * + * On success, the file exists as a fully-formed, zero-length file. A + * pointer to the relevant A2File structure is returned. + */ + enum { + /* valid values for CreateParms; must match ProDOS enum */ + kStorageSeedling = 1, + kStorageExtended = 5, + kStorageDirectory = 13, + }; + typedef struct CreateParms { + const char* pathName; // full pathname + char fssep; + int storageType; // determines normal, subdir, or forked + long fileType; + long auxType; + int access; + time_t createWhen; + time_t modWhen; + } CreateParms; + virtual DIError CreateFile(const CreateParms* pParms, A2File** ppNewFile) + { return kDIErrNotSupported; } + + /* + * Delete a file from the disk. + */ + virtual DIError DeleteFile(A2File* pFile) + { return kDIErrNotSupported; } + + /* + * Rename a file. + */ + virtual DIError RenameFile(A2File* pFile, const char* newName) + { return kDIErrNotSupported; } + + /* + * Alter file attributes. + */ + virtual DIError SetFileInfo(A2File* pFile, long fileType, long auxType, + long accessFlags) + { return kDIErrNotSupported; } + + /* + * Rename a volume. Also works for changing the disk volume number. + */ + virtual DIError RenameVolume(const char* newName) + { return kDIErrNotSupported; } + + + // Accessor + DiskImg* GetDiskImg(void) const { return fpImg; } + + // Test file and volume names (and volume numbers) + // [these need to be static functions for some things... hmm] + //virtual bool IsValidFileName(const char* name) const { return false; } + //virtual bool IsValidVolumeName(const char* name) const { return false; } + + // Return the disk volume name, or NULL if there isn't one. + virtual const char* GetVolumeName(void) const = 0; + + // Return a printable string identifying the FS type and volume + virtual const char* GetVolumeID(void) const = 0; + + // Return the "bare" volume name. For formats where the volume name + // is actually a number (e.g. DOS 3.3), this returns just the number. + // For formats without a volume name or number (e.g. CP/M), this returns + // nil, indicating that any attempt to change the volume name will fail. + virtual const char* GetBareVolumeName(void) const = 0; + + // Returns "false" if we only support read-only access to this FS type + virtual bool GetReadWriteSupported(void) const = 0; + + // Returns "true" if the filesystem shows evidence of damage. + virtual bool GetFSDamaged(void) const = 0; + + // Returns number of blocks recognized by the filesystem, or -1 if the + // FS isn't block-oriented (e.g. DOS 3.2/3.3) + virtual long GetFSNumBlocks(void) const { return -1; } + + // Get the next file in the list. Start by passing in NULL to get the + // head of the list. Returns NULL when the end of the list is reached. + A2File* GetNextFile(A2File* pCurrent) const; + + // Get a count of the files and directories on this disk. + long GetFileCount(void) const; + + /* + * Find a file by case-insensitive pathname. Assumes fssep=':'. The + * compare function can be overridden for systems like HFS, where "case + * insensitive" has a different meaning because of the native + * character set. + * + * The A2File* returned should not be deleted. + */ + typedef int (*StringCompareFunc)(const char* str1, const char* str2); + A2File* GetFileByName(const char* pathName, StringCompareFunc func = NULL); + + // This controls scanning for sub-volumes; must be set before Initialize(). + SubScanMode GetScanForSubVolumes(void) const { return fScanForSubVolumes; } + void SetScanForSubVolumes(SubScanMode val) { fScanForSubVolumes = val; } + + // some constants for non-ProDOS filesystems to use + enum { kFileAccessLocked = 0x01, kFileAccessUnlocked = 0xc3 }; + + + /* + * We use this as a filename separator character (i.e. between pathname + * components) in all filenames. It's useful to standardize on this + * so that behavior is consistent across all disk and archive formats. + * + * The choice of ':' is good because it's invalid in filenames on + * Windows, Mac OS, GS/OS, and pretty much anywhere else we could be + * running except for UNIX. It's valid under DOS 3.3, but since you + * can't have subdirectories under DOS there's no risk of confusion. + */ + enum { kDIFssep = ':' }; + + + /* + * Return the volume use map. This is a non-const function because + * it might need to do a "just-in-time" usage map update. It returns + * const to keep non-DiskFS classes from altering the map. + */ + const VolumeUsage* GetVolumeUsageMap(void) { + if (fVolumeUsage.GetInitialized()) + return &fVolumeUsage; + else + return NULL; + } + + /* + * Return the total space and free space, in either blocks or sectors + * as appropriate. "*pUnitSize" will be kBlockSize or kSectorSize. + */ + virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const = 0; + + + /* + * Get the next volume in the list. Start by passing in NULL to get the + * head of the list. Returns NULL when the end of the list is reached. + */ + SubVolume* GetNextSubVolume(const SubVolume* pCurrent) const; + + /* + * Set some parameters to tell the DiskFS how to operate. Some of + * these are FS-specific, some may be general. + * + * Parameters are set in the current object and all sub-volume objects. + * + * The enum is part of the interface and must be rigidly defined, but + * it is also used to size an array so it can't be too sparse. + */ + typedef enum DiskFSParameter { + kParmUnknown = 0, + + kParm_CreateUnique = 1, // make new filenames unique + + kParmProDOS_AllowLowerCase = 10, // allow lower case and spaces + kParmProDOS_AllocSparse = 11, // don't store empty blocks + + kParmMax // must be last entry + } DiskFSParameter; + long GetParameter(DiskFSParameter parm); + void SetParameter(DiskFSParameter parm, long val); + + /* + * Flush changed data. + * + * The individual filesystems shouldn't generally do any caching; if + * they do, we would want a virtual "FlushFS()" that gets called by + * Flush. The better answer is to cache in DiskImg, which works for + * everything, and knows if the underlying storage is already in RAM. + * + * For the most part this just needs to recursively flush the DiskImg + * objects in all of the sub-volumes and then the current volume. This + * is a no-op in most cases, but if the archive is compressed this will + * cause a new, compressed archive to be created. + * + * The "mode" value determines whether or not we do "heavy" flushes. It's + * very handy to be able to do "slow" flushes for anything that is being + * written directly to disk (as opposed to being run through Deflate), + * so that the UI doesn't indicate that they're partially written when + * in fact they're fully written. + */ + DIError Flush(DiskImg::FlushMode mode); + + /* + * Set the read-only flag on our DiskImg and those of our subvolumes. + * Used to ensure that a DiskFS with un-flushed data can be deleted + * without corrupting the volume. + */ + void SetAllReadOnly(bool val); + + // debug dump + void DumpFileList(void); + +protected: + /* + * Set the DiskImg pointer. Updates the reference count in DiskImg. + */ + void SetDiskImg(DiskImg* pImg); + + // once added, we own the pDiskImg and the pDiskFS (DO NOT pass the + // same DiskImg or DiskFS in more than once!). Note this copies the + // fParmTable and other stuff (fScanForSubVolumes) from parent to child. + void AddSubVolumeToList(DiskImg* pDiskImg, DiskFS* pDiskFS); + // add files to fpA2Head/fpA2Tail + void AddFileToList(A2File* pFile); + // only need for hierarchical filesystems; insert file after pPrev + void InsertFileInList(A2File* pFile, A2File* pPrev); + // delete an entry + void DeleteFileFromList(A2File* pFile); + + // scan for damaged or suspicious files + void ScanForDamagedFiles(bool* pDamaged, bool* pSuspicious); + + // pointer to the DiskImg structure underlying this filesystem + DiskImg* fpImg; + + VolumeUsage fVolumeUsage; + SubScanMode fScanForSubVolumes; + + +private: + A2File* SkipSubdir(A2File* pSubdir); + void CopyInheritables(DiskFS* pNewFS); + void DeleteFileList(void); + void DeleteSubVolumeList(void); + + long fParmTable[kParmMax]; // for DiskFSParameter + + A2File* fpA2Head; + A2File* fpA2Tail; + SubVolume* fpSubVolumeHead; + SubVolume* fpSubVolumeTail; + +private: + DiskFS& operator=(const DiskFS&); + DiskFS(const DiskFS&); +}; + + +/* + * Apple II file class, representing a file on an Apple II volume. This is an + * abstract base class. + * + * There is a different sub-class for each filesystem type. The A2File object + * encapsulates all of the knowledge required to read a file from a disk + * image. + * + * The prev/next pointers, used to maintain a linked list of files, are only + * accessible from DiskFS functions. At some point we may want to rearrange + * the way this is handled, e.g. by not maintaining a list at all, so it's + * important that everything go through DiskFS requests. + * + * The FSFormat is made an explicit member, because sub-classes may not + * understand exactly where the file came from (e.g. was it DOS3.2 or + * DOS 3.3). Somebody might care. + * + * + * NOTE TO SELF: open files need to be generalized. Right now the internal + * implementations only allow a single open, which is okay for our purposes + * but bad for a general FS implementation. As it stands, you can't even + * open both forks at the same time on ProDOS/HFS. + * + * UMMM: The handling of "damaged" files could be more consistent. + */ +class DISKIMG_API A2File { +public: + friend class DiskFS; + + A2File(DiskFS* pDiskFS) : fpDiskFS(pDiskFS) { + fpPrev = fpNext = NULL; + fFileQuality = kQualityGood; + } + virtual ~A2File(void) {} + + /* + * All Apple II files have certain characteristics, of which ProDOS + * is roughly a superset. (Yes, you can have HFS on a IIgs, but + * all that fancy creator type stuff is decidedly Mac-centric. Still, + * we want to assume 4-byte file and aux types.) + * + * NEED: something distinguishing between files and disk images? + * + * NOTE: there is no guarantee that GetPathName will return unique values + * (duplicates are possible). We don't guarantee that you won't get an + * empty string back (it's valid to have an empty filename in the dir in + * DOS 3.3, and it's possible for other filesystems to be damaged). The + * pathname may receive some minor sanitizing, e.g. removal or conversion + * of high ASCII and control characters, but some filesystems (like HFS) + * make use of them. + * + * We do guarantee that the contents of subdirectories are grouped + * together. This makes it much easier to construct a hierarchy out of + * the linear list. This becomes important when dynamically adding + * files to a disk. + */ + virtual const char* GetFileName(void) const = 0; // name of this file + virtual const char* GetPathName(void) const = 0; // full path + virtual char GetFssep(void) const = 0; // '\0' if none + virtual long GetFileType(void) const = 0; + virtual long GetAuxType(void) const = 0; + virtual long GetAccess(void) const = 0; // ProDOS-style perms + virtual time_t GetCreateWhen(void) const = 0; + virtual time_t GetModWhen(void) const = 0; + virtual di_off_t GetDataLength(void) const = 0; // len of data fork + virtual di_off_t GetDataSparseLength(void) const = 0; // len w/o sparse areas + virtual di_off_t GetRsrcLength(void) const = 0; // len or -1 if no rsrc + virtual di_off_t GetRsrcSparseLength(void) const = 0; // len or -1 if no rsrc + virtual DiskImg::FSFormat GetFSFormat(void) const { + return fpDiskFS->GetDiskImg()->GetFSFormat(); + } + virtual bool IsDirectory(void) const { return false; } + virtual bool IsVolumeDirectory(void) const { return false; } + + /* + * Open a file. Treat the A2FileDescr like an fd. + */ + virtual DIError Open(A2FileDescr** ppOpenFile, bool readOnly, + bool rsrcFork = false) = 0; + + /* + * This is called by the A2FileDescr object when somebody invokes Close(). + * The A2File object should remove the A2FileDescr from its list of open + * files and delete the storage. The implementation must not call the + * A2FileDescr's Close function, since that would cause a recursive loop. + * + * This should not be called by an application. + */ + virtual void CloseDescr(A2FileDescr* pOpenFile) = 0; + + /* + * This is only useful for hierarchical filesystems like ProDOS, + * where the order of items in the linear list is significant. It + * allows an unambiguous determination of which subdir a file resides + * in, even if somebody has sector-edited the filesystem so that two + * subdirs have the same name. (It's also a bit speedier to compare + * than pathname substrings would be.) + */ + virtual A2File* GetParent(void) const { return NULL; } + + /* + * Returns "true" if either fork of the file is open, "false" if not. + */ + virtual bool IsFileOpen(void) const = 0; + + virtual void Dump(void) const = 0; // debugging + + typedef enum FileQuality { + kQualityUnknown = 0, + kQualityGood, + kQualitySuspicious, + kQualityDamaged, + } FileQuality; + virtual FileQuality GetQuality(void) const { return fFileQuality; } + virtual void SetQuality(FileQuality quality); + virtual void ResetQuality(void); + + DiskFS* GetDiskFS(void) const { return fpDiskFS; } + +protected: + DiskFS* fpDiskFS; + virtual void SetParent(A2File* pParent) { /* do nothing */ } + +private: + A2File* GetPrev(void) const { return fpPrev; } + void SetPrev(A2File* pFile) { fpPrev = pFile; } + A2File* GetNext(void) const { return fpNext; } + void SetNext(A2File* pFile) { fpNext = pFile; } + + // Set when file structure is damaged and application should not try + // to open the file. + FileQuality fFileQuality; + + A2File* fpPrev; + A2File* fpNext; + + +private: + A2File& operator=(const A2File&); + A2File(const A2File&); +}; + + +/* + * Abstract representation of an open file. This contains all active state + * and all information required to read and write a file. + * + * Do not delete these objects; instead, invoke the Close method, so that they + * can be removed from the parents' list of open files. + * TODO: consider making the destructor "protected" + */ +class DISKIMG_API A2FileDescr { +public: + A2FileDescr(A2File* pFile) : fpFile(pFile), fProgressUpdateFunc(NULL) {} + virtual ~A2FileDescr(void) { fpFile = NULL; /*paranoia*/ } + + virtual DIError Read(void* buf, size_t len, size_t* pActual = NULL) = 0; + virtual DIError Write(const void* buf, size_t len, size_t* pActual = NULL) = 0; + virtual DIError Seek(di_off_t offset, DIWhence whence) = 0; + virtual di_off_t Tell(void) = 0; + virtual DIError Close(void) = 0; + + virtual DIError GetStorage(long sectorIdx, long* pTrack, long* pSector) + const = 0; + virtual DIError GetStorage(long blockIdx, long* pBlock) + const = 0; + virtual long GetSectorCount(void) const = 0; + virtual long GetBlockCount(void) const = 0; + + virtual DIError Rewind(void) { return Seek(0, kSeekSet); } + + A2File* GetFile(void) const { return fpFile; } + + /* + * Progress update callback mechanism. Pass in the length or (for writes) + * expected length of the file. This invokes the callback with the + * lengths and some pointers. + * + * If the progress callback returns "true", progress continues. If it + * returns "false", the read or write function will return with + * kDIErrCancelled. + */ + typedef bool (*ProgressUpdater)(A2FileDescr* pFile, di_off_t max, + di_off_t current, void* vState); + void SetProgressUpdater(ProgressUpdater func, di_off_t max, void* state) { + fProgressUpdateFunc = func; + fProgressUpdateMax = max; + fProgressUpdateState = state; + } + void ClearProgressUpdater(void) { + fProgressUpdateFunc = NULL; + } + +protected: + A2File* fpFile; + + /* + * Internal utility functions for mapping blocks to sectors and vice-versa. + */ + virtual void TrackSectorToBlock(long track, long sector, long* pBlock, + bool* pSecondHalf) const + { + int numSectPerTrack = fpFile->GetDiskFS()->GetDiskImg()->GetNumSectPerTrack(); + assert(track < fpFile->GetDiskFS()->GetDiskImg()->GetNumTracks()); + assert(sector < numSectPerTrack); + long dblBlock = track * numSectPerTrack + sector; + *pBlock = dblBlock / 2; + *pSecondHalf = (dblBlock & 0x01) != 0; + } + virtual void BlockToTrackSector(long block, bool secondHalf, long* pTrack, + long* pSector) const + { + assert(block < fpFile->GetDiskFS()->GetDiskImg()->GetNumBlocks()); + int numSectPerTrack = fpFile->GetDiskFS()->GetDiskImg()->GetNumSectPerTrack(); + int dblBlock = block * 2; + if (secondHalf) + dblBlock++; + *pTrack = dblBlock / numSectPerTrack; + *pSector = dblBlock % numSectPerTrack; + } + + /* + * Call this from FS-specific read/write functions on successful + * completion (and perhaps more often for filesystems with potentially + * large files, e.g. ProDOS/HFS). + * + * Test the return value; if "false", user wishes to cancel the op, and + * long read or write calls should return immediately. + */ + inline bool UpdateProgress(di_off_t current) { + if (fProgressUpdateFunc != NULL) { + return (*fProgressUpdateFunc)(this, fProgressUpdateMax, current, + fProgressUpdateState); + } else { + return true; + } + } + +private: + A2FileDescr& operator=(const A2FileDescr&); + A2FileDescr(const A2FileDescr&); + + /* storage for progress update goodies */ + ProgressUpdater fProgressUpdateFunc; + di_off_t fProgressUpdateMax; + void* fProgressUpdateState; +}; + +}; // namespace DiskImgLib + +#endif /*__DISK_IMG__*/ diff --git a/diskimg/DiskImgDetail.h b/diskimg/DiskImgDetail.h new file mode 100644 index 0000000..44d02b7 --- /dev/null +++ b/diskimg/DiskImgDetail.h @@ -0,0 +1,2964 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Sub-classes of the base classes defined in DiskImg.h. + * + * Most applications will not need to include this file, because the + * polymorphic interfaces do everything they need. If something needs to + * examine the actual directory structure of a file, it can do so through + * these declarations. + */ +#ifndef __DISKIMGDETAIL__ +#define __DISKIMGDETAIL__ + +#include "../prebuilt/NufxLib.h" +#define ZLIB_DLL +#include "../prebuilt/zlib.h" + +#include "DiskImg.h" + +#ifndef EXCISE_GPL_CODE +# include "libhfs/hfs.h" +#endif + +namespace DiskImgLib { + +/* + * =========================================================================== + * Outer wrappers + * =========================================================================== + */ + +/* + * Outer wrapper class, representing a compression utility or archive + * format that must be stripped away so we can get to the Apple II stuff. + * + * Outer wrappers usually have a filename embedded in them, representing + * the original name of the file. We want to use the extension from this + * name when evaluating the file contents. Usually. + */ +class OuterWrapper { +public: + OuterWrapper(void) {} + virtual ~OuterWrapper(void) {} + + // all sub-classes should have one of these + //static DIError Test(GenericFD* pGFD, long outerLength); + + // open the file and prepare to access it; fills out return values + // NOTE: pGFD must be a GFDFile. + virtual DIError Load(GenericFD* pOuterGFD, di_off_t outerLength, bool readOnly, + di_off_t* pWrapperLength, GenericFD** ppWrapperGFD) = 0; + + virtual DIError Save(GenericFD* pOuterGFD, GenericFD* pWrapperGFD, + di_off_t wrapperLength) = 0; + + // set on recoverable errors, like a CRC failure + virtual bool IsDamaged(void) const = 0; + + // indicate that we don't have a "fast" flush + virtual bool HasFastFlush(void) const { return false; } + + virtual const char* GetExtension(void) const = 0; + +private: + OuterWrapper& operator=(const OuterWrapper&); + OuterWrapper(const OuterWrapper&); +}; + +class OuterGzip : public OuterWrapper { +public: + OuterGzip(void) { fWrapperDamaged = false; } + virtual ~OuterGzip(void) {} + + static DIError Test(GenericFD* pGFD, di_off_t outerLength); + virtual DIError Load(GenericFD* pGFD, di_off_t outerLength, bool readOnly, + di_off_t* pTotalLength, GenericFD** ppNewGFD); + virtual DIError Save(GenericFD* pOuterGFD, GenericFD* pWrapperGFD, + di_off_t wrapperLength); + + virtual bool IsDamaged(void) const { return fWrapperDamaged; } + + virtual const char* GetExtension(void) const { return NULL; } + +private: + DIError ExtractGzipImage(gzFile gzfp, char** pBuf, di_off_t* pLength); + DIError CloseGzip(void); + + // Largest possible ProDOS volume; quite a bit to hold in RAM. Add a + // little extra for .hdv format. + enum { kMaxUncompressedSize = kGzipMax +256 }; + + bool fWrapperDamaged; +}; + +class OuterZip : public OuterWrapper { +public: + OuterZip(void) : fStoredFileName(NULL), fExtension(NULL) {} + virtual ~OuterZip(void) { + delete[] fStoredFileName; + delete[] fExtension; + } + + static DIError Test(GenericFD* pGFD, di_off_t outerLength); + virtual DIError Load(GenericFD* pGFD, di_off_t outerLength, bool readOnly, + di_off_t* pTotalLength, GenericFD** ppNewGFD); + virtual DIError Save(GenericFD* pOuterGFD, GenericFD* pWrapperGFD, + di_off_t wrapperLength); + + virtual bool IsDamaged(void) const { return false; } + + virtual const char* GetExtension(void) const { return fExtension; } + +private: + class LocalFileHeader { + public: + LocalFileHeader(void) : + fVersionToExtract(0), + fGPBitFlag(0), + fCompressionMethod(0), + fLastModFileTime(0), + fLastModFileDate(0), + fCRC32(0), + fCompressedSize(0), + fUncompressedSize(0), + fFileNameLength(0), + fExtraFieldLength(0), + fFileName(NULL) + {} + virtual ~LocalFileHeader(void) { delete[] fFileName; } + + DIError Read(GenericFD* pGFD); + DIError Write(GenericFD* pGFD); + void SetFileName(const char* name); + + // unsigned long fSignature; + unsigned short fVersionToExtract; + unsigned short fGPBitFlag; + unsigned short fCompressionMethod; + unsigned short fLastModFileTime; + unsigned short fLastModFileDate; + unsigned long fCRC32; + unsigned long fCompressedSize; + unsigned long fUncompressedSize; + unsigned short fFileNameLength; + unsigned short fExtraFieldLength; + unsigned char* fFileName; + // extra field + + enum { + kSignature = 0x04034b50, + kLFHLen = 30, // LocalFileHdr len, excl. var fields + }; + + void Dump(void) const; + }; + + class CentralDirEntry { + public: + CentralDirEntry(void) : + fVersionMadeBy(0), + fVersionToExtract(0), + fGPBitFlag(0), + fCompressionMethod(0), + fLastModFileTime(0), + fLastModFileDate(0), + fCRC32(0), + fCompressedSize(0), + fUncompressedSize(0), + fFileNameLength(0), + fExtraFieldLength(0), + fFileCommentLength(0), + fDiskNumberStart(0), + fInternalAttrs(0), + fExternalAttrs(0), + fLocalHeaderRelOffset(0), + fFileName(NULL), + fFileComment(NULL) + {} + virtual ~CentralDirEntry(void) { + delete[] fFileName; + delete[] fFileComment; + } + + DIError Read(GenericFD* pGFD); + DIError Write(GenericFD* pGFD); + void SetFileName(const char* name); + + // unsigned long fSignature; + unsigned short fVersionMadeBy; + unsigned short fVersionToExtract; + unsigned short fGPBitFlag; + unsigned short fCompressionMethod; + unsigned short fLastModFileTime; + unsigned short fLastModFileDate; + unsigned long fCRC32; + unsigned long fCompressedSize; + unsigned long fUncompressedSize; + unsigned short fFileNameLength; + unsigned short fExtraFieldLength; + unsigned short fFileCommentLength; + unsigned short fDiskNumberStart; + unsigned short fInternalAttrs; + unsigned long fExternalAttrs; + unsigned long fLocalHeaderRelOffset; + unsigned char* fFileName; + // extra field + unsigned char* fFileComment; // alloc with new[] + + void Dump(void) const; + + enum { + kSignature = 0x02014b50, + kCDELen = 46, // CentralDirEnt len, excl. var fields + }; + }; + + class EndOfCentralDir { + public: + EndOfCentralDir(void) : + fDiskNumber(0), + fDiskWithCentralDir(0), + fNumEntries(0), + fTotalNumEntries(0), + fCentralDirSize(0), + fCentralDirOffset(0), + fCommentLen(0) + {} + virtual ~EndOfCentralDir(void) {} + + DIError ReadBuf(const unsigned char* buf, int len); + DIError Write(GenericFD* pGFD); + + // unsigned long fSignature; + unsigned short fDiskNumber; + unsigned short fDiskWithCentralDir; + unsigned short fNumEntries; + unsigned short fTotalNumEntries; + unsigned long fCentralDirSize; + unsigned long fCentralDirOffset; // offset from first disk + unsigned short fCommentLen; + // archive comment + + enum { + kSignature = 0x06054b50, + kEOCDLen = 22, // EndOfCentralDir len, excl. comment + }; + + void Dump(void) const; + }; + + enum { + kDataDescriptorSignature = 0x08074b50, + + kMaxCommentLen = 65535, // longest possible in ushort + kMaxEOCDSearch = kMaxCommentLen + EndOfCentralDir::kEOCDLen, + + kZipFssep = '/', + kDefaultVersion = 20, + kMaxUncompressedSize = kGzipMax +256, + }; + enum { + kCompressStored = 0, // no compression + //kCompressShrunk = 1, + //kCompressImploded = 6, + kCompressDeflated = 8, // standard deflate + }; + + static DIError ReadCentralDir(GenericFD* pGFD, di_off_t outerLength, + CentralDirEntry* pDirEntry); + DIError ExtractZipEntry(GenericFD* pOuterGFD, CentralDirEntry* pCDE, + unsigned char** pBuf, di_off_t* pLength); + DIError InflateGFDToBuffer(GenericFD* pGFD, unsigned long compSize, + unsigned long uncompSize, unsigned char* buf); + DIError DeflateGFDToGFD(GenericFD* pDst, GenericFD* pSrc, di_off_t length, + di_off_t* pCompLength, unsigned long* pCRC); + +private: + void SetExtension(const char* ext); + void SetStoredFileName(const char* name); + void GetMSDOSTime(unsigned short* pDate, unsigned short* pTime); + void DOSTime(time_t when, unsigned short* pDate, unsigned short* pTime); + + char* fStoredFileName; + char* fExtension; +}; + + +/* + * =========================================================================== + * Image wrappers + * =========================================================================== + */ + +/* + * Image wrapper class, representing the format of the Windows files. + * Might be "raw" data, might be data with a header, might be a complex + * or compressed format that must be extracted to a buffer. + */ +class ImageWrapper { +public: + ImageWrapper(void) {} + virtual ~ImageWrapper(void) {} + + // all sub-classes should have one of these + // static DIError Test(GenericFD* pGFD, di_off_t wrappedLength); + + // open the file and prepare to access it; fills out return values + virtual DIError Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, + di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, + DiskImg::SectorOrder* pOrder, short* pDiskVolNum, + LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD) = 0; + + // fill out the wrapper, using the specified parameters + virtual DIError Create(di_off_t length, DiskImg::PhysicalFormat physical, + DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, + di_off_t* pWrappedLength, GenericFD** pDataFD) = 0; + + // push altered data to the wrapper GFD + virtual DIError Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, + di_off_t dataLen, di_off_t* pWrappedLen) = 0; + + // set the storage name (used by some formats) + virtual void SetStorageName(const char* name) { + // default implementation + assert(false); + } + + // indicate that we have a "fast" flush + virtual bool HasFastFlush(void) const = 0; + + // set by "Prep" on recoverable errors, like a CRC failure, for some fmts + virtual bool IsDamaged(void) const { return false; } + + // if this wrapper format includes a file comment, return it + //virtual const char* GetComment(void) const { return NULL; } + + /* + * Some additional goodies required for accessing variable-length nibble + * tracks in TrackStar images. A default implementation is provided and + * used for everything but TrackStar. + */ + virtual int GetNibbleTrackLength(DiskImg::PhysicalFormat physical, int track) const + { + if (physical == DiskImg::kPhysicalFormatNib525_6656) + return kTrackLenNib525; + else if (physical == DiskImg::kPhysicalFormatNib525_6384) + return kTrackLenNb2525; + else { + assert(false); + return -1; + } + } + virtual void SetNibbleTrackLength(int track, int length) { /*do nothing*/ } + virtual int GetNibbleTrackOffset(DiskImg::PhysicalFormat physical, int track) const + { + if (physical == DiskImg::kPhysicalFormatNib525_6656 || + physical == DiskImg::kPhysicalFormatNib525_6384) + { + /* fixed-length tracks */ + return GetNibbleTrackLength(physical, 0) * track; + } else { + assert(false); + return -1; + } + } + // TrackStar images can have more, but otherwise all nibble images have 35 + virtual int GetNibbleNumTracks(void) const + { + return kTrackCount525; + } + +private: + ImageWrapper& operator=(const ImageWrapper&); + ImageWrapper(const ImageWrapper&); +}; + + +class Wrapper2MG : public ImageWrapper { +public: + static DIError Test(GenericFD* pGFD, di_off_t wrappedLength); + virtual DIError Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, + di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, + DiskImg::SectorOrder* pOrder, short* pDiskVolNum, + LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD); + virtual DIError Create(di_off_t length, DiskImg::PhysicalFormat physical, + DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, + di_off_t* pWrappedLength, GenericFD** pDataFD); + virtual DIError Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, + di_off_t dataLen, di_off_t* pWrappedLen); + virtual bool HasFastFlush(void) const { return true; } + //virtual const char* GetComment(void) const { return NULL; } + // (need to hold TwoImgHeader in the struct, rather than as temp, or + // need to copy the comment out into Wrapper2MG storage e.g. StorageName) +}; + +class WrapperNuFX : public ImageWrapper { +public: + WrapperNuFX(void) : fpArchive(NULL), fThreadIdx(0), fStorageName(NULL), + fCompressType(kNuThreadFormatLZW2) + {} + virtual ~WrapperNuFX(void) { CloseNuFX(); delete[] fStorageName; } + + static DIError Test(GenericFD* pGFD, di_off_t wrappedLength); + virtual DIError Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, + di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, + DiskImg::SectorOrder* pOrder, short* pDiskVolNum, + LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD); + virtual DIError Create(di_off_t length, DiskImg::PhysicalFormat physical, + DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, + di_off_t* pWrappedLength, GenericFD** pDataFD); + virtual DIError Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, + di_off_t dataLen, di_off_t* pWrappedLen); + virtual bool HasFastFlush(void) const { return false; } + + void SetStorageName(const char* name) { + delete[] fStorageName; + if (name != NULL) { + fStorageName = new char[strlen(name)+1]; + strcpy(fStorageName, name); + } else + fStorageName = NULL; + } + void SetCompressType(NuThreadFormat format) { fCompressType = format; } + +private: + enum { kDefaultStorageFssep = ':' }; + static NuResult ErrMsgHandler(NuArchive* pArchive, void* vErrorMessage); + static DIError OpenNuFX(const char* pathName, NuArchive** ppArchive, + NuThreadIdx* pThreadIdx, long* pLength, bool readOnly); + DIError GetNuFXDiskImage(NuArchive* pArchive, NuThreadIdx threadIdx, + long length, char** ppData); + static char* GenTempPath(const char* path); + DIError CloseNuFX(void); + void UNIXTimeToDateTime(const time_t* pWhen, NuDateTime *pDateTime); + + NuArchive* fpArchive; + NuThreadIdx fThreadIdx; + char* fStorageName; + NuThreadFormat fCompressType; +}; + +class WrapperDiskCopy42 : public ImageWrapper { +public: + WrapperDiskCopy42(void) : fStorageName(NULL), fBadChecksum(false) + {} + virtual ~WrapperDiskCopy42(void) { delete[] fStorageName; } + + static DIError Test(GenericFD* pGFD, di_off_t wrappedLength); + virtual DIError Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, + di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, + DiskImg::SectorOrder* pOrder, short* pDiskVolNum, + LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD); + virtual DIError Create(di_off_t length, DiskImg::PhysicalFormat physical, + DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, + di_off_t* pWrappedLength, GenericFD** pDataFD); + virtual DIError Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, + di_off_t dataLen, di_off_t* pWrappedLen); + void SetStorageName(const char* name) { + delete[] fStorageName; + if (name != NULL) { + fStorageName = new char[strlen(name)+1]; + strcpy(fStorageName, name); + } else + fStorageName = NULL; + } + + virtual bool HasFastFlush(void) const { return false; } + virtual bool IsDamaged(void) const { return fBadChecksum; } + +private: + typedef struct DC42Header DC42Header; + static void DumpHeader(const DC42Header* pHeader); + void InitHeader(DC42Header* pHeader); + static int ReadHeader(GenericFD* pGFD, DC42Header* pHeader); + DIError WriteHeader(GenericFD* pGFD, const DC42Header* pHeader); + static DIError ComputeChecksum(GenericFD* pGFD, + unsigned long* pChecksum); + + char* fStorageName; + bool fBadChecksum; +}; + +class WrapperDDD : public ImageWrapper { +public: + static DIError Test(GenericFD* pGFD, di_off_t wrappedLength); + virtual DIError Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, + di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, + DiskImg::SectorOrder* pOrder, short* pDiskVolNum, + LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD); + virtual DIError Create(di_off_t length, DiskImg::PhysicalFormat physical, + DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, + di_off_t* pWrappedLength, GenericFD** pDataFD); + virtual DIError Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, + di_off_t dataLen, di_off_t* pWrappedLen); + virtual bool HasFastFlush(void) const { return false; } + + enum { + kMaxDDDZeroCount = 4, // 3 observed, 4 suspected + }; + +private: + class BitBuffer; + enum { + kNumTracks = 35, + kNumSectors = 16, + kSectorSize = 256, + kTrackLen = kNumSectors * kSectorSize, + }; + + static DIError CheckForRuns(GenericFD* pGFD); + static DIError Unpack(GenericFD* pGFD, GenericFD** ppNewGFD, + short* pDiskVolNum); + + static DIError UnpackDisk(GenericFD* pGFD, GenericFD* pNewGFD, + short* pDiskVolNum); + static bool UnpackTrack(BitBuffer* pBitBuffer, unsigned char* trackBuf); + static DIError PackDisk(GenericFD* pSrcGFD, GenericFD* pWrapperGFD, + short diskVolNum); + static void PackTrack(const unsigned char* trackBuf, BitBuffer* pBitBuf); + static void ComputeFreqCounts(const unsigned char* trackBuf, + unsigned short* freqCounts); + static void ComputeFavorites(unsigned short* freqCounts, + unsigned char* favorites); + + short fDiskVolumeNum; +}; + +class WrapperSim2eHDV : public ImageWrapper { +public: + static DIError Test(GenericFD* pGFD, di_off_t wrappedLength); + virtual DIError Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, + di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, + DiskImg::SectorOrder* pOrder, short* pDiskVolNum, + LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD); + virtual DIError Create(di_off_t length, DiskImg::PhysicalFormat physical, + DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, + di_off_t* pWrappedLength, GenericFD** pDataFD); + virtual DIError Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, + di_off_t dataLen, di_off_t* pWrappedLen); + virtual bool HasFastFlush(void) const { return true; } +}; + +class WrapperTrackStar : public ImageWrapper { +public: + enum { + kTrackStarNumTracks = 40, + kFileTrackStorageLen = 6656, + kMaxTrackLen = kFileTrackStorageLen - (128+1+2), // header + footer + kCommentFieldLen = 0x2e, + }; + + WrapperTrackStar(void) : fStorageName(NULL) { + memset(&fNibbleTrackInfo, 0, sizeof(fNibbleTrackInfo)); + fNibbleTrackInfo.numTracks = -1; + } + virtual ~WrapperTrackStar(void) { delete[] fStorageName; } + + static DIError Test(GenericFD* pGFD, di_off_t wrappedLength); + virtual DIError Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, + di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, + DiskImg::SectorOrder* pOrder, short* pDiskVolNum, + LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD); + virtual DIError Create(di_off_t length, DiskImg::PhysicalFormat physical, + DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, + di_off_t* pWrappedLength, GenericFD** pDataFD); + virtual DIError Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, + di_off_t dataLen, di_off_t* pWrappedLen); + virtual bool HasFastFlush(void) const { return false; } + + virtual void SetStorageName(const char* name) + { + delete[] fStorageName; + if (name != NULL) { + fStorageName = new char[strlen(name)+1]; + strcpy(fStorageName, name); + } else + fStorageName = NULL; + } + +private: + static DIError VerifyTrack(int track, const unsigned char* trackBuf); + DIError Unpack(GenericFD* pGFD, GenericFD** ppNewGFD); + DIError UnpackDisk(GenericFD* pGFD, GenericFD* pNewGFD); + + int fImageTracks; + char* fStorageName; + + /* + * Data structure for managing nibble images with variable-length tracks. + */ + typedef struct { + int numTracks; // should be 35 or 40 + int length[kMaxNibbleTracks525]; + int offset[kMaxNibbleTracks525]; + } NibbleTrackInfo; + NibbleTrackInfo fNibbleTrackInfo; // count and lengths for variable formats + + // nibble images can have variable-length data fields + virtual int GetNibbleTrackLength(DiskImg::PhysicalFormat physical, int track) const + { + assert(physical == DiskImg::kPhysicalFormatNib525_Var); + assert(fNibbleTrackInfo.numTracks > 0); + + return fNibbleTrackInfo.length[track]; + } + virtual void SetNibbleTrackLength(int track, int length); +#if 0 + { + assert(track >= 0); + assert(length > 0 && length <= kMaxTrackLen); + assert(track < fNibbleTrackInfo.numTracks); + + fNibbleTrackInfo.length[track] = length; + } +#endif + virtual int GetNibbleTrackOffset(DiskImg::PhysicalFormat physical, int track) const + { + assert(physical == DiskImg::kPhysicalFormatNib525_Var); + assert(fNibbleTrackInfo.numTracks > 0); + + return fNibbleTrackInfo.offset[track]; + } + virtual int GetNibbleNumTracks(void) const + { + return kTrackStarNumTracks; + } +}; + +class WrapperFDI : public ImageWrapper { +public: + WrapperFDI(void) {} + virtual ~WrapperFDI(void) {} + + static DIError Test(GenericFD* pGFD, di_off_t wrappedLength); + virtual DIError Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, + di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, + DiskImg::SectorOrder* pOrder, short* pDiskVolNum, + LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD); + virtual DIError Create(di_off_t length, DiskImg::PhysicalFormat physical, + DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, + di_off_t* pWrappedLength, GenericFD** pDataFD); + virtual DIError Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, + di_off_t dataLen, di_off_t* pWrappedLen); + virtual bool HasFastFlush(void) const { return false; } + + enum { + kSignatureLen = 27, + kCreatorLen = 30, + kCommentLen = 80, + }; + +private: + static const char* kFDIMagic; + + /* what type of disk is this? */ + typedef enum DiskType { + kDiskType8 = 0, + kDiskType525 = 1, + kDiskType35 = 2, + kDiskType3 = 3 + } DiskType; + + /* + * Contents of FDI header. + */ + typedef struct FDIHeader { + char signature[kSignatureLen+1]; + char creator[kCreatorLen+1]; + // CR + LF + char comment[kCommentLen+1]; + // MS-DOS EOF + unsigned short version; + unsigned short lastTrack; + unsigned char lastHead; + unsigned char type; // DiskType enum + unsigned char rotSpeed; + unsigned char flags; + unsigned char tpi; + unsigned char headWidth; + unsigned short reserved; + // track descriptors follow, at byte 152 + } FDIHeader; + + /* + * Header for pulse-index streams track. + */ + typedef struct PulseIndexHeader { + long numPulses; + long avgStreamLen; + int avgStreamCompression; + long minStreamLen; + int minStreamCompression; + long maxStreamLen; + int maxStreamCompression; + long idxStreamLen; + int idxStreamCompression; + + unsigned long* avgStream; // 4 bytes/pulse + unsigned long* minStream; // 4 bytes/pulse; optional + unsigned long* maxStream; // 4 bytes/pulse; optional + unsigned long* idxStream; // 2 bytes/pulse; optional? + } PulseIndexHeader; + + enum { + kTrackDescrOffset = 152, + kMaxHeads = 2, + kMaxHeaderBlockTracks = 180, // max 90 double-sided cylinders + kMinHeaderLen = 512, + kMinVersion = 0x0200, // v2.0 + + kMaxNibbleTracks35 = 80, // 80 double-sided tracks + kNibbleBufLen = 10240, // max seems to be a little under 10K + kBitBufferSize = kNibbleBufLen + (kNibbleBufLen / 4), + + kMaxSectors35 = 12, // max #of sectors per track + //kBytesPerSector35 = 512, // bytes per sector on 3.5" disk + + kPulseStreamDataOffset = 16, // start of header to avg stream + + kBitRate525 = 250000, // 250Kbits/sec + }; + + /* meaning of the two-bit compression format value */ + typedef enum CompressedFormat { + kCompUncompressed = 0, + kCompHuffman = 1, + } CompressedFormat; + + /* node in the Huffman tree */ + typedef struct HuffNode { + unsigned short val; + struct HuffNode* left; + struct HuffNode* right; + } HuffNode; + + /* + * Keep a copy of the header around while we work. None of the formats + * we're interested in have more than kMaxHeaderBlockTracks tracks in + * them, so we don't need anything beyond the initial 512-byte header. + */ + unsigned char fHeaderBuf[kMinHeaderLen]; + + static void UnpackHeader(const unsigned char* headerBuf, FDIHeader* hdr); + static void DumpHeader(const FDIHeader* pHdr); + + DIError Unpack525(GenericFD* pGFD, GenericFD** ppNewGFD, int numCyls, + int numHeads); + DIError Unpack35(GenericFD* pGFD, GenericFD** ppNewGFD, int numCyls, + int numHeads, LinearBitmap** ppBadBlockMap); + DIError PackDisk(GenericFD* pSrcGFD, GenericFD* pWrapperGFD); + + DIError UnpackDisk525(GenericFD* pGFD, GenericFD* pNewGFD, int numCyls, + int numHeads); + DIError UnpackDisk35(GenericFD* pGFD, GenericFD* pNewGFD, int numCyls, + int numHeads, LinearBitmap* pBadBlockMap); + void GetTrackInfo(int trk, int* pType, int* pLength256); + + int BitRate35(int trk); + void FixBadNibbles(unsigned char* nibbleBuf, long nibbleLen); + bool DecodePulseTrack(const unsigned char* inputBuf, long inputLen, + int bitRate, unsigned char* nibbleBuf, long* pNibbleLen); + bool UncompressPulseStream(const unsigned char* inputBuf, long inputLen, + unsigned long* outputBuf, long numPulses, int format, int bytesPerPulse); + bool ExpandHuffman(const unsigned char* inputBuf, long inputLen, + unsigned long* outputBuf, long numPulses); + const unsigned char* HuffExtractTree(const unsigned char* inputBuf, + HuffNode* pNode, unsigned char* pBits, unsigned char* pBitMask); + const unsigned char* HuffExtractValues16(const unsigned char* inputBuf, + HuffNode* pNode); + const unsigned char* HuffExtractValues8(const unsigned char* inputBuf, + HuffNode* pNode); + void HuffFreeNodes(HuffNode* pNode); + unsigned long HuffSignExtend16(unsigned long val); + unsigned long HuffSignExtend8(unsigned long val); + bool ConvertPulseStreamsToNibbles(PulseIndexHeader* pHdr, int bitRate, + unsigned char* nibbleBuf, long* pNibbleLen); + bool ConvertPulsesToBits(const unsigned long* avgStream, + const unsigned long* minStream, const unsigned long* maxStream, + const unsigned long* idxStream, int numPulses, int maxIndex, + int indexOffset, unsigned long totalAvg, int bitRate, + unsigned char* outputBuf, int* pOutputLen); + int MyRand(void); + bool ConvertBitsToNibbles(const unsigned char* bitBuffer, int bitCount, + unsigned char* nibbleBuf, long* pNibbleLen); + + + int fImageTracks; + char* fStorageName; + + + /* + * Data structure for managing nibble images with variable-length tracks. + */ + typedef struct { + int numTracks; // expect 35 or 40 for 5.25" + int length[kMaxNibbleTracks525]; + int offset[kMaxNibbleTracks525]; + } NibbleTrackInfo; + NibbleTrackInfo fNibbleTrackInfo; // count and lengths for variable formats + + // nibble images can have variable-length data fields + virtual int GetNibbleTrackLength(DiskImg::PhysicalFormat physical, int track) const + { + assert(physical == DiskImg::kPhysicalFormatNib525_Var); + assert(fNibbleTrackInfo.numTracks > 0); + + return fNibbleTrackInfo.length[track]; + } + virtual void SetNibbleTrackLength(int track, int length); + virtual int GetNibbleTrackOffset(DiskImg::PhysicalFormat physical, int track) const + { + assert(physical == DiskImg::kPhysicalFormatNib525_Var); + assert(fNibbleTrackInfo.numTracks > 0); + + return fNibbleTrackInfo.offset[track]; + } + virtual int GetNibbleNumTracks(void) const + { + return fNibbleTrackInfo.numTracks; + } +}; + + +class WrapperUnadornedNibble : public ImageWrapper { +public: + static DIError Test(GenericFD* pGFD, di_off_t wrappedLength); + virtual DIError Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, + di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, + DiskImg::SectorOrder* pOrder, short* pDiskVolNum, + LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD); + virtual DIError Create(di_off_t length, DiskImg::PhysicalFormat physical, + DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, + di_off_t* pWrappedLength, GenericFD** pDataFD); + virtual DIError Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, + di_off_t dataLen, di_off_t* pWrappedLen); + virtual bool HasFastFlush(void) const { return true; } +}; + +class WrapperUnadornedSector : public ImageWrapper { +public: + static DIError Test(GenericFD* pGFD, di_off_t wrappedLength); + virtual DIError Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, + di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, + DiskImg::SectorOrder* pOrder, short* pDiskVolNum, + LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD); + virtual DIError Create(di_off_t length, DiskImg::PhysicalFormat physical, + DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, + di_off_t* pWrappedLength, GenericFD** pDataFD); + virtual DIError Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, + di_off_t dataLen, di_off_t* pWrappedLen); + virtual bool HasFastFlush(void) const { return true; } +}; + + +/* + * =========================================================================== + * Non-FS DiskFSs + * =========================================================================== + */ + +/* + * A "raw" disk, i.e. no filesystem is known. Useful as a placeholder + * for applications that demand a DiskFS object even when the filesystem + * isn't known. + */ +class DISKIMG_API DiskFSUnknown : public DiskFS { +public: + DiskFSUnknown(void) : DiskFS() { + strcpy(fDiskVolumeName, "[Unknown]"); + strcpy(fDiskVolumeID, "Unknown FS"); + } + virtual ~DiskFSUnknown(void) {} + + virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { + SetDiskImg(pImg); + return kDIErrNone; + } + + virtual const char* GetVolumeName(void) const { return fDiskVolumeName; } + virtual const char* GetVolumeID(void) const { return fDiskVolumeID; } + virtual const char* GetBareVolumeName(void) const { return NULL; } + virtual bool GetReadWriteSupported(void) const { return false; } + virtual bool GetFSDamaged(void) const { return false; } + virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const + { return kDIErrNotSupported; } + + // Use this if *something* is known about the filesystem, e.g. the + // partition type on a MacPart disk. + void SetVolumeInfo(const char* descr) { + if (strlen(descr) > kMaxVolumeName) + return; + + fDiskVolumeName[0] = '['; + strcpy(fDiskVolumeName+1, descr); + strcat(fDiskVolumeName, "]"); + strcpy(fDiskVolumeID, "Unknown FS - "); + strcat(fDiskVolumeID, descr); + } + +private: + enum { kMaxVolumeName = 64 }; + + char fDiskVolumeName[kMaxVolumeName+3]; + char fDiskVolumeID[kMaxVolumeName + 20]; +}; + + +/* + * Generic "container" DiskFS class. Contains some common functions shared + * among classes that are just containers for other filesystems. This class + * is not expected to be instantiated. + * + * TODO: create a common OpenSubVolume() function. + */ +class DISKIMG_API DiskFSContainer : public DiskFS { +public: + DiskFSContainer(void) : DiskFS() {} + virtual ~DiskFSContainer(void) {} + +protected: + virtual const char* GetDebugName(void) = 0; + virtual DIError CreatePlaceholder(long startBlock, long numBlocks, + const char* partName, const char* partType, + DiskImg** ppNewImg, DiskFS** ppNewFS); + virtual void SetVolumeUsageMap(void); +}; + +/* + * UNIDOS disk, an 800K floppy with two 400K DOS 3.3 volumes on it. + * + * The disk itself has no files; instead, it has two embedded sub-volumes. + */ +class DISKIMG_API DiskFSUNIDOS : public DiskFSContainer { +public: + DiskFSUNIDOS(void) : DiskFSContainer() {} + virtual ~DiskFSUNIDOS(void) {} + + static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency); + static DIError TestWideFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency); + + virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { + SetDiskImg(pImg); + return Initialize(); + } + + virtual const char* GetVolumeName(void) const { return "[UNIDOS]"; } + virtual const char* GetVolumeID(void) const { return "[UNIDOS]"; } + virtual const char* GetBareVolumeName(void) const { return NULL; } + virtual bool GetReadWriteSupported(void) const { return false; } + virtual bool GetFSDamaged(void) const { return false; } + virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const + { return kDIErrNotSupported; } + +private: + virtual const char* GetDebugName(void) { return "UNIDOS"; } + DIError Initialize(void); + DIError OpenSubVolume(int idx); +}; + +/* + * OzDOS disk, an 800K floppy with two 400K DOS 3.3 volumes on it. They + * put the files for disk 1 in the odd sectors and the files for disk 2 + * in the even sectors (the top and bottom halves of a 512-byte block). + * + * The disk itself has no files; instead, it has two embedded sub-volumes. + * Because of the funky layout, we have to use the "sector pairing" feature + * of DiskImg to treat this like a DOS 3.3 disk. + */ +class DISKIMG_API DiskFSOzDOS : public DiskFSContainer { +public: + DiskFSOzDOS(void) : DiskFSContainer() {} + virtual ~DiskFSOzDOS(void) {} + + static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency); + + virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { + SetDiskImg(pImg); + return Initialize(); + } + + virtual const char* GetVolumeName(void) const { return "[OzDOS]"; } + virtual const char* GetVolumeID(void) const { return "[OzDOS]"; } + virtual const char* GetBareVolumeName(void) const { return NULL; } + virtual bool GetReadWriteSupported(void) const { return false; } + virtual bool GetFSDamaged(void) const { return false; } + virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const + { return kDIErrNotSupported; } + +private: + virtual const char* GetDebugName(void) { return "OzDOS"; } + DIError Initialize(void); + DIError OpenSubVolume(int idx); +}; + +/* + * CFFA volume. A potentially very large volume with multiple partitions. + * + * This DiskFS is just a container that describes the position and sizes + * of the sub-volumes. + */ +class DISKIMG_API DiskFSCFFA : public DiskFSContainer { +public: + DiskFSCFFA(void) : DiskFSContainer() {} + virtual ~DiskFSCFFA(void) {} + + static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency); + + virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { + SetDiskImg(pImg); + return Initialize(); + } + + virtual const char* GetVolumeName(void) const { return "[CFFA]"; } + virtual const char* GetVolumeID(void) const { return "[CFFA]"; } + virtual const char* GetBareVolumeName(void) const { return NULL; } + virtual bool GetReadWriteSupported(void) const { return false; } + virtual bool GetFSDamaged(void) const { return false; } + virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const + { return kDIErrNotSupported; } + +private: + virtual const char* GetDebugName(void) { return "CFFA"; } + + static DIError TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder, + DiskImg::FSFormat* pFormatFound); + static DIError OpenSubVolume(DiskImg* pImg, long startBlock, + long numBlocks, bool scanOnly, DiskImg** ppNewImg, DiskFS** ppNewFS); + DIError Initialize(void); + DIError FindSubVolumes(void); + DIError AddVolumeSeries(int start, int count, long blocksPerVolume, + long& startBlock, long& totalBlocksLeft); + + enum { + kMinInterestingBlocks = 65536 + 1024, // less than this, ignore + kEarlyVolExpectedSize = 65536, // 32MB in 512-byte blocks + kOneGB = 1024*1024*(1024/512), // 1GB in 512-byte blocks + }; +}; + + +/* + * Macintosh-style partitioned disk image. + * + * This DiskFS is just a container that describes the position and sizes + * of the sub-volumes. + */ +class DISKIMG_API DiskFSMacPart : public DiskFSContainer { +public: + DiskFSMacPart(void) : DiskFSContainer() {} + virtual ~DiskFSMacPart(void) {} + + static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency); + + virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { + SetDiskImg(pImg); + return Initialize(); + } + + virtual const char* GetVolumeName(void) const { return "[MacPartition]"; } + virtual const char* GetVolumeID(void) const { return "[MacPartition]"; } + virtual const char* GetBareVolumeName(void) const { return NULL; } + virtual bool GetReadWriteSupported(void) const { return false; } + virtual bool GetFSDamaged(void) const { return false; } + virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const + { return kDIErrNotSupported; } + +private: + virtual const char* GetDebugName(void) { return "MacPart"; } + + struct PartitionMap; // fwd + struct DriverDescriptorRecord; // fwd + static void UnpackDDR(const unsigned char* buf, + DriverDescriptorRecord* pDDR); + static void DumpDDR(const DriverDescriptorRecord* pDDR); + static void UnpackPartitionMap(const unsigned char* buf, + PartitionMap* pMap); + static void DumpPartitionMap(long block, const PartitionMap* pMap); + + static DIError TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder); + DIError OpenSubVolume(const PartitionMap* pMap); + DIError Initialize(void); + DIError FindSubVolumes(void); + + enum { + kMinInterestingBlocks = 2048, // less than this, ignore + kDDRSignature = 0x4552, // 'ER' + kPartitionSignature = 0x504d, // 'PM' + }; +}; + + +/* + * Partitioning for Joachim Lange's MicroDrive card. + * + * This DiskFS is just a container that describes the position and sizes + * of the sub-volumes. + */ +class DISKIMG_API DiskFSMicroDrive : public DiskFSContainer { +public: + DiskFSMicroDrive(void) : DiskFSContainer() {} + virtual ~DiskFSMicroDrive(void) {} + + static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency); + + virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { + SetDiskImg(pImg); + return Initialize(); + } + + virtual const char* GetVolumeName(void) const { return "[MicroDrive]"; } + virtual const char* GetVolumeID(void) const { return "[MicroDrive]"; } + virtual const char* GetBareVolumeName(void) const { return NULL; } + virtual bool GetReadWriteSupported(void) const { return false; } + virtual bool GetFSDamaged(void) const { return false; } + virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const + { return kDIErrNotSupported; } + +private: + virtual const char* GetDebugName(void) { return "MicroDrive"; } + + struct PartitionMap; // fwd + static void UnpackPartitionMap(const unsigned char* buf, + PartitionMap* pMap); + static void DumpPartitionMap(const PartitionMap* pMap); + + static DIError TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder); + DIError OpenSubVolume(long startBlock, long numBlocks); + DIError OpenVol(int idx, long startBlock, long numBlocks); + DIError Initialize(void); + DIError FindSubVolumes(void); + + enum { + kMinInterestingBlocks = 2048, // less than this, ignore + kPartitionSignature = 0xccca, // 'JL' in little-endian high-ASCII + }; +}; + + +/* + * Partitioning for Parsons Engineering FocusDrive card. + * + * This DiskFS is just a container that describes the position and sizes + * of the sub-volumes. + */ +class DISKIMG_API DiskFSFocusDrive : public DiskFSContainer { +public: + DiskFSFocusDrive(void) : DiskFSContainer() {} + virtual ~DiskFSFocusDrive(void) {} + + static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency); + + virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { + SetDiskImg(pImg); + return Initialize(); + } + + virtual const char* GetVolumeName(void) const { return "[FocusDrive]"; } + virtual const char* GetVolumeID(void) const { return "[FocusDrive]"; } + virtual const char* GetBareVolumeName(void) const { return NULL; } + virtual bool GetReadWriteSupported(void) const { return false; } + virtual bool GetFSDamaged(void) const { return false; } + virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const + { return kDIErrNotSupported; } + +private: + virtual const char* GetDebugName(void) { return "FocusDrive"; } + + struct PartitionMap; // fwd + static void UnpackPartitionMap(const unsigned char* buf, + const unsigned char* nameBuf, PartitionMap* pMap); + static void DumpPartitionMap(const PartitionMap* pMap); + + static DIError TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder); + DIError OpenSubVolume(long startBlock, long numBlocks, + const char* name); + DIError OpenVol(int idx, long startBlock, long numBlocks, + const char* name); + DIError Initialize(void); + DIError FindSubVolumes(void); + + enum { + kMinInterestingBlocks = 2048, // less than this, ignore + }; +}; + + +/* + * =========================================================================== + * DOS 3.2/3.3 + * =========================================================================== + */ + +class A2FileDOS; + +/* + * DOS 3.2/3.3 disk. + */ +class DISKIMG_API DiskFSDOS33 : public DiskFS { +public: + DiskFSDOS33(void) : DiskFS() { + fVTOCLoaded = false; + fDiskIsGood = false; + } + virtual ~DiskFSDOS33(void) {} + + static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency); + + virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { + SetDiskImg(pImg); + return Initialize(initMode); + } + virtual DIError Format(DiskImg* pDiskImg, const char* volName); + + virtual const char* GetVolumeName(void) const { return fDiskVolumeName; } + virtual const char* GetVolumeID(void) const { return fDiskVolumeID; } + virtual const char* GetBareVolumeName(void) const { + // this is fragile -- skip over the "DOS" part, return 3 digits + assert(strlen(fDiskVolumeName) > 3); + return fDiskVolumeName+3; + } + virtual bool GetReadWriteSupported(void) const { return true; } + virtual bool GetFSDamaged(void) const { return !fDiskIsGood; } + virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const; + virtual DIError NormalizePath(const char* path, char fssep, + char* normalizedBuf, int* pNormalizedBufLen); + virtual DIError CreateFile(const CreateParms* pParms, A2File** ppNewFile); + virtual DIError DeleteFile(A2File* pFile); + virtual DIError RenameFile(A2File* pFile, const char* newName); + virtual DIError SetFileInfo(A2File* pFile, long fileType, long auxType, + long accessFlags); + virtual DIError RenameVolume(const char* newName); + + /* + * Unique to DOS 3.3 disks. + */ + int GetDiskVolumeNum(void) const { return fDiskVolumeNum; } + void SetDiskVolumeNum(int val); + + static bool IsValidFileName(const char* name); + static bool IsValidVolumeName(const char* name); + + // utility function + static void LowerASCII(unsigned char* buf, long len); + static void ReplaceFssep(char* str, char replacement); + + enum { + kMinTracks = 17, // need to put the catalog track here + kMaxTracks = 50, + kMaxCatalogSectors = 64, // two tracks on a 32-sector disk + }; + + /* a T/S pair */ + typedef struct TrackSector { + char track; + char sector; + } TrackSector; + + friend class A2FDDOS; // for Write + +private: + DIError Initialize(InitMode initMode); + DIError ReadVTOC(void); + void UpdateVolumeNum(void); + void DumpVTOC(void); + void SetSectorUsage(long track, long sector, + VolumeUsage::ChunkPurpose purpose); + void FixVolumeUsageMap(void); + DIError ReadCatalog(void); + DIError ProcessCatalogSector(int catTrack, int catSect, + const unsigned char* sctBuf); + DIError GetFileLengths(void); + DIError ComputeLength(A2FileDOS* pFile, const TrackSector* tsList, + int tsCount); + DIError TrimLastSectorUp(A2FileDOS* pFile, TrackSector lastTS); + void MarkFileUsage(A2FileDOS* pFile, TrackSector* tsList, int tsCount, + TrackSector* indexList, int indexCount); + //DIError TrimLastSectorDown(A2FileDOS* pFile, unsigned short* tsBuf, + // int maxZeroCount); + void DoNormalizePath(const char* name, char fssep, char* outBuf); + DIError MakeFileNameUnique(char* fileName); + DIError GetFreeCatalogEntry(TrackSector* pCatSect, int* pCatEntry, + unsigned char* sctBuf, A2FileDOS** ppPrevEntry); + void CreateDirEntry(unsigned char* sctBuf, int catEntry, + const char* fileName, TrackSector* pTSSect, unsigned char fileType, + int access); + void FreeTrackSectors(TrackSector* pList, int count); + + bool CheckDiskIsGood(void); + + DIError WriteDOSTracks(int sectPerTrack); + + DIError ScanVolBitmap(void); + DIError LoadVolBitmap(void); + DIError SaveVolBitmap(void); + void FreeVolBitmap(void); + DIError AllocSector(TrackSector* pTS); + DIError CreateEmptyBlockMap(bool withDOS); + bool GetSectorUseEntry(long track, int sector) const; + void SetSectorUseEntry(long track, int sector, bool inUse); + inline unsigned long GetVTOCEntry(const unsigned char* pVTOC, + long track) const; + + // Largest interesting volume is 400K (50 tracks, 32 sectors), but + // we may be looking at it in 16-sector mode, so max tracks is 100. + enum { + kMaxInterestingTracks = 100, + kSectorSize = 256, + kDefaultVolumeNum = 254, + kMaxExtensionLen = 4, // used when normalizing; ".gif" is 4 + }; + + // DOS track images, for initializing disk images + static const unsigned char gDOS33Tracks[]; + static const unsigned char gDOS32Tracks[]; + + /* some fields from the VTOC */ + int fFirstCatTrack; + int fFirstCatSector; + int fVTOCVolumeNumber; + int fVTOCNumTracks; + int fVTOCNumSectors; + + /* private data */ + int fDiskVolumeNum; // usually 254 + char fDiskVolumeName[7]; // "DOS" + num, e.g. "DOS001", "DOS254" + char fDiskVolumeID[32]; // sizeof "DOS 3.3 Volume " +3 +1 + unsigned char fVTOC[kSectorSize]; + bool fVTOCLoaded; + + /* + * There are some things we need to be careful of when reading the + * catalog track, like bad links and infinite loops. By storing a list + * of known good catalog sectors, we only have to handle that stuff once. + * The catalog doesn't grow or shrink, so this never needs to be updated. + */ + TrackSector fCatalogSectors[kMaxCatalogSectors]; + + bool fDiskIsGood; +}; + +/* + * File descriptor for an open DOS file. + */ +class DISKIMG_API A2FDDOS : public A2FileDescr { +public: + A2FDDOS(A2File* pFile) : A2FileDescr(pFile) { + fTSList = NULL; + fIndexList = NULL; + fOffset = 0; + fModified = false; + } + virtual ~A2FDDOS(void) { + delete[] fTSList; + delete[] fIndexList; + //fTSList = fIndexList = NULL; + } + + //typedef DiskFSDOS33::TrackSector TrackSector; + + friend class A2FileDOS; + + virtual DIError Read(void* buf, size_t len, size_t* pActual = NULL); + virtual DIError Write(const void* buf, size_t len, size_t* pActual = NULL); + virtual DIError Seek(di_off_t offset, DIWhence whence); + virtual di_off_t Tell(void); + virtual DIError Close(void); + + virtual long GetSectorCount(void) const; + virtual long GetBlockCount(void) const; + virtual DIError GetStorage(long sectorIdx, long* pTrack, long* pSector) const; + virtual DIError GetStorage(long blockIdx, long* pBlock) const; + +private: + typedef DiskFSDOS33::TrackSector TrackSector; + + TrackSector* fTSList; // T/S entries for data sectors + int fTSCount; + TrackSector* fIndexList; // T/S entries for T/S list sectors + int fIndexCount; + di_off_t fOffset; // current position in file + + di_off_t fOpenEOF; // how big the file currently is + long fOpenSectorsUsed; // how many sectors it occupies + bool fModified; // if modified, update stuff on Close + + void DumpTSList(void) const; +}; + +/* + * Holds DOS files. Works for DOS33, DOS32, and "wide" DOS implementations. + * + * Catalog contents are public so anyone who wants gory details of DOS 3.3 + * stuff can poke at whatever they want. Anybody else can use the virtual + * interfaces to get standardized answers for things like file type. + * + * The embedded address and length fields found in Applesoft, Integer, and + * Binary files are quietly skipped over with the fDataOffset field when + * files are read. + * + * THOUGHT: have "get filename" and "get raw filename" interfaces? There + * are no directories, so maybe we don't care about "raw pathname"?? Might + * be better to always return the "raw" value and let the caller deal with + * things like high ASCII. + */ +class DISKIMG_API A2FileDOS : public A2File { +public: + A2FileDOS(DiskFS* pDiskFS); + virtual ~A2FileDOS(void); + + // assorted constants + enum { + kMaxFileName = 30, + }; + typedef enum { + kTypeUnknown = -1, + kTypeText = 0x00, // 'T' + kTypeInteger = 0x01, // 'I' + kTypeApplesoft = 0x02, // 'A' + kTypeBinary = 0x04, // 'B' + kTypeS = 0x08, // 'S' + kTypeReloc = 0x10, // 'R' + kTypeA = 0x20, // 'A' + kTypeB = 0x40, // 'B' + + kTypeLocked = 0x80 // bitwise OR with previous values + } FileType; + + /* + * Implementations of standard interfaces. + */ + virtual const char* GetFileName(void) const { return fFileName; } + virtual const char* GetPathName(void) const { return fFileName; } + virtual char GetFssep(void) const { return '\0'; } + virtual long GetFileType(void) const; + virtual long GetAuxType(void) const { return fAuxType; } + virtual long GetAccess(void) const; + virtual time_t GetCreateWhen(void) const { return 0; } + virtual time_t GetModWhen(void) const { return 0; } + virtual di_off_t GetDataLength(void) const { return fLength; } + virtual di_off_t GetDataSparseLength(void) const { return fSparseLength; } + virtual di_off_t GetRsrcLength(void) const { return -1; } + virtual di_off_t GetRsrcSparseLength(void) const { return -1; } + + virtual DIError Open(A2FileDescr** ppOpenFile, bool readOnly, + bool rsrcFork = false); + virtual void CloseDescr(A2FileDescr* pOpenFile) { + assert(pOpenFile == fpOpenFile); + delete fpOpenFile; + fpOpenFile = NULL; + } + virtual bool IsFileOpen(void) const { return fpOpenFile != NULL; } + + void Dump(void) const; + + typedef DiskFSDOS33::TrackSector TrackSector; + + /* + * Contents of directory entry. + * + * We don't hold deleted or unused entries, so fTSListTrack is always + * valid. + */ + short fTSListTrack; // (could use TrackSector here) + short fTSListSector; + unsigned short fLengthInSectors; + bool fLocked; + char fFileName[kMaxFileName+1]; // "fixed" version + FileType fFileType; + + TrackSector fCatTS; // track/sector for our catalog entry + int fCatEntryNum; // entry number within cat sector + + // these are computed or determined from the file contents + unsigned short fAuxType; // addr for bin, etc. + short fDataOffset; // for 'A'/'B'/'I' with embedded len + di_off_t fLength; // file length, in bytes + di_off_t fSparseLength; // file length, factoring sparse out + + void FixFilename(void); + + DIError LoadTSList(TrackSector** pTSList, int* pTSCount, + TrackSector** pIndexList = NULL, int* pIndexCount = NULL); + static FileType ConvertFileType(long prodosType, di_off_t fileLen); + static bool IsValidType(long prodosType); + static void MakeDOSName(char* buf, const char* name); + static void TrimTrailingSpaces(char* filename); + +private: + DIError ExtractTSPairs(const unsigned char* sctBuf, TrackSector* tsList, + int* pLastNonZero); + + A2FDDOS* fpOpenFile; +}; + + +/* + * =========================================================================== + * ProDOS + * =========================================================================== + */ + +class A2FileProDOS; + +/* + * ProDOS disk. + * + * THOUGHT: it would be undesirable for the CiderPress UI, but it would + * make things somewhat easier internally if we treated the volume dir + * like a subdirectory under which everything else sits, instead of special- + * casing it like we do. This is awkward because volume dirs have names + * under ProDOS, giving every pathname an extra component that they don't + * really need. We can never treat the volume dir purely as a subdir, + * because it can't expand beyond 51 files, but the storage_type in the + * header is sufficient to identify it as such (assuming the disk isn't + * broken). Certain operations, such as changing the file type or aux type, + * simply aren't possible on a volume dir, and deleting a volume dir doesn't + * make sense. So in some respects we simply trade one kind of special-case + * behavior for another. + */ +class DISKIMG_API DiskFSProDOS : public DiskFS { +public: + DiskFSProDOS(void) : fBitMapPointer(0), fTotalBlocks(0), fBlockUseMap(NULL) + {} + virtual ~DiskFSProDOS(void) { + if (fBlockUseMap != NULL) { + assert(false); // unexpected + delete[] fBlockUseMap; + } + } + + static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency); + + virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { + SetDiskImg(pImg); + return Initialize(initMode); + } + virtual DIError Format(DiskImg* pDiskImg, const char* volName); + virtual DIError NormalizePath(const char* path, char fssep, + char* normalizedBuf, int* pNormalizedBufLen); + virtual DIError CreateFile(const CreateParms* pParms, A2File** ppNewFile); + virtual DIError DeleteFile(A2File* pFile); + virtual DIError RenameFile(A2File* pFile, const char* newName); + virtual DIError SetFileInfo(A2File* pFile, long fileType, long auxType, + long accessFlags); + virtual DIError RenameVolume(const char* newName); + + // assorted constants + enum { + kMaxVolumeName = 15, + }; + typedef unsigned long ProDate; + + virtual const char* GetVolumeName(void) const { return fVolumeName; } + virtual const char* GetVolumeID(void) const { return fVolumeID; } + virtual const char* GetBareVolumeName(void) const { return fVolumeName; } + virtual bool GetReadWriteSupported(void) const { return true; } + virtual bool GetFSDamaged(void) const { return !fDiskIsGood; } + virtual long GetFSNumBlocks(void) const { return fTotalBlocks; } + virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const; + + //A2FileProDOS* GetVolDir(void) const { return fpVolDir; } + + static bool IsValidFileName(const char* name); + static bool IsValidVolumeName(const char* name); + static unsigned short GenerateLowerCaseBits(const char* upperName, + const char* lowerName, bool forAppleWorks); + static void GenerateLowerCaseName(const char* upperName, + char* lowerNameNoTerm, unsigned short lcFlags, bool fromAppleWorks); + + friend class A2FDProDOS; + +private: + struct DirHeader; + + enum { kMaxExtensionLen = 4 }; // used when normalizing; ".gif" is 4 + + DIError Initialize(InitMode initMode); + DIError LoadVolHeader(void); + void SetVolumeID(void); + void DumpVolHeader(void); + DIError ScanVolBitmap(void); + DIError LoadVolBitmap(void); + DIError SaveVolBitmap(void); + void FreeVolBitmap(void); + long AllocBlock(void); + int GetNumBitmapBlocks(void) const { + /* use fTotalBlocks rather than GetNumBlocks() */ + assert(fTotalBlocks > 0); + const int kBitsPerBlock = 512 * 8; + int numBlocks = (fTotalBlocks + kBitsPerBlock-1) / kBitsPerBlock; + return numBlocks; + } + DIError CreateEmptyBlockMap(void); + bool GetBlockUseEntry(long block) const; + void SetBlockUseEntry(long block, bool inUse); + bool ScanForExtraEntries(void) const; + + void SetBlockUsage(long block, VolumeUsage::ChunkPurpose purpose); + DIError GetDirHeader(const unsigned char* blkBuf, DirHeader* pHeader); + DIError RecursiveDirAdd(A2File* pParent, unsigned short dirBlock, + const char* basePath, int depth); + DIError SlurpEntries(A2File* pParent, const DirHeader* pHeader, + const unsigned char* blkBuf, bool skipFirst, int* pCount, + const char* basePath, unsigned short thisBlock, int depth); + DIError ReadExtendedInfo(A2FileProDOS* pFile); + DIError ScanFileUsage(void); + void ScanBlockList(long blockCount, unsigned short* blockList, + long indexCount, unsigned short* indexList, long* pSparseCount); + DIError ScanForSubVolumes(void); + DIError FindSubVolume(long blockStart, long blockCount, + DiskImg** ppDiskImg, DiskFS** ppDiskFS); + void MarkSubVolumeBlocks(long block, long count); + + A2File* FindFileByKeyBlock(A2File* pStart, unsigned short keyBlock); + DIError AllocInitialFileStorage(const CreateParms* pParms, + const char* upperName, unsigned short dirBlock, int dirEntrySlot, + long* pKeyBlock, int* pBlocksUsed, int* pNewEOF); + DIError WriteBootBlocks(void); + DIError DoNormalizePath(const char* path, char fssep, + char** pNormalizedPath); + void UpperCaseName(char* upperName, const char* name); + bool CheckDiskIsGood(void); + DIError AllocDirEntry(A2FileDescr* pOpenSubdir, unsigned char** ppDir, + long* pDirLen, unsigned char** ppDirEntry, unsigned short* pDirKeyBlock, + int* pDirEntrySlot, unsigned short* pDirBlock); + unsigned char* GetPrevDirEntry(unsigned char* buf, unsigned char* ptr); + DIError MakeFileNameUnique(const unsigned char* dirBuf, long dirLen, + char* fileName); + bool NameExistsInDir(const unsigned char* dirBuf, long dirLen, + const char* fileName); + + DIError FreeBlocks(long blockCount, unsigned short* blockList); + DIError RegeneratePathName(A2FileProDOS* pFile); + + /* some items from the volume header */ + char fVolumeName[kMaxVolumeName+1]; + char fVolumeID[kMaxVolumeName + 16]; // add "ProDOS /" + unsigned char fAccess; + ProDate fCreateWhen; + ProDate fModWhen; + unsigned short fBitMapPointer; + unsigned short fTotalBlocks; + //unsigned short fPrevBlock; + //unsigned short fNextBlock; + //unsigned char fVersion; + //unsigned char fMinVersion; + //unsigned char fEntryLength; + //unsigned char fEntriesPerBlock; + unsigned short fVolDirFileCount; + +// A2FileProDOS* fpVolDir; // a "fake" file entry for the volume dir + + /* + * This is a working copy of the block use map from blocks 6+. It should + * be loaded when we're about to modify files on the disk and freed + * immediately afterward. The goal is to facilitate speedy updates to the + * bitmap without creating problems if the application decides to modify + * one of the bitmap blocks directly (e.g. with the disk sector editor). + * It should never be held across calls. + */ + unsigned char* fBlockUseMap; + + /* + * Set this if the disk is "perfect". If it's not, we disallow write + * access for safety reasons. + */ + bool fDiskIsGood; + + /* set if something fixes damage so CheckDiskIsGood can't see it */ + bool fEarlyDamage; +}; + +/* + * File descriptor for an open ProDOS file. + * + * This only represents one fork. + */ +class DISKIMG_API A2FDProDOS : public A2FileDescr { +public: + A2FDProDOS(A2File* pFile) : A2FileDescr(pFile), fModified(false), + fBlockList(NULL), fOffset(0) + {} + virtual ~A2FDProDOS(void) { + delete[] fBlockList; + fBlockList = NULL; + } + + friend class A2FileProDOS; + + virtual DIError Read(void* buf, size_t len, size_t* pActual = NULL); + virtual DIError Write(const void* buf, size_t len, size_t* pActual = NULL); + virtual DIError Seek(di_off_t offset, DIWhence whence); + virtual di_off_t Tell(void); + virtual DIError Close(void); + + virtual long GetSectorCount(void) const; + virtual long GetBlockCount(void) const; + virtual DIError GetStorage(long sectorIdx, long* pTrack, long* pSector) const; + virtual DIError GetStorage(long blockIdx, long* pBlock) const; + + void DumpBlockList(void) const; + +private: + bool IsEmptyBlock(const unsigned char* blk); + DIError WriteDirectory(const void* buf, size_t len, size_t* pActual); + + /* state for open files */ + bool fModified; + long fBlockCount; + unsigned short* fBlockList; + di_off_t fOpenEOF; // current EOF + unsigned short fOpenBlocksUsed; // #of block used by open piece + int fOpenStorageType; + bool fOpenRsrcFork; // is this the resource fork? + di_off_t fOffset; // current file offset +}; + +/* + * Holds a ProDOS file. + */ +class DISKIMG_API A2FileProDOS : public A2File { +public: + A2FileProDOS(DiskFS* pDiskFS) : A2File(pDiskFS) { + fPathName = NULL; + fSparseDataEof = fSparseRsrcEof = -1; + fpOpenFile = NULL; + fParentDirBlock = 0; + fParentDirIdx = -1; + fpParent = NULL; + } + virtual ~A2FileProDOS(void) { + delete fpOpenFile; + delete[] fPathName; + } + + typedef DiskFSProDOS::ProDate ProDate; + + /* assorted constants */ + enum { + kMaxFileName = 15, + kFssep = ':', + kInvalidBlockNum = 1, // boot block, can't be in file + kMaxBlocksPerIndex = 256, + }; + /* ProDOS access permissions */ + enum { + kAccessRead = 0x01, + kAccessWrite = 0x02, + kAccessInvisible = 0x04, + kAccessBackup = 0x20, + kAccessRename = 0x40, + kAccessDelete = 0x80 + }; + /* contents of a directory entry */ + typedef struct DirEntry { + int storageType; + char fileName[kMaxFileName+1]; // shows lower case + unsigned char fileType; + unsigned short keyPointer; + unsigned short blocksUsed; + unsigned long eof; + ProDate createWhen; + unsigned char version; + unsigned char minVersion; + unsigned char access; + unsigned short auxType; + ProDate modWhen; + unsigned short headerPointer; + } DirEntry; + typedef struct ExtendedInfo { + unsigned char storageType; + unsigned short keyBlock; + unsigned short blocksUsed; + unsigned long eof; + } ExtendedInfo; + typedef enum StorageType { + kStorageDeleted = 0, /* indicates deleted file */ + kStorageSeedling = 1, /* <= 512 bytes */ + kStorageSapling = 2, /* < 128KB */ + kStorageTree = 3, /* < 16MB */ + kStoragePascalVolume = 4, /* see ProDOS technote 25 */ + kStorageExtended = 5, /* forked */ + kStorageDirectory = 13, + kStorageSubdirHeader = 14, + kStorageVolumeDirHeader = 15, + } StorageType; + + static bool IsRegularFile(int type) { + return (type == kStorageSeedling || type == kStorageSapling || + type == kStorageTree); + } + + /* + * Implementations of standard interfaces. + */ + virtual const char* GetFileName(void) const { return fDirEntry.fileName; } + virtual const char* GetPathName(void) const { return fPathName; } + virtual char GetFssep(void) const { return kFssep; } + virtual long GetFileType(void) const { return fDirEntry.fileType; } + virtual long GetAuxType(void) const { return fDirEntry.auxType; } + virtual long GetAccess(void) const { return fDirEntry.access; } + virtual time_t GetCreateWhen(void) const; + virtual time_t GetModWhen(void) const; + virtual di_off_t GetDataLength(void) const { + if (GetQuality() == kQualityDamaged) + return 0; + if (fDirEntry.storageType == kStorageExtended) + return fExtData.eof; + else + return fDirEntry.eof; + } + virtual di_off_t GetRsrcLength(void) const { + if (fDirEntry.storageType == kStorageExtended) { + if (GetQuality() == kQualityDamaged) + return 0; + else + return fExtRsrc.eof; + } else + return -1; + } + virtual di_off_t GetDataSparseLength(void) const { + if (GetQuality() == kQualityDamaged) + return 0; + else + return fSparseDataEof; + } + virtual di_off_t GetRsrcSparseLength(void) const { + if (GetQuality() == kQualityDamaged) + return 0; + else + return fSparseRsrcEof; + } + virtual bool IsDirectory(void) const { + return (fDirEntry.storageType == kStorageDirectory || + fDirEntry.storageType == kStorageVolumeDirHeader); + } + virtual bool IsVolumeDirectory(void) const { + return (fDirEntry.storageType == kStorageVolumeDirHeader); + } + + virtual DIError Open(A2FileDescr** ppOpenFile, bool readOnly, + bool rsrcFork = false); + virtual void CloseDescr(A2FileDescr* pOpenFile) { + assert(pOpenFile == fpOpenFile); + delete fpOpenFile; + fpOpenFile = NULL; + } + virtual bool IsFileOpen(void) const { return fpOpenFile != NULL; } + + virtual void SetParent(A2File* pParent) { fpParent = pParent; } + virtual A2File* GetParent(void) const { return fpParent; } + + static char NameToLower(char ch); + static void InitDirEntry(DirEntry* pEntry, + const unsigned char* entryBuf); + + virtual void Dump(void) const; + + /* directory entry contents for this file */ + DirEntry fDirEntry; + + /* pointer to directory entry (update dir if file size or dates change) */ + unsigned short fParentDirBlock; // directory block + int fParentDirIdx; // index in dir block + + /* these are only valid if storageType == kStorageExtended */ + ExtendedInfo fExtData; + ExtendedInfo fExtRsrc; + + void SetPathName(const char* basePath, const char* fileName); + static time_t ConvertProDate(ProDate proDate); + static ProDate ConvertProDate(time_t unixDate); + + /* returns "true" if AppleWorks aux type is used for lower-case name */ + static bool UsesAppleWorksAuxType(unsigned char fileType) { + return (fileType >= 0x19 && fileType <= 0x1b); + } + +#if 0 + /* change fPathName; should only be used by DiskFS rename */ + void SetPathName(const char* name) { + delete[] fPathName; + if (name == NULL) { + fPathName = NULL; + } else { + fPathName = new char[strlen(name)+1]; + if (fPathName != NULL) + strcpy(fPathName, name); + } + } +#endif + + DIError LoadBlockList(int storageType, unsigned short keyBlock, + long eof, long* pBlockCount, unsigned short** pBlockList, + long* pIndexBlockCount=NULL, unsigned short** pIndexBlockList=NULL); + DIError LoadDirectoryBlockList(unsigned short keyBlock, + long eof, long* pBlockCount, unsigned short** pBlockList); + + /* fork lengths without sparseness */ + di_off_t fSparseDataEof; + di_off_t fSparseRsrcEof; + +private: + DIError LoadIndexBlock(unsigned short block, unsigned short* list, + int maxCount); + DIError ValidateBlockList(const unsigned short* list, long count); + + char* fPathName; // full pathname to file on this volume + + A2FDProDOS* fpOpenFile; // only one fork can be open at a time + A2File* fpParent; +}; + + +/* + * =========================================================================== + * Pascal + * =========================================================================== + */ + +/* + * Pascal disk. + * + * There is no allocation map or file index blocks, just a linear collection + * of files with contiguous blocks. + */ +class A2FilePascal; + +class DISKIMG_API DiskFSPascal : public DiskFS { +public: + DiskFSPascal(void) : fDirectory(NULL) {} + virtual ~DiskFSPascal(void) { + if (fDirectory != NULL) { + assert(false); // unexpected + delete[] fDirectory; + } + } + + static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency); + + virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { + SetDiskImg(pImg); + return Initialize(); + } + virtual DIError Format(DiskImg* pDiskImg, const char* volName); + + // assorted constants + enum { + kMaxVolumeName = 7, + kDirectoryEntryLen = 26, + }; + typedef unsigned short PascalDate; + + virtual const char* GetVolumeName(void) const { return fVolumeName; } + virtual const char* GetVolumeID(void) const { return fVolumeID; } + virtual const char* GetBareVolumeName(void) const { return fVolumeName; } + virtual bool GetReadWriteSupported(void) const { return true; } + virtual bool GetFSDamaged(void) const { return !fDiskIsGood; } + virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const; + virtual DIError NormalizePath(const char* path, char fssep, + char* normalizedBuf, int* pNormalizedBufLen); + virtual DIError CreateFile(const CreateParms* pParms, A2File** ppNewFile); + virtual DIError DeleteFile(A2File* pFile); + virtual DIError RenameFile(A2File* pFile, const char* newName); + virtual DIError SetFileInfo(A2File* pFile, long fileType, long auxType, + long accessFlags); + virtual DIError RenameVolume(const char* newName); + + static bool IsValidVolumeName(const char* name); + static bool IsValidFileName(const char* name); + + unsigned short GetTotalBlocks(void) const { return fTotalBlocks; } + + friend class A2FDPascal; + +private: + DIError Initialize(void); + DIError LoadVolHeader(void); + void SetVolumeID(void); + void DumpVolHeader(void); + DIError LoadCatalog(void); + DIError SaveCatalog(void); + void FreeCatalog(void); + DIError ProcessCatalog(void); + DIError ScanFileUsage(void); + void SetBlockUsage(long block, VolumeUsage::ChunkPurpose purpose); + DIError WriteBootBlocks(void); + bool CheckDiskIsGood(void); + void DoNormalizePath(const char* name, char fssep, char* outBuf); + DIError MakeFileNameUnique(char* fileName); + DIError FindLargestFreeArea(int *pPrevIdx, A2FilePascal** ppPrevFile); + unsigned char* FindDirEntry(A2FilePascal* pFile); + + enum { kMaxExtensionLen = 5 }; // used when normalizing; ".code" is 4 + + /* some items from the volume header */ + unsigned short fStartBlock; // first block of dir hdr; always 2 + unsigned short fNextBlock; // i.e. first block with data + char fVolumeName[kMaxVolumeName+1]; + char fVolumeID[kMaxVolumeName + 16]; // add "Pascal ___:" + unsigned short fTotalBlocks; + unsigned short fNumFiles; + PascalDate fAccessWhen; // PascalDate last access + PascalDate fDateSetWhen; // PascalDate last date setting + unsigned short fStuff1; // + unsigned short fStuff2; // + + /* other goodies */ + bool fDiskIsGood; + bool fEarlyDamage; + + /* + * Pascal disks have one fixed-size directory. The contents aren't + * divided into blocks, which means you can't always edit an entry + * by loading a single block from disk and writing it back. Also, + * deleted entries are squeezed out, so if we delete an entry we + * have to reshuffle the entries below it. + * + * We want to keep the copy on disk synced up, so we don't hold on + * to this longer than necessary. Possibly less efficient that way; + * if it becomes a problem it's easy enough to change the behavior. + */ + unsigned char* fDirectory; +}; + +/* + * File descriptor for an open Pascal file. + */ +class DISKIMG_API A2FDPascal : public A2FileDescr { +public: + A2FDPascal(A2File* pFile) : A2FileDescr(pFile) { + fOffset = 0; + } + virtual ~A2FDPascal(void) { + /* nothing to clean up */ + } + + friend class A2FilePascal; + + virtual DIError Read(void* buf, size_t len, size_t* pActual = NULL); + virtual DIError Write(const void* buf, size_t len, size_t* pActual = NULL); + virtual DIError Seek(di_off_t offset, DIWhence whence); + virtual di_off_t Tell(void); + virtual DIError Close(void); + + virtual long GetSectorCount(void) const; + virtual long GetBlockCount(void) const; + virtual DIError GetStorage(long sectorIdx, long* pTrack, long* pSector) const; + virtual DIError GetStorage(long blockIdx, long* pBlock) const; + +private: + di_off_t fOffset; // where we are + di_off_t fOpenEOF; // how big the file currently is + long fOpenBlocksUsed; // how many blocks it occupies + bool fModified; // if modified, update dir on Close +}; + +/* + * File on a Pascal disk. + */ +class DISKIMG_API A2FilePascal : public A2File { +public: + A2FilePascal(DiskFS* pDiskFS) : A2File(pDiskFS) { + fpOpenFile = NULL; + } + virtual ~A2FilePascal(void) { + /* this comes back and calls CloseDescr */ + if (fpOpenFile != NULL) + fpOpenFile->Close(); + } + + typedef DiskFSPascal::PascalDate PascalDate; + + // assorted constants + enum { + kMaxFileName = 15, + }; + typedef enum FileType { + kTypeUntyped = 0, // NON + kTypeXdsk = 1, // BAD (bad blocks) + kTypeCode = 2, // PCD + kTypeText = 3, // PTX + kTypeInfo = 4, // ? + kTypeData = 5, // PDA + kTypeGraf = 6, // ? + kTypeFoto = 7, // FOT? (hires image) + kTypeSecurdir = 8 // ?? + } FileType; + + /* + * Implementations of standard interfaces. + */ + virtual const char* GetFileName(void) const { return fFileName; } + virtual const char* GetPathName(void) const { return fFileName; } + virtual char GetFssep(void) const { return '\0'; } + virtual long GetFileType(void) const; + virtual long GetAuxType(void) const { return 0; } + virtual long GetAccess(void) const { return DiskFS::kFileAccessUnlocked; } + virtual time_t GetCreateWhen(void) const { return 0; } + virtual time_t GetModWhen(void) const; + virtual di_off_t GetDataLength(void) const { return fLength; } + virtual di_off_t GetDataSparseLength(void) const { return fLength; } + virtual di_off_t GetRsrcLength(void) const { return -1; } + virtual di_off_t GetRsrcSparseLength(void) const { return -1; } + + virtual DIError Open(A2FileDescr** pOpenFile, bool readOnly, + bool rsrcFork = false); + virtual void CloseDescr(A2FileDescr* pOpenFile) { + assert(pOpenFile == fpOpenFile); + delete fpOpenFile; + fpOpenFile = NULL; + } + virtual bool IsFileOpen(void) const { return fpOpenFile != NULL; } + + + virtual void Dump(void) const; + + static time_t ConvertPascalDate(PascalDate pascalDate); + static A2FilePascal::PascalDate ConvertPascalDate(time_t unixDate); + static A2FilePascal::FileType ConvertFileType(long prodosType); + + /* fields pulled out of directory block */ + unsigned short fStartBlock; + unsigned short fNextBlock; + FileType fFileType; + char fFileName[kMaxFileName+1]; + unsigned short fBytesRemaining; + PascalDate fModWhen; + + /* derived fields */ + di_off_t fLength; + + /* note to self: don't try to store a directory offset here; they shift + every time you add or delete a file */ + +private: + A2FileDescr* fpOpenFile; +}; + + +/* + * =========================================================================== + * CP/M + * =========================================================================== + */ + +/* + * CP/M disk. + * + * We really ought to be using 1K blocks here, since that's the native + * CP/M format, but there's little value in making an exception for such + * a rarely used Apple II format. + * + * There is no allocation map or file index blocks, just a single 2K + * directory filled with files that have up to 16 1K blocks each. If + * a file is longer than 16K, a second entry with the identical name + * and user number is made. These "extents" may be sparse, so it's + * necessary to use the "records" field to determine the actual file length. + */ +class A2FileCPM; +class DISKIMG_API DiskFSCPM : public DiskFS { +public: + DiskFSCPM(void) : fDiskIsGood(false) {} + virtual ~DiskFSCPM(void) {} + + static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency); + + virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { + SetDiskImg(pImg); + return Initialize(); + } + + virtual const char* GetVolumeName(void) const { return "CP/M"; } + virtual const char* GetVolumeID(void) const { return "CP/M"; } + virtual const char* GetBareVolumeName(void) const { return NULL; } + virtual bool GetReadWriteSupported(void) const { return false; } + virtual bool GetFSDamaged(void) const { return !fDiskIsGood; } + virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const + { return kDIErrNotSupported; } + + // assorted constants + enum { + kDirectoryEntryLen = 32, + kVolDirBlock = 24, // ProDOS block where volume dir starts + kDirFileNameLen = 11, // 8+3 without the '.' + kFullDirSize = 2048, // blocks 0 and 1 + kDirEntryBlockCount = 16, // #of blocks held in dir slot + kNumDirEntries = kFullDirSize/kDirectoryEntryLen, + kExtentsInLowByte = 32, + + kDirEntryFlagContinued = 0x8000, // "flags" word + }; + // Contents of the raw 32-byte directory entry. + // + // From http://www.seasip.demon.co.uk/Cpm/format22.html + // + // UU F1 F2 F3 F4 F5 F6 F7 F8 T1 T2 T3 EX S1 S2 RC .FILENAMETYP.... + // AL AL AL AL AL AL AL AL AL AL AL AL AL AL AL AL ................ + // + // If the high bit of T1 is set, the file is read-only. If the high + // bit of T2 is set, the file is a "system" file. + // + // Files larger than (1024 * 16) have multiple "extent" entries, i.e. + // entries with the same user number and file name. + typedef struct DirEntry { + unsigned char userNumber; // 0-15 or 0-31 (usually 0), e5=unused + unsigned char fileName[kDirFileNameLen+1]; + unsigned short extent; // extent (EX + S2 * 32) + unsigned char S1; // reserved, should be zero + unsigned char records; // #of 128-byte records in this extent + unsigned char blocks[kDirEntryBlockCount]; + bool readOnly; + bool system; + bool badBlockList; // set if block list is damaged + } DirEntry; + + static long CPMToProDOSBlock(long cpmBlock) { + return kVolDirBlock + (cpmBlock*2); + } + +private: + DIError Initialize(void); + DIError ReadCatalog(void); + DIError ScanFileUsage(void); + void SetBlockUsage(long block, VolumeUsage::ChunkPurpose purpose); + void FormatName(char* dstBuf, const char* srcBuf); + DIError ComputeLength(A2FileCPM* pFile); + bool CheckDiskIsGood(void); + + // the full set of raw dir entries + DirEntry fDirEntry[kNumDirEntries]; + + bool fDiskIsGood; +}; + +/* + * File descriptor for an open CP/M file. + */ +class DISKIMG_API A2FDCPM : public A2FileDescr { +public: + A2FDCPM(A2File* pFile) : A2FileDescr(pFile) { + //fOpen = false; + fBlockList = NULL; + } + virtual ~A2FDCPM(void) { + delete fBlockList; + fBlockList = NULL; + } + + friend class A2FileCPM; + + virtual DIError Read(void* buf, size_t len, size_t* pActual = NULL); + virtual DIError Write(const void* buf, size_t len, size_t* pActual = NULL); + virtual DIError Seek(di_off_t offset, DIWhence whence); + virtual di_off_t Tell(void); + virtual DIError Close(void); + + virtual long GetSectorCount(void) const; + virtual long GetBlockCount(void) const; + virtual DIError GetStorage(long sectorIdx, long* pTrack, long* pSector) const; + virtual DIError GetStorage(long blockIdx, long* pBlock) const; + +private: + //bool fOpen; + di_off_t fOffset; + long fBlockCount; + unsigned char* fBlockList; +}; + +/* + * File on a CP/M disk. + */ +class DISKIMG_API A2FileCPM : public A2File { +public: + typedef DiskFSCPM::DirEntry DirEntry; + + A2FileCPM(DiskFS* pDiskFS, DirEntry* pDirEntry) : + A2File(pDiskFS), fpDirEntry(pDirEntry) + { + fDirIdx = -1; + fpOpenFile = NULL; + } + virtual ~A2FileCPM(void) { + delete fpOpenFile; + } + + // assorted constants + enum { + kMaxFileName = 12, // 8+3 including '.' + }; + + /* + * Implementations of standard interfaces. + */ + virtual const char* GetFileName(void) const { return fFileName; } + virtual const char* GetPathName(void) const { return fFileName; } + virtual char GetFssep(void) const { return '\0'; } + virtual long GetFileType(void) const { return 0; } + virtual long GetAuxType(void) const { return 0; } + virtual long GetAccess(void) const { + if (fReadOnly) + return DiskFS::kFileAccessLocked; + else + return DiskFS::kFileAccessUnlocked; + } + virtual time_t GetCreateWhen(void) const { return 0; } + virtual time_t GetModWhen(void) const { return 0; } + virtual di_off_t GetDataLength(void) const { return fLength; } + virtual di_off_t GetDataSparseLength(void) const { return fLength; } + virtual di_off_t GetRsrcLength(void) const { return -1; } + virtual di_off_t GetRsrcSparseLength(void) const { return -1; } + + virtual DIError Open(A2FileDescr** ppOpenFile, bool readOnly, + bool rsrcFork = false); + virtual void CloseDescr(A2FileDescr* pOpenFile) { + assert(pOpenFile == fpOpenFile); + delete fpOpenFile; + fpOpenFile = NULL; + } + virtual bool IsFileOpen(void) const { return fpOpenFile != NULL; } + + virtual void Dump(void) const; + + /* fields pulled out of directory block */ + char fFileName[kMaxFileName+1]; + bool fReadOnly; + + /* derived fields */ + di_off_t fLength; + int fDirIdx; // index into fDirEntry for part #1 + + DIError GetBlockList(long* pBlockCount, unsigned char* blockBuf) const; + +private: + const DirEntry* fpDirEntry; + A2FileDescr* fpOpenFile; +}; + + +/* + * =========================================================================== + * RDOS + * =========================================================================== + */ + +/* + * RDOS disk. + * + * There is no allocation map or file index blocks, just a linear collection + * of files with contiguous sectors. Very similar to Pascal. + * + * The one interesting quirk is the "converted 13-sector disk" format, where + * only 13 of 16 sectors are actually used. The linear sector addressing + * must take that into account. + */ +class A2FileRDOS; +class DISKIMG_API DiskFSRDOS : public DiskFS { +public: + DiskFSRDOS(void) {} + virtual ~DiskFSRDOS(void) {} + + static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency); + + virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { + SetDiskImg(pImg); + return Initialize(); + } + + virtual const char* GetVolumeName(void) const { return fVolumeName; } + virtual const char* GetVolumeID(void) const { return fVolumeName; } + virtual const char* GetBareVolumeName(void) const { return NULL; } + virtual bool GetReadWriteSupported(void) const { return false; } + virtual bool GetFSDamaged(void) const { return false; } + virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const + { return kDIErrNotSupported; } + + int GetOurSectPerTrack(void) const { return fOurSectPerTrack; } + +private: + static DIError TestCommon(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + FSLeniency leniency, DiskImg::FSFormat* pFormatFound); + + DIError Initialize(void); + DIError ReadCatalog(void); + DIError ScanFileUsage(void); + void SetSectorUsage(long track, long sector, + VolumeUsage::ChunkPurpose purpose); + + char fVolumeName[10]; // e.g. "RDOS 3.3" + int fOurSectPerTrack; +}; + +/* + * File descriptor for an open RDOS file. + */ +class DISKIMG_API A2FDRDOS : public A2FileDescr { +public: + A2FDRDOS(A2File* pFile) : A2FileDescr(pFile) { + fOffset = 0; + } + virtual ~A2FDRDOS(void) { + /* nothing to clean up */ + } + + friend class A2FileRDOS; + + virtual DIError Read(void* buf, size_t len, size_t* pActual = NULL); + virtual DIError Write(const void* buf, size_t len, size_t* pActual = NULL); + virtual DIError Seek(di_off_t offset, DIWhence whence); + virtual di_off_t Tell(void); + virtual DIError Close(void); + + virtual long GetSectorCount(void) const; + virtual long GetBlockCount(void) const; + virtual DIError GetStorage(long sectorIdx, long* pTrack, long* pSector) const; + virtual DIError GetStorage(long blockIdx, long* pBlock) const; + +private: + /* RDOS is unique in that it can put 13-sector disks on 16-sector tracks */ + inline int GetOurSectPerTrack(void) const { + DiskFSRDOS* pDiskFS = (DiskFSRDOS*) fpFile->GetDiskFS(); + return pDiskFS->GetOurSectPerTrack(); + } + + //bool fOpen; + di_off_t fOffset; +}; + +/* + * File on an RDOS disk. + */ +class DISKIMG_API A2FileRDOS : public A2File { +public: + A2FileRDOS(DiskFS* pDiskFS) : A2File(pDiskFS) { + //fOpen = false; + fpOpenFile = NULL; + } + virtual ~A2FileRDOS(void) { + delete fpOpenFile; + } + + // assorted constants + enum { + kMaxFileName = 24, + }; + typedef enum FileType { + kTypeUnknown = 0, + kTypeApplesoft, // 'A' + kTypeBinary, // 'B' + kTypeText, // 'T' + } FileType; + + /* + * Implementations of standard interfaces. + */ + virtual const char* GetFileName(void) const { return fFileName; } + virtual const char* GetPathName(void) const { return fFileName; } + virtual char GetFssep(void) const { return '\0'; } + virtual long GetFileType(void) const; + virtual long GetAuxType(void) const { return fLoadAddr; } + virtual long GetAccess(void) const { return DiskFS::kFileAccessUnlocked; } + virtual time_t GetCreateWhen(void) const { return 0; } + virtual time_t GetModWhen(void) const { return 0; }; + virtual di_off_t GetDataLength(void) const { return fLength; } + virtual di_off_t GetDataSparseLength(void) const { return fLength; } + virtual di_off_t GetRsrcLength(void) const { return -1; } + virtual di_off_t GetRsrcSparseLength(void) const { return -1; } + + virtual DIError Open(A2FileDescr** ppOpenFile, bool readOnly, + bool rsrcFork = false); + virtual void CloseDescr(A2FileDescr* pOpenFile) { + assert(pOpenFile == fpOpenFile); + delete fpOpenFile; + fpOpenFile = NULL; + } + virtual bool IsFileOpen(void) const { return fpOpenFile != NULL; } + + void FixFilename(void); + virtual void Dump(void) const; + + /* fields pulled out of directory block */ + char fFileName[kMaxFileName+1]; + FileType fFileType; + unsigned short fNumSectors; + unsigned short fLoadAddr; + unsigned short fLength; + unsigned short fStartSector; + +private: + void TrimTrailingSpaces(char* filename); + + A2FileDescr* fpOpenFile; +}; + + +/* + * =========================================================================== + * HFS + * =========================================================================== + */ + +/* + * HFS disk. + */ +class A2FileHFS; +class DISKIMG_API DiskFSHFS : public DiskFS { +public: + DiskFSHFS(void) { + fLocalTimeOffset = -1; + fDiskIsGood = true; +#ifndef EXCISE_GPL_CODE + fHfsVol = NULL; +#endif + } + virtual ~DiskFSHFS(void) { +#ifndef EXCISE_GPL_CODE + hfs_callback_close(fHfsVol); + fHfsVol = (hfsvol*) 0xcdaaaacd; +#endif + } + + static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency); + + virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { + SetDiskImg(pImg); + return Initialize(initMode); + } + +#ifndef EXCISE_GPL_CODE + /* these are optional, defined as no-ops in the parent class */ + virtual DIError Format(DiskImg* pDiskImg, const char* volName); + virtual DIError NormalizePath(const char* path, char fssep, + char* normalizedBuf, int* pNormalizedBufLen); + virtual DIError CreateFile(const CreateParms* pParms, A2File** ppNewFile); + virtual DIError DeleteFile(A2File* pFile); + virtual DIError RenameFile(A2File* pFile, const char* newName); + virtual DIError SetFileInfo(A2File* pFile, long fileType, long auxType, + long accessFlags); + virtual DIError RenameVolume(const char* newName); +#endif + + // assorted constants + enum { + kMaxVolumeName = 27, + kMaxExtensionLen = 4, // used when normalizing; ".gif" is 4 + }; + + /* mandatory functions */ + virtual const char* GetVolumeName(void) const { return fVolumeName; } + virtual const char* GetVolumeID(void) const { return fVolumeID; } + virtual const char* GetBareVolumeName(void) const { return fVolumeName; } + virtual bool GetReadWriteSupported(void) const { return true; } + virtual bool GetFSDamaged(void) const { return false; } + virtual long GetFSNumBlocks(void) const { return fTotalBlocks; } + virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const; + +#ifndef EXCISE_GPL_CODE + hfsvol* GetHfsVol(void) const { return fHfsVol; } +#endif + + // utility function, used by app + static bool IsValidVolumeName(const char* name); + static bool IsValidFileName(const char* name); + +private: + enum { + // Macintosh 32-bit dates start in 1904, everybody else starts in + // 1970. Take the Mac date and adjust it 66 years plus 17 leap days. + // The annoying part is that HFS stores dates in local time, which + // means it's impossible to know absolutely when a file was modified. + // libhfs converts timestamps to the current time zone, so that a + // file written January 1st 2006 at 6pm in London will appear to have + // been written January 1st 2006 at 6pm in San Francisco if you + // happen to be sitting in California. + // + // This was fixed in HFS+, but we have to deal with it for now. The + // value below converts the date to local time in Greenwich; the + // current GMT offset and daylight saving time must be added to it. + // + // Curiously, the volume dates shown by Cmd-I on the volume on my + // Quadra are off by an hour, even though the file dates match. + kDateTimeOffset = (1970 - 1904) * 60 * 60 * 24 * 365 + + (60 * 60 * 24 * 17), + + kExpectedMinBlocks = 1440, // ignore volumes under 720K + }; + + struct MasterDirBlock; // fwd + static void UnpackMDB(const unsigned char* buf, MasterDirBlock* pMDB); + static DIError TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder); + + DIError Initialize(InitMode initMode); + DIError LoadVolHeader(void); + void SetVolumeID(void); + void DumpVolHeader(void); + void SetVolumeUsageMap(void); + +#ifdef EXCISE_GPL_CODE + void CreateFakeFile(void); +#else + DIError RecursiveDirAdd(A2File* pParent, const char* basePath, int depth); + //void Sanitize(unsigned char* str); + DIError DoNormalizePath(const char* path, char fssep, + char** pNormalizedPath); + static int CompareMacFileNames(const char* str1, const char* str2); + DIError RegeneratePathName(A2FileHFS* pFile); + DIError MakeFileNameUnique(const char* pathName, char** pUniqueName); + + /* libhfs stuff */ + static unsigned long LibHFSCB(void* vThis, int op, unsigned long arg1, + void* arg2); + hfsvol* fHfsVol; +#endif + + + /* some items from the volume header */ + char fVolumeName[kMaxVolumeName+1]; + char fVolumeID[kMaxVolumeName + 8]; // add "HFS :" + unsigned long fTotalBlocks; + unsigned long fAllocationBlockSize; + unsigned long fNumAllocationBlocks; + unsigned long fCreatedDateTime; + unsigned long fModifiedDateTime; + unsigned long fNumFiles; + unsigned long fNumDirectories; + + long fLocalTimeOffset; + bool fDiskIsGood; +}; + +/* + * File descriptor for an open HFS file. + */ +class DISKIMG_API A2FDHFS : public A2FileDescr { +public: +#ifdef EXCISE_GPL_CODE + A2FDHFS(A2File* pFile, void* unused) + : A2FileDescr(pFile), fOffset(0) + {} +#else + A2FDHFS(A2File* pFile, hfsfile* pHfsFile) + : A2FileDescr(pFile), fHfsFile(pHfsFile), fModified(false) + {} +#endif + virtual ~A2FDHFS(void) { +#ifndef EXCISE_GPL_CODE + if (fHfsFile != NULL) + hfs_close(fHfsFile); +#endif + } + + friend class A2FileHFS; + + virtual DIError Read(void* buf, size_t len, size_t* pActual = NULL); + virtual DIError Write(const void* buf, size_t len, size_t* pActual = NULL); + virtual DIError Seek(di_off_t offset, DIWhence whence); + virtual di_off_t Tell(void); + virtual DIError Close(void); + + virtual long GetSectorCount(void) const; + virtual long GetBlockCount(void) const; + virtual DIError GetStorage(long sectorIdx, long* pTrack, long* pSector) const; + virtual DIError GetStorage(long blockIdx, long* pBlock) const; + +private: +#ifdef EXCISE_GPL_CODE + di_off_t fOffset; +#else + hfsfile* fHfsFile; + bool fModified; +#endif +}; + +/* + * File on an HFS disk. + */ +class DISKIMG_API A2FileHFS : public A2File { +public: + A2FileHFS(DiskFS* pDiskFS) : A2File(pDiskFS) { + fPathName = NULL; + fpOpenFile = NULL; +#ifdef EXCISE_GPL_CODE + fFakeFileBuf = NULL; +#else + //fOrigPathName = NULL; +#endif + } + virtual ~A2FileHFS(void) { + delete fpOpenFile; + delete[] fPathName; +#ifdef EXCISE_GPL_CODE + delete[] fFakeFileBuf; +#else + //delete[] fOrigPathName; +#endif + } + + /* + * Implementations of standard interfaces. + */ + virtual const char* GetFileName(void) const { return fFileName; } + virtual const char* GetPathName(void) const { return fPathName; } + virtual char GetFssep(void) const { return kFssep; } + virtual long GetFileType(void) const; + virtual long GetAuxType(void) const; + virtual long GetAccess(void) const { return fAccess; } + virtual time_t GetCreateWhen(void) const { return fCreateWhen; } + virtual time_t GetModWhen(void) const { return fModWhen; } + virtual di_off_t GetDataLength(void) const { return fDataLength; } + virtual di_off_t GetDataSparseLength(void) const { return fDataLength; } + virtual di_off_t GetRsrcLength(void) const { return fRsrcLength; } + virtual di_off_t GetRsrcSparseLength(void) const { return fRsrcLength; } + virtual bool IsDirectory(void) const { return fIsDir; } + virtual bool IsVolumeDirectory(void) const { return fIsVolumeDir; } + + virtual DIError Open(A2FileDescr** pOpenFile, bool readOnly, + bool rsrcFork = false); + virtual void CloseDescr(A2FileDescr* pOpenFile) { + assert(pOpenFile == fpOpenFile); + delete fpOpenFile; + fpOpenFile = NULL; + } + virtual bool IsFileOpen(void) const { return fpOpenFile != NULL; } + + enum { + kMaxFileName = 31, + kFssep = ':', + kPdosType = 0x70646f73, // 'pdos' + }; + + void SetPathName(const char* basePath, const char* fileName); + virtual void Dump(void) const; + +#ifdef EXCISE_GPL_CODE + void SetFakeFile(void* buf, long len) { + assert(len > 0); + if (fFakeFileBuf != NULL) + delete[] fFakeFileBuf; + fFakeFileBuf = new char[len]; + memcpy(fFakeFileBuf, buf, len); + fDataLength = len; + } + const void* GetFakeFileBuf(void) const { return fFakeFileBuf; } +#else + void InitEntry(const hfsdirent* dirEntry); + void SetOrigPathName(const char* pathName); + virtual void SetParent(A2File* pParent) { fpParent = pParent; } + virtual A2File* GetParent(void) const { return fpParent; } + char* GetLibHFSPathName(void) const; + static void ConvertTypeToHFS(long fileType, long auxType, + char* pType, char* pCreator); +#endif + + bool fIsDir; + bool fIsVolumeDir; + long fType; + long fCreator; + char fFileName[kMaxFileName+1]; + char* fPathName; + di_off_t fDataLength; + di_off_t fRsrcLength; + time_t fCreateWhen; + time_t fModWhen; + long fAccess; + +private: +#ifdef EXCISE_GPL_CODE + char* fFakeFileBuf; +#else + //char* fOrigPathName; + A2File* fpParent; +#endif + A2FileDescr* fpOpenFile; // only one fork can be open at a time +}; + + +/* + * =========================================================================== + * FAT (including FAT12, FAT16, and FAT32) + * =========================================================================== + */ + +/* + * MS-DOS FAT disk. + * + * This is currently just the minimum necessary to properly recognize + * the disk. + */ +class A2FileFAT; +class DISKIMG_API DiskFSFAT : public DiskFS { +public: + DiskFSFAT(void) {} + virtual ~DiskFSFAT(void) {} + + static DIError TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency); + + virtual DIError Initialize(DiskImg* pImg, InitMode initMode) { + SetDiskImg(pImg); + return Initialize(); + } + + // assorted constants + enum { + kMaxVolumeName = 11, + }; + + virtual const char* GetVolumeName(void) const { return fVolumeName; } + virtual const char* GetVolumeID(void) const { return fVolumeID; } + virtual const char* GetBareVolumeName(void) const { return fVolumeName; } + virtual bool GetReadWriteSupported(void) const { return false; } + virtual bool GetFSDamaged(void) const { return false; } + virtual long GetFSNumBlocks(void) const { return fTotalBlocks; } + virtual DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const + { return kDIErrNotSupported; } + +private: + enum { + kExpectedMinBlocks = 720, // ignore volumes under 360K + }; + + struct MasterBootRecord; // fwd + struct BootSector; + static bool UnpackMBR(const unsigned char* buf, MasterBootRecord* pOut); + static bool UnpackBootSector(const unsigned char* buf, BootSector* pOut); + static DIError TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder); + + DIError Initialize(void); + DIError LoadVolHeader(void); + void DumpVolHeader(void); + void SetVolumeUsageMap(void); + void CreateFakeFile(void); + + /* some items from the volume header */ + char fVolumeName[kMaxVolumeName+1]; + char fVolumeID[kMaxVolumeName + 8]; // add "FAT %s:" + unsigned long fTotalBlocks; +}; + +/* + * File descriptor for an open FAT file. + */ +class DISKIMG_API A2FDFAT : public A2FileDescr { +public: + A2FDFAT(A2File* pFile) : A2FileDescr(pFile) { + fOffset = 0; + } + virtual ~A2FDFAT(void) { + /* nothing to clean up */ + } + + friend class A2FileFAT; + + virtual DIError Read(void* buf, size_t len, size_t* pActual = NULL); + virtual DIError Write(const void* buf, size_t len, size_t* pActual = NULL); + virtual DIError Seek(di_off_t offset, DIWhence whence); + virtual di_off_t Tell(void); + virtual DIError Close(void); + + virtual long GetSectorCount(void) const; + virtual long GetBlockCount(void) const; + virtual DIError GetStorage(long sectorIdx, long* pTrack, long* pSector) const; + virtual DIError GetStorage(long blockIdx, long* pBlock) const; + +private: + di_off_t fOffset; +}; + +/* + * File on a FAT disk. + */ +class DISKIMG_API A2FileFAT : public A2File { +public: + A2FileFAT(DiskFS* pDiskFS) : A2File(pDiskFS) { + fFakeFileBuf = NULL; + //fFakeFileLen = -1; + fpOpenFile = NULL; + } + virtual ~A2FileFAT(void) { + delete fpOpenFile; + delete[] fFakeFileBuf; + } + + /* + * Implementations of standard interfaces. + */ + virtual const char* GetFileName(void) const { return fFileName; } + virtual const char* GetPathName(void) const { return fFileName; } + virtual char GetFssep(void) const { return '\0'; } + virtual long GetFileType(void) const { return 0; }; + virtual long GetAuxType(void) const { return 0; } + virtual long GetAccess(void) const { return DiskFS::kFileAccessUnlocked; } + virtual time_t GetCreateWhen(void) const { return 0; } + virtual time_t GetModWhen(void) const { return 0; } + virtual di_off_t GetDataLength(void) const { return fLength; } + virtual di_off_t GetDataSparseLength(void) const { return fLength; } + virtual di_off_t GetRsrcLength(void) const { return -1; } + virtual di_off_t GetRsrcSparseLength(void) const { return -1; } + + virtual DIError Open(A2FileDescr** pOpenFile, bool readOnly, + bool rsrcFork = false); + virtual void CloseDescr(A2FileDescr* pOpenFile) { + assert(pOpenFile == fpOpenFile); + delete fpOpenFile; + fpOpenFile = NULL; + } + virtual bool IsFileOpen(void) const { return fpOpenFile != NULL; } + + enum { kMaxFileName = 31 }; + + virtual void Dump(void) const; + + void SetFakeFile(void* buf, long len) { + assert(len > 0); + if (fFakeFileBuf != NULL) + delete[] fFakeFileBuf; + fFakeFileBuf = new char[len]; + memcpy(fFakeFileBuf, buf, len); + fLength = len; + } + const void* GetFakeFileBuf(void) const { return fFakeFileBuf; } + + char fFileName[kMaxFileName+1]; + di_off_t fLength; + +private: + char* fFakeFileBuf; + //long fFakeFileLen; + A2FileDescr* fpOpenFile; +}; + +}; // namespace DiskImgLib + +#endif /*__DISKIMGDETAIL__*/ diff --git a/diskimg/DiskImgPriv.h b/diskimg/DiskImgPriv.h new file mode 100644 index 0000000..d033ad6 --- /dev/null +++ b/diskimg/DiskImgPriv.h @@ -0,0 +1,364 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Declarations common within but private to the DiskImg library. + * + * External code should not include this. + */ +#ifndef __DISK_IMG_PRIV__ +#define __DISK_IMG_PRIV__ + +#include "DiskImgDetail.h" +#include +#include +// "GenericFD.h" included at end + +using namespace DiskImgLib; // make life easy for all internal code + +namespace DiskImgLib { + +#ifndef _DEBUG_LOG +//# define _DEBUG_LOG /* define this to force log msgs in non-debug build */ +#endif + +#if defined(_DEBUG) || defined(_DEBUG_LOG) +# define _DEBUG_MSGS +#endif + +/* + * Win32-style debug message macros. + */ +#ifdef _DEBUG_MSGS +#define WMSG0(fmt) \ + Global::PrintDebugMsg(__FILE__, __LINE__, fmt) +#define WMSG1(fmt, arg0) \ + Global::PrintDebugMsg(__FILE__, __LINE__, fmt, arg0) +#define WMSG2(fmt, arg0, arg1) \ + Global::PrintDebugMsg(__FILE__, __LINE__, fmt, arg0, arg1) +#define WMSG3(fmt, arg0, arg1, arg2) \ + Global::PrintDebugMsg(__FILE__, __LINE__, fmt, arg0, arg1, arg2) +#define WMSG4(fmt, arg0, arg1, arg2, arg3) \ + Global::PrintDebugMsg(__FILE__, __LINE__, fmt, arg0, arg1, arg2, arg3) +#define WMSG5(fmt, arg0, arg1, arg2, arg3, arg4) \ + Global::PrintDebugMsg(__FILE__, __LINE__, fmt, arg0, arg1, arg2, arg3, arg4) +#else +#define WMSG0(fmt) ((void) 0) +#define WMSG1(fmt, arg0) ((void) 0) +#define WMSG2(fmt, arg0, arg1) ((void) 0) +#define WMSG3(fmt, arg0, arg1, arg2) ((void) 0) +#define WMSG4(fmt, arg0, arg1, arg2, arg3) ((void) 0) +#define WMSG5(fmt, arg0, arg1, arg2, arg3, arg4) ((void) 0) +#endif + +/* put this in to break on interesting events when built debug */ +#if defined(_DEBUG) +# define DebugBreak() { assert(false); } +#else +# define DebugBreak() ((void) 0) +#endif + +/* + * Standard goodies. + */ +#define NELEM(x) ((int) (sizeof(x) / sizeof(x[0]))) + +#define nil NULL + +#define ErrnoOrGeneric() (errno != 0 ? (DIError) errno : kDIErrGeneric) + + +/* filename manipulation functions */ +const char* FilenameOnly(const char* pathname, char fssep); +const char* FindExtension(const char* pathname, char fssep); +char* StrcpyNew(const char* str); + +/* get/set integer values out of a memory buffer */ +unsigned short GetShortLE(const unsigned char* buf); +unsigned long GetLongLE(const unsigned char* buf); +unsigned short GetShortBE(const unsigned char* buf); +unsigned long GetLongBE(const unsigned char* buf); +unsigned long Get24BE(const unsigned char* ptr); +void PutShortLE(unsigned char* ptr, unsigned short val); +void PutLongLE(unsigned char* ptr, unsigned long val); +void PutShortBE(unsigned char* ptr, unsigned short val); +void PutLongBE(unsigned char* ptr, unsigned long val); + +/* little-endian read/write, for file headers (mainly 2MG and DC42) */ +DIError ReadShortLE(GenericFD* pGFD, short* pBuf); +DIError ReadLongLE(GenericFD* pGFD, long* pBuf); +DIError WriteShortLE(FILE* fp, unsigned short val); +DIError WriteLongLE(FILE* fp, unsigned long val); +DIError WriteShortLE(GenericFD* pGFD, unsigned short val); +DIError WriteLongLE(GenericFD* pGFD, unsigned long val); +DIError WriteShortBE(GenericFD* pGFD, unsigned short val); +DIError WriteLongBE(GenericFD* pGFD, unsigned long val); + +#ifdef _WIN32 +/* Windows helpers */ +DIError LastErrorToDIError(void); +bool IsWin9x(void); +#endif + + +/* + * Provide access to a buffer of data as if it were a circular buffer. + * Access is through the C array operator ([]). + * + * This DOES NOT own the array it is handed, and will not try to + * free it. + */ +class CircularBufferAccess { +public: + CircularBufferAccess(unsigned char* buf, long len) : + fBuf(buf), fLen(len) + { assert(fLen > 0); assert(fBuf != nil); } + CircularBufferAccess(const unsigned char* buf, long len) : + fBuf(const_cast(buf)), fLen(len) + { assert(fLen > 0); assert(fBuf != nil); } + ~CircularBufferAccess(void) {} + + /* + * Be circular. Assume that we won't stray far past the end, so + * it's cheaper to subtract than mod. + */ + unsigned char& operator[](int idx) const { + if (idx < 0) { + assert(false); + } + while (idx >= fLen) + idx -= fLen; + return fBuf[idx]; + } + + //unsigned char* GetPointer(int idx) const { + // while (idx >= fLen) + // idx -= fLen; + // return &fBuf[idx]; + //} + + int Normalize(int idx) const { + while (idx >= fLen) + idx -= fLen; + return idx; + } + + long GetSize(void) const { + return fLen; + } + +private: + unsigned char* fBuf; + long fLen; +}; + +/* + * Manage an output buffer into which we write one bit at a time. + * + * Bits fill in from the MSB to the LSB. If we write 10 bits, the + * output buffer will look like this: + * + * xxxxxxxx xx000000 + * + * Call WriteBit() repeatedly. When done, call Finish() to write any pending + * data and return the number of bits in the buffer. + */ +class BitOutputBuffer { +public: + /* pass in the output buffer and the output buffer's size */ + BitOutputBuffer(unsigned char* buf, int size) { + fBufStart = fBuf = buf; + fBufSize = size; + fBitMask = 0x80; + fByte = 0; + fOverflow = false; + } + virtual ~BitOutputBuffer(void) {} + + /* write a single bit */ + void WriteBit(int val) { + if (fBuf - fBufStart >= fBufSize) { + if (!fOverflow) { + WMSG0("Overran bit output buffer\n"); + DebugBreak(); + fOverflow = true; + } + return; + } + + if (val) + fByte |= fBitMask; + fBitMask >>= 1; + if (fBitMask == 0) { + *fBuf++ = fByte; + fBitMask = 0x80; + fByte = 0; + } + } + + /* flush pending bits; returns length in bits (or -1 on overrun) */ + int Finish(void) { + int outputBits; + + if (fOverflow) + return -1; + + outputBits = (fBuf - fBufStart) * 8; + + if (fBitMask != 0x80) { + *fBuf++ = fByte; + + assert(fBitMask != 0); + while (fBitMask != 0x80) { + outputBits++; + fBitMask <<= 1; + } + } + return outputBits; + } + +private: + unsigned char* fBufStart; + unsigned char* fBuf; + int fBufSize; + unsigned char fBitMask; + unsigned char fByte; + bool fOverflow; +}; + +/* + * Extract data from the buffer one bit or one byte at a time. + */ +class BitInputBuffer { +public: + BitInputBuffer(const unsigned char* buf, int bitCount) { + fBufStart = fBuf = buf; + fBitCount = bitCount; + fCurrentBit = 0; + fBitPosn = 7; + fBitsConsumed = 0; + } + virtual ~BitInputBuffer(void) {} + + /* + * Get the next bit. Returns 0 or 1. + * + * If we wrapped around to the start of the buffer, and "pWrap" is + * non-null, set "*pWrap". (This does *not* set it to "false" if we + * don't wrap.) + */ + unsigned char GetBit(bool* pWrap) { + unsigned char val; + + //assert(fBitPosn == 7 - (fCurrentBit & 0x07)); + + if (fCurrentBit == fBitCount) { + /* end reached, wrap to start */ + fCurrentBit = 0; + fBitPosn = 7; + fBuf = fBufStart; + //fByte = *fBuf++; + if (pWrap != nil) + *pWrap = true; + } + + val = (*fBuf >> fBitPosn) & 0x01; + + fCurrentBit++; + fBitPosn--; + if (fBitPosn < 0) { + fBitPosn = 7; + fBuf++; + } + + fBitsConsumed++; + return val; + } + + /* + * Get the next 8 bits. + */ + unsigned char GetByte(bool* pWrap) { + unsigned char val; + int i; + + if (true || fCurrentBit > fBitCount-8) { + /* near end, use single-bit function iteratively */ + val = 0; + for (i = 0; i < 8; i++) + val = (val << 1) | GetBit(pWrap); + } else { + /* room to spare, grab it in one or two chunks */ + assert(false); + } + return val; + } + + /* + * Set the start position. + */ + void SetStartPosition(int bitOffset) { + assert(bitOffset >= 0 && bitOffset < fBitCount); + fCurrentBit = bitOffset; + fBitPosn = 7 - (bitOffset & 0x07); // mod 8, 0 to MSB + fBuf = fBufStart + (bitOffset >> 3); // div 8 + } + + /* used to ensure we consume exactly 100% of bits */ + void ResetBitsConsumed(void) { fBitsConsumed = 0; } + int GetBitsConsumed(void) const { return fBitsConsumed; } + +private: + const unsigned char* fBufStart; + const unsigned char* fBuf; + int fBitCount; // #of bits in buffer + int fCurrentBit; // where we are in buffer + int fBitPosn; // which bit to access within byte + //unsigned char fByte; + + int fBitsConsumed; // sanity check - all bits used? +}; + +/* + * Linear bitmap. Suitable for use as a bad block map. + */ +class LinearBitmap { +public: + LinearBitmap(int numBits) { + assert(numBits > 0); + fBits = new unsigned char[(numBits + 7) / 8]; + memset(fBits, 0, (numBits + 7) / 8); + fNumBits = numBits; + } + ~LinearBitmap(void) { + delete[] fBits; + } + + /* + * Set or get the status of bit N. + */ + bool IsSet(int bit) const { + assert(bit >= 0 && bit < fNumBits); + return ((fBits[bit >> 3] >> (bit & 0x07)) & 0x01) != 0; + } + void Set(int bit) { + assert(bit >= 0 && bit < fNumBits); + fBits[bit >> 3] |= 1 << (bit & 0x07); + } + +private: + unsigned char* fBits; + int fNumBits; +}; + + +}; // namespace DiskImgLib + +/* + * Most of the code needs these. + */ +#include "GenericFD.h" + +#endif /*__DISK_IMG_PRIV__*/ diff --git a/diskimg/FAT.cpp b/diskimg/FAT.cpp new file mode 100644 index 0000000..99c26a4 --- /dev/null +++ b/diskimg/FAT.cpp @@ -0,0 +1,523 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Implementation of the Windows FAT filesystem. + * + * Right now we just try to identify that a disk is in a PC format rather + * than Apple II. The trick here is to figure out whether block 0 is a + * Master Boot Record or merely a Boot Sector. + */ +#include "StdAfx.h" +#include "DiskImgPriv.h" + + +/* + * =========================================================================== + * DiskFSFAT + * =========================================================================== + */ + +const int kBlkSize = 512; +const long kBootBlock = 0; +const unsigned short kSignature = 0xaa55; // MBR or boot sector +const int kSignatureOffset = 0x1fe; +const unsigned char kOpcodeMumble = 0x33; // seen on 2nd drive +const unsigned char kOpcodeBranch = 0xeb; +const unsigned char kOpcodeSetInt = 0xfa; + +typedef struct PartitionTableEntry { + unsigned char driveNum; // dl (0x80 or 0x00) + unsigned char startHead; // dh + unsigned char startSector; // cl (&0x3f=sector, +two hi bits cyl) + unsigned char startCylinder; // ch (low 8 bits of 10-bit cylinder) + unsigned char type; // partition type + unsigned char endHead; // dh + unsigned char endSector; // cl + unsigned char endCylinder; // ch + unsigned long startLBA; // in blocks + unsigned long size; // in blocks +} PartitionTableEntry; + +/* + * Definition of a Master Boot Record, which is block 0 of a physical volume. + */ +typedef struct DiskFSFAT::MasterBootRecord { + /* + * Begins immediately with code, usually 0xfa (set interrupt flag) or + * 0xeb (relative branch). + */ + unsigned char firstByte; + + /* + * Partition table starts at 0x1be. Four entries, each 16 bytes. + */ + PartitionTableEntry parTab[4]; +} MasterBootRecord; + +/* + * Definition of a boot sector, which is block 0 of a logical volume. + */ +typedef struct DiskFSFAT::BootSector { + /* + * The first few bytes of the boot sector is called the BIOS Parameter + * Block, or BPB. + */ + unsigned char jump[3]; // usually EB XX 90 + unsigned char oemName[8]; // e.g. "MSWIN4.1" or "MSDOS5.0" + unsigned short bytesPerSector; // usually (always?) 512 + unsigned char sectPerCluster; + unsigned short reservedSectors; + unsigned char numFAT; + unsigned short numRootDirEntries; + unsigned short numSectors; // if set, ignore numSectorsHuge + unsigned char mediaType; + unsigned short numFATSectors; + unsigned short sectorsPerTrack; + unsigned short numHeads; + unsigned long numHiddenSectors; + unsigned long numSectorsHuge; // only if numSectors==0 + /* + * This next part can start immediately after the above (at 0x24) for + * FAT12/FAT16, or somewhat later (0x42) for FAT32. It doesn't seem + * to exist for NTFS. Probably safest to assume it doesn't exist. + * + * The only way to be sure of what we're dealing with is to know the + * partition type, but if this is our block 0 then we can't know what + * that is. + */ + unsigned char driveNum; + unsigned char reserved; + unsigned char signature; // 0x29 + unsigned long volumeID; + unsigned char volumeLabel[11]; // e.g. "FUBAR " + unsigned char fileSysType[8]; // e.g. "FAT12 " + + /* + * Code follows. Signature 0xaa55 in the last two bytes. + */ +} BootSector; + +// some values for MediaType +enum MediaType { + kMediaTypeLarge = 0xf0, // 1440KB or 2800KB 3.5" disk + kMediaTypeHardDrive = 0xf8, + kMediaTypeMedium = 0xf9, // 720KB 3.5" disk or 1.2MB 5.25" disk + kMediaTypeSmall = 0xfd, // 360KB 5.25" disk +}; + + +/* + * Unpack the MBR. + * + * Returns "true" if this looks like an MBR, "false" otherwise. + */ +/*static*/ bool +DiskFSFAT::UnpackMBR(const unsigned char* buf, MasterBootRecord* pOut) +{ + const unsigned char* ptr; + int i; + + pOut->firstByte = buf[0x00]; + + ptr = &buf[0x1be]; + for (i = 0; i < 4; i++) { + pOut->parTab[i].driveNum = ptr[0x00]; + pOut->parTab[i].startHead = ptr[0x01]; + pOut->parTab[i].startSector = ptr[0x02]; + pOut->parTab[i].startCylinder = ptr[0x03]; + pOut->parTab[i].type = ptr[0x04]; + pOut->parTab[i].endHead = ptr[0x05]; + pOut->parTab[i].endSector = ptr[0x06]; + pOut->parTab[i].endCylinder = ptr[0x07]; + pOut->parTab[i].startLBA = GetLongLE(&ptr[0x08]); + pOut->parTab[i].size = GetLongLE(&ptr[0x0c]); + + ptr += 16; + } + + if (pOut->firstByte != kOpcodeBranch && + pOut->firstByte != kOpcodeSetInt && + pOut->firstByte != kOpcodeMumble) + return false; + bool foundActive = false; + for (i = 0; i < 4; i++) { + if (pOut->parTab[i].driveNum == 0x80) + foundActive = true; + else if (pOut->parTab[i].driveNum != 0x00) + return false; // must be 0x00 or 0x80 + } + // CFFA cards don't seem to set the "active" flag + //if (!foundActive) + // return false; + return true; +} + +/* + * Unpack the boot sector. + * + * Returns "true" if this looks like a boot sector, "false" otherwise. + */ +/*static*/ bool +DiskFSFAT::UnpackBootSector(const unsigned char* buf, BootSector* pOut) +{ + memcpy(pOut->jump, &buf[0x00], sizeof(pOut->jump)); + memcpy(pOut->oemName, &buf[0x03], sizeof(pOut->oemName)); + pOut->bytesPerSector = GetShortLE(&buf[0x0b]); + pOut->sectPerCluster = buf[0x0d]; + pOut->reservedSectors = GetShortLE(&buf[0x0e]); + pOut->numFAT = buf[0x10]; + pOut->numRootDirEntries = GetShortLE(&buf[0x11]); + pOut->numSectors = GetShortLE(&buf[0x13]); + pOut->mediaType = buf[0x15]; + pOut->numFATSectors = GetShortLE(&buf[0x16]); + pOut->sectorsPerTrack = GetShortLE(&buf[0x18]); + pOut->numHeads = GetShortLE(&buf[0x1a]); + pOut->numHiddenSectors = GetLongLE(&buf[0x1c]); + pOut->numSectorsHuge = GetLongLE(&buf[0x20]); + + if (pOut->jump[0] != kOpcodeBranch && pOut->jump[0] != kOpcodeSetInt) + return false; + if (pOut->bytesPerSector != 512) + return false; + return true; +} + +/* + * See if this looks like a FAT volume. + */ +/*static*/ DIError +DiskFSFAT::TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder) +{ + DIError dierr = kDIErrNone; + unsigned char blkBuf[kBlkSize]; + MasterBootRecord mbr; + BootSector bs; + + dierr = pImg->ReadBlockSwapped(kBootBlock, blkBuf, imageOrder, + DiskImg::kSectorOrderProDOS); + if (dierr != kDIErrNone) + goto bail; + + /* + * Both MBR and boot sectors have the same signature in block 0. + */ + if (GetShortLE(&blkBuf[kSignatureOffset]) != kSignature) { + dierr = kDIErrFilesystemNotFound; + goto bail; + } + + /* + * Decode it as an MBR and as a partition table. Figure out which + * one makes sense. If neither make sense, fail. + */ + bool hasMBR, hasBS; + hasMBR = UnpackMBR(blkBuf, &mbr); + hasBS = UnpackBootSector(blkBuf, &bs); + WMSG2(" FAT hasMBR=%d hasBS=%d\n", hasMBR, hasBS); + + if (!hasMBR && !hasBS) { + dierr = kDIErrFilesystemNotFound; + goto bail; + } + if (hasMBR) { + WMSG0(" FAT partition table found:\n"); + for (int i = 0; i < 4; i++) { + WMSG4(" %d: type=0x%02x start LBA=%-9lu size=%lu\n", + i, mbr.parTab[i].type, + mbr.parTab[i].startLBA, mbr.parTab[i].size); + } + } + if (hasBS) { + WMSG0(" FAT boot sector found:\n"); + WMSG1(" OEMName is '%.8s'\n", bs.oemName); + } + + // looks good! + +bail: + return dierr; +} + +/* + * Test to see if the image is a FAT disk. + */ +/*static*/ DIError +DiskFSFAT::TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency) +{ + /* must be block format, should be at least 360K */ + if (!pImg->GetHasBlocks() || pImg->GetNumBlocks() < kExpectedMinBlocks) + return kDIErrFilesystemNotFound; + if (pImg->GetIsEmbedded()) // don't look for FAT inside CFFA! + return kDIErrFilesystemNotFound; + + DiskImg::SectorOrder ordering[DiskImg::kSectorOrderMax]; + + DiskImg::GetSectorOrderArray(ordering, *pOrder); + + for (int i = 0; i < DiskImg::kSectorOrderMax; i++) { + if (ordering[i] == DiskImg::kSectorOrderUnknown) + continue; + if (TestImage(pImg, ordering[i]) == kDIErrNone) { + *pOrder = ordering[i]; + *pFormat = DiskImg::kFormatMSDOS; + return kDIErrNone; + } + } + + WMSG0(" FAT didn't find valid FS\n"); + return kDIErrFilesystemNotFound; +} + +/* + * Get things rolling. + */ +DIError +DiskFSFAT::Initialize(void) +{ + DIError dierr = kDIErrNone; + + strcpy(fVolumeName, "[MS-DOS]"); // max 11 chars + strcpy(fVolumeID, "FATxx [MS-DOS]"); + + // take the easy way out + fTotalBlocks = fpImg->GetNumBlocks(); + + CreateFakeFile(); + + SetVolumeUsageMap(); + + return dierr; +} + + +/* + * Blank out the volume usage map. + */ +void +DiskFSFAT::SetVolumeUsageMap(void) +{ + VolumeUsage::ChunkState cstate; + long block; + + fVolumeUsage.Create(fpImg->GetNumBlocks()); + + cstate.isUsed = true; + cstate.isMarkedUsed = true; + cstate.purpose = VolumeUsage::kChunkPurposeUnknown; + + for (block = fTotalBlocks-1; block >= 0; block--) + fVolumeUsage.SetChunkState(block, &cstate); +} + + +/* + * Fill a buffer with some interesting stuff, and add it to the file list. + */ +void +DiskFSFAT::CreateFakeFile(void) +{ + A2FileFAT* pFile; + char buf[768]; // currently running about 430 + static const char* kFormatMsg = +"The FAT12/16/32 and NTFS filesystems are not supported. CiderPress knows\r\n" +"how to recognize MS-DOS and Windows volumes so that it can identify\r\n" +"PC data on removable media, but it does not know how to view or extract\r\n" +"files from them.\r\n" +"\r\n" +"Some information about this FAT volume:\r\n" +"\r\n" +" Volume name : '%s'\r\n" +" Volume size : %ld blocks (%.2fMB)\r\n" +"\r\n" +"(CiderPress limits itself to 8GB, so larger volume sizes may not be shown.)\r\n" +; + long capacity; + + capacity = fTotalBlocks; + + memset(buf, 0, sizeof(buf)); + sprintf(buf, kFormatMsg, + fVolumeName, + capacity, + (double) capacity / 2048.0); + + pFile = new A2FileFAT(this); + pFile->SetFakeFile(buf, strlen(buf)); + strcpy(pFile->fFileName, "(not supported)"); + + AddFileToList(pFile); +} + + +/* + * =========================================================================== + * A2FileFAT + * =========================================================================== + */ + +/* + * Dump the contents of the A2File structure. + */ +void +A2FileFAT::Dump(void) const +{ + WMSG1("A2FileFAT '%s'\n", fFileName); +} + +/* + * Not a whole lot to do. + */ +DIError +A2FileFAT::Open(A2FileDescr** ppOpenFile, bool readOnly, + bool rsrcFork /*=false*/) +{ + A2FDFAT* pOpenFile = nil; + + if (fpOpenFile != nil) + return kDIErrAlreadyOpen; + if (rsrcFork) + return kDIErrForkNotFound; + assert(readOnly == true); + + pOpenFile = new A2FDFAT(this); + + fpOpenFile = pOpenFile; + *ppOpenFile = pOpenFile; + + return kDIErrNone; +} + + +/* + * =========================================================================== + * A2FDFAT + * =========================================================================== + */ + +/* + * Read a chunk of data from the fake file. + */ +DIError +A2FDFAT::Read(void* buf, size_t len, size_t* pActual) +{ + WMSG3(" FAT reading %d bytes from '%s' (offset=%ld)\n", + len, fpFile->GetPathName(), (long) fOffset); + + A2FileFAT* pFile = (A2FileFAT*) fpFile; + + /* don't allow them to read past the end of the file */ + if (fOffset + (long)len > pFile->fLength) { + if (pActual == nil) + return kDIErrDataUnderrun; + len = (size_t) (pFile->fLength - fOffset); + } + if (pActual != nil) + *pActual = len; + + memcpy(buf, pFile->GetFakeFileBuf(), len); + + fOffset += len; + + return kDIErrNone; +} + +/* + * Write data at the current offset. + */ +DIError +A2FDFAT::Write(const void* buf, size_t len, size_t* pActual) +{ + return kDIErrNotSupported; +} + +/* + * Seek to a new offset. + */ +DIError +A2FDFAT::Seek(di_off_t offset, DIWhence whence) +{ + di_off_t fileLen = ((A2FileFAT*) fpFile)->fLength; + + switch (whence) { + case kSeekSet: + if (offset < 0 || offset > fileLen) + return kDIErrInvalidArg; + fOffset = offset; + break; + case kSeekEnd: + if (offset > 0 || offset < -fileLen) + return kDIErrInvalidArg; + fOffset = fileLen + offset; + break; + case kSeekCur: + if (offset < -fOffset || + offset >= (fileLen - fOffset)) + { + return kDIErrInvalidArg; + } + fOffset += offset; + break; + default: + assert(false); + return kDIErrInvalidArg; + } + + assert(fOffset >= 0 && fOffset <= fileLen); + return kDIErrNone; +} + +/* + * Return current offset. + */ +di_off_t +A2FDFAT::Tell(void) +{ + return fOffset; +} + +/* + * Release file state, and tell our parent to destroy us. + */ +DIError +A2FDFAT::Close(void) +{ + fpFile->CloseDescr(this); + return kDIErrNone; +} + +/* + * Return the #of sectors/blocks in the file. + */ +long +A2FDFAT::GetSectorCount(void) const +{ + A2FileFAT* pFile = (A2FileFAT*) fpFile; + return (long) ((pFile->fLength+255) / 256); +} +long +A2FDFAT::GetBlockCount(void) const +{ + A2FileFAT* pFile = (A2FileFAT*) fpFile; + return (long) ((pFile->fLength+511) / 512); +} + +/* + * Return the Nth track/sector in this file. + */ +DIError +A2FDFAT::GetStorage(long sectorIdx, long* pTrack, long* pSector) const +{ + return kDIErrNotSupported; +} +/* + * Return the Nth 512-byte block in this file. + */ +DIError +A2FDFAT::GetStorage(long blockIdx, long* pBlock) const +{ + return kDIErrNotSupported; +} diff --git a/diskimg/FDI.cpp b/diskimg/FDI.cpp new file mode 100644 index 0000000..beb11f5 --- /dev/null +++ b/diskimg/FDI.cpp @@ -0,0 +1,1541 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Support for Formatted Disk Image (FDI) format. + * + * Based on the v2.0 spec and "fdi2raw.c". The latter was released under + * version 2 of the GPL, so this code may be subject to it. + * + * (Note: I tend to abuse the term "nibble" here. Instead of 4 bits, I + * use it to refer to 8 bits of "nibblized" data. Sorry.) + * + * THOUGHT: we have access to the self-sync byte data. We could use this + * to pretty easily convert a track to 6656-byte format, which would allow + * conversion to .NIB instead of .APP. This would probably need to be + * specified as a global preference (how to open .FDI), though we could + * just drag the self-sync flags around in a parallel data structure and + * invent a format-conversion API. The former seems easier, and should + * be easy to explain in the UI. + */ +#include "StdAfx.h" +#include "DiskImgPriv.h" + + +/* + * =========================================================================== + * FDI compression functions + * =========================================================================== + */ + +/* + * Pack a disk image with FDI. + */ +DIError +WrapperFDI::PackDisk(GenericFD* pSrcGFD, GenericFD* pWrapperGFD) +{ + DIError dierr = kDIErrGeneric; // not yet + return dierr; +} + + +/* + * =========================================================================== + * FDI expansion functions + * =========================================================================== + */ + +/* + * Unpack an FDI-encoded disk image from "pGFD" to a new memory buffer + * created in "*ppNewGFD". The output is a collection of variable-length + * nibble tracks. + * + * "pNewGFD" will need to hold (kTrackAllocSize * numCyls * numHeads) + * bytes of data. + * + * Fills in "fNibbleTrackInfo". + */ +DIError +WrapperFDI::UnpackDisk525(GenericFD* pGFD, GenericFD* pNewGFD, int numCyls, + int numHeads) +{ + DIError dierr = kDIErrNone; + unsigned char nibbleBuf[kNibbleBufLen]; + unsigned char* inputBuf = nil; + bool goodTracks[kMaxNibbleTracks525]; + int inputBufLen = -1; + int badTracks = 0; + int trk, type, length256; + long nibbleLen; + bool result; + + assert(numHeads == 1); + memset(goodTracks, false, sizeof(goodTracks)); + + dierr = pGFD->Seek(kMinHeaderLen, kSeekSet); + if (dierr != kDIErrNone) { + WMSG1("FDI: track seek failed (offset=%d)\n", kMinHeaderLen); + goto bail; + } + + for (trk = 0; trk < numCyls * numHeads; trk++) { + GetTrackInfo(trk, &type, &length256); + WMSG5("%2d.%d: t=0x%02x l=%d (%d)\n", trk / numHeads, trk % numHeads, + type, length256, length256 * 256); + + /* if we have data to read, read it */ + if (length256 > 0) { + if (length256 * 256 > inputBufLen) { + /* allocate or increase the size of the input buffer */ + delete[] inputBuf; + inputBufLen = length256 * 256; + inputBuf = new unsigned char[inputBufLen]; + if (inputBuf == nil) { + dierr = kDIErrMalloc; + goto bail; + } + } + + dierr = pGFD->Read(inputBuf, length256 * 256); + if (dierr != kDIErrNone) + goto bail; + } else { + assert(type == 0x00); + } + + /* figure out what we want to do with this track */ + switch (type) { + case 0x00: + /* blank track */ + badTracks++; + memset(nibbleBuf, 0xff, sizeof(nibbleBuf)); + nibbleLen = kTrackLenNb2525; + break; + case 0x80: + case 0x90: + case 0xa0: + case 0xb0: + /* low-level pulse-index */ + nibbleLen = kNibbleBufLen; + result = DecodePulseTrack(inputBuf, length256*256, kBitRate525, + nibbleBuf, &nibbleLen); + if (!result) { + /* something failed in the decoder; fake it */ + badTracks++; + memset(nibbleBuf, 0xff, sizeof(nibbleBuf)); + nibbleLen = kTrackLenNb2525; + } else { + goodTracks[trk] = true; + } + if (nibbleLen > kTrackAllocSize) { + WMSG2(" FDI: decoded %ld nibbles, buffer is only %d\n", + nibbleLen, kTrackAllocSize); + dierr = kDIErrBadRawData; + goto bail; + } + break; + default: + WMSG1("FDI: unexpected track type 0x%04x\n", type); + dierr = kDIErrUnsupportedImageFeature; + goto bail; + } + + fNibbleTrackInfo.offset[trk] = trk * kTrackAllocSize; + fNibbleTrackInfo.length[trk] = nibbleLen; + FixBadNibbles(nibbleBuf, nibbleLen); + dierr = pNewGFD->Seek(fNibbleTrackInfo.offset[trk], kSeekSet); + if (dierr != kDIErrNone) + goto bail; + dierr = pNewGFD->Write(nibbleBuf, nibbleLen); + if (dierr != kDIErrNone) + goto bail; + WMSG2(" FDI: track %d: wrote %ld nibbles\n", trk, nibbleLen); + + //offset += 256 * length256; + //break; // DEBUG DEBUG + } + + WMSG2(" FDI: %d of %d tracks bad or blank\n", + badTracks, numCyls * numHeads); + if (badTracks > (numCyls * numHeads) / 2) { + WMSG0("FDI: too many bad tracks\n"); + dierr = kDIErrBadRawData; + goto bail; + } + + /* + * For convenience we want this to be 35 or 40 tracks. Start by + * reducing trk to 35 if there are no good tracks at 35+. + */ + bool want40; + int i; + + want40 = false; + for (i = kTrackCount525; i < kMaxNibbleTracks525; i++) { + if (goodTracks[i]) { + want40 = true; + break; + } + } + if (!want40 && trk > kTrackCount525) { + WMSG2(" FDI: no good tracks past %d, reducing from %d\n", + kTrackCount525, trk); + trk = kTrackCount525; // nothing good out there, roll back + } + + /* + * Now pad us *up* to 35 if we have fewer than that. + */ + memset(nibbleBuf, 0xff, sizeof(nibbleBuf)); + for ( ; trk < kMaxNibbleTracks525; trk++) { + if (trk == kTrackCount525) + break; + + fNibbleTrackInfo.offset[trk] = trk * kTrackAllocSize; + fNibbleTrackInfo.length[trk] = kTrackLenNb2525; + fNibbleTrackInfo.numTracks++; + + dierr = pNewGFD->Seek(fNibbleTrackInfo.offset[trk], kSeekSet); + if (dierr != kDIErrNone) + goto bail; + dierr = pNewGFD->Write(nibbleBuf, nibbleLen); + if (dierr != kDIErrNone) + goto bail; + } + + assert(trk == kTrackCount525 || trk == kMaxNibbleTracks525); + fNibbleTrackInfo.numTracks = trk; + +bail: + delete[] inputBuf; + return dierr; +} + +/* + * Unpack an FDI-encoded disk image from "pGFD" to 800K of ProDOS-ordered + * 512-byte blocks in "pNewGFD". + * + * We could keep the 12-byte "tags" on each block, but they were never + * really used in the Apple II world. + * + * We also need to set up a "bad block" map to identify parts that we had + * trouble unpacking. + */ +DIError +WrapperFDI::UnpackDisk35(GenericFD* pGFD, GenericFD* pNewGFD, int numCyls, + int numHeads, LinearBitmap* pBadBlockMap) +{ + DIError dierr = kDIErrNone; + unsigned char nibbleBuf[kNibbleBufLen]; + unsigned char* inputBuf = nil; + unsigned char outputBuf[kMaxSectors35 * kBlockSize]; // 6KB + int inputBufLen = -1; + int badTracks = 0; + int trk, type, length256; + long nibbleLen; + bool result; + + assert(numHeads == 2); + + dierr = pGFD->Seek(kMinHeaderLen, kSeekSet); + if (dierr != kDIErrNone) { + WMSG1("FDI: track seek failed (offset=%d)\n", kMinHeaderLen); + goto bail; + } + + pNewGFD->Rewind(); + + for (trk = 0; trk < numCyls * numHeads; trk++) { + GetTrackInfo(trk, &type, &length256); + WMSG5("%2d.%d: t=0x%02x l=%d (%d)\n", trk / numHeads, trk % numHeads, + type, length256, length256 * 256); + + /* if we have data to read, read it */ + if (length256 > 0) { + if (length256 * 256 > inputBufLen) { + /* allocate or increase the size of the input buffer */ + delete[] inputBuf; + inputBufLen = length256 * 256; + inputBuf = new unsigned char[inputBufLen]; + if (inputBuf == nil) { + dierr = kDIErrMalloc; + goto bail; + } + } + + dierr = pGFD->Read(inputBuf, length256 * 256); + if (dierr != kDIErrNone) + goto bail; + } else { + assert(type == 0x00); + } + + /* figure out what we want to do with this track */ + switch (type) { + case 0x00: + /* blank track */ + badTracks++; + memset(nibbleBuf, 0xff, sizeof(nibbleBuf)); + nibbleLen = kTrackLenNb2525; + break; + case 0x80: + case 0x90: + case 0xa0: + case 0xb0: + /* low-level pulse-index */ + nibbleLen = kNibbleBufLen; + result = DecodePulseTrack(inputBuf, length256*256, + BitRate35(trk/numHeads), nibbleBuf, &nibbleLen); + if (!result) { + /* something failed in the decoder; fake it */ + badTracks++; + memset(nibbleBuf, 0xff, sizeof(nibbleBuf)); + nibbleLen = kTrackLenNb2525; + } + if (nibbleLen > kNibbleBufLen) { + WMSG2(" FDI: decoded %ld nibbles, buffer is only %d\n", + nibbleLen, kTrackAllocSize); + dierr = kDIErrBadRawData; + goto bail; + } + break; + default: + WMSG1("FDI: unexpected track type 0x%04x\n", type); + dierr = kDIErrUnsupportedImageFeature; + goto bail; + } + + WMSG2(" FDI: track %d got %ld nibbles\n", trk, nibbleLen); + + /* + fNibbleTrackInfo.offset[trk] = trk * kTrackAllocSize; + fNibbleTrackInfo.length[trk] = nibbleLen; + dierr = pNewGFD->Seek(fNibbleTrackInfo.offset[trk], kSeekSet); + if (dierr != kDIErrNone) + goto bail; + dierr = pNewGFD->Write(nibbleBuf, nibbleLen); + if (dierr != kDIErrNone) + goto bail; + */ + + dierr = DiskImg::UnpackNibbleTrack35(nibbleBuf, nibbleLen, outputBuf, + trk / numHeads, trk % numHeads, pBadBlockMap); + if (dierr != kDIErrNone) + goto bail; + + dierr = pNewGFD->Write(outputBuf, + kBlockSize * DiskImg::SectorsPerTrack35(trk / numHeads)); + if (dierr != kDIErrNone) { + WMSG2("FDI: failed writing disk blocks (%d * %d)\n", + kBlockSize, DiskImg::SectorsPerTrack35(trk / numHeads)); + goto bail; + } + } + + //fNibbleTrackInfo.numTracks = numCyls * numHeads; + +bail: + delete[] inputBuf; + return dierr; +} + +/* + * Return the approximate bit rate for the specified cylinder, in bits/sec. + */ +int +WrapperFDI::BitRate35(int cyl) +{ + if (cyl >= 0 && cyl <= 15) + return 375000; // 394rpm + else if (cyl <= 31) + return 343750; // 429rpm + else if (cyl <= 47) + return 312500; // 472rpm + else if (cyl <= 63) + return 281250; // 525rpm + else if (cyl <= 79) + return 250000; // 590rpm + else { + WMSG1(" FDI: invalid 3.5 cylinder %d\n", cyl); + return 250000; + } +} + +/* + * Fix any obviously-bad nibble values. + * + * This should be unlikely, but if we find several zeroes in a row due to + * garbled data from the drive, it can happen. We clean it up here so that, + * when we convert to another format (e.g. TrackStar), we don't flunk a + * simple high-bit screening test. + * + * (We could be more rigorous and test against valid disk bytes, but that's + * probably excessive.) + */ +void +WrapperFDI::FixBadNibbles(unsigned char* nibbleBuf, long nibbleLen) +{ + int badCount = 0; + + while (nibbleLen--) { + if ((*nibbleBuf & 0x80) == 0) { + badCount++; + *nibbleBuf = 0xff; + } + nibbleBuf++; + } + + if (badCount != 0) { + WMSG1(" FDI: fixed %d bad nibbles\n", badCount); + } +} + + +/* + * Get the info for the Nth track. The track number is used as an index + * into the track descriptor table. + * + * Returns the track type and amount of data (/256). + */ +void +WrapperFDI::GetTrackInfo(int trk, int* pType, int* pLength256) +{ + unsigned short trackDescr; + trackDescr = fHeaderBuf[kTrackDescrOffset + trk * 2] << 8 | + fHeaderBuf[kTrackDescrOffset + trk * 2 +1]; + + *pType = (trackDescr & 0xff00) >> 8; + *pLength256 = trackDescr & 0x00ff; + + switch (trackDescr & 0xf000) { + case 0x0000: + /* high-level type */ + switch (trackDescr & 0xff00) { + case 0x0000: + /* blank track */ + break; + default: + /* miscellaneous high-level type */ + break; + } + break; + case 0x8000: + case 0x9000: + case 0xa000: + case 0xb000: + /* low-level type, length is 14 bits */ + *pType = (trackDescr & 0xc000) >> 8; + *pLength256 = trackDescr & 0x3fff; + break; + case 0xc000: + case 0xd000: + /* mid-level format, value in 0n00 holds a bit rate index */ + break; + case 0xe000: + case 0xf000: + /* raw MFM; for 0xf000, the value in 0n00 holds a bit rate index */ + break; + default: + WMSG1("Unexpected trackDescr 0x%04x\n", trackDescr); + *pType = 0x7e; // return an invalid value + *pLength256 = 0; + break; + } +} + + +/* + * Convert a track encoded as one or more pulse streams to nibbles. + * + * This decompresses the pulse streams in "inputBuf", then converts them + * to nibble form in "nibbleBuf". + * + * "*pNibbleLen" should hold the maximum size of the buffer. On success, + * it will hold the actual number of bytes used. + * + * Returns "true" on success, "false" on failure. + */ +bool +WrapperFDI::DecodePulseTrack(const unsigned char* inputBuf, long inputLen, + int bitRate, unsigned char* nibbleBuf, long* pNibbleLen) +{ + const int kSizeValueMask = 0x003fffff; + const int kSizeCompressMask = 0x00c00000; + const int kSizeCompressShift = 22; + PulseIndexHeader hdr; + unsigned long val; + bool result = false; + + memset(&hdr, 0, sizeof(hdr)); + + hdr.numPulses = GetLongBE(&inputBuf[0x00]); + val = Get24BE(&inputBuf[0x04]); + hdr.avgStreamLen = val & kSizeValueMask; + hdr.avgStreamCompression = (val & kSizeCompressMask) >> kSizeCompressShift; + val = Get24BE(&inputBuf[0x07]); + hdr.minStreamLen = val & kSizeValueMask; + hdr.minStreamCompression = (val & kSizeCompressMask) >> kSizeCompressShift; + val = Get24BE(&inputBuf[0x0a]); + hdr.maxStreamLen = val & kSizeValueMask; + hdr.maxStreamCompression = (val & kSizeCompressMask) >> kSizeCompressShift; + val = Get24BE(&inputBuf[0x0d]); + hdr.idxStreamLen = val & kSizeValueMask; + hdr.idxStreamCompression = (val & kSizeCompressMask) >> kSizeCompressShift; + + if (hdr.numPulses < 64 || hdr.numPulses > 131072) { + /* should be about 40,000 */ + WMSG1(" FDI: bad pulse count %ld in track\n", hdr.numPulses); + return false; + } + + /* advance past the 16 hdr bytes; now pointing at "average" stream */ + inputBuf += kPulseStreamDataOffset; + + WMSG1(" pulses: %ld\n", hdr.numPulses); + //WMSG2(" avg: len=%d comp=%d\n", hdr.avgStreamLen, hdr.avgStreamCompression); + //WMSG2(" min: len=%d comp=%d\n", hdr.minStreamLen, hdr.minStreamCompression); + //WMSG2(" max: len=%d comp=%d\n", hdr.maxStreamLen, hdr.maxStreamCompression); + //WMSG2(" idx: len=%d comp=%d\n", hdr.idxStreamLen, hdr.idxStreamCompression); + + /* + * Uncompress or endian-swap the pulse streams. + */ + hdr.avgStream = new unsigned long[hdr.numPulses]; + if (hdr.avgStream == nil) + goto bail; + if (!UncompressPulseStream(inputBuf, hdr.avgStreamLen, hdr.avgStream, + hdr.numPulses, hdr.avgStreamCompression, 4)) + { + goto bail; + } + inputBuf += hdr.avgStreamLen; + + if (hdr.minStreamLen > 0) { + hdr.minStream = new unsigned long[hdr.numPulses]; + if (hdr.minStream == nil) + goto bail; + if (!UncompressPulseStream(inputBuf, hdr.minStreamLen, hdr.minStream, + hdr.numPulses, hdr.minStreamCompression, 4)) + { + goto bail; + } + inputBuf += hdr.minStreamLen; + } + if (hdr.maxStreamLen > 0) { + hdr.maxStream = new unsigned long[hdr.numPulses]; + if (!UncompressPulseStream(inputBuf, hdr.maxStreamLen, hdr.maxStream, + hdr.numPulses, hdr.maxStreamCompression, 4)) + { + goto bail; + } + inputBuf += hdr.maxStreamLen; + } + if (hdr.idxStreamLen > 0) { + hdr.idxStream = new unsigned long[hdr.numPulses]; + if (!UncompressPulseStream(inputBuf, hdr.idxStreamLen, hdr.idxStream, + hdr.numPulses, hdr.idxStreamCompression, 2)) + { + goto bail; + } + inputBuf += hdr.idxStreamLen; + } + + /* + * Convert the pulse streams to a nibble stream. + */ + result = ConvertPulseStreamsToNibbles(&hdr, bitRate, nibbleBuf, pNibbleLen); + // fall through with result + +bail: + /* clean up */ + if (hdr.avgStream != nil) + delete[] hdr.avgStream; + if (hdr.minStream != nil) + delete[] hdr.minStream; + if (hdr.maxStream != nil) + delete[] hdr.maxStream; + if (hdr.idxStream != nil) + delete[] hdr.idxStream; + return result; +} + +/* + * Uncompress, or at least endian-swap, the input data. + * + * "inputLen" is the length in bytes of the input stream. For an uncompressed + * stream this should be equal to numPulses*bytesPerPulse, for a compressed + * stream it's the length of the compressed data. + * + * "bytesPerPulse" indicates the width of the input data. This will usually + * be 4, but is 2 for the index stream. The output is always 4 bytes/pulse. + * For Huffman-compressed data, it appears that the input is always 4 bytes. + * + * Returns "true" if all went well, "false" if we hit something that we + * couldn't handle. + */ +bool +WrapperFDI::UncompressPulseStream(const unsigned char* inputBuf, long inputLen, + unsigned long* outputBuf, long numPulses, int format, int bytesPerPulse) +{ + assert(bytesPerPulse == 2 || bytesPerPulse == 4); + + /* + * Sample code has a snippet that says: if the format is "uncompressed" + * but inputLen < (numPulses*2), treat it as compressed. This may be + * for handling some badly-formed images. Not currently doing it here. + */ + + if (format == kCompUncompressed) { + int i; + + WMSG0("NOT TESTED\n"); // remove this when we've tested it + + if (inputLen != numPulses * bytesPerPulse) { + WMSG2(" FDI: got unc inputLen=%ld, outputLen=%ld\n", + inputLen, numPulses * bytesPerPulse); + return false; + } + if (bytesPerPulse == 2) { + for (i = 0; i < numPulses; i++) { + *outputBuf++ = GetShortBE(inputBuf); + inputBuf += 2; + } + } else { + for (i = 0; i < numPulses; i++) { + *outputBuf++ = GetLongBE(inputBuf); + inputBuf += 4; + } + } + } else if (format == kCompHuffman) { + if (!ExpandHuffman(inputBuf, inputLen, outputBuf, numPulses)) + return false; + //WMSG0(" FDI: Huffman expansion succeeded\n"); + } else { + WMSG1(" FDI: got weird compression format %d\n", format); + return false; + } + + return true; +} + +/* + * Expand a Huffman-compressed stream. + * + * The code takes bit-slices across the entire input and compresses them + * separately with a static Huffman variant. + * + * "outputBuf" is expected to hold "numPulses" entries. + * + * This implementation is based on the fdi2raw code. + */ +bool +WrapperFDI::ExpandHuffman(const unsigned char* inputBuf, long inputLen, + unsigned long* outputBuf, long numPulses) +{ + HuffNode root; + const unsigned char* origInputBuf = inputBuf; + bool signExtend, sixteenBits; + int i, subStreamShift; + unsigned char bits; + unsigned char bitMask; + + memset(outputBuf, 0, numPulses * sizeof(unsigned long)); + subStreamShift = 1; + + while (subStreamShift != 0) { + if (inputBuf - origInputBuf >= inputLen) { + WMSG0(" FDI: overran input(1)\n"); + return false; + } + + /* decode the sub-stream header */ + bits = *inputBuf++; + subStreamShift = bits & 0x7f; // low-order bit number + signExtend = (bits & 0x80) != 0; + bits = *inputBuf++; + sixteenBits = (bits & 0x80) != 0; // ignore redundant high-order + + //WMSG3(" FDI: shift=%d ext=%d sixt=%d\n", + // subStreamShift, signExtend, sixteenBits); + + /* decode the Huffman tree structure */ + root.left = nil; + root.right = nil; + bitMask = 0; + inputBuf = HuffExtractTree(inputBuf, &root, &bits, &bitMask); + + //WMSG1(" after tree: off=%d\n", inputBuf - origInputBuf); + + /* extract the Huffman node values */ + if (sixteenBits) + inputBuf = HuffExtractValues16(inputBuf, &root); + else + inputBuf = HuffExtractValues8(inputBuf, &root); + + if (inputBuf - origInputBuf >= inputLen) { + WMSG0(" FDI: overran input(2)\n"); + return false; + } + //WMSG1(" after values: off=%d\n", inputBuf - origInputBuf); + + /* decode the data over all pulses */ + bitMask = 0; + for (i = 0; i < numPulses; i++) { + unsigned long outVal; + const HuffNode* pCurrent = &root; + + /* chase down the tree until we hit a leaf */ + /* (note: nodes have two kids or none) */ + while (true) { + if (pCurrent->left == nil) { + break; + } else { + bitMask >>= 1; + if (bitMask == 0) { + bitMask = 0x80; + bits = *inputBuf++; + } + if (bits & bitMask) + pCurrent = pCurrent->right; + else + pCurrent = pCurrent->left; + } + } + + outVal = outputBuf[i]; + if (signExtend) { + if (sixteenBits) + outVal |= HuffSignExtend16(pCurrent->val) << subStreamShift; + else + outVal |= HuffSignExtend8(pCurrent->val) << subStreamShift; + } else { + outVal |= pCurrent->val << subStreamShift; + } + outputBuf[i] = outVal; + } + HuffFreeNodes(root.left); + HuffFreeNodes(root.right); + } + + if (inputBuf - origInputBuf != inputLen) { + WMSG2(" FDI: warning: Huffman input %d vs. %ld\n", + inputBuf - origInputBuf, inputLen); + return false; + } + + return true; +} + + +/* + * Recursively extract the Huffman tree structure for this sub-stream. + */ +const unsigned char* +WrapperFDI::HuffExtractTree(const unsigned char* inputBuf, HuffNode* pNode, + unsigned char* pBits, unsigned char* pBitMask) +{ + unsigned char val; + + if (*pBitMask == 0) { + *pBits = *inputBuf++; + *pBitMask = 0x80; + } + val = *pBits & *pBitMask; + (*pBitMask) >>= 1; + + //WMSG1(" val=%d\n", val); + + if (val != 0) { + assert(pNode->left == nil); + assert(pNode->right == nil); + return inputBuf; + } else { + pNode->left = new HuffNode; + memset(pNode->left, 0, sizeof(HuffNode)); + inputBuf = HuffExtractTree(inputBuf, pNode->left, pBits, pBitMask); + pNode->right = new HuffNode; + memset(pNode->right, 0, sizeof(HuffNode)); + return HuffExtractTree(inputBuf, pNode->right, pBits, pBitMask); + } +} + +/* + * Recursively get the 16-bit values for our Huffman tree from the stream. + */ +const unsigned char* +WrapperFDI::HuffExtractValues16(const unsigned char* inputBuf, HuffNode* pNode) +{ + if (pNode->left == nil) { + pNode->val = (*inputBuf++) << 8; + pNode->val |= *inputBuf++; + return inputBuf; + } else { + inputBuf = HuffExtractValues16(inputBuf, pNode->left); + return HuffExtractValues16(inputBuf, pNode->right); + } +} + +/* + * Recursively get the 8-bit values for our Huffman tree from the stream. + */ +const unsigned char* +WrapperFDI::HuffExtractValues8(const unsigned char* inputBuf, HuffNode* pNode) +{ + if (pNode->left == nil) { + pNode->val = *inputBuf++; + return inputBuf; + } else { + inputBuf = HuffExtractValues8(inputBuf, pNode->left); + return HuffExtractValues8(inputBuf, pNode->right); + } +} + +/* + * Recursively free up the current node and all nodes beneath it. + */ +void +WrapperFDI::HuffFreeNodes(HuffNode* pNode) +{ + if (pNode != nil) { + HuffFreeNodes(pNode->left); + HuffFreeNodes(pNode->right); + delete pNode; + } + +} + +/* + * Sign-extend a 16-bit value to 32 bits. + */ +unsigned long +WrapperFDI::HuffSignExtend16(unsigned long val) +{ + if (val & 0x8000) + val |= 0xffff0000; + return val; +} + +/* + * Sign-extend an 8-bit value to 32 bits. + */ +unsigned long +WrapperFDI::HuffSignExtend8(unsigned long val) +{ + if (val & 0x80) + val |= 0xffffff00; + return val; +} + + +/* use these to extract values from the index stream */ +#define ZeroStateCount(_val) (((_val) >> 8) & 0xff) +#define OneStateCount(_val) ((_val) & 0xff) + +/* + * Convert our collection of pulse streams into (what we hope will be) + * Apple II nibble form. + * + * This modifies the contents of the minStream, maxStream, and idxStream + * arrays. + * + * "*pNibbleLen" should hold the maximum size of the buffer. On success, + * it will hold the actual number of bytes used. + */ +bool +WrapperFDI::ConvertPulseStreamsToNibbles(PulseIndexHeader* pHdr, int bitRate, + unsigned char* nibbleBuf, long* pNibbleLen) +{ + unsigned long* fakeIdxStream = nil; + bool result = false; + int i; + + /* + * Stream pointers. If we don't have a stream, fake it. + */ + unsigned long* avgStream; + unsigned long* minStream; + unsigned long* maxStream; + unsigned long* idxStream; + + avgStream = pHdr->avgStream; + if (pHdr->minStream != nil && pHdr->maxStream != nil) { + minStream = pHdr->minStream; + maxStream = pHdr->maxStream; + + /* adjust the values in the min/max streams */ + for (i = 0; i < pHdr->numPulses; i++) { + maxStream[i] = avgStream[i] + minStream[i] - maxStream[i]; + minStream[i] = avgStream[i] - minStream[i]; + } + } else { + minStream = pHdr->avgStream; + maxStream = pHdr->avgStream; + } + + if (pHdr->idxStream != nil) + idxStream = pHdr->idxStream; + else { + /* + * The UAE sample code has some stuff to fake it. The code there + * is broken, so I'm guessing it has never been used, but I'm going + * to replicate it here (and probably never test it either). This + * assumes that the original was written for a big-endian machine. + */ + WMSG0(" FDI: HEY: using fake index stream\n"); + DebugBreak(); + fakeIdxStream = new unsigned long[pHdr->numPulses]; + if (fakeIdxStream == nil) { + WMSG0(" FDI: unable to alloc fake idx stream\n"); + goto bail; + } + for (i = 1; i < pHdr->numPulses; i++) + fakeIdxStream[i] = 0x0200; // '1' for two, '0' for zero + fakeIdxStream[0] = 0x0101; // '1' for one, '0' for one + + idxStream = fakeIdxStream; + } + + /* + * Compute a value for maxIndex. + */ + unsigned long maxIndex; + + maxIndex = 0; + for (i = 0; i < pHdr->numPulses; i++) { + unsigned long sum; + + /* add up the two single-byte values in the index stream */ + sum = ZeroStateCount(idxStream[i]) + OneStateCount(idxStream[i]); + if (sum > maxIndex) + maxIndex = sum; + } + + /* + * Compute a value for indexOffset. + */ + int indexOffset; + + indexOffset = 0; + for (i = 0; i < pHdr->numPulses && OneStateCount(idxStream[i]) != 0; i++) { + /* "falling edge, replace with ZeroStateCount for rising edge" */ + } + if (i < pHdr->numPulses) { + int start = i; + do { + i++; + if (i >= pHdr->numPulses) + i = 0; // wrapped around + } while (i != start && ZeroStateCount(idxStream[i]) == 0); + if (i != start) { + /* index pulse detected */ + while (i != start && + ZeroStateCount(idxStream[i]) > OneStateCount(idxStream[i])) + { + i++; + if (i >= pHdr->numPulses) + i = 0; + } + if (i != start) + indexOffset = i; /* index position detected */ + } + } + + /* + * Compute totalAvg and weakBits, and rewrite idxStream. + * (We don't actually use weakBits.) + */ + unsigned long totalAvg; + int weakBits; + + totalAvg = weakBits = 0; + for (i = 0; i < pHdr->numPulses; i++) { + unsigned int sum; + sum = ZeroStateCount(idxStream[i]) + OneStateCount(idxStream[i]); + if (sum >= maxIndex) + totalAvg += avgStream[i]; // could this overflow...? + else + weakBits++; + + idxStream[i] = sum; + } + + WMSG4(" FDI: maxIndex=%lu indexOffset=%d totalAvg=%lu weakBits=%d\n", + maxIndex, indexOffset, totalAvg, weakBits); + + /* + * Take our altered stream values and the stuff we've calculated, + * and convert the pulse values into bits. + */ + unsigned char bitBuffer[kBitBufferSize]; + int bitCount; + + bitCount = kBitBufferSize; + + if (!ConvertPulsesToBits(avgStream, minStream, maxStream, idxStream, + pHdr->numPulses, maxIndex, indexOffset, totalAvg, bitRate, + bitBuffer, &bitCount)) + { + WMSG0(" FDI: ConvertPulsesToBits() failed\n"); + goto bail; + } + + //WMSG1(" Got %d bits\n", bitCount); + if (bitCount < 0) { + WMSG0(" FDI: overran output bit buffer\n"); + goto bail; + } + + /* + * We have a bit stream with the GCR bits as they appear coming out of + * the IWM. Convert it to 8-bit nibble form. + * + * We currently discard self-sync byte information. + */ + if (!ConvertBitsToNibbles(bitBuffer, bitCount, nibbleBuf, pNibbleLen)) + { + WMSG0(" FDI: ConvertBitsToNibbles() failed\n"); + goto bail; + } + + result = true; + +bail: + delete[] fakeIdxStream; + return result; +} + + +/* + * Local data structures. Not worth putting in the header file. + */ +const int kPulseLimitVal = 15; /* "tolerance of 15%" */ + +typedef struct PulseSamples { + unsigned long size; + int numBits; +} PulseSamples; + +class PulseSampleCollection { +public: + PulseSampleCollection(void) { + fArrayIndex = fTotalDiv = -1; + fTotal = 0; + } + ~PulseSampleCollection(void) {} + + void Create(int stdMFM2BitCellSize, int numBits) { + int i; + + fArrayIndex = 0; + fTotal = 0; + fTotalDiv = 0; + for (i = 0; i < kSampleArrayMax; i++) { + // "That is (total track length / 50000) for Amiga double density" + fArray[i].size = stdMFM2BitCellSize; + fTotal += fArray[i].size; + fArray[i].numBits = numBits; + fTotalDiv += fArray[i].numBits; + } + assert(fTotalDiv != 0); + } + + unsigned long GetTotal(void) const { return fTotal; } + int GetTotalDiv(void) const { return fTotalDiv; } + + void AdjustTotal(long val) { fTotal += val; } + void AdjustTotalDiv(int val) { fTotalDiv += val; } + void IncrIndex(void) { + fArrayIndex++; + if (fArrayIndex >= kSampleArrayMax) + fArrayIndex = 0; + } + + PulseSamples* GetCurrentArrayEntry(void) { + return &fArray[fArrayIndex]; + } + + enum { + kSampleArrayMax = 10, + }; + +private: + PulseSamples fArray[kSampleArrayMax]; + int fArrayIndex; + unsigned long fTotal; + int fTotalDiv; +}; + +#define MY_RANDOM +#ifdef MY_RANDOM +/* replace rand() with my function */ +#define rand() MyRand() + +/* + * My psuedo-random number generator, which is even less random than + * rand(). It is, however, consistent across all platforms, and the + * value for RAND_MAX is small enough to avoid some integer overflow + * problems that the code has with (2^31-1) implementations. + */ +#undef RAND_MAX +#define RAND_MAX 32767 +int +WrapperFDI::MyRand(void) +{ + const int kNumStates = 31; + const int kQuantum = RAND_MAX / (kNumStates+1); + static int state = 0; + int retVal; + + state++; + if (state == kNumStates) + state = 0; + + retVal = (kQuantum * state) + (kQuantum / 2); + assert(retVal >= 0 && retVal <= RAND_MAX); + return retVal; +} +#endif + +/* + * Convert the pulses we've read to a bit stream. This is a tad complex + * because the FDI scanner was reading a GCR disk with an MFM drive. + * + * Pass the output buffer size in bytes in "*pOutputLen". The actual number + * of *bits* output is returned in it. + * + * This is a fairly direct conversion from the sample code. There's a lot + * here that I haven't taken the time to figure out. + */ +bool +WrapperFDI::ConvertPulsesToBits(const unsigned long* avgStream, + const unsigned long* minStream, const unsigned long* maxStream, + const unsigned long* idxStream, int numPulses, int maxIndex, + int indexOffset, unsigned long totalAvg, int bitRate, + unsigned char* outputBuf, int* pOutputLen) +{ + PulseSampleCollection samples; + BitOutputBuffer bitOutput(outputBuf, *pOutputLen); + /* magic numbers, from somewhere */ + const unsigned long kStdMFM2BitCellSize = (totalAvg * 5) / bitRate; + const unsigned long kStdMFM8BitCellSize = (totalAvg * 20) / bitRate; + int mfmMagic = 0; // if set to 1, decode as MFM rather than GCR + bool result = false; + int i; + //int debugCounter = 0; + + /* sample code doesn't do this, but I want consistent results */ + srand(0); + + /* + * "detects a long-enough stable pulse coming just after another + * stable pulse" + */ + i = 1; + while (i < numPulses && + (idxStream[i] < (unsigned long) maxIndex || + idxStream[i-1] < (unsigned long) maxIndex || + minStream[i] < (kStdMFM2BitCellSize - (kStdMFM2BitCellSize / 4)) + )) + { + i++; + } + if (i == numPulses) { + WMSG0(" FDI: no stable and long-enough pulse in track\n"); + goto bail; + } + + /* + * Set up some variables. + */ + int nextI, endOfData, adjust, bitOffset, step; + unsigned long refPulse; + long jitter; + + samples.Create(kStdMFM2BitCellSize, 1 + mfmMagic); + nextI = i; + endOfData = i; + i--; + adjust = 0; + bitOffset = 0; + refPulse = 0; + jitter = 0; + step = -1; + + /* + * Run through the data three times: + * (-1) do stuff + * (0) do more stuff + * (1) output bits + */ + while (step < 2) { + /* + * Calculates the current average bit rate from previously + * decoded data. + */ + unsigned long avgSize; + int kCell8Limit = (kPulseLimitVal * kStdMFM8BitCellSize) / 100; + + /* this is the new average size for one MFM bit */ + avgSize = (samples.GetTotal() << (2 + mfmMagic)) / samples.GetTotalDiv(); + + /* + * Prevent avgSize from getting too far out of whack. + * + * "you can try tighter ranges than 25%, or wider ranges. I would + * probably go for tighter..." + */ + if ((avgSize < kStdMFM8BitCellSize - kCell8Limit) || + (avgSize > kStdMFM8BitCellSize + kCell8Limit)) + { + avgSize = kStdMFM8BitCellSize; + } + + /* + * Get the next long-enough pulse (may require more than one pulse). + */ + unsigned long pulse; + + pulse = 0; + while (pulse < ((avgSize / 4) - (avgSize / 16))) { + unsigned long avgPulse, minPulse, maxPulse; + + /* advance i */ + i++; + if (i >= numPulses) + i = 0; // wrapped around + + /* advance nextI */ + if (i == nextI) { + do { + nextI++; + if (nextI >= numPulses) + nextI = 0; + } while (idxStream[nextI] < (unsigned long) maxIndex); + } + + if (idxStream[i] >= (unsigned long) maxIndex) { + /* stable pulse */ + avgPulse = avgStream[i] - jitter; + minPulse = minStream[i]; + maxPulse = maxStream[i]; + if (jitter >= 0) + maxPulse -= jitter; + else + minPulse -= jitter; + + if (maxStream[nextI] - avgStream[nextI] < avgPulse - minPulse) + minPulse = avgPulse - (maxStream[nextI] - avgStream[nextI]); + if (avgStream[nextI] - minStream[nextI] < maxPulse - avgPulse) + maxPulse = avgPulse + (avgStream[nextI] - minStream[nextI]); + if (minPulse < refPulse) + minPulse = refPulse; + + /* + * This appears to use a pseudo-random number generator + * to dither the signal. This strikes me as highly + * questionable, but I'm trying to recreate what the sample + * code does, and I don't fully understand this stuff. + */ + int randVal; + + randVal = rand(); + if (randVal < (RAND_MAX / 2)) { + if (randVal > (RAND_MAX / 4)) { + if (randVal <= (3 * (RAND_MAX / 8))) + randVal = (2 * randVal) - (RAND_MAX / 4); + else + randVal = (4 * randVal) - RAND_MAX; + } + jitter = 0 - (randVal * (avgPulse - minPulse)) / RAND_MAX; + } else { + randVal -= RAND_MAX / 2; + if (randVal > (RAND_MAX / 4)) { + if (randVal <= (3 * (RAND_MAX / 8))) + randVal = (2 * randVal) - (RAND_MAX / 4); + else + randVal = (4 * randVal) - RAND_MAX; + } + jitter = (randVal * (maxPulse - avgPulse)) / RAND_MAX; + } + avgPulse += jitter; + + if (avgPulse < minPulse || avgPulse > maxPulse) { + /* this is bad -- we're out of bounds */ + WMSG3(" FDI: avgPulse out of bounds: avg=%lu min=%lu max=%lu\n", + avgPulse, minPulse, maxPulse); + } + if (avgPulse < refPulse) { + /* I guess this is also bad */ + WMSG2(" FDI: avgPulse < refPulse (%lu %lu)\n", + avgPulse, refPulse); + } + pulse += avgPulse - refPulse; + refPulse = 0; + + /* + * If we've reached the end, advance to the next step. + */ + if (i == endOfData) + step++; + } else if ((unsigned long) rand() <= (idxStream[i] * RAND_MAX) / maxIndex) { + /* futz with it */ + int randVal; + + avgPulse = avgStream[i]; + minPulse = minStream[i]; + maxPulse = maxStream[i]; + + randVal = rand(); + if (randVal < (RAND_MAX / 2)) { + if (randVal > (RAND_MAX / 4)) { + if (randVal <= (3 * (RAND_MAX / 8))) + randVal = (2 * randVal) - (RAND_MAX / 4); + else + randVal = (4 * randVal) - RAND_MAX; + } + avgPulse -= (randVal * (avgPulse - minPulse)) / RAND_MAX; + } else { + randVal -= RAND_MAX / 2; + if (randVal > (RAND_MAX / 4)) { + if (randVal <= (3 * (RAND_MAX / 8))) + randVal = (2 * randVal) - (RAND_MAX / 4); + else + randVal = (4 * randVal) - RAND_MAX; + } + avgPulse += (randVal * (maxPulse - avgPulse)) / RAND_MAX; + } + if (avgPulse > refPulse && + avgPulse < (avgStream[nextI] - jitter)) + { + pulse += avgPulse - refPulse; + refPulse = avgPulse; + } + } else { + // do nothing + } + } + + /* + * "gets the size in bits from the pulse width, considering the current + * average bitrate" + * + * "realSize" will end up holding the number of bits we're going + * to output for this pulse. + */ + unsigned long adjustedPulse; + int realSize; + + adjustedPulse = pulse; + realSize = 0; + if (mfmMagic != 0) { + while (adjustedPulse >= avgSize) { + realSize += 4; + adjustedPulse -= avgSize / 2; + } + adjustedPulse <<= 3; + while (adjustedPulse >= ((avgSize * 4) + (avgSize / 4))) { + realSize += 2; + adjustedPulse -= avgSize * 2; + } + if (adjustedPulse >= ((avgSize * 3) + (avgSize / 4))) { + if (adjustedPulse <= ((avgSize * 4) - (avgSize / 4))) { + if ((2* ((adjustedPulse >> 2) - adjust)) <= + ((2 * avgSize) - (avgSize / 4))) + { + realSize += 3; + } else { + realSize += 4; + } + } else { + realSize += 4; + } + } else { + if (adjustedPulse > ((avgSize * 3) - (avgSize / 4))) { + realSize += 3; + } else { + if (adjustedPulse >= ((avgSize * 2) + (avgSize / 4))) { + if ((2 * ((adjustedPulse >> 2) - adjust)) < + (avgSize + (avgSize / 4))) + { + realSize += 2; + } else { + realSize += 3; + } + } else { + realSize += 2; + } + } + } + } else { + /* mfmMagic == 0, whatever that means */ + while (adjustedPulse >= (2 * avgSize)) { + realSize += 4; + adjustedPulse -= avgSize; + } + adjustedPulse <<= 2; + + while (adjustedPulse >= ((avgSize * 3) + (avgSize / 4))) { + realSize += 2; + adjustedPulse -= avgSize * 2; + } + if (adjustedPulse >= ((avgSize * 2) + (avgSize / 4))) { + if (adjustedPulse <= ((avgSize * 3) - (avgSize / 4))) { + if (((adjustedPulse >> 1) - adjust) < + (avgSize + (avgSize / 4))) + { + realSize += 2; + } else { + realSize += 3; + } + } else { + realSize += 3; + } + } else { + if (adjustedPulse > ((avgSize * 2) - (avgSize / 4))) + realSize += 2; + else { + if (adjustedPulse >= (avgSize + (avgSize / 4))) { + if (((adjustedPulse >> 1) - adjust) <= + (avgSize - (avgSize / 4))) + { + realSize++; + } else { + realSize += 2; + } + } else { + realSize++; + } + } + } + } + + /* + * "after one pass to correctly initialize the average bitrate, + * outputs the bits" + */ + if (step == 1) { + int j; + + for (j = realSize; j > 1; j--) + bitOutput.WriteBit(0); + bitOutput.WriteBit(1); + } + + /* + * Prepare for next pulse. + */ + adjust = ((realSize * avgSize) / (4 << mfmMagic)) - pulse; + + PulseSamples* pSamples; + pSamples = samples.GetCurrentArrayEntry(); + samples.AdjustTotal(-(long)pSamples->size); + samples.AdjustTotalDiv(-pSamples->numBits); + pSamples->size = pulse; + pSamples->numBits = realSize; + samples.AdjustTotal(pulse); + samples.AdjustTotalDiv(realSize); + samples.IncrIndex(); + } + + *pOutputLen = bitOutput.Finish(); + WMSG1(" FDI: converted pulses to %d bits\n", *pOutputLen); + result = true; + +bail: + return result; +} + + + +/* + * Convert a stream of GCR bits into nibbles. + * + * The stream includes 9-bit and 10-bit self-sync bytes. We need to process + * the bits as if we were an Apple II, shifting bits into a register until + * we get a 1 in the msb. + * + * There is a (roughly) 7 in 8 chance that we will not start out reading + * the stream on a byte boundary. We have to read for a bit to let the + * self-sync bytes do their job. + * + * "*pNibbleLen" should hold the maximum size of the buffer. On success, + * it will hold the actual number of bytes used. + */ +bool +WrapperFDI::ConvertBitsToNibbles(const unsigned char* bitBuffer, int bitCount, + unsigned char* nibbleBuf, long* pNibbleLen) +{ + BitInputBuffer inputBuffer(bitBuffer, bitCount); + const unsigned char* nibbleBufStart = nibbleBuf; + long outputBufSize = *pNibbleLen; + bool result = false; + unsigned char val; + bool wrap; + + /* + * Start 3/4 of the way through the buffer. That should give us a + * couple of self-sync zones before we hit the end of the buffer. + */ + inputBuffer.SetStartPosition(3 * (bitCount / 4)); + + /* + * Run until we wrap. We should be in sync by that point. + */ + wrap = false; + while (!wrap) { + val = inputBuffer.GetByte(&wrap); + if ((val & 0x80) == 0) + val = (val << 1) | inputBuffer.GetBit(&wrap); + if ((val & 0x80) == 0) + val = (val << 1) | inputBuffer.GetBit(&wrap); + if ((val & 0x80) == 0) { + // not allowed by GCR encoding, probably garbage between sectors + WMSG0(" FDI: WARNING: more than 2 consecutive zeroes (sync)\n"); + } + } + + /* + * Extract the nibbles. + */ + inputBuffer.ResetBitsConsumed(); + wrap = false; + while (true) { + val = inputBuffer.GetByte(&wrap); + if ((val & 0x80) == 0) + val = (val << 1) | inputBuffer.GetBit(&wrap); + if ((val & 0x80) == 0) + val = (val << 1) | inputBuffer.GetBit(&wrap); + if ((val & 0x80) == 0) { + WMSG0(" FDI: WARNING: more than 2 consecutive zeroes (read)\n"); + } + + if (nibbleBuf - nibbleBufStart >= outputBufSize) { + WMSG0(" FDI: bits overflowed nibble buffer\n"); + goto bail; + } + *nibbleBuf++ = val; + + /* if we wrapped around on this one, we've reached the start point */ + if (wrap) + break; + } + + if (inputBuffer.GetBitsConsumed() != bitCount) { + /* we dropped some or double-counted some */ + WMSG2(" FDI: WARNING: consumed %d of %d bits\n", + inputBuffer.GetBitsConsumed(), bitCount); + } + + WMSG4(" FDI: consumed %d of %d (first=0x%02x last=0x%02x)\n", + inputBuffer.GetBitsConsumed(), bitCount, + *nibbleBufStart, *(nibbleBuf-1)); + + *pNibbleLen = nibbleBuf - nibbleBufStart; + result = true; + +bail: + return result; +} + diff --git a/diskimg/FocusDrive.cpp b/diskimg/FocusDrive.cpp new file mode 100644 index 0000000..abcb262 --- /dev/null +++ b/diskimg/FocusDrive.cpp @@ -0,0 +1,362 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * This is a container for the Parsons Engineering FocusDrive. + * + * The format was reverse-engineered by Ranger Harke. + */ +#include "StdAfx.h" +#include "DiskImgPriv.h" + + +const int kBlkSize = 512; +const int kPartMapBlock = 0; // partition map lives here +const int kMaxPartitions = 30; // max allowed partitions on a drive +const int kPartNameStart = 1; // partition names start here (2 blocks) +const int kPartNameLen = 32; // max length of partition name + +static const char* kSignature = "Parsons Engin."; +const int kSignatureLen = 14; + +/* + * Format of partition map. It resides in the first 256 bytes of block 0. + * All values are in little-endian order. + * + * We also make space here for the partition names, which live on blocks 1+2. + */ +typedef struct DiskFSFocusDrive::PartitionMap { + unsigned char signature[kSignatureLen]; + unsigned char unknown1; + unsigned char partCount; // could be ushort, combined w/unknown1 + unsigned char unknown2[16]; + struct Entry { + unsigned long startBlock; + unsigned long blockCount; + unsigned long unknown1; + unsigned long unknown2; + unsigned char name[kPartNameLen+1]; + } entry[kMaxPartitions]; +} PartitionMap; + + +/* + * Figure out if this is a FocusDrive partition. + * + * The "imageOrder" parameter has no use here, because (in the current + * version) embedded parent volumes are implicitly ProDOS-ordered. + */ +/*static*/ DIError +DiskFSFocusDrive::TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder) +{ + DIError dierr = kDIErrNone; + unsigned char blkBuf[kBlkSize]; + int partCount; + + /* + * See if block 0 is a FocusDrive partition map. + */ + dierr = pImg->ReadBlockSwapped(kPartMapBlock, blkBuf, imageOrder, + DiskImg::kSectorOrderProDOS); + if (dierr != kDIErrNone) + goto bail; + + if (memcmp(blkBuf, kSignature, kSignatureLen) != 0) { + WMSG0(" FocusDrive partition signature not found in first part block\n"); + dierr = kDIErrFilesystemNotFound; + goto bail; + } + + partCount = blkBuf[0x0f]; + if (partCount == 0 || partCount > kMaxPartitions) { + WMSG1(" FocusDrive partition count looks bad (%d)\n", partCount); + dierr = kDIErrFilesystemNotFound; + goto bail; + } + + // success! + WMSG1(" Looks like FocusDrive with %d partitions\n", partCount); + +bail: + return dierr; +} + + +/* + * Unpack a partition map block into a partition map data structure. + */ +/*static*/ void +DiskFSFocusDrive::UnpackPartitionMap(const unsigned char* buf, + const unsigned char* nameBuf, PartitionMap* pMap) +{ + const unsigned char* ptr; + const unsigned char* namePtr; + int i; + + memcpy(pMap->signature, &buf[0x00], kSignatureLen); + pMap->unknown1 = buf[0x0e]; + pMap->partCount = buf[0x0f]; + memcpy(pMap->unknown2, &buf[0x10], 16); + + ptr = &buf[0x20]; + namePtr = &nameBuf[kPartNameLen]; // not sure what first 32 bytes are + for (i = 0; i < kMaxPartitions; i++) { + pMap->entry[i].startBlock = GetLongLE(ptr); + pMap->entry[i].blockCount = GetLongLE(ptr+4); + pMap->entry[i].unknown1 = GetLongLE(ptr+8); + pMap->entry[i].unknown2 = GetLongLE(ptr+12); + + memcpy(pMap->entry[i].name, namePtr, kPartNameLen); + pMap->entry[i].name[kPartNameLen] = '\0'; + + ptr += 0x10; + namePtr += kPartNameLen; + } + + assert(ptr == buf + kBlkSize); +} + +/* + * Debug: dump the contents of the partition map. + */ +/*static*/ void +DiskFSFocusDrive::DumpPartitionMap(const PartitionMap* pMap) +{ + int i; + + WMSG1(" FocusDrive partition map (%d partitions):\n", pMap->partCount); + for (i = 0; i < pMap->partCount; i++) { + WMSG4(" %2d: %8ld %8ld '%s'\n", i, pMap->entry[i].startBlock, + pMap->entry[i].blockCount, pMap->entry[i].name); + } +} + + +/* + * Open up a sub-volume. + */ +DIError +DiskFSFocusDrive::OpenSubVolume(long startBlock, long numBlocks, + const char* name) +{ + DIError dierr = kDIErrNone; + DiskFS* pNewFS = nil; + DiskImg* pNewImg = nil; + //bool tweaked = false; + + WMSG2("Adding %ld +%ld\n", startBlock, numBlocks); + + if (startBlock > fpImg->GetNumBlocks()) { + WMSG2("FocusDrive start block out of range (%ld vs %ld)\n", + startBlock, fpImg->GetNumBlocks()); + return kDIErrBadPartition; + } + if (startBlock + numBlocks > fpImg->GetNumBlocks()) { + WMSG2("FocusDrive partition too large (%ld vs %ld avail)\n", + numBlocks, fpImg->GetNumBlocks() - startBlock); + fpImg->AddNote(DiskImg::kNoteInfo, + "Reduced partition from %ld blocks to %ld.\n", + numBlocks, fpImg->GetNumBlocks() - startBlock); + numBlocks = fpImg->GetNumBlocks() - startBlock; + //tweaked = true; + } + + pNewImg = new DiskImg; + if (pNewImg == nil) { + dierr = kDIErrMalloc; + goto bail; + } + + dierr = pNewImg->OpenImage(fpImg, startBlock, numBlocks); + if (dierr != kDIErrNone) { + WMSG3(" FocusDriveSub: OpenImage(%ld,%ld) failed (err=%d)\n", + startBlock, numBlocks, dierr); + goto bail; + } + + //WMSG2(" +++ CFFASub: new image has ro=%d (parent=%d)\n", + // pNewImg->GetReadOnly(), pImg->GetReadOnly()); + + /* figure out what the format is */ + dierr = pNewImg->AnalyzeImage(); + if (dierr != kDIErrNone) { + WMSG1(" FocusDriveSub: analysis failed (err=%d)\n", dierr); + goto bail; + } + + /* we allow unrecognized partitions */ + if (pNewImg->GetFSFormat() == DiskImg::kFormatUnknown || + pNewImg->GetSectorOrder() == DiskImg::kSectorOrderUnknown) + { + WMSG2(" FocusDriveSub (%ld,%ld): unable to identify filesystem\n", + startBlock, numBlocks); + DiskFSUnknown* pUnknownFS = new DiskFSUnknown; + if (pUnknownFS == nil) { + dierr = kDIErrInternal; + goto bail; + } + //pUnknownFS->SetVolumeInfo((const char*)pMap->pmParType); + pNewFS = pUnknownFS; + } else { + /* open a DiskFS for the sub-image */ + WMSG2(" FocusDriveSub (%ld,%ld) analyze succeeded!\n", startBlock, numBlocks); + pNewFS = pNewImg->OpenAppropriateDiskFS(true); + if (pNewFS == nil) { + WMSG0(" FocusDriveSub: OpenAppropriateDiskFS failed\n"); + dierr = kDIErrUnsupportedFSFmt; + goto bail; + } + } + pNewImg->AddNote(DiskImg::kNoteInfo, "Partition name='%s'.", name); + + /* we encapsulate arbitrary stuff, so encourage child to scan */ + pNewFS->SetScanForSubVolumes(kScanSubEnabled); + + /* + * Load the files from the sub-image. When doing our initial tests, + * or when loading data for the volume copier, we don't want to dig + * into our sub-volumes, just figure out what they are and where. + * + * If "initialize" fails, the sub-volume won't get added to the list. + * It's important that a failure at this stage doesn't cause the whole + * thing to fall over. + */ + InitMode initMode; + if (GetScanForSubVolumes() == kScanSubContainerOnly) + initMode = kInitHeaderOnly; + else + initMode = kInitFull; + dierr = pNewFS->Initialize(pNewImg, initMode); + if (dierr != kDIErrNone) { + WMSG1(" FocusDriveSub: error %d reading list of files from disk", dierr); + goto bail; + } + + /* add it to the list */ + AddSubVolumeToList(pNewImg, pNewFS); + pNewImg = nil; + pNewFS = nil; + +bail: + delete pNewFS; + delete pNewImg; + return dierr; +} + +/* + * Check to see if this is a FocusDrive volume. + */ +/*static*/ DIError +DiskFSFocusDrive::TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency) +{ + if (pImg->GetNumBlocks() < kMinInterestingBlocks) + return kDIErrFilesystemNotFound; + if (pImg->GetIsEmbedded()) // don't look for partitions inside + return kDIErrFilesystemNotFound; + + /* assume ProDOS -- shouldn't matter, since it's embedded */ + if (TestImage(pImg, DiskImg::kSectorOrderProDOS) == kDIErrNone) { + *pFormat = DiskImg::kFormatFocusDrive; + *pOrder = DiskImg::kSectorOrderProDOS; + return kDIErrNone; + } + + WMSG0(" FS didn't find valid FocusDrive\n"); + return kDIErrFilesystemNotFound; +} + + +/* + * Prep the FocusDrive "container" for use. + */ +DIError +DiskFSFocusDrive::Initialize(void) +{ + DIError dierr = kDIErrNone; + + WMSG1("FocusDrive initializing (scanForSub=%d)\n", fScanForSubVolumes); + + /* seems pointless *not* to, but we just do what we're told */ + if (fScanForSubVolumes != kScanSubDisabled) { + dierr = FindSubVolumes(); + if (dierr != kDIErrNone) + return dierr; + } + + /* blank out the volume usage map */ + SetVolumeUsageMap(); + + return dierr; +} + + +/* + * Find the various sub-volumes and open them. + */ +DIError +DiskFSFocusDrive::FindSubVolumes(void) +{ + DIError dierr = kDIErrNone; + unsigned char buf[kBlkSize]; + unsigned char nameBuf[kBlkSize*2]; + PartitionMap map; + int i; + + dierr = fpImg->ReadBlock(kPartMapBlock, buf); + if (dierr != kDIErrNone) + goto bail; + dierr = fpImg->ReadBlock(kPartNameStart, nameBuf); + if (dierr != kDIErrNone) + goto bail; + dierr = fpImg->ReadBlock(kPartNameStart+1, nameBuf+kBlkSize); + if (dierr != kDIErrNone) + goto bail; + UnpackPartitionMap(buf, nameBuf, &map); + DumpPartitionMap(&map); + + for (i = 0; i < map.partCount; i++) { + dierr = OpenVol(i, map.entry[i].startBlock, map.entry[i].blockCount, + (const char*)map.entry[i].name); + if (dierr != kDIErrNone) + goto bail; + } + +bail: + return dierr; +} + +/* + * Open the volume. If it fails, open a placeholder instead. (If *that* + * fails, return with an error.) + */ +DIError +DiskFSFocusDrive::OpenVol(int idx, long startBlock, long numBlocks, + const char* name) +{ + DIError dierr; + + dierr = OpenSubVolume(startBlock, numBlocks, name); + if (dierr != kDIErrNone) { + if (dierr == kDIErrCancelled) + goto bail; + DiskFS* pNewFS = nil; + DiskImg* pNewImg = nil; + + WMSG1(" FocusDrive failed opening sub-volume %d\n", idx); + dierr = CreatePlaceholder(startBlock, numBlocks, name, NULL, + &pNewImg, &pNewFS); + if (dierr == kDIErrNone) { + AddSubVolumeToList(pNewImg, pNewFS); + } else { + WMSG1(" FocusDrive unable to create placeholder (err=%d)\n", + dierr); + // fall out with error + } + } + +bail: + return dierr; +} diff --git a/diskimg/GenericFD.cpp b/diskimg/GenericFD.cpp new file mode 100644 index 0000000..3b63c16 --- /dev/null +++ b/diskimg/GenericFD.cpp @@ -0,0 +1,876 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Generic file descriptor class. + */ +#include "StdAfx.h" +#include "DiskImgPriv.h" + +/* + * =========================================================================== + * GenericFD utility functions + * =========================================================================== + */ + +/* + * Copy "length" bytes from "pSrc" to "pDst". Both GenericFDs should be + * seeked to their initial positions. + * + * If "pCRC" is non-nil, this computes a CRC32 as it goes, using the zlib + * library function. + */ +/*static*/ DIError +GenericFD::CopyFile(GenericFD* pDst, GenericFD* pSrc, di_off_t length, + unsigned long* pCRC) +{ + DIError dierr = kDIErrNone; + const int kCopyBufSize = 32768; + unsigned char* copyBuf = nil; + int copySize; + + WMSG1("+++ CopyFile: %ld bytes\n", (long) length); + + if (pDst == nil || pSrc == nil || length < 0) + return kDIErrInvalidArg; + if (length == 0) + return kDIErrNone; + + copyBuf = new unsigned char[kCopyBufSize]; + if (copyBuf == nil) + return kDIErrMalloc; + + if (pCRC != nil) + *pCRC = crc32(0L, Z_NULL, 0); + + while (length != 0) { + copySize = kCopyBufSize; + if (copySize > length) + copySize = (int) length; + + dierr = pSrc->Read(copyBuf, copySize); + if (dierr != kDIErrNone) + goto bail; + + if (pCRC != nil) + *pCRC = crc32(*pCRC, copyBuf, copySize); + + dierr = pDst->Write(copyBuf, copySize); + if (dierr != kDIErrNone) + goto bail; + + length -= copySize; + } + +bail: + delete[] copyBuf; + return dierr; +} + + +/* + * =========================================================================== + * GFDFile + * =========================================================================== + */ + +/* + * The stdio functions (fopen/fread/fwrite/fseek/ftell) are buffered and, + * therefore, faster for small operations. Unfortunately we need 64-bit + * file offsets, and it doesn't look like the Windows stdio stuff will + * support it cleanly (e.g. even the _FPOSOFF macro returns a "long"). + * + * Recent versions of Linux have "fseeko", which is like fseek but takes + * an off_t, so we can continue to use the FILE* functions there. Under + * Windows "lseek" takes a long, so we have to use their specific 64-bit + * variant. + */ +#ifdef HAVE_FSEEKO + +DIError +GFDFile::Open(const char* filename, bool readOnly) +{ + DIError dierr = kDIErrNone; + + if (fFp != nil) + return kDIErrAlreadyOpen; + if (filename == nil) + return kDIErrInvalidArg; + if (filename[0] == '\0') + return kDIErrInvalidArg; + + delete[] fPathName; + fPathName = new char[strlen(filename) +1]; + strcpy(fPathName, filename); + + fFp = fopen(filename, readOnly ? "rb" : "r+b"); + if (fFp == nil) { + if (errno == EACCES) + dierr = kDIErrAccessDenied; + else + dierr = ErrnoOrGeneric(); + WMSG3(" GDFile Open failed opening '%s', ro=%d (err=%d)\n", + filename, readOnly, dierr); + return dierr; + } + fReadOnly = readOnly; + return dierr; +} + +DIError +GFDFile::Read(void* buf, size_t length, size_t* pActual) +{ + DIError dierr = kDIErrNone; + size_t actual; + + if (fFp == nil) + return kDIErrNotReady; + actual = ::fread(buf, 1, length, fFp); + if (actual == 0) { + if (feof(fFp)) + return kDIErrEOF; + if (ferror(fFp)) { + dierr = ErrnoOrGeneric(); + return dierr; + } + WMSG0("MYSTERY FREAD RESULT\n"); + return kDIErrInternal; + } + + if (pActual == nil) { + if (actual != length) { + dierr = ErrnoOrGeneric(); + WMSG3(" GDFile Read failed on %d bytes (actual=%d, err=%d)\n", + length, actual, dierr); + return dierr; + } + } else { + *pActual = actual; + } + return dierr; +} + +DIError +GFDFile::Write(const void* buf, size_t length, size_t* pActual) +{ + DIError dierr = kDIErrNone; + + if (fFp == nil) + return kDIErrNotReady; + if (fReadOnly) + return kDIErrAccessDenied; + assert(pActual == nil); // not handling this yet + if (::fwrite(buf, length, 1, fFp) != 1) { + dierr = ErrnoOrGeneric(); + WMSG2(" GDFile Write failed on %d bytes (err=%d)\n", length, dierr); + return dierr; + } + return dierr; +} + +DIError +GFDFile::Seek(di_off_t offset, DIWhence whence) +{ + DIError dierr = kDIErrNone; + //static const long kOneGB = 1024*1024*1024; + //static const long kAlmostTwoGB = kOneGB + (kOneGB -1); + + if (fFp == nil) + return kDIErrNotReady; + //assert(offset <= kAlmostTwoGB); + //if (::fseek(fFp, (long) offset, whence) != 0) { + if (::fseeko(fFp, offset, whence) != 0) { + dierr = ErrnoOrGeneric(); + WMSG1(" GDFile Seek failed (err=%d)\n", dierr); + return dierr; + } + return dierr; +} + +di_off_t +GFDFile::Tell(void) +{ + DIError dierr = kDIErrNone; + di_off_t result; + + if (fFp == nil) + return kDIErrNotReady; + //result = ::ftell(fFp); + result = ::ftello(fFp); + if (result == -1) { + dierr = ErrnoOrGeneric(); + WMSG1(" GDFile Tell failed (err=%d)\n", dierr); + return result; + } + return result; +} + +DIError +GFDFile::Truncate(void) +{ +#if defined(HAVE_FTRUNCATE) + int cc; + cc = ::ftruncate(fileno(fFp), (long) Tell()); + if (cc != 0) + return kDIErrWriteFailed; +#elif defined(HAVE_CHSIZE) + assert(false); // not tested + int cc; + cc = ::chsize(fFd, (long) Tell()); + if (cc != 0) + return kDIErrWriteFailed; +#else +# error "missing truncate" +#endif + return kDIErrNone; +} + +DIError +GFDFile::Close(void) +{ + if (fFp == nil) + return kDIErrNotReady; + + WMSG1(" GFDFile closing '%s'\n", fPathName); + fclose(fFp); + fFp = nil; + return kDIErrNone; +} + +#else /*HAVE_FSEEKO*/ + +DIError +GFDFile::Open(const char* filename, bool readOnly) +{ + DIError dierr = kDIErrNone; + + if (fFd >= 0) + return kDIErrAlreadyOpen; + if (filename == nil) + return kDIErrInvalidArg; + if (filename[0] == '\0') + return kDIErrInvalidArg; + + delete[] fPathName; + fPathName = new char[strlen(filename) +1]; + strcpy(fPathName, filename); + + fFd = open(filename, readOnly ? O_RDONLY|O_BINARY : O_RDWR|O_BINARY, 0); + if (fFd < 0) { + if (errno == EACCES) + dierr = kDIErrAccessDenied; + else + dierr = ErrnoOrGeneric(); + WMSG3(" GDFile Open failed opening '%s', ro=%d (err=%d)\n", + filename, readOnly, dierr); + return dierr; + } + fReadOnly = readOnly; + return dierr; +} + +DIError +GFDFile::Read(void* buf, size_t length, size_t* pActual) +{ + DIError dierr; + ssize_t actual; + + if (fFd < 0) + return kDIErrNotReady; + actual = ::read(fFd, buf, length); + if (actual == 0) + return kDIErrEOF; + if (actual < 0) { + dierr = ErrnoOrGeneric(); + WMSG3(" GDFile Read failed on %d bytes (actual=%d, err=%d)\n", + length, actual, dierr); + return dierr; + } + + if (pActual == nil) { + if (actual != (ssize_t) length) { + WMSG2(" GDFile Read partial (wanted=%d actual=%d)\n", + length, actual); + return kDIErrReadFailed; + } + } else { + *pActual = actual; + } + return kDIErrNone; +} + +DIError +GFDFile::Write(const void* buf, size_t length, size_t* pActual) +{ + DIError dierr; + ssize_t actual; + + if (fFd < 0) + return kDIErrNotReady; + if (fReadOnly) + return kDIErrAccessDenied; + assert(pActual == nil); // not handling partial writes yet + actual = ::write(fFd, buf, length); + if (actual != (ssize_t) length) { + dierr = ErrnoOrGeneric(); + WMSG3(" GDFile Write failed on %d bytes (actual=%d err=%d)\n", + length, actual, dierr); + return dierr; + } + return kDIErrNone; +} + +DIError +GFDFile::Seek(di_off_t offset, DIWhence whence) +{ + DIError dierr = kDIErrNone; + if (fFd < 0) + return kDIErrNotReady; + +#ifdef WIN32 + __int64 newPosn; + const __int64 kFailure = (__int64) -1; + newPosn = ::_lseeki64(fFd, (__int64) offset, whence); +#else + di_off_t newPosn; + const di_off_t kFailure = (di_off_t) -1; + newPosn = lseek(fFd, offset, whence); +#endif + + if (newPosn == kFailure) { + assert((unsigned long) offset != 0xccccccccUL); // uninitialized data! + dierr = ErrnoOrGeneric(); + WMSG3(" GDFile Seek %ld-%lu failed (err=%d)\n", + (long) (offset >> 32), (unsigned long) offset, dierr); + } + return dierr; +} + +di_off_t +GFDFile::Tell(void) +{ + DIError dierr = kDIErrNone; + di_off_t result; + + if (fFd < 0) + return kDIErrNotReady; + +#ifdef WIN32 + result = ::_lseeki64(fFd, 0, SEEK_CUR); +#else + result = lseek(fFd, 0, SEEK_CUR); +#endif + + if (result == -1) { + dierr = ErrnoOrGeneric(); + WMSG1(" GDFile Tell failed (err=%d)\n", dierr); + return result; + } + return result; +} + +DIError +GFDFile::Truncate(void) +{ +#if defined(HAVE_FTRUNCATE) + int cc; + cc = ::ftruncate(fFd, (long) Tell()); + if (cc != 0) + return kDIErrWriteFailed; +#elif defined(HAVE_CHSIZE) + int cc; + cc = ::chsize(fFd, (long) Tell()); + if (cc != 0) + return kDIErrWriteFailed; +#else +# error "missing truncate" +#endif + return kDIErrNone; +} + +DIError +GFDFile::Close(void) +{ + if (fFd < 0) + return kDIErrNotReady; + + WMSG1(" GFDFile closing '%s'\n", fPathName); + ::close(fFd); + fFd = -1; + return kDIErrNone; +} +#endif /*HAVE_FSEEKO else*/ + + +/* + * =========================================================================== + * GFDBuffer + * =========================================================================== + */ + +DIError +GFDBuffer::Open(void* buffer, di_off_t length, bool doDelete, bool doExpand, + bool readOnly) +{ + if (fBuffer != nil) + return kDIErrAlreadyOpen; + if (length <= 0) + return kDIErrInvalidArg; + if (length > kMaxReasonableSize) { + // be reasonable + WMSG1(" GFDBuffer refusing to allocate buffer size(long)=%ld bytes\n", + (long) length); + return kDIErrInvalidArg; + } + + /* if buffer is nil, allocate it ourselves */ + if (buffer == nil) { + fBuffer = (void*) new char[(int) length]; + if (fBuffer == nil) + return kDIErrMalloc; + } else + fBuffer = buffer; + + fLength = (long) length; + fAllocLength = (long) length; + fDoDelete = doDelete; + fDoExpand = doExpand; + fReadOnly = readOnly; + + fCurrentOffset = 0; + + return kDIErrNone; +} + +DIError +GFDBuffer::Read(void* buf, size_t length, size_t* pActual) +{ + if (fBuffer == nil) + return kDIErrNotReady; + if (length == 0) + return kDIErrInvalidArg; + + if (fCurrentOffset + (long)length > fLength) { + if (pActual == nil) { + WMSG3(" GFDBuffer underrrun off=%ld len=%d flen=%ld\n", + (long) fCurrentOffset, length, (long) fLength); + return kDIErrDataUnderrun; + } else { + /* set *pActual and adjust "length" */ + assert(fLength >= fCurrentOffset); + length = (size_t) (fLength - fCurrentOffset); + *pActual = length; + + if (length == 0) + return kDIErrEOF; + } + } + if (pActual != nil) + *pActual = length; + + memcpy(buf, (const char*)fBuffer + fCurrentOffset, length); + fCurrentOffset += length; + + return kDIErrNone; +} + +DIError +GFDBuffer::Write(const void* buf, size_t length, size_t* pActual) +{ + if (fBuffer == nil) + return kDIErrNotReady; + assert(pActual == nil); // not handling this yet + if (fCurrentOffset + (long)length > fLength) { + if (!fDoExpand) { + WMSG3(" GFDBuffer overrun off=%ld len=%d flen=%ld\n", + (long) fCurrentOffset, length, (long) fLength); + return kDIErrDataOverrun; + } + + /* + * Expand the buffer as needed. + * + * We delete the old buffer unless "doDelete" is not set, in + * which case we just drop the pointer. Anything we allocate + * here can and will be deleted; "doDelete" only applies to the + * pointer initially passed in. + */ + if (fCurrentOffset + (long)length <= fAllocLength) { + /* fits inside allocated space, so just extend length */ + fLength = (long) fCurrentOffset + (long)length; + } else { + /* does not fit, realloc buffer */ + fAllocLength = (long) fCurrentOffset + (long)length + 8*1024; + WMSG1("Reallocating buffer (new size = %ld)\n", fAllocLength); + assert(fAllocLength < kMaxReasonableSize); + char* newBuf = new char[(int) fAllocLength]; + if (newBuf == nil) + return kDIErrMalloc; + + memcpy(newBuf, fBuffer, fLength); + + if (fDoDelete) + delete[] (char*)fBuffer; + else + fDoDelete = true; // future deletions are okay + + fBuffer = newBuf; + fLength = (long) fCurrentOffset + (long)length; + } + } + + memcpy((char*)fBuffer + fCurrentOffset, buf, length); + fCurrentOffset += length; + + return kDIErrNone; +} + +DIError +GFDBuffer::Seek(di_off_t offset, DIWhence whence) +{ + if (fBuffer == nil) + return kDIErrNotReady; + + switch (whence) { + case kSeekSet: + if (offset < 0 || offset >= fLength) + return kDIErrInvalidArg; + fCurrentOffset = offset; + break; + case kSeekEnd: + if (offset > 0 || offset < -fLength) + return kDIErrInvalidArg; + fCurrentOffset = fLength + offset; + break; + case kSeekCur: + if (offset < -fCurrentOffset || + offset >= (fLength - fCurrentOffset)) + { + return kDIErrInvalidArg; + } + fCurrentOffset += offset; + break; + default: + assert(false); + return kDIErrInvalidArg; + } + + assert(fCurrentOffset >= 0 && fCurrentOffset <= fLength); + return kDIErrNone; +} + +di_off_t +GFDBuffer::Tell(void) +{ + if (fBuffer == nil) + return (di_off_t) -1; + return fCurrentOffset; +} + +DIError +GFDBuffer::Close(void) +{ + if (fBuffer == nil) + return kDIErrNone; + + if (fDoDelete) { + WMSG0(" GFDBuffer closing and deleting\n"); + delete[] (char*) fBuffer; + } else { + WMSG0(" GFDBuffer closing\n"); + } + fBuffer = nil; + + return kDIErrNone; +} + + +#ifdef _WIN32 +/* + * =========================================================================== + * GFDWinVolume + * =========================================================================== + */ + +/* + * This class is intended for use with logical volumes under Win32. Such + * devices must be accessed on 512-byte boundaries, which means no arbitrary + * seeks or reads. The device driver doesn't seem too adept at figuring + * out how large the device is, either, so we need to work that out for + * ourselves. (The standard approach appears to involve examining the + * partition map for the logical or physical volume, but we don't have a + * partition map to look at.) + */ + +/* + * Prepare a logical volume device for reading or writing. "deviceName" + * must have the form "N:\" for a logical volume or "80:\" for a physical + * volume. + */ +DIError +GFDWinVolume::Open(const char* deviceName, bool readOnly) +{ + DIError dierr = kDIErrNone; + HANDLE handle = nil; + //unsigned long kTwoGBBlocks; + + if (fVolAccess.Ready()) + return kDIErrAlreadyOpen; + if (deviceName == nil) + return kDIErrInvalidArg; + if (deviceName[0] == '\0') + return kDIErrInvalidArg; + + delete[] fPathName; + fPathName = new char[strlen(deviceName) +1]; + strcpy(fPathName, deviceName); + + dierr = fVolAccess.Open(deviceName, readOnly); + if (dierr != kDIErrNone) + goto bail; + + fBlockSize = fVolAccess.GetBlockSize(); // must be power of 2 + assert(fBlockSize > 0); + //kTwoGBBlocks = kTwoGB / fBlockSize; + + unsigned long totalBlocks; + totalBlocks = fVolAccess.GetTotalBlocks(); + fVolumeEOF = (di_off_t)totalBlocks * fBlockSize; + + assert(fVolumeEOF > 0); + + fReadOnly = readOnly; + +bail: + return dierr; +} + +DIError +GFDWinVolume::Read(void* buf, size_t length, size_t* pActual) +{ + DIError dierr = kDIErrNone; + unsigned char* blkBuf = nil; + + //WMSG2(" GFDWinVolume: reading %ld bytes from offset %ld\n", length, + // fCurrentOffset); + + if (!fVolAccess.Ready()) + return kDIErrNotReady; + + // don't allow reading past the end of file + if (fCurrentOffset + (long) length > fVolumeEOF) { + if (pActual == nil) + return kDIErrDataUnderrun; + length = (size_t) (fVolumeEOF - fCurrentOffset); + } + if (pActual != nil) + *pActual = length; + if (length == 0) + return kDIErrNone; + + long advanceLen = length; + + blkBuf = new unsigned char[fBlockSize]; // get this off the heap?? + long blockIndex = (long) (fCurrentOffset / fBlockSize); + int bufOffset = (int) (fCurrentOffset % fBlockSize); // req power of 2 + assert(blockIndex >= 0); + + /* + * When possible, do multi-block reads directly into "buf". The first + * and last block may require special handling. + */ + while (length) { + assert(length > 0); + + if (bufOffset != 0 || length < (size_t) fBlockSize) { + assert(bufOffset >= 0 && bufOffset < fBlockSize); + + size_t thisCount; + + dierr = fVolAccess.ReadBlocks(blockIndex, 1, blkBuf); + if (dierr != kDIErrNone) + goto bail; + + thisCount = fBlockSize - bufOffset; + if (thisCount > length) + thisCount = length; + + //WMSG2(" Copying %d bytes from block %d\n", + // thisCount, blockIndex); + + memcpy(buf, blkBuf + bufOffset, thisCount); + length -= thisCount; + buf = (char*) buf + thisCount; + + bufOffset = 0; + blockIndex++; + } else { + assert(bufOffset == 0); + + long blockCount = length / fBlockSize; + assert(blockCount < 32768); + + dierr = fVolAccess.ReadBlocks(blockIndex, (short) blockCount, buf); + if (dierr != kDIErrNone) + goto bail; + + length -= blockCount * fBlockSize; + buf = (char*) buf + blockCount * fBlockSize; + + blockIndex += blockCount; + } + + } + + fCurrentOffset += advanceLen; + +bail: + delete[] blkBuf; + return dierr; +} + +DIError +GFDWinVolume::Write(const void* buf, size_t length, size_t* pActual) +{ + DIError dierr = kDIErrNone; + unsigned char* blkBuf = nil; + + //WMSG2(" GFDWinVolume: writing %ld bytes at offset %ld\n", length, + // fCurrentOffset); + + if (!fVolAccess.Ready()) + return kDIErrNotReady; + if (fReadOnly) + return kDIErrAccessDenied; + + // don't allow writing past the end of the volume + if (fCurrentOffset + (long) length > fVolumeEOF) { + if (pActual == nil) + return kDIErrDataOverrun; + length = (size_t) (fVolumeEOF - fCurrentOffset); + } + if (pActual != nil) + *pActual = length; + if (length == 0) + return kDIErrNone; + + long advanceLen = length; + + blkBuf = new unsigned char[fBlockSize]; // get this out of the heap?? + long blockIndex = (long) (fCurrentOffset / fBlockSize); + int bufOffset = (int) (fCurrentOffset % fBlockSize); // req power of 2 + assert(blockIndex >= 0); + + /* + * When possible, do multi-block writes directly from "buf". The first + * and last block may require special handling. + */ + while (length) { + assert(length > 0); + + if (bufOffset != 0 || length < (size_t) fBlockSize) { + assert(bufOffset >= 0 && bufOffset < fBlockSize); + + size_t thisCount; + + dierr = fVolAccess.ReadBlocks(blockIndex, 1, blkBuf); + if (dierr != kDIErrNone) + goto bail; + + thisCount = fBlockSize - bufOffset; + if (thisCount > length) + thisCount = length; + + //WMSG3(" Copying %d bytes into block %d (off=%d)\n", + // thisCount, blockIndex, bufOffset); + + memcpy(blkBuf + bufOffset, buf, thisCount); + length -= thisCount; + buf = (char*) buf + thisCount; + + dierr = fVolAccess.WriteBlocks(blockIndex, 1, blkBuf); + if (dierr != kDIErrNone) + goto bail; + + bufOffset = 0; + blockIndex++; + } else { + assert(bufOffset == 0); + + long blockCount = length / fBlockSize; + assert(blockCount < 32768); + + dierr = fVolAccess.WriteBlocks(blockIndex, (short) blockCount, buf); + if (dierr != kDIErrNone) + goto bail; + + length -= blockCount * fBlockSize; + buf = (char*) buf + blockCount * fBlockSize; + + blockIndex += blockCount; + } + + } + + fCurrentOffset += advanceLen; + +bail: + delete[] blkBuf; + return dierr; +} + +DIError +GFDWinVolume::Seek(di_off_t offset, DIWhence whence) +{ + if (!fVolAccess.Ready()) + return kDIErrNotReady; + + switch (whence) { + case kSeekSet: + if (offset < 0 || offset >= fVolumeEOF) + return kDIErrInvalidArg; + fCurrentOffset = offset; + break; + case kSeekEnd: + if (offset > 0 || offset < -fVolumeEOF) + return kDIErrInvalidArg; + fCurrentOffset = fVolumeEOF + offset; + break; + case kSeekCur: + if (offset < -fCurrentOffset || + offset >= (fVolumeEOF - fCurrentOffset)) + { + return kDIErrInvalidArg; + } + fCurrentOffset += offset; + break; + default: + assert(false); + return kDIErrInvalidArg; + } + + assert(fCurrentOffset >= 0 && fCurrentOffset <= fVolumeEOF); + return kDIErrNone; +} + +di_off_t +GFDWinVolume::Tell(void) +{ + if (!fVolAccess.Ready()) + return (di_off_t) -1; + return fCurrentOffset; +} + +DIError +GFDWinVolume::Close(void) +{ + if (!fVolAccess.Ready()) + return kDIErrNotReady; + + WMSG0(" GFDWinVolume closing\n"); + fVolAccess.Close(); + return kDIErrNone; +} +#endif /*_WIN32*/ diff --git a/diskimg/GenericFD.h b/diskimg/GenericFD.h new file mode 100644 index 0000000..bcd7c60 --- /dev/null +++ b/diskimg/GenericFD.h @@ -0,0 +1,317 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Declarations for GenericFD class and sub-classes. + */ +#ifndef __GENERIC_FD__ +#define __GENERIC_FD__ + +#include "Win32BlockIO.h" + +namespace DiskImgLib { + +#if 0 +/* + * Embedded file descriptor class, representing an open file on a disk image. + * + * Useful for opening disk images that are stored as files inside of other + * disk images. For stuff like UNIDOS images, which don't have a file + * associated with them, we can either open them as raw blocks, or create + * a "fake" file to access them. The latter is more general, and will work + * for sub-volumes of sub-volumes. + */ +class DISKIMG_API EmbeddedFD { +public: + EmbeddedFD(void) { + fpDiskFS = NULL; + fpA2File = NULL; + } + virtual ~EmbeddedFD(void) {} + + typedef enum Fork { kForkData = 0, kForkRsrc = 1 } Fork; + // bit-flag values for Open call's "access" parameter + enum { + kAccessNone = 0, // somewhat useless + kAccessRead = 0x01, // O_RDONLY + kAccessWrite = 0x02, // O_WRONLY + kAccessCreate = 0x04, // O_CREAT + kAccessMustNotExist = 0x08, // O_EXCL, pointless w/o O_CREAT + + kAccessReadWrite = (kAccessRead | kAccessWrite), + }; + + /* + * Standard set of calls. + */ + DIError Open(DiskFS* pDiskFS, const char* filename, Fork fork = kForkData, + int access = kAccessRead, int fileCreatePerms = 0); + DIError OpenBlocks(DiskFS* pDiskFS, long blockStart, long blockCount, + int access = kAccessRead); + DIError Read(void* buf, size_t length); + DIError Write(const void* buf, size_t length); + DIError Seek(di_off_t offset, DIWhence whence); + DIError Close(void); + +private: + // prevent bitwise copying behavior + EmbeddedFD& operator=(const EmbeddedFD&); + EmbeddedFD(const EmbeddedFD&); + + DiskFS* fpDiskFS; + A2File* fpA2File; +}; +#endif + + +/* + * Generic file source base class. Allows us to treat files on disk, memory + * buffers, and files embedded inside disk images equally. + * + * The file represented by the class is available in its entirety; skipping + * past "wrapper headers" is expected to be done by the caller. + * + * The Read and Write calls take an optional parameter that allows the caller + * to see how much data was actually read or written. If the parameter is + * not specified (or specified as nil), then failure to return the exact + * amount of data requested results an error. + * + * This is not meant to be the end-all of file wrapper classes; in + * particular, it does not support file creation. + * + * Some libraries, such as NufxLib, require an actual filename to operate + * (bad architecture?). The GetPathName call will return the original + * filename if one exists, or nil if there isn't one. (At which point the + * caller has the option of creating a temp file, copying the data into + * it, and cranking up NufxLib or zlib on that.) + * + * NOTE to self: see fsopen() to control sharing. + * + * NOTE: the Seek() implementations currently do not consistently allow or + * disallow seeking past the current EOF of a file. When writing a file this + * can be very useful, so someday we should implement it for all classes. + */ +class GenericFD { +public: + GenericFD(void) : fReadOnly(true) {} + virtual ~GenericFD(void) {} /* = 0 */ + + // All sub-classes must provide these, plus a type-specific Open call. + virtual DIError Read(void* buf, size_t length, + size_t* pActual = nil) = 0; + virtual DIError Write(const void* buf, size_t length, + size_t* pActual = nil) = 0; + virtual DIError Seek(di_off_t offset, DIWhence whence) = 0; + virtual di_off_t Tell(void) = 0; + virtual DIError Truncate(void) = 0; + virtual DIError Close(void) = 0; + virtual const char* GetPathName(void) const = 0; + + // Flush-data call, only needed for physical devices + virtual DIError Flush(void) { return kDIErrNone; } + + // Utility functions. + virtual DIError Rewind(void) { return Seek(0, kSeekSet); } + + virtual bool GetReadOnly(void) const { return fReadOnly; } + + /* + typedef enum { + kGFDTypeUnknown = 0, + kGFDTypeFile, + kGFDTypeBuffer, + kGFDTypeWinVolume, + kGFDTypeGFD + } GFDType; + virtual GFDType GetGFDType(void) const = 0; + */ + + /* + * Utility function to copy data from one GFD to another. Both GFDs must + * be seeked to their initial positions. "length" bytes will be copied. + */ + static DIError CopyFile(GenericFD* pDst, GenericFD* pSrc, di_off_t length, + unsigned long* pCRC = nil); + +protected: + GenericFD& operator=(const GenericFD&); + GenericFD(const GenericFD&); + + bool fReadOnly; // set when file is opened +}; + +class GFDFile : public GenericFD { +public: +#ifdef HAVE_FSEEKO + GFDFile(void) : fPathName(nil), fFp(nil) {} +#else + GFDFile(void) : fPathName(nil), fFd(-1) {} +#endif + virtual ~GFDFile(void) { Close(); delete[] fPathName; } + + virtual DIError Open(const char* filename, bool readOnly); + virtual DIError Read(void* buf, size_t length, + size_t* pActual = nil); + virtual DIError Write(const void* buf, size_t length, + size_t* pActual = nil); + virtual DIError Seek(di_off_t offset, DIWhence whence); + virtual di_off_t Tell(void); + virtual DIError Truncate(void); + virtual DIError Close(void); + virtual const char* GetPathName(void) const { return fPathName; } + +private: + char* fPathName; + +#ifdef HAVE_FSEEKO + FILE* fFp; +#else + int fFd; +#endif +}; + +#ifdef _WIN32 +class GFDWinVolume : public GenericFD { +public: + GFDWinVolume(void) : fPathName(nil), fCurrentOffset(0), fVolumeEOF(-1) + {} + virtual ~GFDWinVolume(void) { delete[] fPathName; } + + virtual DIError Open(const char* deviceName, bool readOnly); + virtual DIError Read(void* buf, size_t length, + size_t* pActual = nil); + virtual DIError Write(const void* buf, size_t length, + size_t* pActual = nil); + virtual DIError Seek(di_off_t offset, DIWhence whence); + virtual di_off_t Tell(void); + virtual DIError Truncate(void) { return kDIErrNotSupported; } + virtual DIError Close(void); + virtual const char* GetPathName(void) const { return fPathName; } + + virtual DIError Flush(void) { return fVolAccess.FlushCache(false); } + +private: + char* fPathName; // for display only + Win32VolumeAccess fVolAccess; + di_off_t fCurrentOffset; + di_off_t fVolumeEOF; + int fBlockSize; // usually 512 +}; +#endif + +class GFDBuffer : public GenericFD { +public: + GFDBuffer(void) : fBuffer(nil) {} + virtual ~GFDBuffer(void) { Close(); } + + // If "doDelete" is set, the buffer will be freed with delete[] when + // Close is called. This should ONLY be used for storage allocated + // by the DiskImg library, as under Windows it can cause problems + // because DLLs can have their own heap. + // + // "doExpand" will cause writing past the end of the buffer to + // reallocate the buffer. Again, for internally-allocated storage + // only. We expect the initial size to be close to accurate, so we + // don't aggressively expand the buffer. + virtual DIError Open(void* buffer, di_off_t length, bool doDelete, + bool doExpand, bool readOnly); + virtual DIError Read(void* buf, size_t length, + size_t* pActual = nil); + virtual DIError Write(const void* buf, size_t length, + size_t* pActual = nil); + virtual DIError Seek(di_off_t offset, DIWhence whence); + virtual di_off_t Tell(void); + virtual DIError Truncate(void) { + fLength = (long) Tell(); + return kDIErrNone; + } + virtual DIError Close(void); + virtual const char* GetPathName(void) const { return nil; } + + // Back door; try not to use this. + void* GetBuffer(void) const { return fBuffer; } + +private: + enum { kMaxReasonableSize = 256 * 1024 * 1024 }; + void* fBuffer; + long fLength; // these sit in memory, so there's no + long fAllocLength; // value in using di_off_t here + bool fDoDelete; + bool fDoExpand; + di_off_t fCurrentOffset; // actually limited to (long) +}; + +#if 0 +class GFDEmbedded : public GenericFD { +public: + GFDEmbedded(void) : fEFD(nil) {} + virtual ~GFDEmbedded(void) { Close(); } + + virtual DIError Open(EmbeddedFD* pEFD, bool readOnly); + virtual DIError Read(void* buf, size_t length, + size_t* pActual = nil); + virtual DIError Write(const void* buf, size_t length, + size_t* pActual = nil); + virtual DIError Seek(di_off_t offset, DIWhence whence); + virtual di_off_t Tell(void); + virtual DIError Close(void); + virtual const char* GetPathName(void) const { return nil; } + +private: + EmbeddedFD* fEFD; +}; +#endif + +/* pass all requests straight through to another GFD (with offset bias) */ +class GFDGFD : public GenericFD { +public: + GFDGFD(void) : fpGFD(nil), fOffset(0) {} + virtual ~GFDGFD(void) { Close(); } + + virtual DIError Open(GenericFD* pGFD, di_off_t offset, bool readOnly) { + if (pGFD == nil) + return kDIErrInvalidArg; + if (!readOnly && pGFD->GetReadOnly()) + return kDIErrAccessDenied; // can't convert to read-write + fpGFD = pGFD; + fOffset = offset; + fReadOnly = readOnly; + Seek(0, kSeekSet); + return kDIErrNone; + } + virtual DIError Read(void* buf, size_t length, + size_t* pActual = nil) + { + return fpGFD->Read(buf, length, pActual); + } + virtual DIError Write(const void* buf, size_t length, + size_t* pActual = nil) + { + return fpGFD->Write(buf, length, pActual); + } + virtual DIError Seek(di_off_t offset, DIWhence whence) { + return fpGFD->Seek(offset + fOffset, whence); + } + virtual di_off_t Tell(void) { + return fpGFD->Tell() -fOffset; + } + virtual DIError Truncate(void) { + return fpGFD->Truncate(); + } + virtual DIError Close(void) { + /* do NOT close underlying descriptor */ + fpGFD = nil; + return kDIErrNone; + } + virtual const char* GetPathName(void) const { return fpGFD->GetPathName(); } + +private: + GenericFD* fpGFD; + di_off_t fOffset; +}; + +}; // namespace DiskImgLib + +#endif /*__GENERIC_FD__*/ diff --git a/diskimg/Global.cpp b/diskimg/Global.cpp new file mode 100644 index 0000000..6654260 --- /dev/null +++ b/diskimg/Global.cpp @@ -0,0 +1,189 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Implementation of DiskImgLib globals. + */ +#include "StdAfx.h" +#include "DiskImgPriv.h" +#include "ASPI.h" + +/*static*/ bool Global::fAppInitCalled = false; + +/*static*/ ASPI* Global::fpASPI = nil; + +/* global constant */ +const char* DiskImgLib::kASPIDev = "ASPI:"; + +/* + * Perform one-time DLL initialization. + */ +/*static*/ DIError +Global::AppInit(void) +{ + NuError nerr; + long major, minor, bug; + + if (fAppInitCalled) { + WMSG0("DiskImg AppInit already called\n"); + return kDIErrNone; + } + + WMSG3("AppInit for DiskImg library v%d.%d.%d\n", + kDiskImgVersionMajor, kDiskImgVersionMinor, kDiskImgVersionBug); + +#ifdef _WIN32 + HMODULE hModule; + char fileNameBuf[256]; + hModule = ::GetModuleHandle("DiskImg4.dll"); + if (hModule != nil && + ::GetModuleFileName(hModule, fileNameBuf, sizeof(fileNameBuf)) != 0) + { + // GetModuleHandle does not increase ref count, so no need to release + WMSG1("DiskImg DLL loaded from '%s'\n", fileNameBuf); + } else { + WMSG0("Unable to get DiskImg DLL filename\n"); + } +#endif + + /* + * Make sure we're linked against a compatible version of NufxLib. + */ + nerr = NuGetVersion(&major, &minor, &bug, NULL, NULL); + if (nerr != kNuErrNone) { + WMSG0("Unable to get version number from NufxLib."); + return kDIErrNufxLibInitFailed; + } + + if (major != kNuVersionMajor || minor < kNuVersionMinor) { + WMSG3("Unexpected NufxLib version %ld.%ld.%ld\n", + major, minor, bug); + return kDIErrNufxLibInitFailed; + } + + /* + * Do one-time init over in the DiskImg class. + */ + DiskImg::CalcNibbleInvTables(); + +#ifdef HAVE_WINDOWS_CDROM + if (kAlwaysTryASPI || IsWin9x()) { + fpASPI = new ASPI; + if (fpASPI->Init() != kDIErrNone) { + delete fpASPI; + fpASPI = nil; + } + } +#endif + WMSG2("DiskImg HasSPTI=%d HasASPI=%d\n", GetHasSPTI(), GetHasASPI()); + + fAppInitCalled = true; + + return kDIErrNone; +} + +/* + * Perform cleanup at application shutdown time. + */ +/*static*/ DIError +Global::AppCleanup(void) +{ + WMSG0("DiskImgLib cleanup\n"); + delete fpASPI; + return kDIErrNone; +} + +/* + * Simple getters. + * + * SPTI is enabled if we're in Win2K *and* ASPI isn't loaded. If ASPI is + * loaded, it can interfere with SPTI, so we want to stick with one or + * the other. + */ +#ifdef _WIN32 +/*static*/ bool Global::GetHasSPTI(void) { return !IsWin9x() && fpASPI == nil; } +/*static*/ bool Global::GetHasASPI(void) { return fpASPI != nil; } +/*static*/ unsigned long Global::GetASPIVersion(void) { + assert(fpASPI != nil); + return fpASPI->GetVersion(); +} +#else +/*static*/ bool Global::GetHasSPTI(void) { return false; } +/*static*/ bool Global::GetHasASPI(void) { return false; } +/*static*/ unsigned long Global::GetASPIVersion(void) { assert(false); return 0; } +#endif + + +/* + * Return current library versions. + */ +/*static*/ void +Global::GetVersion(long* pMajor, long* pMinor, long* pBug) +{ + if (pMajor != nil) + *pMajor = kDiskImgVersionMajor; + if (pMinor != nil) + *pMinor = kDiskImgVersionMinor; + if (pBug != nil) + *pBug = kDiskImgVersionBug; +} + + +/* + * Pointer to debug message handler function. + */ +/*static*/ Global::DebugMsgHandler Global::gDebugMsgHandler = nil; + +/* + * Change the debug message handler. The previous handler is returned. + */ +Global::DebugMsgHandler +Global::SetDebugMsgHandler(DebugMsgHandler handler) +{ + DebugMsgHandler oldHandler; + + oldHandler = gDebugMsgHandler; + gDebugMsgHandler = handler; + return oldHandler; +} + +/* + * Send a debug message to the debug message handler. + * + * Even if _DEBUG_MSGS is disabled we can still get here from the NuFX error + * handler. + */ +/*static*/ void +Global::PrintDebugMsg(const char* file, int line, const char* fmt, ...) +{ + if (gDebugMsgHandler == nil) { + /* + * This can happen if the app decides to bail with an exit() + * call. I'm not sure what's zapping the pointer. + * + * We get here on "-install" or "-uninstall", which really + * should be using a more Windows-friendly exit strategy. + */ + DebugBreak(); + return; + } + + char buf[512]; + va_list args; + + va_start(args, fmt); +#if defined(HAVE_VSNPRINTF) + (void) vsnprintf(buf, sizeof(buf), fmt, args); +#elif defined(HAVE__VSNPRINTF) + (void) _vsnprintf(buf, sizeof(buf), fmt, args); +#else +# error "hosed" +#endif + va_end(args); + + buf[sizeof(buf)-1] = '\0'; + + (*gDebugMsgHandler)(file, line, buf); +} diff --git a/diskimg/HFS.cpp b/diskimg/HFS.cpp new file mode 100644 index 0000000..312506c --- /dev/null +++ b/diskimg/HFS.cpp @@ -0,0 +1,2348 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Implementation of the Macintosh HFS filesystem. + * + * Most of the stuff lives in libhfs. To avoid problems that could arise + * from people ejecting floppies or trying to use a disk image while + * CiderPress is still open, we call hfs_flush() to force updates to be + * written. (Even with the "no caching" flag set, the master dir block and + * volume bitmap aren't written until flush is called.) + * + * The libhfs code is licensed under the full GPL, making it awkward to + * use in a commercial product. Support for libhfs can be removed with + * the EXCISE_GPL_CODE ifdefs. A stub will remain that can recognize HFS + * volumes, which is useful when dealing with Apple II hard drive and CFFA + * images. + */ +#include "StdAfx.h" +#include "DiskImgPriv.h" + + +/* + * =========================================================================== + * DiskFSHFS + * =========================================================================== + */ + +const int kBlkSize = 512; +const int kMasterDirBlock = 2; // also a copy in next-to-last block +const unsigned short kSignature = 0x4244; // or 0xd2d7 for MFS +const int kMaxDirectoryDepth = 128; // not sure what HFS limit is + +//namespace DiskImgLib { + +/* extent descriptor */ +typedef struct ExtDescriptor { + unsigned short xdrStABN; // first allocation block + unsigned short xdrNumABlks; // #of allocation blocks +} ExtDescriptor; +/* extent data record */ +typedef struct ExtDataRec { + ExtDescriptor extDescriptor[3]; +} ExtDataRec; + +/* + * Contents of the HFS MDB. Information comes from "Inside Macintosh: Files", + * chapter 2 ("Data Organization on Volumes"), pages 2-60 to 2-62. + */ +typedef struct DiskFSHFS::MasterDirBlock { + unsigned short drSigWord; // volume signature + unsigned long drCrDate; // date/time of volume creation + unsigned long drLsMod; // date/time of last modification + unsigned short drAtrb; // volume attributes + unsigned short drNmPls; // #of files in root directory + unsigned short drVBMSt; // first block of volume bitmap + unsigned short drAllocPtr; // start of next allocation search + unsigned short drNmAlBlks; // number of allocation blocks in volume + unsigned long drAlBlkSiz; // size (bytes) of allocation blocks + unsigned long drClpSiz; // default clump size + unsigned short drAlBlSt; // first allocation block in volume + unsigned long drNxtCNID; // next unused catalog node ID + unsigned short drFreeBks; // number of unused allocation blocks + unsigned char drVN[28]; // volume name (pascal string) + unsigned long drVolBkUp; // date/time of last backup + unsigned short drVSeqNum; // volume backup sequence number + unsigned long drWrCnt; // volume write count + unsigned long drXTClpSiz; // clump size for extents overflow file + unsigned long drCTClpSiz; // clump size for catalog file + unsigned short drNmRtDirs; // #of directories in root directory + unsigned long drFilCnt; // #of files in volume + unsigned long drDirCnt; // #of directories in volume + unsigned long drFndrInfo[8]; // information used by the Finder + unsigned short drVCSize; // size (blocks) of volume cache + unsigned short drVBMCSize; // size (blocks) of volume bitmap cache + unsigned short drCtlCSize; // size (blocks) of common volume cache + unsigned long drXTFlSize; // size (bytes) of extents overflow file + ExtDataRec drXTExtRec; // extent record for extents overflow file + unsigned long drCTFlSize; // size (bytes) of catalog file + ExtDataRec drCTExtRec; // extent record for catalog file +} MasterDirBlock; + +//}; // namespace DiskImgLib + +/* + * Extract fields from a Master Directory Block. + */ +/*static*/ void +DiskFSHFS::UnpackMDB(const unsigned char* buf, MasterDirBlock* pMDB) +{ + pMDB->drSigWord = GetShortBE(&buf[0x00]); + pMDB->drCrDate = GetLongBE(&buf[0x02]); + pMDB->drLsMod = GetLongBE(&buf[0x06]); + pMDB->drAtrb = GetShortBE(&buf[0x0a]); + pMDB->drNmPls = GetShortBE(&buf[0x0c]); + pMDB->drVBMSt = GetShortBE(&buf[0x0e]); + pMDB->drAllocPtr = GetShortBE(&buf[0x10]); + pMDB->drNmAlBlks = GetShortBE(&buf[0x12]); + pMDB->drAlBlkSiz = GetLongBE(&buf[0x14]); + pMDB->drClpSiz = GetLongBE(&buf[0x18]); + pMDB->drAlBlSt = GetShortBE(&buf[0x1c]); + pMDB->drNxtCNID = GetLongBE(&buf[0x1e]); + pMDB->drFreeBks = GetShortBE(&buf[0x22]); + memcpy(pMDB->drVN, &buf[0x24], sizeof(pMDB->drVN)); + pMDB->drVolBkUp = GetLongBE(&buf[0x40]); + pMDB->drVSeqNum = GetShortBE(&buf[0x44]); + pMDB->drWrCnt = GetLongBE(&buf[0x46]); + pMDB->drXTClpSiz = GetLongBE(&buf[0x4a]); + pMDB->drCTClpSiz = GetLongBE(&buf[0x4e]); + pMDB->drNmRtDirs = GetShortBE(&buf[0x52]); + pMDB->drFilCnt = GetLongBE(&buf[0x54]); + pMDB->drDirCnt = GetLongBE(&buf[0x58]); + for (int i = 0; i < (int) NELEM(pMDB->drFndrInfo); i++) + pMDB->drFndrInfo[i] = GetLongBE(&buf[0x5c + i * 4]); + pMDB->drVCSize = GetShortBE(&buf[0x7c]); + pMDB->drVBMCSize = GetShortBE(&buf[0x7e]); + pMDB->drCtlCSize = GetShortBE(&buf[0x80]); + pMDB->drXTFlSize = GetLongBE(&buf[0x82]); + //UnpackExtDataRec(&pMDB->drXTExtRec, &buf[0x86]); // 12 bytes + pMDB->drCTFlSize = GetLongBE(&buf[0x92]); + //UnpackExtDataRec(&pMDB->drXTExtRec, &buf[0x96]); + // next field at 0xa2 +} + +/* + * See if this looks like an HFS volume. + * + * We test a few fields in the master directory block for validity. + */ +/*static*/ DIError +DiskFSHFS::TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder) +{ + DIError dierr = kDIErrNone; + MasterDirBlock mdb; + unsigned char blkBuf[kBlkSize]; + + dierr = pImg->ReadBlockSwapped(kMasterDirBlock, blkBuf, imageOrder, + DiskImg::kSectorOrderProDOS); + if (dierr != kDIErrNone) + goto bail; + + UnpackMDB(blkBuf, &mdb); + + if (mdb.drSigWord != kSignature) { + dierr = kDIErrFilesystemNotFound; + goto bail; + } + if ((mdb.drAlBlkSiz & 0x1ff) != 0) { + // allocation block size must be a multiple of 512 + WMSG1(" HFS: found allocation block size = %lu, rejecting\n", + mdb.drAlBlkSiz); + dierr = kDIErrFilesystemNotFound; + goto bail; + } + if (mdb.drVN[0] == 0 || mdb.drVN[0] > kMaxVolumeName) { + WMSG1(" HFS: volume name has len = %d, rejecting\n", mdb.drVN[0]); + dierr = kDIErrFilesystemNotFound; + goto bail; + } + + long minBlocks; + minBlocks = mdb.drNmAlBlks * (mdb.drAlBlkSiz / kBlkSize) + mdb.drAlBlSt + 2; + if (minBlocks > pImg->GetNumBlocks()) { + // We're probably trying to open a 1GB volume as if it were only + // 32MB. Maybe this is a full HFS partition and we're trying to + // see if it's a CFFA image. Whatever the case, we can't do this. + WMSG2("HFS: volume exceeds disk image size (%ld vs %ld)\n", + minBlocks, pImg->GetNumBlocks()); + dierr = kDIErrFilesystemNotFound; + goto bail; + } + + // looks good! + +bail: + return dierr; +} + +/* + * Test to see if the image is an HFS disk. + */ +/*static*/ DIError +DiskFSHFS::TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency) +{ + //return kDIErrFilesystemNotFound; // DEBUG DEBUG DEBUG + + /* must be block format, should be at least 720K */ + if (!pImg->GetHasBlocks() || pImg->GetNumBlocks() < kExpectedMinBlocks) + return kDIErrFilesystemNotFound; + + DiskImg::SectorOrder ordering[DiskImg::kSectorOrderMax]; + + DiskImg::GetSectorOrderArray(ordering, *pOrder); + + for (int i = 0; i < DiskImg::kSectorOrderMax; i++) { + if (ordering[i] == DiskImg::kSectorOrderUnknown) + continue; + if (TestImage(pImg, ordering[i]) == kDIErrNone) { + *pOrder = ordering[i]; + *pFormat = DiskImg::kFormatMacHFS; + return kDIErrNone; + } + } + + WMSG0(" HFS didn't find valid FS\n"); + return kDIErrFilesystemNotFound; +} + + +/* + * Load some stuff from the volume header. + */ +DIError +DiskFSHFS::LoadVolHeader(void) +{ + DIError dierr = kDIErrNone; + MasterDirBlock mdb; + unsigned char blkBuf[kBlkSize]; + + if (fLocalTimeOffset == -1) { + struct tm* ptm; + struct tm tmWhen; + time_t when; + int isDst; + + when = time(nil); + isDst = localtime(&when)->tm_isdst; + + ptm = gmtime(&when); + if (ptm != nil) { + tmWhen = *ptm; // make a copy -- static buffers in time functions + tmWhen.tm_isdst = isDst; + + fLocalTimeOffset = when - mktime(&tmWhen); + } else + fLocalTimeOffset = 0; + + WMSG1(" HFS computed local time offset = %.3f hours\n", + fLocalTimeOffset / 3600.0); + } + + dierr = fpImg->ReadBlock(kMasterDirBlock, blkBuf); + if (dierr != kDIErrNone) + goto bail; + + UnpackMDB(blkBuf, &mdb); + + /* + * The minimum size of the volume is "number of allocation blocks" plus + * "first allocation block" (to avoid the OS overhead) plus 2 (because + * there's a backup copy of the MDB in the next-to-last block, and + * nothing at all in the very last block). + * + * This isn't the total size, because on larger volumes there can be + * some padding between the last usable block and the backup MDB. The + * only way to find the MDB is to take the DiskImg's block size and + * subtract 2. + */ + assert((mdb.drAlBlkSiz % kBlkSize) == 0); + fNumAllocationBlocks = mdb.drNmAlBlks; + fAllocationBlockSize = mdb.drAlBlkSiz; + fTotalBlocks = fpImg->GetNumBlocks(); + + unsigned long minBlocks; + minBlocks = mdb.drNmAlBlks * (mdb.drAlBlkSiz / kBlkSize) + mdb.drAlBlSt + 2; + assert(fTotalBlocks >= minBlocks); // verified during fs tests + + int volNameLen; + volNameLen = mdb.drVN[0]; + if (volNameLen > kMaxVolumeName) { + assert(false); // should've been trapped earlier + volNameLen = kMaxVolumeName; + } + memcpy(fVolumeName, &mdb.drVN[1], volNameLen); + fVolumeName[volNameLen] = '\0'; + SetVolumeID(); + + fNumFiles = mdb.drFilCnt; + fNumDirectories = mdb.drDirCnt; + fCreatedDateTime = mdb.drCrDate; + fModifiedDateTime = mdb.drLsMod; + + /* + * Create a "magic" directory entry for the volume directory. This + * must come first in the file list. + */ + A2FileHFS* pFile; + pFile = new A2FileHFS(this); + if (pFile == nil) { + dierr = kDIErrMalloc; + goto bail; + } + + pFile->fIsDir = true; + pFile->fIsVolumeDir = true; + pFile->fType = 0; + pFile->fCreator = 0; + strcpy(pFile->fFileName, fVolumeName); // vol names are shorter than + pFile->SetPathName(":", fVolumeName); // filenames, so it fits + pFile->fDataLength = 0; + pFile->fRsrcLength = -1; + pFile->fCreateWhen = + (time_t) (fCreatedDateTime - kDateTimeOffset) - fLocalTimeOffset; + pFile->fModWhen = + (time_t) (fModifiedDateTime - kDateTimeOffset) - fLocalTimeOffset; + pFile->fAccess = DiskFS::kFileAccessUnlocked; + + //WMSG2("GOT *** '%s' '%s'\n", pFile->fFileName, pFile->fPathName); + + AddFileToList(pFile); + +bail: + return dierr; +} + +/* + * Set the volume ID based on fVolumeName. + */ +void +DiskFSHFS::SetVolumeID(void) +{ + strcpy(fVolumeID, "HFS "); + strcat(fVolumeID, fVolumeName); +} + +/* + * Blank out the volume usage map. The HFS volume bitmap is not yet supported. + */ +void +DiskFSHFS::SetVolumeUsageMap(void) +{ + VolumeUsage::ChunkState cstate; + long block; + + fVolumeUsage.Create(fpImg->GetNumBlocks()); + + cstate.isUsed = true; + cstate.isMarkedUsed = true; + cstate.purpose = VolumeUsage::kChunkPurposeUnknown; + + for (block = fTotalBlocks-1; block >= 0; block--) + fVolumeUsage.SetChunkState(block, &cstate); +} + + +/* + * Print some interesting fields to the debug log. + */ +void +DiskFSHFS::DumpVolHeader(void) +{ + WMSG0("HFS volume header read:\n"); + WMSG1(" volume name = '%s'\n", fVolumeName); + WMSG4(" total blocks = %ld (allocSize=%ld [x%lu], numAllocs=%lu)\n", + fTotalBlocks, fAllocationBlockSize, fAllocationBlockSize / kBlkSize, + fNumAllocationBlocks); + WMSG2(" num directories=%ld, num files=%ld\n", + fNumDirectories, fNumFiles); + time_t when; + when = (time_t) (fCreatedDateTime - kDateTimeOffset - fLocalTimeOffset); + WMSG2(" cre date=0x%08lx %.24s\n", fCreatedDateTime, ctime(&when)); + when = (time_t) (fModifiedDateTime - kDateTimeOffset - fLocalTimeOffset); + WMSG2(" mod date=0x%08lx %.24s\n", fModifiedDateTime, ctime(&when)); +} + + +#ifndef EXCISE_GPL_CODE + +/* + * Get things rolling. + * + * Since we're assured that this is a valid disk, errors encountered from here + * on out must be handled somehow, possibly by claiming that the disk is + * completely full and has no files on it. + */ +DIError +DiskFSHFS::Initialize(InitMode initMode) +{ + DIError dierr = kDIErrNone; + char msg[kMaxVolumeName + 32]; + + dierr = LoadVolHeader(); + if (dierr != kDIErrNone) + goto bail; + DumpVolHeader(); + + if (initMode == kInitHeaderOnly) { + WMSG0(" HFS - headerOnly set, skipping file load\n"); + goto bail; + } + + sprintf(msg, "Scanning %s", fVolumeName); + if (!fpImg->UpdateScanProgress(msg)) { + WMSG0(" HFS cancelled by user\n"); + dierr = kDIErrCancelled; + goto bail; + } + + /* + * Open the volume with libhfs. We used to set HFS_OPT_NOCACHE to avoid + * consistency problems and reduce the risk of disk corruption should + * CiderPress fail, but it turns out libhfs doesn't write the volume + * bitmap or master dir block unless explicitly flushed anyway. Since + * the caching helps us a lot when just reading -- 4 seconds vs. 9 for + * a CD-ROM over gigabit Ethernet -- we leave it on, and explicitly + * flush every time we make a change. + */ + fHfsVol = hfs_callback_open(LibHFSCB, this, /*HFS_OPT_NOCACHE |*/ + (fpImg->GetReadOnly() ? HFS_MODE_RDONLY : HFS_MODE_RDWR)); + if (fHfsVol == nil) { + WMSG1("ERROR: hfs_opencallback failed: %s\n", hfs_error); + return kDIErrGeneric; + } + + /* volume dir is guaranteed to come first; if not, we need a lookup func */ + A2FileHFS* pVolumeDir; + pVolumeDir = (A2FileHFS*) GetNextFile(nil); + + dierr = RecursiveDirAdd(pVolumeDir, ":", 0); + if (dierr != kDIErrNone) + goto bail; + + SetVolumeUsageMap(); + + /* + * Make sure there's nothing lingering. libhfs will fiddle around with + * the MDB if it looks like the volume wasn't unmounted cleanly last time. + */ + hfs_flush(fHfsVol); + +bail: + return dierr; +} + +/* + * Callback function from libhfs. Can read/write/seek. + * + * This is a little clumsy, but it allows us to maintain a separation from + * the libhfs code (which is GPLed). + * + * Returns -1 on failure. + */ +unsigned long +DiskFSHFS::LibHFSCB(void* vThis, int op, unsigned long arg1, void* arg2) +{ + DiskFSHFS* pThis = (DiskFSHFS*) vThis; + unsigned long result = (unsigned long) -1; + + assert(pThis != nil); + + switch (op) { + case HFS_CB_VOLSIZE: + //WMSG1(" HFSCB vol size = %ld blocks\n", pThis->fTotalBlocks); + result = pThis->fTotalBlocks; + break; + case HFS_CB_READ: // arg1=block, arg2=buffer + //WMSG1(" HFSCB read block %lu\n", arg1); + if (arg1 < pThis->fTotalBlocks && arg2 != nil) { + DIError err = pThis->fpImg->ReadBlock(arg1, arg2); + if (err == kDIErrNone) + result = 0; + else { + WMSG1(" HFSCB read %lu failed\n", arg1); + } + } + break; + case HFS_CB_WRITE: + WMSG1(" HFSCB write block %lu\n", arg1); + if (arg1 < pThis->fTotalBlocks && arg2 != nil) { + DIError err = pThis->fpImg->WriteBlock(arg1, arg2); + if (err == kDIErrNone) + result = 0; + else { + WMSG1(" HFSCB write %lu failed\n", arg1); + } + } + break; + case HFS_CB_SEEK: // arg1=block, arg2=unused + /* just verify that the seek is legal */ + //WMSG1(" HFSCB seek block %lu\n", arg1); + if (arg1 < pThis->fTotalBlocks) + result = arg1; + break; + default: + assert(false); + } + + //WMSG1("--- HFSCB returning %lu\n", result); + return result; +} + + +/* + * Determine the amount of free space on the disk. + */ +DIError +DiskFSHFS::GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const +{ + assert(fHfsVol != nil); + + hfsvolent volEnt; + if (hfs_vstat(fHfsVol, &volEnt) != 0) + return kDIErrGeneric; + + *pTotalUnits = volEnt.totbytes / 512; + *pFreeUnits = volEnt.freebytes / 512; + *pUnitSize = 512; + + return kDIErrNone; +} + +/* + * Recursively traverse the filesystem. + */ +DIError +DiskFSHFS::RecursiveDirAdd(A2File* pParent, const char* basePath, int depth) +{ + DIError dierr = kDIErrNone; + hfsdir* dir; + hfsdirent dirEntry; + char* pathBuf = nil; + int nameOffset; + + /* if we get too deep, assume it's a loop */ + if (depth > kMaxDirectoryDepth) { + dierr = kDIErrDirectoryLoop; + goto bail; + } + + //WMSG1(" HFS RecursiveDirAdd '%s'\n", basePath); + dir = hfs_opendir(fHfsVol, basePath); + if (dir == nil) { + printf(" HFS unable to open dir '%s'\n", basePath); + WMSG1(" HFS unable to open dir '%s'\n", basePath); + dierr = kDIErrGeneric; + goto bail; + } + + if (strcmp(basePath, ":") == 0) + basePath = ""; + + nameOffset = strlen(basePath) +1; + pathBuf = new char[nameOffset + A2FileHFS::kMaxFileName +1]; + if (pathBuf == nil) { + dierr = kDIErrMalloc; + goto bail; + } + strcpy(pathBuf, basePath); + pathBuf[nameOffset-1] = A2FileHFS::kFssep; + pathBuf[nameOffset] = '\0'; // not needed + + while (hfs_readdir(dir, &dirEntry) != -1) { + A2FileHFS* pFile; + + pFile = new A2FileHFS(this); + + pFile->InitEntry(&dirEntry); + + pFile->SetPathName(basePath, pFile->fFileName); + pFile->SetParent(pParent); + AddFileToList(pFile); + + if (!fpImg->UpdateScanProgress(nil)) { + WMSG0(" HFS cancelled by user\n"); + dierr = kDIErrCancelled; + goto bail; + } + + if (dirEntry.flags & HFS_ISDIR) { + strcpy(pathBuf + nameOffset, dirEntry.name); + dierr = RecursiveDirAdd(pFile, pathBuf, depth+1); + if (dierr != kDIErrNone) + goto bail; + } + } + +bail: + delete[] pathBuf; + return dierr; +} + +/* + * Initialize an A2FileHFS structure from the stuff in an hfsdirent. + */ +void A2FileHFS::InitEntry(const hfsdirent* dirEntry) +{ + //printf("--- File '%s' flags=0x%08x fdflags=0x%08x type='%s'\n", + // dirEntry.name, dirEntry.flags, dirEntry.fdflags, + // dirEntry.u.file.type); + + fIsVolumeDir = false; + memcpy(fFileName, dirEntry->name, A2FileHFS::kMaxFileName+1); + fFileName[A2FileHFS::kMaxFileName] = '\0'; // make sure + + if (dirEntry->flags & HFS_ISLOCKED) + fAccess = DiskFS::kFileAccessLocked; + else + fAccess = DiskFS::kFileAccessUnlocked; + if (dirEntry->fdflags & HFS_FNDR_ISINVISIBLE) + fAccess |= A2FileProDOS::kAccessInvisible; + + if (dirEntry->flags & HFS_ISDIR) { + fIsDir = true; + fType = fCreator = 0; + fDataLength = 0; + fRsrcLength = -1; + } else { + unsigned char* pType; + + fIsDir = false; + + pType = (unsigned char*) dirEntry->u.file.type; + fType = + pType[0] << 24 | pType[1] << 16 | pType[2] << 8 | pType[3]; + pType = (unsigned char*) dirEntry->u.file.creator; + fCreator = + pType[0] << 24 | pType[1] << 16 | pType[2] << 8 | pType[3]; + fDataLength = dirEntry->u.file.dsize; + fRsrcLength = dirEntry->u.file.rsize; + + /* + * Resource fork must be at least 512 bytes for Finder, so if + * it has zero length then the file must not have one. + */ + if (fRsrcLength == 0) + fRsrcLength = -1; + } + + /* + * Create/modified dates (we ignore the "last backup" date). The + * hfslib functions convert to time_t for us. + */ + fCreateWhen = dirEntry->crdate; + fModWhen = dirEntry->mddate; +} + +/* + * Return "true" if "name" is valid for use as an HFS volume name. + */ +/*static*/ bool +DiskFSHFS::IsValidVolumeName(const char* name) +{ + if (name == nil) + return false; + + int len = strlen(name); + if (len < 1 || len > kMaxVolumeName) + return false; + + while (*name != '\0') { + if (*name == A2FileHFS::kFssep) + return false; + name++; + } + + return true; +} + +/* + * Return "true" if "name" is valid for use as an HFS file name. + */ +/*static*/ bool +DiskFSHFS::IsValidFileName(const char* name) +{ + if (name == nil) + return false; + + int len = strlen(name); + if (len < 1 || len > A2FileHFS::kMaxFileName) + return false; + + while (*name != '\0') { + if (*name == A2FileHFS::kFssep) + return false; + name++; + } + + return true; +} + +/* + * Format the current volume with HFS. + */ +DIError +DiskFSHFS::Format(DiskImg* pDiskImg, const char* volName) +{ + assert(strlen(volName) > 0 && strlen(volName) <= kMaxVolumeName); + + if (!IsValidVolumeName(volName)) + return kDIErrInvalidArg; + + /* set fpImg so calls that rely on it will work; we un-set it later */ + assert(fpImg == nil); + SetDiskImg(pDiskImg); + + /* need this for callback function */ + fTotalBlocks = fpImg->GetNumBlocks(); + + // need HFS_OPT_2048 for CD-ROM? + if (hfs_callback_format(LibHFSCB, this, 0, volName) != 0) { + WMSG1("hfs_callback_format failed (%s)\n", hfs_error); + return kDIErrGeneric; + } + + // no need to flush; HFS volume is closed + + SetDiskImg(nil); // shouldn't really be set by us + return kDIErrNone; +} + + +/* + * Normalize an HFS path. Invokes DoNormalizePath and handles the buffer + * management (if the normalized path doesn't fit in "*pNormalizedBufLen" + * bytes, we set "*pNormalizedBufLen to the required length). + * + * This is invoked from the generalized "add" function in CiderPress, which + * doesn't want to understand the ins and outs of pathnames. + */ +DIError +DiskFSHFS::NormalizePath(const char* path, char fssep, + char* normalizedBuf, int* pNormalizedBufLen) +{ + DIError dierr = kDIErrNone; + char* normalizedPath = nil; + int len; + + assert(pNormalizedBufLen != nil); + assert(normalizedBuf != nil || *pNormalizedBufLen == 0); + + dierr = DoNormalizePath(path, fssep, &normalizedPath); + if (dierr != kDIErrNone) + goto bail; + + assert(normalizedPath != nil); + len = strlen(normalizedPath); + if (normalizedBuf == nil || *pNormalizedBufLen <= len) { + /* too short */ + dierr = kDIErrDataOverrun; + } else { + /* fits */ + strcpy(normalizedBuf, normalizedPath); + } + + *pNormalizedBufLen = len+1; // alloc room for the '\0' + +bail: + delete[] normalizedPath; + return dierr; +} + +/* + * Normalize an HFS path. This requires separating each path component + * out, making it HFS-compliant, and then putting it back in. + * The fssep could be anything, so we need to change it to kFssep. + * + * The caller must delete[] "*pNormalizedPath". + */ +DIError +DiskFSHFS::DoNormalizePath(const char* path, char fssep, + char** pNormalizedPath) +{ + DIError dierr = kDIErrNone; + char* workBuf = nil; + char* partBuf = nil; + char* outputBuf = nil; + char* start; + char* end; + char* outPtr; + + assert(path != nil); + workBuf = new char[strlen(path)+1]; + partBuf = new char[strlen(path)+1 +1]; // need +1 for prepending letter + outputBuf = new char[strlen(path) * 2]; + if (workBuf == nil || partBuf == nil || outputBuf == nil) { + dierr = kDIErrMalloc; + goto bail; + } + + strcpy(workBuf, path); + outputBuf[0] = '\0'; + + outPtr = outputBuf; + start = workBuf; + while (*start != '\0') { + //char* origStart = start; // need for debug msg + int partIdx; + + if (fssep == '\0') { + end = nil; + } else { + end = strchr(start, fssep); + if (end != nil) + *end = '\0'; + } + partIdx = 0; + + /* + * Copy, converting colons to underscores. We should strip out any + * illegal characters here, but there's not much in HFS that's + * considered illegal. + */ + while (*start != '\0') { + if (*start == A2FileHFS::kFssep) + partBuf[partIdx++] = '_'; + else + partBuf[partIdx++] = *start; + start++; + } + + /* + * Truncate at 31 chars, preserving anything that looks like a + * filename extension. "partIdx" represents the length of the + * string at this point. "partBuf" holds the string, which we + * want to null-terminate before proceeding. + * + * Try to keep the filename extension, if any. + */ + partBuf[partIdx] = '\0'; + if (partIdx > A2FileHFS::kMaxFileName) { + const char* pDot = strrchr(partBuf, '.'); + //int DEBUGDOTLEN = pDot - partBuf; + if (pDot != nil && partIdx - (pDot-partBuf) <= kMaxExtensionLen) { + int dotLen = partIdx - (pDot-partBuf); + memmove(partBuf + (A2FileProDOS::kMaxFileName - dotLen), + pDot, dotLen); // don't use memcpy, move might overlap + } + partIdx = A2FileProDOS::kMaxFileName; + } + partBuf[partIdx] = '\0'; + + //WMSG2(" HFS Converted component '%s' to '%s'\n", + // origStart, partBuf); + + if (outPtr != outputBuf) + *outPtr++ = A2FileHFS::kFssep; + strcpy(outPtr, partBuf); + outPtr += partIdx; + + /* + * Continue with next segment. + */ + if (end == nil) + break; + start = end+1; + } + + *outPtr = '\0'; + + WMSG3(" HFS Converted path '%s' to '%s' (fssep='%c')\n", + path, outputBuf, fssep); + assert(*outputBuf != '\0'); + + *pNormalizedPath = outputBuf; + outputBuf = nil; + +bail: + delete[] workBuf; + delete[] partBuf; + delete[] outputBuf; + return dierr; +} + +/* + * Compare two Macintosh filename strings. + * + * This requires some effort because the Macintosh Roman character set + * doesn't sort the same way that ASCII does. HFS is case-insensitive but + * case-preserving, so we need to deal with that too. The hfs_charorder + * table takes care of it. + * + * Returns <0, ==0, or >0 depending on whether sstr1 is lexically less than, + * equal to, or greater than sstr2. + */ +/*static*/ int +DiskFSHFS::CompareMacFileNames(const char* sstr1, const char* sstr2) +{ + const unsigned char* str1 = (const unsigned char*) sstr1; + const unsigned char* str2 = (const unsigned char*) sstr2; + int diff; + + while (*str1 && *str2) { + diff = hfs_charorder[*str1] - hfs_charorder[*str2]; + + if (diff != 0) + return diff; + + str1++; + str2++; + } + + return *str1 - *str2; +} + +/* + * Keep tweaking the filename until it no longer matches an existing file. + * The first time this is called we don't know if the name is unique or not, + * so we need to start by checking that. + * + * We have our choice between the DiskFS GetFileByName(), which traverses + * a linear list, and hfs_stat(), which uses more efficient data structures + * but may require disk reads. We use the DiskFS interface, on the assumption + * that someday we'll switch the linear list to a tree structure. + */ +DIError +DiskFSHFS::MakeFileNameUnique(const char* pathName, char** pUniqueName) +{ + A2File* pFile; + const int kMaxExtra = 3; + const int kMaxDigits = 999; + char* uniqueName; + char* fileName; // points inside uniqueName + + assert(pathName != nil); + assert(pathName[0] == A2FileHFS::kFssep); + + /* see if it exists */ + pFile = GetFileByName(pathName+1); + if (pFile == nil) { + *pUniqueName = nil; + return kDIErrNone; + } + + /* make a copy we can chew on */ + uniqueName = new char[strlen(pathName) + kMaxExtra +1]; + strcpy(uniqueName, pathName); + + fileName = strrchr(uniqueName, A2FileHFS::kFssep); + assert(fileName != nil); + fileName++; + + int nameLen = strlen(fileName); + int dotOffset=0, dotLen=0; + char dotBuf[kMaxExtensionLen+1]; + + /* ensure the result will be null-terminated */ + memset(fileName + nameLen, 0, kMaxExtra+1); + + /* + * If this has what looks like a filename extension, grab it. We want + * to preserve ".gif", ".c", etc., since the filetypes don't necessarily + * do everything we need. + */ + const char* cp = strrchr(fileName, '.'); + if (cp != nil) { + int tmpOffset = cp - fileName; + if (tmpOffset > 0 && nameLen - tmpOffset <= kMaxExtensionLen) { + WMSG1(" HFS (keeping extension '%s')\n", cp); + assert(strlen(cp) <= kMaxExtensionLen); + strcpy(dotBuf, cp); + dotOffset = tmpOffset; + dotLen = nameLen - dotOffset; + } + } + + int digits = 0; + int digitLen; + int copyOffset; + char digitBuf[kMaxExtra+1]; + do { + if (digits == kMaxDigits) + return kDIErrFileExists; + digits++; + + /* not the most efficient way to do this, but it'll do */ + sprintf(digitBuf, "%d", digits); + digitLen = strlen(digitBuf); + if (nameLen + digitLen > A2FileHFS::kMaxFileName) + copyOffset = A2FileHFS::kMaxFileName - dotLen - digitLen; + else + copyOffset = nameLen - dotLen; + memcpy(fileName + copyOffset, digitBuf, digitLen); + if (dotLen != 0) + memcpy(fileName + copyOffset + digitLen, dotBuf, dotLen); + } while (GetFileByName(uniqueName+1) != nil); + + WMSG1(" HFS converted to unique name: %s\n", uniqueName); + + *pUniqueName = uniqueName; + return kDIErrNone; +} + + +/* + * Create a new file or directory. Automatically creates the base path + * if necessary. + * + * NOTE: much of this was cloned out of the ProDOS code. We probably want + * a stronger set of utility functions in the parent class now that we have + * more than one hierarchical file system. + */ +DIError +DiskFSHFS::CreateFile(const CreateParms* pParms, A2File** ppNewFile) +{ + DIError dierr = kDIErrNone; + char typeStr[5], creatorStr[5]; + char* normalizedPath = nil; + char* basePath = nil; + char* fileName = nil; + char* fullPath = nil; + A2FileHFS* pSubdir = nil; + A2FileHFS* pNewFile = nil; + hfsfile* pHfsFile = nil; + const bool createUnique = (GetParameter(kParm_CreateUnique) != 0); + + assert(fHfsVol != nil); + + if (fpImg->GetReadOnly()) + return kDIErrAccessDenied; + + assert(pParms != nil); + assert(pParms->pathName != nil); + assert(pParms->storageType == A2FileProDOS::kStorageSeedling || + pParms->storageType == A2FileProDOS::kStorageExtended || + pParms->storageType == A2FileProDOS::kStorageDirectory); + // kStorageVolumeDirHeader not allowed -- that's created by Format + WMSG1(" HFS ---v--- CreateFile '%s'\n", pParms->pathName); + + /* + * Normalize the pathname so that all components are HFS-safe + * and separated by ':'. + * + * This must not "sanitize" the path. We need to be working with the + * original characters, not the sanitized-for-display versions. + */ + assert(pParms->pathName != nil); + dierr = DoNormalizePath(pParms->pathName, pParms->fssep, + &normalizedPath); + if (dierr != kDIErrNone) + goto bail; + assert(normalizedPath != nil); + + /* + * The normalized path lacks a leading ':', and might need to + * have some digits added to make the name unique. + */ + fullPath = new char[strlen(normalizedPath)+2]; + fullPath[0] = A2FileHFS::kFssep; + strcpy(fullPath+1, normalizedPath); + delete[] normalizedPath; + normalizedPath = nil; + + /* + * Make the name unique within the current directory. This requires + * appending digits until the name doesn't match any others. + */ + if (createUnique && + pParms->storageType != A2FileProDOS::kStorageDirectory) + { + char* uniquePath; + + dierr = MakeFileNameUnique(fullPath, &uniquePath); + if (dierr != kDIErrNone) + goto bail; + if (uniquePath != nil) { + delete[] fullPath; + fullPath = uniquePath; + } + } else { + /* can't make unique; check to see if it already exists */ + hfsdirent dirEnt; + if (hfs_stat(fHfsVol, fullPath, &dirEnt) == 0) { + if (pParms->storageType == A2FileProDOS::kStorageDirectory) + dierr = kDIErrDirectoryExists; + else + dierr = kDIErrFileExists; + goto bail; + } + } + + /* + * Split the base path and filename apart. + */ + char* cp; + cp = strrchr(fullPath, A2FileHFS::kFssep); + assert(cp != nil); + if (cp == fullPath) { + assert(basePath == nil); + fileName = new char[strlen(fullPath) +1]; + strcpy(fileName, fullPath); + } else { + int dirNameLen = cp - fullPath; + + fileName = new char[strlen(cp+1) +1]; + strcpy(fileName, cp+1); + basePath = new char[dirNameLen+1]; + strncpy(basePath, fullPath, dirNameLen); + basePath[dirNameLen] = '\0'; + } + + WMSG2("SPLIT: '%s' '%s'\n", basePath, fileName); + + assert(fileName != nil); + + /* + * Open the base path. If it doesn't exist, create it recursively. + */ + if (basePath != nil) { + WMSG2(" HFS Creating '%s' in '%s'\n", fileName, basePath); + /* + * Open the named subdir, creating it if it doesn't exist. We need + * to check basePath+1 because we're comparing against what's in our + * linear file list, and that doesn't include the leading ':'. + */ + pSubdir = (A2FileHFS*)GetFileByName(basePath+1, CompareMacFileNames); + if (pSubdir == nil) { + WMSG1(" HFS Creating subdir '%s'\n", basePath); + A2File* pNewSub; + CreateParms newDirParms; + newDirParms.pathName = basePath; + newDirParms.fssep = A2FileHFS::kFssep; + newDirParms.storageType = A2FileProDOS::kStorageDirectory; + newDirParms.fileType = 0; + newDirParms.auxType = 0; + newDirParms.access = 0; + newDirParms.createWhen = newDirParms.modWhen = time(nil); + dierr = this->CreateFile(&newDirParms, &pNewSub); + if (dierr != kDIErrNone) + goto bail; + assert(pNewSub != nil); + + pSubdir = (A2FileHFS*) pNewSub; + } + + /* + * And now the annoying part. We need to reconstruct basePath out + * of the filenames actually present, rather than relying on the + * argument passed in. That's because HFS is case-insensitive but + * case-preserving. It's not crucial for our inner workings, but the + * linear file list in the DiskFS should have accurate strings. + * (It'll work just fine, but the display might show the wrong values + * for parent directories until they reload the disk.) + * + * On the bright side, we know exactly how long the string needs + * to be, so we can just stomp on it in place. Assuming, of course, + * that the filename created matches up with what the filename + * normalizer came up with, which we can guarantee since (a) everybody + * uses the same normalizer and (b) the "uniqueify" stuff doesn't + * kick in for subdirs because we wouldn't be creating a new subdir + * if it didn't already exist. + * + * This is essentially the same as RegeneratePathName(), but that's + * meant for a situation where the filename already exists. + */ + A2FileHFS* pBaseDir = pSubdir; + int basePathLen = strlen(basePath); + while (!pBaseDir->IsVolumeDirectory()) { + const char* fixedName = pBaseDir->GetFileName(); + int fixedLen = strlen(fixedName); + if (fixedLen > basePathLen) { + assert(false); + break; + } + assert(basePathLen == fixedLen || + *(basePath + (basePathLen-fixedLen-1)) == kDIFssep); + memcpy(basePath + (basePathLen-fixedLen), fixedName, fixedLen); + basePathLen -= fixedLen+1; + + pBaseDir = (A2FileHFS*) pBaseDir->GetParent(); + assert(pBaseDir != nil); + } + // check the math; we should be left with the leading ':' + if (pSubdir->IsVolumeDirectory()) + assert(basePathLen == 1); + else + assert(basePathLen == 0); + } else { + /* open the volume directory */ + WMSG1(" HFS Creating '%s' in volume dir\n", fileName); + /* volume dir must be first in the list */ + pSubdir = (A2FileHFS*) GetNextFile(nil); + assert(pSubdir != nil); + assert(pSubdir->IsVolumeDirectory()); + } + if (pSubdir == nil) { + WMSG1(" HFS Unable to open subdir '%s'\n", basePath); + dierr = kDIErrFileNotFound; + goto bail; + } + + /* + * Figure out file type. + */ + A2FileHFS::ConvertTypeToHFS(pParms->fileType, pParms->auxType, + typeStr, creatorStr); + + /* + * Create the file or directory. Populate "dirEnt" with the details. + */ + hfsdirent dirEnt; + if (pParms->storageType == A2FileProDOS::kStorageDirectory) { + /* create the directory */ + if (hfs_mkdir(fHfsVol, fullPath) != 0) { + WMSG2(" HFS mkdir '%s' failed: %s\n", fullPath, hfs_error); + dierr = kDIErrGeneric; + goto bail; + } + if (hfs_stat(fHfsVol, fullPath, &dirEnt) != 0) { + WMSG1(" HFS stat on new dir failed: %s\n", hfs_error); + dierr = kDIErrGeneric; + goto bail; + } + /* create date *might* be useful, but probably not worth adjusting */ + } else { + /* create, and open, the file */ + pHfsFile = hfs_create(fHfsVol, fullPath, typeStr, creatorStr); + if (pHfsFile == nil) { + WMSG1(" HFS create failed: %s\n", hfs_error); + dierr = kDIErrGeneric; + goto bail; + } + if (hfs_fstat(pHfsFile, &dirEnt) != 0) { + WMSG1(" HFS fstat on new file failed: %s\n", hfs_error); + dierr = kDIErrGeneric; + goto bail; + } + + /* set the attributes according to pParms, and update the file */ + dirEnt.crdate = pParms->createWhen; + dirEnt.mddate = pParms->modWhen; + if (pParms->access & A2FileProDOS::kAccessInvisible) + dirEnt.fdflags |= HFS_FNDR_ISINVISIBLE; + else + dirEnt.fdflags &= ~HFS_FNDR_ISINVISIBLE; + if ((pParms->access & ~A2FileProDOS::kAccessInvisible) == kFileAccessLocked) + dirEnt.flags |= HFS_ISLOCKED; + else + dirEnt.flags &= ~HFS_ISLOCKED; + + (void) hfs_fsetattr(pHfsFile, &dirEnt); + (void) hfs_close(pHfsFile); + pHfsFile = nil; + } + + /* + * Success! + * + * Create a new entry and set the structure fields. + */ + pNewFile = new A2FileHFS(this); + pNewFile->InitEntry(&dirEnt); + pNewFile->SetPathName(basePath == nil ? "" : basePath, pNewFile->fFileName); + pNewFile->SetParent(pSubdir); + + /* + * Because we're hierarchical, and we guarantee that the contents of + * subdirectories are grouped together, we must insert the file into an + * appropriate place in the list rather than just throwing it onto the + * end. + * + * The proper location for the new file in the linear list is in sorted + * order with the files in the current directory. We have to be careful + * here because libhfs is going to use Macintosh Roman sort ordering, + * which may be different from ASCII ordering. Worst case: we end up + * putting it in the wrong place and it jumps around when the disk image + * is reopened. + * + * All files in a subdir appear in the list after that subdir, but there + * might be intervening entries from deeper directories. So we have to + * chase through some or all of the file list to find the right place. + * Not great, but we don't have enough files or do adds often enough to + * make this worth optimizing. + */ + A2File* pLastSubdirFile; + A2File* pPrevFile; + A2File* pNextFile; + + pPrevFile = pLastSubdirFile = pSubdir; + pNextFile = GetNextFile(pPrevFile); + while (pNextFile != nil) { + if (pNextFile->GetParent() == pNewFile->GetParent()) { + /* in same subdir, compare names */ + if (CompareMacFileNames(pNextFile->GetPathName(), + pNewFile->GetPathName()) > 0) + { + /* passed it; insert new after previous file */ + pLastSubdirFile = pPrevFile; + WMSG2(" HFS Found '%s' > cur(%s)\n", pNextFile->GetPathName(), + pNewFile->GetPathName()); + break; + } + + /* still too early; save in case it's last one in dir */ + pLastSubdirFile = pNextFile; + } + pPrevFile = pNextFile; + pNextFile = GetNextFile(pNextFile); + } + + /* insert us after last file we saw that was part of the same subdir */ + WMSG2(" HFS inserting '%s' after '%s'\n", pNewFile->GetPathName(), + pLastSubdirFile->GetPathName()); + InsertFileInList(pNewFile, pLastSubdirFile); + //WMSG0("LIST NOW:\n"); + //DumpFileList(); + + *ppNewFile = pNewFile; + pNewFile = nil; + +bail: + delete pNewFile; + delete[] normalizedPath; + delete[] basePath; + delete[] fileName; + delete[] fullPath; + hfs_flush(fHfsVol); + WMSG1(" HFS ---^--- CreateFile '%s' DONE\n", pParms->pathName); + return dierr; +} + +/* + * Delete the named file. + * + * We need to use a different call for file vs. directory. + */ +DIError +DiskFSHFS::DeleteFile(A2File* pGenericFile) +{ + DIError dierr = kDIErrNone; + char* pathName = nil; + + if (fpImg->GetReadOnly()) + return kDIErrAccessDenied; + if (!fDiskIsGood) + return kDIErrBadDiskImage; + if (pGenericFile->IsFileOpen()) + return kDIErrFileOpen; + + A2FileHFS* pFile = (A2FileHFS*) pGenericFile; + pathName = pFile->GetLibHFSPathName(); + WMSG1(" Deleting '%s'\n", pathName); + + if (pFile->IsDirectory()) { + if (hfs_rmdir(fHfsVol, pathName) != 0) { + WMSG2(" HFS rmdir failed '%s': '%s'\n", pathName, hfs_error); + dierr = kDIErrGeneric; + goto bail; + } + } else { + if (hfs_delete(fHfsVol, pathName) != 0) { + WMSG2(" HFS delete failed '%s': '%s'\n", pathName, hfs_error); + dierr = kDIErrGeneric; + goto bail; + } + } + + /* + * Remove the A2File* from the list. + */ + DeleteFileFromList(pFile); + +bail: + hfs_flush(fHfsVol); + delete[] pathName; + return kDIErrNone; +} + +/* + * Rename a file. + * + * Pass in a pointer to the file and a string with the new filename (just + * the filename, not a pathname -- this function doesn't move files + * between directories). The new name must already be normalized. + * + * Renaming the magic volume directory "file" is not allowed. + * + * We don't try to keep AppleWorks aux type flags consistent (they're used + * to determine which characters are lower case on ProDOS disks). They'll + * get fixed up when we copy them to a ProDOS disk, which is the only way + * 8-bit AppleWorks can get at them. + */ +DIError +DiskFSHFS::RenameFile(A2File* pGenericFile, const char* newName) +{ + DIError dierr = kDIErrNone; + A2FileHFS* pFile = (A2FileHFS*) pGenericFile; + char* colonOldName = nil; + char* colonNewName = nil; + + if (pFile == nil || newName == nil) + return kDIErrInvalidArg; + if (!IsValidFileName(newName)) + return kDIErrInvalidArg; + if (pFile->IsVolumeDirectory()) + return kDIErrInvalidArg; + if (fpImg->GetReadOnly()) + return kDIErrAccessDenied; + if (!fDiskIsGood) + return kDIErrBadDiskImage; + + char* lastColon; + + colonOldName = pFile->GetLibHFSPathName(); // adds ':' to start of string + lastColon = strrchr(colonOldName, A2FileHFS::kFssep); + assert(lastColon != nil); + if (lastColon == colonOldName) { + /* in root dir */ + colonNewName = new char[1 + strlen(newName) +1]; + colonNewName[0] = A2FileHFS::kFssep; + strcpy(colonNewName+1, newName); + } else { + /* prepend subdir */ + int len = lastColon - colonOldName +1; // e.g. ":path1:path2:" + colonNewName = new char[len + strlen(newName) +1]; + strncpy(colonNewName, colonOldName, len); + strcpy(colonNewName+len, newName); + } + + WMSG2(" HFS renaming '%s' to '%s'\n", colonOldName, colonNewName); + + if (hfs_rename(fHfsVol, colonOldName, colonNewName) != 0) { + WMSG3(" HFS rename('%s','%s') failed: %s\n", + colonOldName, colonNewName, hfs_error); + dierr = kDIErrGeneric; + goto bail; + } + + /* + * Success! Update the file name. + */ + strcpy(pFile->fFileName, newName); + + /* + * Now the fun part. If we simply renamed a file, we can just update the + * one entry. If we renamed a directory, life gets interesting because + * we store the full pathname in every A2FileHFS entry. (It's an + * efficiency win most of the time, but it's really annoying here.) + * + * HFS makes this especially unpleasant because it keeps the files + * arranged in sorted order. If we change a file's name, we may have to + * move it to a new position in the linear file list. If we don't, the + * list no longer reflects the order in which the files actually appear + * on the disk, and they'll shift around when we reload. + * + * There are two approaches: re-sort the list (awkward, since it's stored + * in a linked list -- we'd probably want to sort tags in a parallel + * structure), or find the affected block of files, find the new start + * position, and shift the entire range in one shot. + * + * This doesn't seem like something that anybody but me will ever care + * about, so I'm going to skip it for now. + */ + A2File* pCur; + if (pFile->IsDirectory()) { + /* do all files that come after us */ + pCur = pFile; + while (pCur != nil) { + RegeneratePathName((A2FileHFS*) pCur); + pCur = GetNextFile(pCur); + } + } else { + RegeneratePathName(pFile); + } + +bail: + delete[] colonOldName; + delete[] colonNewName; + hfs_flush(fHfsVol); + return kDIErrNone; +} + +/* + * Regenerate fPathName for the specified file. + * + * Has no effect on the magic volume dir entry. + * + * This could be implemented more efficiently, but it's only used when + * renaming files, so there's not much point. + * + * [This was lifted straight out of the ProDOS sources. It should probably + * be moved into generic DiskFS.] + */ +DIError +DiskFSHFS::RegeneratePathName(A2FileHFS* pFile) +{ + A2FileHFS* pParent; + char* buf = nil; + int len; + + /* nothing to do here */ + if (pFile->IsVolumeDirectory()) + return kDIErrNone; + + /* compute the length of the path name */ + len = strlen(pFile->GetFileName()); + pParent = (A2FileHFS*) pFile->GetParent(); + while (!pParent->IsVolumeDirectory()) { + len++; // leave space for the ':' + len += strlen(pParent->GetFileName()); + + pParent = (A2FileHFS*) pParent->GetParent(); + } + + buf = new char[len+1]; + if (buf == nil) + return kDIErrMalloc; + + /* generate the new path name */ + int partLen; + partLen = strlen(pFile->GetFileName()); + strcpy(buf + len - partLen, pFile->GetFileName()); + len -= partLen; + + pParent = (A2FileHFS*) pFile->GetParent(); + while (!pParent->IsVolumeDirectory()) { + assert(len > 0); + buf[--len] = A2FileHFS::kFssep; + + partLen = strlen(pParent->GetFileName()); + strncpy(buf + len - partLen, pParent->GetFileName(), partLen); + len -= partLen; + assert(len >= 0); + + pParent = (A2FileHFS*) pParent->GetParent(); + } + + WMSG2("Replacing '%s' with '%s'\n", pFile->GetPathName(), buf); + pFile->SetPathName("", buf); + delete[] buf; + + return kDIErrNone; +} + +/* + * Change the HFS volume name. + * + * This uses the same libhfs interface that we use for renaming files. The + * Mac convention is to *not* start the volume name with a colon. In fact, + * the libhfs convention is to *end* the volume names with a colon. + */ +DIError +DiskFSHFS::RenameVolume(const char* newName) +{ + DIError dierr = kDIErrNone; + A2FileHFS* pFile; + char* oldNameColon = nil; + char* newNameColon = nil; + + if (!IsValidVolumeName(newName)) + return kDIErrInvalidArg; + if (fpImg->GetReadOnly()) + return kDIErrAccessDenied; + + /* get file list entry for volume name */ + pFile = (A2FileHFS*) GetNextFile(nil); + assert(strcmp(pFile->GetFileName(), fVolumeName) == 0); + + oldNameColon = new char[strlen(fVolumeName)+2]; + strcpy(oldNameColon, fVolumeName); + strcat(oldNameColon, ":"); + newNameColon = new char[strlen(newName)+2]; + strcpy(newNameColon, newName); + strcat(newNameColon, ":"); + + if (hfs_rename(fHfsVol, oldNameColon, newNameColon) != 0) { + WMSG3(" HFS rename '%s' -> '%s' failed: %s\n", + oldNameColon, newNameColon, hfs_error); + dierr = kDIErrGeneric; + goto bail; + } + + /* update stuff */ + strcpy(fVolumeName, newName); + SetVolumeID(); + strcpy(pFile->fFileName, newName); + pFile->SetPathName("", newName); + +bail: + delete[] oldNameColon; + delete[] newNameColon; + hfs_flush(fHfsVol); + return dierr; +} + +/* + * Set file attributes. + */ +DIError +DiskFSHFS::SetFileInfo(A2File* pGenericFile, long fileType, long auxType, + long accessFlags) +{ + DIError dierr = kDIErrNone; + A2FileHFS* pFile = (A2FileHFS*) pGenericFile; + hfsdirent dirEnt; + char* colonPath; + + if (fpImg->GetReadOnly()) + return kDIErrAccessDenied; + if (pFile == nil) + return kDIErrInvalidArg; + if (pFile->IsDirectory() || pFile->IsVolumeDirectory()) + return kDIErrNone; // impossible; just ignore it + + colonPath = pFile->GetLibHFSPathName(); + + if (hfs_stat(fHfsVol, colonPath, &dirEnt) != 0) { + WMSG2(" HFS unable to stat '%s': %s\n", colonPath, hfs_error); + dierr = kDIErrGeneric; + goto bail; + } + + A2FileHFS::ConvertTypeToHFS(fileType, auxType, + dirEnt.u.file.type, dirEnt.u.file.creator); + + if (accessFlags & A2FileProDOS::kAccessInvisible) + dirEnt.fdflags |= HFS_FNDR_ISINVISIBLE; + else + dirEnt.fdflags &= ~HFS_FNDR_ISINVISIBLE; + + if ((accessFlags & ~A2FileProDOS::kAccessInvisible) == kFileAccessLocked) + dirEnt.flags |= HFS_ISLOCKED; + else + dirEnt.flags &= ~HFS_ISLOCKED; + + WMSG3(" HFS setting '%s' to fdflags=0x%04x flags=0x%04x\n", + colonPath, dirEnt.fdflags, dirEnt.flags); + WMSG2(" type=0x%08lx creator=0x%08lx\n", fileType, auxType); + + if (hfs_setattr(fHfsVol, colonPath, &dirEnt) != 0) { + WMSG2(" HFS setattr '%s' failed: %s\n", colonPath, hfs_error); + dierr = kDIErrGeneric; + goto bail; + } + + /* update our local copy */ + pFile->fType = fileType; + pFile->fCreator = auxType; + pFile->fAccess = accessFlags; // should actually base them on HFS vals + +bail: + delete[] colonPath; + hfs_flush(fHfsVol); + return dierr; +} + +#endif // !EXCISE_GPL_CODE + + +/* + * =========================================================================== + * A2FileHFS + * =========================================================================== + */ + +/* + * Dump the contents of the A2File structure. + */ +void +A2FileHFS::Dump(void) const +{ + WMSG1("A2FileHFS '%s'\n", fFileName); +} + +/* convert hex to decimal */ +inline int FromHex(char hexVal) +{ + if (hexVal >= '0' && hexVal <= '9') + return hexVal - '0'; + else if (hexVal >= 'a' && hexVal <= 'f') + return hexVal -'a' + 10; + else if (hexVal >= 'A' && hexVal <= 'F') + return hexVal - 'A' + 10; + else + return -1; +} + +/* + * If this has a ProDOS filetype, convert it. + * + * This stuff is defined in Technical Note PT515, "Apple File Exchange Q&As". + * In theory we should convert type=BINA and type=TEXT regardless of the + * creator, but since those just go to generic text/binary types I don't + * think we need to handle it here (and I'm more comfortable leaving them + * with their Macintosh creators). + * + * In some respects, converting to ProDOS types is a bad idea, because we + * don't have a 1:1 mapping. If we copy a pdos/p\0\0\0 file we will store it + * as pdos/BINA instead. In practice, for the Apple II world they are + * equivalent, and CiderPress really doesn't need the "raw" file type. If + * it becomes annoying, we can add a DiskFSParameter to control it. + */ +long A2FileHFS::GetFileType(void) const +{ + if (fCreator != kPdosType) + return fType; + + if ((fType & 0xffff) == 0x2020) { + // 'XY ', where XY are hex digits for ProDOS file type + int digit1, digit2; + + digit1 = FromHex((char) (fType >> 24)); + digit2 = FromHex((char) (fType >> 16)); + if (digit1 < 0 || digit2 < 0) { + WMSG1(" Unexpected: pdos + %08lx\n", fType); + return 0x00; + } + return digit1 << 4 | digit2; + } + + unsigned char flag = (unsigned char)(fType >> 24); + if (flag == 0x70) { // 'p' + /* type and aux embedded within */ + return (fType >> 16) & 0xff; + } else { + /* type stored as a string */ + if (fType == 0x42494e41) // 'BINA' + return 0x00; // NON + else if (fType == 0x54455854) // 'TEXT' + return 0x04; + else if (fType == 0x50535953) // 'PSYS' + return 0xff; + else if (fType == 0x50533136) // 'PS16' + return 0xb3; + else + return 0x00; + } +}; + +/* + * If this has a ProDOS aux type, convert it. + */ +long A2FileHFS::GetAuxType(void) const +{ + if (fCreator != kPdosType) + return fCreator; + + unsigned char flag = (unsigned char)(fType >> 24); + if (flag == 0x70) { // 'p' + /* type and aux embedded within */ + return fType & 0xffff; + } else { + return 0x0000; + } +} + +/* + * Set the full pathname to a combination of the base path and the + * current file's name. + * + * If we're in the volume directory, pass in "" for the base path (not nil). + */ +void +A2FileHFS::SetPathName(const char* basePath, const char* fileName) +{ + assert(basePath != nil && fileName != nil); + if (fPathName != nil) + delete[] fPathName; + + // strip leading ':' (but treat ":" specially for volume dir entry) + if (basePath[0] == ':' && basePath[1] != '\0') + basePath++; + + int baseLen = strlen(basePath); + fPathName = new char[baseLen + 1 + strlen(fileName)+1]; + strcpy(fPathName, basePath); + if (baseLen != 0 && + !(baseLen == 1 && basePath[0] == ':')) + { + *(fPathName + baseLen) = kFssep; + baseLen++; + } + strcpy(fPathName + baseLen, fileName); +} + + +#ifndef EXCISE_GPL_CODE + +/* + * Return a copy of the pathname that libhfs will like. + * + * The caller must delete[] the return value. + */ +char* +A2FileHFS::GetLibHFSPathName(void) const +{ + char* nameBuf; + + nameBuf = new char[strlen(fPathName)+2]; + nameBuf[0] = kFssep; + strcpy(nameBuf+1, fPathName); + + return nameBuf; +} + +/* + * Convert numeric file/aux type to HFS strings. "pType" and "pCreator" must + * be able to hold 5 bytes each (4-byte type + nul). + * + * Follows the PT515 recommendations, mostly. The "PSYS" and "PS16" + * conversions discard the file's aux type and therefore are unsuitable, + * and the conversion of SRC throws away its identity. + */ +/*static*/ void +A2FileHFS::ConvertTypeToHFS(long fileType, long auxType, + char* pType, char* pCreator) +{ + if (fileType == 0x00 && auxType == 0x0000) { + strcpy(pCreator, "pdos"); + strcpy(pType, "BINA"); + } else if (fileType == 0x04 && auxType == 0x0000) { + strcpy(pCreator, "pdos"); + strcpy(pType, "TEXT"); + } else if (fileType >= 0 && fileType <= 0xff && + auxType >= 0 && auxType <= 0xffff) + { + pType[0] = 'p'; + pType[1] = (unsigned char) fileType; + pType[2] = (unsigned char) (auxType >> 8); + pType[3] = (unsigned char) auxType; + pType[4] = '\0'; + pCreator[0] = 'p'; + pCreator[1] = 'd'; + pCreator[2] = 'o'; + pCreator[3] = 's'; + pCreator[4] = '\0'; + } else { + pType[0] = (unsigned char)(fileType >> 24); + pType[1] = (unsigned char)(fileType >> 16); + pType[2] = (unsigned char)(fileType >> 8); + pType[3] = (unsigned char) fileType; + pType[4] = '\0'; + pCreator[0] = (unsigned char)(auxType >> 24); + pCreator[1] = (unsigned char)(auxType >> 16); + pCreator[2] = (unsigned char)(auxType >> 8); + pCreator[3] = (unsigned char) auxType; + pCreator[4] = '\0'; + } +} + + +/* + * Open a file through libhfs. + * + * libhfs wants filenames to begin with ':' unless they start with the + * name of the volume. This is the opposite of the convention followed + * by the rest of CiderPress (and most of the civilized world), so instead + * of storing the pathname that way we just tack it on here. + */ +DIError +A2FileHFS::Open(A2FileDescr** ppOpenFile, bool readOnly, + bool rsrcFork /*=false*/) +{ + DIError dierr = kDIErrNone; + A2FDHFS* pOpenFile = nil; + hfsfile* pHfsFile; + char* nameBuf = nil; + + if (fpOpenFile != nil) + return kDIErrAlreadyOpen; + //if (rsrcFork && fRsrcLength < 0) + // return kDIErrForkNotFound; + + nameBuf = GetLibHFSPathName(); + + DiskFSHFS* pDiskFS = (DiskFSHFS*) GetDiskFS(); + pHfsFile = hfs_open(pDiskFS->GetHfsVol(), nameBuf); + if (pHfsFile == NULL) { + WMSG2(" HFS hfs_open(%s) failed: %s\n", nameBuf, hfs_error); + dierr = kDIErrGeneric; // better value might be in errno + goto bail; + } + hfs_setfork(pHfsFile, rsrcFork ? 1 : 0); + + pOpenFile = new A2FDHFS(this, pHfsFile); + + fpOpenFile = pOpenFile; + *ppOpenFile = pOpenFile; + +bail: + delete[] nameBuf; + return dierr; +} + + +/* + * =========================================================================== + * A2FDHFS + * =========================================================================== + */ + +/* + * Read a chunk of data from the fake file. + */ +DIError +A2FDHFS::Read(void* buf, size_t len, size_t* pActual) +{ + long result; + + WMSG3(" HFS reading %d bytes from '%s' (offset=%ld)\n", + len, fpFile->GetPathName(), hfs_seek(fHfsFile, 0, HFS_SEEK_CUR)); + + //A2FileHFS* pFile = (A2FileHFS*) fpFile; + + result = hfs_read(fHfsFile, buf, len); + if (result < 0) + return kDIErrReadFailed; + + if (pActual != nil) { + *pActual = (size_t) result; + } else if (result != (long) len) { + // short read, can't report it, return error + return kDIErrDataUnderrun; + } + + /* + * To do this right we need to break the hfs_read() into smaller + * pieces. However, it only really affects us for files that are + * getting reformatted, because that's the only time we grab the + * entire thing in one big piece. + */ + long offset = hfs_seek(fHfsFile, 0, HFS_SEEK_CUR); + if (!UpdateProgress(offset)) { + return kDIErrCancelled; + } + + return kDIErrNone; +} + +/* + * Write data at the current offset. + * + * (In the current implementation, the entire file is always written in + * one piece. This function does work correctly with multiple smaller + * pieces though, because it lets libhfs do all the work.) + */ +DIError +A2FDHFS::Write(const void* buf, size_t len, size_t* pActual) +{ + long result; + + WMSG3(" HFS writing %d bytes to '%s' (offset=%ld)\n", + len, fpFile->GetPathName(), hfs_seek(fHfsFile, 0, HFS_SEEK_CUR)); + + fModified = true; // assume something gets changed + + //A2FileHFS* pFile = (A2FileHFS*) fpFile; + + result = hfs_write(fHfsFile, buf, len); + if (result < 0) + return kDIErrWriteFailed; + + if (pActual != nil) { + *pActual = (size_t) result; + } else if (result != (long) len) { + // short write, can't report it, return error + return kDIErrDataUnderrun; + } + + /* to make this work right, we need to break hfs_write into pieces */ + long offset = hfs_seek(fHfsFile, 0, HFS_SEEK_CUR); + if (!UpdateProgress(offset)) { + return kDIErrCancelled; + } + + /* + * We don't hfs_flush here, because we don't expect the application to + * hold the file open, and we flush in Close(). + */ + + return kDIErrNone; +} + +/* + * Seek to a new offset. + */ +DIError +A2FDHFS::Seek(di_off_t offset, DIWhence whence) +{ + int hfsWhence; + unsigned long result; + + switch (whence) { + case kSeekSet: hfsWhence = HFS_SEEK_SET; break; + case kSeekEnd: hfsWhence = HFS_SEEK_END; break; + case kSeekCur: hfsWhence = HFS_SEEK_CUR; break; + default: + assert(false); + return kDIErrInvalidArg; + } + + result = hfs_seek(fHfsFile, (long) offset, hfsWhence); + if (result == (unsigned long) -1) { + DebugBreak(); + return kDIErrGeneric; + } + return kDIErrNone; +} + +/* + * Return current offset. + */ +di_off_t +A2FDHFS::Tell(void) +{ + di_off_t offset; + + /* get current position without moving pointer */ + offset = hfs_seek(fHfsFile, 0, HFS_SEEK_CUR); + return offset; +} + +/* + * Release file state, and tell our parent to destroy us. + */ +DIError +A2FDHFS::Close(void) +{ + hfsdirent dirEnt; + + /* + * If the file was written to, update our info. + */ + if (fModified) { + if (hfs_fstat(fHfsFile, &dirEnt) == 0) { + A2FileHFS* pFile = (A2FileHFS*) fpFile; + pFile->fDataLength = dirEnt.u.file.dsize; + pFile->fRsrcLength = dirEnt.u.file.rsize; + if (pFile->fRsrcLength == 0) + pFile->fRsrcLength = -1; + WMSG2(" HFS close set dataLen=%ld rsrcLen=%ld\n", + (long) pFile->fDataLength, (long) pFile->fRsrcLength); + } else { + WMSG1(" HFS Close fstat failed: %s\n", hfs_error); + // close it anyway + } + } + + hfs_close(fHfsFile); + fHfsFile = nil; + + /* flush changes */ + if (fModified) { + DiskFSHFS* pDiskFS = (DiskFSHFS*) fpFile->GetDiskFS(); + + if (hfs_flush(pDiskFS->GetHfsVol()) != 0) { + WMSG0("HEY: Close flush failed!\n"); + DebugBreak(); + } + } + + fpFile->CloseDescr(this); + return kDIErrNone; +} + +/* + * Return the #of sectors/blocks in the file. Not supported, but since HFS + * doesn't support "sparse" files we can fake it. + */ +long +A2FDHFS::GetSectorCount(void) const +{ + A2FileHFS* pFile = (A2FileHFS*) fpFile; + return (long) ((pFile->fDataLength+255) / 256 + + (pFile->fRsrcLength+255) / 256); +} +long +A2FDHFS::GetBlockCount(void) const +{ + A2FileHFS* pFile = (A2FileHFS*) fpFile; + return (long) ((pFile->fDataLength+511) / 512 + + (pFile->fRsrcLength+511) / 512); +} + +/* + * Return the Nth track/sector in this file. Not supported. + */ +DIError +A2FDHFS::GetStorage(long sectorIdx, long* pTrack, long* pSector) const +{ + return kDIErrNotSupported; +} +/* + * Return the Nth 512-byte block in this file. Not supported. + */ +DIError +A2FDHFS::GetStorage(long blockIdx, long* pBlock) const +{ + return kDIErrNotSupported; +} + + + + +#else // EXCISE_GPL_CODE ----------------------------------------------------- + +/* + * Get things rolling. + * + * Since we're assured that this is a valid disk, errors encountered from here + * on out must be handled somehow, possibly by claiming that the disk is + * completely full and has no files on it. + */ +DIError +DiskFSHFS::Initialize(InitMode initMode) +{ + DIError dierr = kDIErrNone; + + dierr = LoadVolHeader(); + if (dierr != kDIErrNone) + goto bail; + DumpVolHeader(); + + CreateFakeFile(); + + SetVolumeUsageMap(); + +bail: + return dierr; +} + + +/* + * Fill a buffer with some interesting stuff, and add it to the file list. + */ +void +DiskFSHFS::CreateFakeFile(void) +{ + A2FileHFS* pFile; + char buf[768]; // currently running about 475 + static const char* kFormatMsg = +"The Macintosh HFS filesystem is not supported. CiderPress knows how to\r" +"recognize HFS volumes so that it can identify partitions on CFFA-formatted\r" +"CompactFlash cards and Apple II CD-ROMs, but the current version does not\r" +"know how to view or extract files.\r" +"\r" +"Some information about this HFS volume:\r" +"\r" +" Volume name : '%s'\r" +" Storage capacity : %ld blocks (%.2fMB)\r" +" Number of files : %ld\r" +" Number of folders : %ld\r" +" Last modified : %s\r" +"\r" +; + char dateBuf[32]; + long capacity; + const char* timeStr; + + capacity = (fAllocationBlockSize / kBlkSize) * fNumAllocationBlocks; + + /* get the mod time, format it, and remove the trailing '\n' */ + time_t when = + (time_t) (fModifiedDateTime - kDateTimeOffset - fLocalTimeOffset); + timeStr = ctime(&when); + if (timeStr == nil) { + WMSG2("Invalid date %ld (orig=%ld)\n", when, fModifiedDateTime); + strcpy(dateBuf, ""); + } else + strncpy(dateBuf, timeStr, sizeof(dateBuf)); + int len = strlen(dateBuf); + if (len > 0) + dateBuf[len-1] = '\0'; + + memset(buf, 0, sizeof(buf)); + sprintf(buf, kFormatMsg, + fVolumeName, + capacity, + (double) capacity / 2048.0, + fNumFiles, + fNumDirectories, + dateBuf); + + pFile = new A2FileHFS(this); + pFile->fIsDir = false; + pFile->fIsVolumeDir = false; + pFile->fType = 0; + pFile->fCreator = 0; + pFile->SetFakeFile(buf, strlen(buf)); + strcpy(pFile->fFileName, "(not supported)"); + pFile->SetPathName("", pFile->fFileName); + pFile->fDataLength = 0; + pFile->fRsrcLength = -1; + pFile->fCreateWhen = 0; + pFile->fModWhen = 0; + + pFile->SetFakeFile(buf, strlen(buf)); + + AddFileToList(pFile); +} + +/* + * We could do this, but there's not much point. + */ +DIError GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const +{ + return kDIErrNotSupported; +} + +/* + * Not a whole lot to do. + */ +DIError +A2FileHFS::Open(A2FileDescr** ppOpenFile, bool readOnly, + bool rsrcFork /*=false*/) +{ + A2FDHFS* pOpenFile = nil; + + if (fpOpenFile != nil) + return kDIErrAlreadyOpen; + if (rsrcFork && fRsrcLength < 0) + return kDIErrForkNotFound; + assert(readOnly == true); + + pOpenFile = new A2FDHFS(this, nil); + + fpOpenFile = pOpenFile; + *ppOpenFile = pOpenFile; + + return kDIErrNone; +} + + +/* + * =========================================================================== + * A2FDHFS + * =========================================================================== + */ + +/* + * Read a chunk of data from the fake file. + */ +DIError +A2FDHFS::Read(void* buf, size_t len, size_t* pActual) +{ + WMSG3(" HFS reading %d bytes from '%s' (offset=%ld)\n", + len, fpFile->GetPathName(), (long) fOffset); + + A2FileHFS* pFile = (A2FileHFS*) fpFile; + + /* don't allow them to read past the end of the file */ + if (fOffset + (long)len > pFile->fDataLength) { + if (pActual == nil) + return kDIErrDataUnderrun; + len = (size_t) (pFile->fDataLength - fOffset); + } + if (pActual != nil) + *pActual = len; + + memcpy(buf, pFile->GetFakeFileBuf(), len); + + fOffset += len; + + return kDIErrNone; +} + +/* + * Write data at the current offset. + */ +DIError +A2FDHFS::Write(const void* buf, size_t len, size_t* pActual) +{ + return kDIErrNotSupported; +} + +/* + * Seek to a new offset. + */ +DIError +A2FDHFS::Seek(di_off_t offset, DIWhence whence) +{ + di_off_t fileLen = ((A2FileHFS*) fpFile)->fDataLength; + + switch (whence) { + case kSeekSet: + if (offset < 0 || offset > fileLen) + return kDIErrInvalidArg; + fOffset = offset; + break; + case kSeekEnd: + if (offset > 0 || offset < -fileLen) + return kDIErrInvalidArg; + fOffset = fileLen + offset; + break; + case kSeekCur: + if (offset < -fOffset || + offset >= (fileLen - fOffset)) + { + return kDIErrInvalidArg; + } + fOffset += offset; + break; + default: + assert(false); + return kDIErrInvalidArg; + } + + assert(fOffset >= 0 && fOffset <= fileLen); + return kDIErrNone; +} + +/* + * Return current offset. + */ +di_off_t +A2FDHFS::Tell(void) +{ + return fOffset; +} + +/* + * Release file state, and tell our parent to destroy us. + */ +DIError +A2FDHFS::Close(void) +{ + fpFile->CloseDescr(this); + return kDIErrNone; +} + +/* + * Return the #of sectors/blocks in the file. + */ +long +A2FDHFS::GetSectorCount(void) const +{ + A2FileHFS* pFile = (A2FileHFS*) fpFile; + return (long) ((pFile->fDataLength+255) / 256); +} +long +A2FDHFS::GetBlockCount(void) const +{ + A2FileHFS* pFile = (A2FileHFS*) fpFile; + return (long) ((pFile->fDataLength+511) / 512); +} + +/* + * Return the Nth track/sector in this file. + */ +DIError +A2FDHFS::GetStorage(long sectorIdx, long* pTrack, long* pSector) const +{ + return kDIErrNotSupported; +} +/* + * Return the Nth 512-byte block in this file. + */ +DIError +A2FDHFS::GetStorage(long blockIdx, long* pBlock) const +{ + return kDIErrNotSupported; +} + +#endif // EXCISE_GPL_CODE --------------------------------------------------- diff --git a/diskimg/ImageWrapper.cpp b/diskimg/ImageWrapper.cpp new file mode 100644 index 0000000..6158562 --- /dev/null +++ b/diskimg/ImageWrapper.cpp @@ -0,0 +1,2531 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Code for handling disk image "wrappers", things like DiskCopy, 2MG, and + * SHK that surround a disk image. + * + * Returning with kDIErrBadChecksum from Test or Prep is taken as a sign + * that, while we have correctly identified the wrapper format, the contents + * of the file are corrupt, and the user needs to be told. + * + * Some formats, such as 2MG, include a DOS volume number. This is useful + * because DOS actually embeds the volume number in sector headers; the value + * stored in the VTOC is ignored by certain things (notably some games with + * trivial copy-protection). This value needs to be preserved. It's + * unclear how useful this will actually be; mostly we just want to preserve + * it when translating from one format to another. + * + * If a library (such as NufxLib) needs to read an actual file, it can + * (usually) pry the name out of the GFD. + * + * In general, it should be possible to write to any "wrapped" file that we + * can read from. For things like NuFX and DDD, this means we need to be + * able to re-compress the image file when we're done with it. + */ +#include "StdAfx.h" +#include "DiskImgPriv.h" +#include "TwoImg.h" + + +/* + * =========================================================================== + * 2MG (a/k/a 2IMG) + * =========================================================================== + */ + +/* + * Test to see if this is a 2MG file. + * + * The easiest way to do that is to open up the header and see if + * it looks valid. + */ +/*static*/ DIError +Wrapper2MG::Test(GenericFD* pGFD, di_off_t wrappedLength) +{ + TwoImgHeader header; + + WMSG0("Testing for 2MG\n"); + + // HEY: should test for wrappedLength > 2GB; if so, skip + + pGFD->Rewind(); + if (header.ReadHeader(pGFD, (long) wrappedLength) != 0) + return kDIErrGeneric; + + WMSG0("Looks like valid 2MG\n"); + return kDIErrNone; +} + +/* + * Get the header (again) and use it to locate the data. + */ +DIError +Wrapper2MG::Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, + di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, + DiskImg::SectorOrder* pOrder, short* pDiskVolNum, + LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD) +{ + TwoImgHeader header; + long offset; + + WMSG0("Prepping for 2MG\n"); + pGFD->Rewind(); + if (header.ReadHeader(pGFD, (long) wrappedLength) != 0) + return kDIErrGeneric; + + offset = header.fDataOffset; + + if (header.fFlags & TwoImgHeader::kDOSVolumeSet) + *pDiskVolNum = header.GetDOSVolumeNum(); + + *pLength = header.fDataLen; + *pPhysical = DiskImg::kPhysicalFormatSectors; + if (header.fImageFormat == TwoImgHeader::kImageFormatDOS) + *pOrder = DiskImg::kSectorOrderDOS; + else if (header.fImageFormat == TwoImgHeader::kImageFormatProDOS) + *pOrder = DiskImg::kSectorOrderProDOS; + else if (header.fImageFormat == TwoImgHeader::kImageFormatNibble) { + *pOrder = DiskImg::kSectorOrderPhysical; + if (*pLength == kTrackCount525 * kTrackLenNib525) { + WMSG0(" Prepping for 6656-byte 2MG-NIB\n"); + *pPhysical = DiskImg::kPhysicalFormatNib525_6656; + } else if (*pLength == kTrackCount525 * kTrackLenNb2525) { + WMSG0(" Prepping for 6384-byte 2MG-NB2\n"); + *pPhysical = DiskImg::kPhysicalFormatNib525_6384; + } else { + WMSG1(" NIB 2MG with length=%ld rejected\n", (long) *pLength); + return kDIErrOddLength; + } + } + + *ppNewGFD = new GFDGFD; + return ((GFDGFD*)*ppNewGFD)->Open(pGFD, offset, readOnly); +} + +/* + * Initialize fields for a new file. + */ +DIError +Wrapper2MG::Create(di_off_t length, DiskImg::PhysicalFormat physical, + DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, + di_off_t* pWrappedLength, GenericFD** pDataFD) +{ + TwoImgHeader header; + int cc; + + switch (physical) { + case DiskImg::kPhysicalFormatNib525_6656: + if (length != kTrackLenNib525 * kTrackCount525) { + WMSG1("Invalid 2MG nibble length %ld\n", (long) length); + return kDIErrInvalidArg; + } + header.InitHeader(TwoImgHeader::kImageFormatNibble, (long) length, + 8 * kTrackCount525); // 8 blocks per track + break; + case DiskImg::kPhysicalFormatSectors: + if ((length % 512) != 0) { + WMSG1("Invalid 2MG length %ld\n", (long) length); + return kDIErrInvalidArg; + } + if (order == DiskImg::kSectorOrderProDOS) + cc = header.InitHeader(TwoImgHeader::kImageFormatProDOS, + (long) length, (long) length / 512); + else if (order == DiskImg::kSectorOrderDOS) + cc = header.InitHeader(TwoImgHeader::kImageFormatDOS, + (long) length, (long) length / 512); + else { + WMSG1("Invalid 2MG sector order %d\n", order); + return kDIErrInvalidArg; + } + if (cc != 0) { + WMSG1("TwoImg InitHeader failed (len=%ld)\n", (long) length); + return kDIErrInvalidArg; + } + break; + default: + WMSG1("Invalid 2MG physical %d\n", physical); + return kDIErrInvalidArg; + } + + if (dosVolumeNum != DiskImg::kVolumeNumNotSet) + header.SetDOSVolumeNum(dosVolumeNum); + + cc = header.WriteHeader(pWrapperGFD); + if (cc != 0) { + WMSG1("ERROR: 2MG header write failed (cc=%d)\n", cc); + return kDIErrGeneric; + } + + long footerLen = header.fCmtLen + header.fCreatorLen; + if (footerLen > 0) { + // This is currently impossible, which is good because the Seek call + // will fail if pWrapperGFD is a buffer. + assert(false); + pWrapperGFD->Seek(header.fDataOffset + length, kSeekSet); + header.WriteFooter(pWrapperGFD); + } + + long offset = header.fDataOffset; + + + *pWrappedLength = length + offset + footerLen; + *pDataFD = new GFDGFD; + return ((GFDGFD*)*pDataFD)->Open(pWrapperGFD, offset, false); +} + +/* + * We only use GFDGFD, so there's nothing to do here. + * + * If we want to support changing the comment field in an open image, we'd + * need to handle making the file longer or shorter here. Right now we + * just ignore everything that comes before or after the start of the data. + * Since there's no checksum, none of the header fields change, so we + * don't even deal with that. + */ +DIError +Wrapper2MG::Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, + di_off_t dataLen, di_off_t* pWrappedLen) +{ + return kDIErrNone; +} + + +/* + * =========================================================================== + * SHK (ShrinkIt NuFX), also .SDK and .BXY + * =========================================================================== + */ + +/* + * NOTE: this doesn't override the global error message callback because + * we expect it to be set by the application. + */ + +/* + * Display error messages... or not. + */ +/*static*/ NuResult +WrapperNuFX::ErrMsgHandler(NuArchive* /*pArchive*/, void* vErrorMessage) +{ + const NuErrorMessage* pErrorMessage = (const NuErrorMessage*) vErrorMessage; + + if (pErrorMessage->isDebug) { + Global::PrintDebugMsg(pErrorMessage->file, pErrorMessage->line, + "[D] %s\n", pErrorMessage->message); + } else { + Global::PrintDebugMsg(pErrorMessage->file, pErrorMessage->line, + "%s\n", pErrorMessage->message); + } + + return kNuOK; +} + +/* + * Open a NuFX archive, and verify that it holds exactly one disk archive. + * + * On success, the NuArchive pointer and thread idx are set, and 0 is + * returned. Returns -1 on failure. + */ +/*static*/ DIError +WrapperNuFX::OpenNuFX(const char* pathName, NuArchive** ppArchive, + NuThreadIdx* pThreadIdx, long* pLength, bool readOnly) +{ + NuError nerr = kNuErrNone; + NuArchive* pArchive = nil; + NuRecordIdx recordIdx; + NuAttr attr; + const NuRecord* pRecord; + const NuThread* pThread = nil; + int idx; + + WMSG1("Opening file '%s' to test for NuFX\n", pathName); + + /* + * Open the archive. + */ + if (readOnly) { + nerr = NuOpenRO(pathName, &pArchive); + if (nerr != kNuErrNone) { + WMSG1(" NuFX unable to open archive (err=%d)\n", nerr); + goto bail; + } + } else { + char* tmpPath; + + tmpPath = GenTempPath(pathName); + if (tmpPath == nil) { + nerr = kNuErrInternal; + goto bail; + } + + nerr = NuOpenRW(pathName, tmpPath, 0, &pArchive); + if (nerr != kNuErrNone) { + WMSG1(" NuFX OpenRW failed (nerr=%d)\n", nerr); + nerr = kNuErrGeneric; + delete[] tmpPath; + goto bail; + } + delete[] tmpPath; + } + + NuSetErrorMessageHandler(pArchive, ErrMsgHandler); + + nerr = NuGetAttr(pArchive, kNuAttrNumRecords, &attr); + if (nerr != kNuErrNone) { + WMSG1(" NuFX unable to get record count (err=%d)\n", nerr); + goto bail; + } + if (attr != 1) { + WMSG1(" NuFX archive has %ld entries, not disk-only\n", attr); + nerr = kNuErrGeneric; + if (attr > 1) + goto file_archive; + else + goto bail; // shouldn't get zero-count archives, but... + } + + /* get the first record */ + nerr = NuGetRecordIdxByPosition(pArchive, 0, &recordIdx); + if (nerr != kNuErrNone) { + WMSG1(" NuFX unable to get first recordIdx (err=%d)\n", nerr); + goto bail; + } + nerr = NuGetRecord(pArchive, recordIdx, &pRecord); + if (nerr != kNuErrNone) { + WMSG1(" NuFX unable to get first record (err=%d)\n", nerr); + goto bail; + } + + /* find a disk image thread */ + for (idx = 0; idx < (int)NuRecordGetNumThreads(pRecord); idx++) { + pThread = NuGetThread(pRecord, idx); + + if (NuGetThreadID(pThread) == kNuThreadIDDiskImage) + break; + } + if (idx == (int)NuRecordGetNumThreads(pRecord)) { + WMSG0(" NuFX no disk image found in first record\n"); + nerr = kNuErrGeneric; + goto file_archive; + } + assert(pThread != nil); + *pThreadIdx = pThread->threadIdx; + + /* + * Don't allow zero-length disks. + */ + *pLength = pThread->actualThreadEOF; + if (!*pLength) { + WMSG1(" NuFX length of disk image is bad (%ld)\n", *pLength); + nerr = kNuErrGeneric; + goto bail; + } + + /* + * Success! + */ + assert(nerr == kNuErrNone); + *ppArchive = pArchive; + pArchive = nil; + +bail: + if (pArchive != nil) + NuClose(pArchive); + if (nerr == kNuErrNone) + return kDIErrNone; + else if (nerr == kNuErrBadMHCRC || nerr == kNuErrBadRHCRC) + return kDIErrBadChecksum; + else + return kDIErrGeneric; + +file_archive: + if (pArchive != nil) + NuClose(pArchive); + return kDIErrFileArchive; +} + +/* + * Load a disk image into memory. + * + * Allocates a buffer with the specified length and loads the desired + * thread into it. + * + * In an LZW-I compressed thread, the third byte of the compressed thread + * data is the disk volume number that P8 ShrinkIt would use when formatting + * the disk. In an LZW-II compressed thread, it's the first byte of the + * compressed data. Uncompressed disk images simply don't have the disk + * volume number in them. Until NufxLib provides a simple way to access + * this bit of loveliness, we're going to pretend it's not there. + * + * Returns 0 on success, -1 on error. + */ +DIError +WrapperNuFX::GetNuFXDiskImage(NuArchive* pArchive, NuThreadIdx threadIdx, + long length, char** ppData) +{ + NuError err; + NuDataSink* pDataSink = nil; + unsigned char* buf = nil; + + assert(length > 0); + buf = new unsigned char[length]; + if (buf == nil) + return kDIErrMalloc; + + /* + * Create a buffer and expand the disk image into it. + */ + err = NuCreateDataSinkForBuffer(true, kNuConvertOff, buf, length, + &pDataSink); + if (err != kNuErrNone) { + WMSG1(" NuFX: unable to create data sink (err=%d)\n", err); + goto bail; + } + + err = NuExtractThread(pArchive, threadIdx, pDataSink); + if (err != kNuErrNone) { + WMSG1(" NuFX: unable to extract thread (err=%d)\n", err); + goto bail; + } + + //err = kNuErrBadThreadCRC; goto bail; // debug test only + + *ppData = (char*)buf; + +bail: + NuFreeDataSink(pDataSink); + if (err != kNuErrNone) { + WMSG1(" NuFX GetNuFXDiskImage returning after nuerr=%d\n", err); + delete buf; + } + if (err == kNuErrNone) + return kDIErrNone; + else if (err == kNuErrBadDataCRC || err == kNuErrBadThreadCRC) + return kDIErrBadChecksum; + else if (err == kNuErrBadData) + return kDIErrBadCompressedData; + else if (err == kNuErrBadFormat) + return kDIErrUnsupportedCompression; + else + return kDIErrGeneric; +} + +/* + * Test to see if this is a single-record NuFX archive with a disk archive + * in it. + */ +/*static*/ DIError +WrapperNuFX::Test(GenericFD* pGFD, di_off_t wrappedLength) +{ + DIError dierr; + NuArchive* pArchive = nil; + NuThreadIdx threadIdx; + long length; + const char* imagePath; + + imagePath = pGFD->GetPathName(); + if (imagePath == nil) { + WMSG0("Can't test NuFX on non-file\n"); + return kDIErrNotSupported; + } + WMSG0("Testing for NuFX\n"); + dierr = OpenNuFX(imagePath, &pArchive, &threadIdx, &length, true); + if (dierr != kDIErrNone) + return dierr; + + /* success; throw away state in case they don't like us anyway */ + assert(pArchive != nil); + NuClose(pArchive); + + return kDIErrNone; +} + +/* + * Open the archive, extract the disk image into a memory buffer. + */ +DIError +WrapperNuFX::Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, + di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, + DiskImg::SectorOrder* pOrder, short* pDiskVolNum, + LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD) +{ + DIError dierr = kDIErrNone; + NuThreadIdx threadIdx; + GFDBuffer* pNewGFD = nil; + char* buf = nil; + long length = -1; + const char* imagePath; + + imagePath = pGFD->GetPathName(); + if (imagePath == nil) { + assert(false); // should've been caught in Test + return kDIErrNotSupported; + } + pGFD->Close(); // don't hold the file open + dierr = OpenNuFX(imagePath, &fpArchive, &threadIdx, &length, readOnly); + if (dierr != kDIErrNone) + goto bail; + + dierr = GetNuFXDiskImage(fpArchive, threadIdx, length, &buf); + if (dierr != kDIErrNone) + goto bail; + + pNewGFD = new GFDBuffer; + dierr = pNewGFD->Open(buf, length, true, false, readOnly); + if (dierr != kDIErrNone) + goto bail; + buf = nil; // now owned by pNewGFD; + + /* + * Success! + */ + assert(dierr == kDIErrNone); + *ppNewGFD = pNewGFD; + *pLength = length; + *pPhysical = DiskImg::kPhysicalFormatSectors; + *pOrder = DiskImg::kSectorOrderProDOS; + + WMSG1(" NuFX is ready, threadIdx=%ld\n", threadIdx); + fThreadIdx = threadIdx; + +bail: + if (dierr != kDIErrNone) { + NuClose(fpArchive); + fpArchive = nil; + delete pNewGFD; + delete buf; + } + return dierr; +} + +/* + * Given a filename, create a suitable temp pathname. + * + * This is really the wrong place to be doing this -- the application + * should get to deal with this -- but it's not the end of the world + * if we handle it here. Add to wish list: fix NufxLib so that the + * temp file can be a memory buffer. + * + * Returns a string allocated with new[]. + */ +/*static*/ char* +WrapperNuFX::GenTempPath(const char* path) +{ + static const char* kTmpTemplate = "DItmp_XXXXXX"; + char* tmpPath; + + assert(path != nil); + assert(strlen(path) > 0); + + tmpPath = new char[strlen(path) + 32]; + if (tmpPath == nil) + return nil; + + strcpy(tmpPath, path); + + /* back up to the first thing that looks like it's an fssep */ + char* cp; + cp = tmpPath + strlen(tmpPath); + while (--cp >= tmpPath) { + if (*cp == '/' || *cp == '\\' || *cp == ':') + break; + } + + /* we either fell off the back end or found an fssep; advance */ + cp++; + + strcpy(cp, kTmpTemplate); + + WMSG2(" NuFX GenTempPath '%s' -> '%s'\n", path, tmpPath); + + return tmpPath; +} + +/* + * Initialize fields for a new file. + * + * "pWrapperGFD" will be fairly useless after this, because we're + * recreating the underlying file. (If it doesn't have an underlying + * file, then we're hosed.) + */ +DIError +WrapperNuFX::Create(di_off_t length, DiskImg::PhysicalFormat physical, + DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, + di_off_t* pWrappedLength, GenericFD** pDataFD) +{ + assert(physical == DiskImg::kPhysicalFormatSectors); + assert(order == DiskImg::kSectorOrderProDOS); + + DIError dierr = kDIErrNone; + NuArchive* pArchive; + const char* imagePath; + char* tmpPath = nil; + unsigned char* buf = nil; + NuError nerr; + + /* + * Create the NuFX archive, stomping on the existing file. (This + * makes pWrapperGFD invalid, but such is life with NufxLib.) + */ + imagePath = pWrapperGFD->GetPathName(); + if (imagePath == nil) { + assert(false); // must not have an outer wrapper + dierr = kDIErrNotSupported; + goto bail; + } + pWrapperGFD->Close(); // don't hold the file open + tmpPath = GenTempPath(imagePath); + if (tmpPath == nil) { + dierr = kDIErrInternal; + goto bail; + } + + nerr = NuOpenRW(imagePath, tmpPath, kNuOpenCreat, &pArchive); + if (nerr != kNuErrNone) { + WMSG1(" NuFX OpenRW failed (nerr=%d)\n", nerr); + dierr = kDIErrGeneric; + goto bail; + } + + /* + * Create a blank chunk of memory for the image. + */ + assert(length > 0); + buf = new unsigned char[(int) length]; + if (buf == nil) { + dierr = kDIErrMalloc; + goto bail; + } + + GFDBuffer* pNewGFD; + pNewGFD = new GFDBuffer; + dierr = pNewGFD->Open(buf, length, true, false, false); + if (dierr != kDIErrNone) { + delete pNewGFD; + goto bail; + } + *pDataFD = pNewGFD; + buf = nil; // now owned by pNewGFD; + + /* + * Success! Set misc stuff. + */ + fThreadIdx = 0; // don't have one to overwrite + fpArchive = pArchive; + +bail: + delete[] tmpPath; + delete[] buf; + return dierr; +} + +/* + * Close the NuFX archive. + */ +DIError +WrapperNuFX::CloseNuFX(void) +{ + NuError nerr; + + /* throw away any un-flushed changes so that "close" can't fail */ + (void) NuAbort(fpArchive); + + nerr = NuClose(fpArchive); + if (nerr != kNuErrNone) { + WMSG0("WARNING: NuClose failed\n"); + return kDIErrGeneric; + } + return kDIErrNone; +} + +/* + * Write the data using the default compression method. + * + * Doesn't touch "pWrapperGFD" or "pWrappedLen". Could probably update + * "pWrappedLen", but that's really only useful if we have a gzip Outer + * that wants to know how much data we have. Because we don't write to + * pWrapperGFD, we can't have a gzip wrapper, so there's no point in + * updating it. + */ +DIError +WrapperNuFX::Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, + di_off_t dataLen, di_off_t* pWrappedLen) +{ + NuError nerr = kNuErrNone; + NuFileDetails fileDetails; + NuRecordIdx recordIdx; + NuThreadIdx threadIdx; + NuDataSource* pDataSource = nil; + + if (fThreadIdx != 0) { + /* + * Mark the old record for deletion. + */ + nerr = NuGetRecordIdxByPosition(fpArchive, 0, &recordIdx); + if (nerr != kNuErrNone) { + WMSG1(" NuFX unable to get first recordIdx (err=%d)\n", nerr); + goto bail; + } + nerr = NuDeleteRecord(fpArchive, recordIdx); + if (nerr != kNuErrNone) { + WMSG1(" NuFX unable to delete first record (err=%d)\n", nerr); + goto bail; + } + } + + assert((dataLen % 512) == 0); + + nerr = NuSetValue(fpArchive, kNuValueDataCompression, + fCompressType + kNuCompressNone); + if (nerr != kNuErrNone) { + WMSG1("WARNING: unable to set compression to format %d\n", + fCompressType); + nerr = kNuErrNone; + } else { + WMSG2(" NuFX set compression to %d/%d\n", fCompressType, + fCompressType + kNuCompressNone); + } + + /* + * Fill out the fileDetails record appropriately. + */ + memset(&fileDetails, 0, sizeof(fileDetails)); + fileDetails.threadID = kNuThreadIDDiskImage; + if (fStorageName != nil) + fileDetails.storageName = fStorageName; + else + fileDetails.storageName = "NEW.DISK"; + fileDetails.fileSysID = kNuFileSysUnknown; + fileDetails.fileSysInfo = kDefaultStorageFssep; + fileDetails.storageType = 512; + fileDetails.extraType = (long) (dataLen / 512); + fileDetails.access = kNuAccessUnlocked; + + time_t now; + now = time(nil); + UNIXTimeToDateTime(&now, &fileDetails.archiveWhen); + UNIXTimeToDateTime(&now, &fileDetails.modWhen); + UNIXTimeToDateTime(&now, &fileDetails.createWhen); + + /* + * Create the new record. + */ + nerr = NuAddRecord(fpArchive, &fileDetails, &recordIdx); + if (nerr != kNuErrNone) { + WMSG1(" NuFX AddRecord failed (nerr=%d)\n", nerr); + goto bail; + } + + /* + * Create a data source for the thread. + * + * We need to get the memory buffer from pDataGFD, which we do in + * a somewhat unwholesome manner. However, there's no other way to + * feed the data into NufxLib. + */ + nerr = NuCreateDataSourceForBuffer(kNuThreadFormatUncompressed, 0, + (const unsigned char*) ((GFDBuffer*) pDataGFD)->GetBuffer(), + 0, (long) dataLen, nil, &pDataSource); + if (nerr != kNuErrNone) { + WMSG1(" NuFX unable to create NufxLib data source (nerr=%d)", nerr); + goto bail; + } + + /* + * Add the thread. + */ + nerr = NuAddThread(fpArchive, recordIdx, kNuThreadIDDiskImage, + pDataSource, &threadIdx); + if (nerr != kNuErrNone) { + WMSG1(" NuFX AddThread failed (nerr=%d)\n", nerr); + goto bail; + } + pDataSource = nil; // now owned by NufxLib + WMSG2(" NuFX added thread %ld in record %ld, flushing changes\n", + threadIdx, recordIdx); + + /* + * Flush changes (does the actual compression). + */ + long status; + nerr = NuFlush(fpArchive, &status); + if (nerr != kNuErrNone) { + WMSG2(" NuFX flush failed (nerr=%d, status=%ld)\n", nerr, status); + goto bail; + } + + /* update the threadID */ + fThreadIdx = threadIdx; + +bail: + NuFreeDataSource(pDataSource); + if (nerr != kNuErrNone) + return kDIErrGeneric; + return kDIErrNone; +} + +/* + * Common NuFX utility function. This ought to be in NufxLib. + */ +void +WrapperNuFX::UNIXTimeToDateTime(const time_t* pWhen, NuDateTime *pDateTime) +{ + struct tm* ptm; + + assert(pWhen != nil); + assert(pDateTime != nil); + + ptm = localtime(pWhen); + pDateTime->second = ptm->tm_sec; + pDateTime->minute = ptm->tm_min; + pDateTime->hour = ptm->tm_hour; + pDateTime->day = ptm->tm_mday -1; + pDateTime->month = ptm->tm_mon; + pDateTime->year = ptm->tm_year; + pDateTime->extra = 0; + pDateTime->weekDay = ptm->tm_wday +1; +} + + +/* + * =========================================================================== + * DDD (DDD 2.1, DDD Pro) + * =========================================================================== + */ + +/* + * There really isn't a way to test if the file is a DDD archive, except + * to try to unpack it. One thing we can do fairly quickly is look for + * runs of repeated bytes, which will be impossible in a DDD file because + * we compress runs of repeated bytes with RLE. + */ +/*static*/ DIError +WrapperDDD::Test(GenericFD* pGFD, di_off_t wrappedLength) +{ + DIError dierr; + GenericFD* pNewGFD = nil; + WMSG0("Testing for DDD\n"); + + pGFD->Rewind(); + + dierr = CheckForRuns(pGFD); + if (dierr != kDIErrNone) + return dierr; + + dierr = Unpack(pGFD, &pNewGFD, nil); + delete pNewGFD; + return dierr; +} + +/* + * Load a bunch of data and check it for repeated byte sequences that + * would be removed by RLE. Runs of 4 bytes or longer should have been + * stripped out. DDD adds a couple of zeroes onto the end, so to avoid + * special cases we assume that a run of 5 is okay, and only flunk the + * data when it gets to 6. + * + * One big exception: the "favorites" table isn't run-length encoded, + * and if the track is nothing but zeroes the entire thing will be + * filled with 0xff. So we allow runs of 0xff bytes. + * + * PROBLEM: some sequences, such as repeated d5aa, can turn into what looks + * like a run of bytes in the output. We can't assume that arbitrary + * sequences of bytes won't be repeated. It does appear that we can assume + * that 00 bytes won't be repeated, so we can still scan for a series of + * zeroes and reject the image if found (which should clear us for all + * uncompressed formats and any compressed format with a padded file header). + * + * The goal is to detect uncompressed data sources. The test for DDD + * should come after other compressed data formats. + * + * For speed we crank the data in 8K at a time and don't correctly handle + * the boundaries. We do, however, need to avoid scanning the last 256 + * bytes of the file, because DOS DDD just fills it with junk, and it's + * possible that junk might have runs in it. + */ +/*static*/ DIError +WrapperDDD::CheckForRuns(GenericFD* pGFD) +{ + DIError dierr = kDIErrNone; + int kRunThreshold = 5; + unsigned char buf[8192]; + size_t bufCount; + int runLen; + di_off_t fileLen; + int i; + + dierr = pGFD->Seek(0, kSeekEnd); + if (dierr != kDIErrNone) + goto bail; + fileLen = pGFD->Tell(); + pGFD->Rewind(); + + fileLen -= 256; // could be extra data from DOS DDD + + while (fileLen) { + bufCount = (size_t) fileLen; + if (bufCount > sizeof(buf)) + bufCount = sizeof(buf); + fileLen -= bufCount; + + dierr = pGFD->Read(buf, bufCount); + if (dierr != kDIErrNone) + goto bail; + //WMSG1(" DDD READ %d bytes\n", bufCount); + if (dierr != kDIErrNone) { + WMSG1(" DDD CheckForRuns read failed (err=%d)\n", dierr); + return dierr; + } + + runLen = 0; + for (i = 1; i < (int) bufCount; i++) { + if (buf[i] == 0 && buf[i] == buf[i-1]) { + runLen++; + if (runLen == kRunThreshold && buf[i] != 0xff) { + WMSG2(" DDD found run of >= %d of 0x%02x, bailing\n", + runLen+1, buf[i]); + return kDIErrGeneric; + } + } else { + runLen = 0; + } + } + } + + WMSG0(" DDD CheckForRuns scan complete, no long runs found\n"); + +bail: + return dierr; +} + +/* + * Prepping is much the same as testing, but we fill in a few more details. + */ +DIError +WrapperDDD::Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, + di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, + DiskImg::SectorOrder* pOrder, short* pDiskVolNum, + LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD) +{ + DIError dierr; + WMSG0("Prepping for DDD\n"); + + assert(*ppNewGFD == nil); + + dierr = Unpack(pGFD, ppNewGFD, pDiskVolNum); + if (dierr != kDIErrNone) + return dierr; + + *pLength = kNumTracks * kTrackLen; + *pPhysical = DiskImg::kPhysicalFormatSectors; + *pOrder = DiskImg::kSectorOrderDOS; + + return dierr; +} + +/* + * Unpack a compressed disk image from "pGFD" to a new memory buffer + * created in "*ppNewGFD". + */ +/*static*/ DIError +WrapperDDD::Unpack(GenericFD* pGFD, GenericFD** ppNewGFD, short* pDiskVolNum) +{ + DIError dierr; + GFDBuffer* pNewGFD = nil; + unsigned char* buf = nil; + short diskVolNum; + + pGFD->Rewind(); + + buf = new unsigned char[kNumTracks * kTrackLen]; + if (buf == nil) { + dierr = kDIErrMalloc; + goto bail; + } + + pNewGFD = new GFDBuffer; + if (pNewGFD == nil) { + dierr = kDIErrMalloc; + goto bail; + } + dierr = pNewGFD->Open(buf, kNumTracks * kTrackLen, true, false, false); + if (dierr != kDIErrNone) + goto bail; + buf = nil; // now owned by pNewGFD; + + dierr = UnpackDisk(pGFD, pNewGFD, &diskVolNum); + if (dierr != kDIErrNone) + goto bail; + + if (pDiskVolNum != nil) + *pDiskVolNum = diskVolNum; + *ppNewGFD = pNewGFD; + pNewGFD = nil; // now owned by caller + +bail: + delete[] buf; + delete pNewGFD; + return dierr; +} + +/* + * Initialize stuff for a new file. There's no file header or other + * goodies, so we leave "pWrapperGFD" and "pWrappedLength" alone. + */ +DIError +WrapperDDD::Create(di_off_t length, DiskImg::PhysicalFormat physical, + DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, + di_off_t* pWrappedLength, GenericFD** pDataFD) +{ + assert(length == kNumTracks * kTrackLen); + assert(physical == DiskImg::kPhysicalFormatSectors); + assert(order == DiskImg::kSectorOrderDOS); + + DIError dierr; + unsigned char* buf = nil; + + /* + * Create a blank chunk of memory for the image. + */ + buf = new unsigned char[(int) length]; + if (buf == nil) { + dierr = kDIErrMalloc; + goto bail; + } + + GFDBuffer* pNewGFD; + pNewGFD = new GFDBuffer; + dierr = pNewGFD->Open(buf, length, true, false, false); + if (dierr != kDIErrNone) { + delete pNewGFD; + goto bail; + } + *pDataFD = pNewGFD; + buf = nil; // now owned by pNewGFD; + + // can't set *pWrappedLength yet + + if (dosVolumeNum != DiskImg::kVolumeNumNotSet) + fDiskVolumeNum = dosVolumeNum; + else + fDiskVolumeNum = kDefaultNibbleVolumeNum; + +bail: + delete[] buf; + return dierr; +} + +/* + * Compress the disk image. + */ +DIError +WrapperDDD::Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, + di_off_t dataLen, di_off_t* pWrappedLen) +{ + DIError dierr; + + assert(dataLen == kNumTracks * kTrackLen); + + pDataGFD->Rewind(); + + dierr = PackDisk(pDataGFD, pWrapperGFD, fDiskVolumeNum); + if (dierr != kDIErrNone) + return dierr; + + *pWrappedLen = pWrapperGFD->Tell(); + WMSG2(" DDD compressed from %d to %ld\n", + kNumTracks * kTrackLen, (long) *pWrappedLen); + + return kDIErrNone; +} + + +/* + * =========================================================================== + * DiskCopy (primarily a Mac format) + * =========================================================================== + */ + +/* + * DiskCopy 4.2 header, from FTN $e0/0005. + * + * All values are BIG-endian. + */ +const int kDC42NameLen = 64; +const int kDC42ChecksumOffset = 72; // where the checksum lives +const int kDC42DataOffset = 84; // header is always this long +const int kDC42PrivateMagic = 0x100; +const int kDC42FakeTagLen = 19200; // add a "fake" tag to match Mac + +typedef struct DiskImgLib::DC42Header { + char diskName[kDC42NameLen+1]; // from pascal string + long dataSize; // in bytes + long tagSize; + unsigned long dataChecksum; + unsigned long tagChecksum; + unsigned char diskFormat; // should be 1 for 800K + unsigned char formatByte; // should be $24, sometimes $22 + unsigned short privateWord; // must be 0x0100 + // userData begins at +84 + // tagData follows user data +} DC42Header; + +/* + * Dump the contents of a DC42Header. + */ +/*static*/ void +WrapperDiskCopy42::DumpHeader(const DC42Header* pHeader) +{ + WMSG0("--- header contents:\n"); + WMSG1("\tdiskName = '%s'\n", pHeader->diskName); + WMSG2("\tdataSize = %ld (%ldK)\n", pHeader->dataSize, + pHeader->dataSize / 1024); + WMSG1("\ttagSize = %ld\n", pHeader->tagSize); + WMSG1("\tdataChecksum = 0x%08lx\n", pHeader->dataChecksum); + WMSG1("\ttagChecksum = 0x%08lx\n", pHeader->tagChecksum); + WMSG1("\tdiskFormat = %d\n", pHeader->diskFormat); + WMSG1("\tformatByte = 0x%02x\n", pHeader->formatByte); + WMSG1("\tprivateWord = 0x%04x\n", pHeader->privateWord); +} + +/* + * Init a DC42 header for an 800K ProDOS disk. + */ +void +WrapperDiskCopy42::InitHeader(DC42Header* pHeader) +{ + memset(pHeader, 0, sizeof(*pHeader)); + if (fStorageName == NULL || strlen(fStorageName) == 0) + strcpy(pHeader->diskName, "-not a Macintosh disk"); + else + strcpy(pHeader->diskName, fStorageName); + pHeader->dataSize = 819200; + pHeader->tagSize = kDC42FakeTagLen; // emulate Mac behavior + pHeader->dataChecksum = 0xffffffff; // fixed during Flush + pHeader->tagChecksum = 0x00000000; // 19200 zeroes + pHeader->diskFormat = 1; + pHeader->formatByte = 0x24; + pHeader->privateWord = kDC42PrivateMagic; +} + +/* + * Read the header from a DC42 file and verify it. + * + * Returns 0 on success, -1 on error or invalid header. + */ +/*static*/ int +WrapperDiskCopy42::ReadHeader(GenericFD* pGFD, DC42Header* pHeader) +{ + unsigned char hdrBuf[kDC42DataOffset]; + + if (pGFD->Read(hdrBuf, kDC42DataOffset) != kDIErrNone) + return -1; + + // test the Pascal length byte + if (hdrBuf[0] >= kDC42NameLen) + return -1; + + memcpy(pHeader->diskName, &hdrBuf[1], hdrBuf[0]); + pHeader->diskName[hdrBuf[0]] = '\0'; + + pHeader->dataSize = GetLongBE(&hdrBuf[64]); + pHeader->tagSize = GetLongBE(&hdrBuf[68]); + pHeader->dataChecksum = GetLongBE(&hdrBuf[72]); + pHeader->tagChecksum = GetLongBE(&hdrBuf[76]); + pHeader->diskFormat = hdrBuf[80]; + pHeader->formatByte = hdrBuf[81]; + pHeader->privateWord = GetShortBE(&hdrBuf[82]); + + if (pHeader->dataSize != 800 * 1024 || + pHeader->diskFormat != 1 || + (pHeader->formatByte != 0x22 && pHeader->formatByte != 0x24) || + pHeader->privateWord != kDC42PrivateMagic) + { + return -1; + } + + return 0; +} + +/* + * Write the header for a DC42 file. + */ +DIError +WrapperDiskCopy42::WriteHeader(GenericFD* pGFD, const DC42Header* pHeader) +{ + unsigned char hdrBuf[kDC42DataOffset]; + + pGFD->Rewind(); + + memset(hdrBuf, 0, sizeof(hdrBuf)); + /* + * Disks created on a Mac include the null byte in the count; not sure + * if this applies to volume labels or just the "not a Macintosh disk" + * magic string. To be safe, we only increment it if it starts with '-'. + * (Need access to a Macintosh to test this.) + */ + hdrBuf[0] = strlen(pHeader->diskName); + if (pHeader->diskName[0] == '-' && hdrBuf[0] < (kDC42NameLen-1)) + hdrBuf[0]++; + memcpy(&hdrBuf[1], pHeader->diskName, hdrBuf[0]); + + PutLongBE(&hdrBuf[64], pHeader->dataSize); + PutLongBE(&hdrBuf[68], pHeader->tagSize); + PutLongBE(&hdrBuf[72], pHeader->dataChecksum); + PutLongBE(&hdrBuf[76], pHeader->tagChecksum); + hdrBuf[80] = pHeader->diskFormat; + hdrBuf[81] = pHeader->formatByte; + PutShortBE(&hdrBuf[82], pHeader->privateWord); + + return pGFD->Write(hdrBuf, kDC42DataOffset); +} + +/* + * Check to see if this is a DiskCopy 4.2 image. + * + * The format doesn't really have a magic number, but if we're stringent + * about our interpretation of some of the header fields (e.g. we only + * recognize 800K disks) we should be okay. + */ +/*static*/ DIError +WrapperDiskCopy42::Test(GenericFD* pGFD, di_off_t wrappedLength) +{ + DC42Header header; + + WMSG0("Testing for DiskCopy\n"); + + if (wrappedLength < 800 * 1024 + kDC42DataOffset) + return kDIErrGeneric; + + pGFD->Rewind(); + if (ReadHeader(pGFD, &header) != 0) + return kDIErrGeneric; + + DumpHeader(&header); + + return kDIErrNone; +} + +/* + * Compute the funky DiskCopy checksum. + * + * Position "pGFD" at the start of data. + */ +/*static*/ DIError +WrapperDiskCopy42::ComputeChecksum(GenericFD* pGFD, + unsigned long* pChecksum) +{ + DIError dierr = kDIErrNone; + unsigned char buf[512]; + long dataRem = 800 * 1024 /*pHeader->dataSize*/; + unsigned long checksum; + + assert(dataRem % sizeof(buf) == 0); + assert((sizeof(buf) & 0x01) == 0); // we take it two bytes at a time + + checksum = 0; + while (dataRem) { + int i; + + dierr = pGFD->Read(buf, sizeof(buf)); + if (dierr != kDIErrNone) { + WMSG2(" DC42 read failed, dataRem=%ld (err=%d)\n", dataRem, dierr); + return dierr; + } + + for (i = 0; i < (int) sizeof(buf); i += 2) { + unsigned short val = GetShortBE(buf+i); + + checksum += val; + if (checksum & 0x01) + checksum = checksum >> 1 | 0x80000000; + else + checksum = checksum >> 1; + } + + dataRem -= sizeof(buf); + } + + *pChecksum = checksum; + + return dierr; +} + +/* + * Prepare a DiskCopy image for use. + */ +DIError +WrapperDiskCopy42::Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, + di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, + DiskImg::SectorOrder* pOrder, short* pDiskVolNum, + LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD) +{ + DIError dierr; + DC42Header header; + + WMSG0("Prepping for DiskCopy 4.2\n"); + pGFD->Rewind(); + if (ReadHeader(pGFD, &header) != 0) + return kDIErrGeneric; + + /* + * Verify checksum. File should already be seeked to appropriate place. + */ + unsigned long checksum; + dierr = ComputeChecksum(pGFD, &checksum); + if (dierr != kDIErrNone) + return dierr; + + if (checksum != header.dataChecksum) { + WMSG2(" DC42 checksum mismatch (got 0x%08lx, expected 0x%08lx)\n", + checksum, header.dataChecksum); + fBadChecksum = true; + //return kDIErrBadChecksum; + } else { + WMSG0(" DC42 checksum matches!\n"); + } + + + /* looks good! */ + *pLength = header.dataSize; + *pPhysical = DiskImg::kPhysicalFormatSectors; + *pOrder = DiskImg::kSectorOrderProDOS; + + *ppNewGFD = new GFDGFD; + return ((GFDGFD*)*ppNewGFD)->Open(pGFD, kDC42DataOffset, readOnly); + +} + +/* + * Initialize fields for a new file. + */ +DIError +WrapperDiskCopy42::Create(di_off_t length, DiskImg::PhysicalFormat physical, + DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, + di_off_t* pWrappedLength, GenericFD** pDataFD) +{ + DIError dierr; + DC42Header header; + + assert(length == 800 * 1024); + assert(physical == DiskImg::kPhysicalFormatSectors); + //assert(order == DiskImg::kSectorOrderProDOS); + + InitHeader(&header); // set all but checksum + + dierr = WriteHeader(pWrapperGFD, &header); + if (dierr != kDIErrNone) { + WMSG1("ERROR: 2MG header write failed (err=%d)\n", dierr); + return dierr; + } + + *pWrappedLength = length + kDC42DataOffset; + *pDataFD = new GFDGFD; + return ((GFDGFD*)*pDataFD)->Open(pWrapperGFD, kDC42DataOffset, false); +} + +/* + * We only use GFDGFD, so there's no data to write. However, we do need + * to update the checksum, and append our "fake" tag section. + */ +DIError +WrapperDiskCopy42::Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, + di_off_t dataLen, di_off_t* pWrappedLen) +{ + DIError dierr; + unsigned long checksum; + + /* compute the data checksum */ + dierr = pWrapperGFD->Seek(kDC42DataOffset, kSeekSet); + if (dierr != kDIErrNone) + goto bail; + + dierr = ComputeChecksum(pWrapperGFD, &checksum); + if (dierr != kDIErrNone) { + WMSG1(" DC42 failed while computing checksum (err=%d)\n", dierr); + goto bail; + } + + /* write it into the wrapper */ + dierr = pWrapperGFD->Seek(kDC42ChecksumOffset, kSeekSet); + if (dierr != kDIErrNone) + goto bail; + + dierr = WriteLongBE(pWrapperGFD, checksum); + if (dierr != kDIErrNone) + goto bail; + + /* add the tag bytes */ + dierr = pWrapperGFD->Seek(kDC42DataOffset + 800*1024, kSeekSet); + char* tmpBuf; + tmpBuf = new char[kDC42FakeTagLen]; + if (tmpBuf == nil) + return kDIErrMalloc; + memset(tmpBuf, 0, kDC42FakeTagLen); + dierr = pWrapperGFD->Write(tmpBuf, kDC42FakeTagLen, nil); + delete[] tmpBuf; + if (dierr != kDIErrNone) + goto bail; + +bail: + return dierr; +} + + +/* + * =========================================================================== + * Sim2eHDV (Sim2e virtual hard-drive images) + * =========================================================================== + */ + +/* +// mkhdv.c +// +// Create a Hard Disk Volume File (.HDV) for simIIe +static int mkhdv(FILE *op, uint blocks) +{ + byte sector[512]; + byte data[15]; + uint i; + + memset(data, 0, sizeof(data)); + memcpy(data, "SIMSYSTEM_HDV", 13); + data[13] = (blocks & 0xff); + data[14] = (blocks & 0xff00) >> 8; + fwrite(data, 1, sizeof(data), op); + + memset(sector, 0, sizeof(sector)); + for (i = 0; i < blocks; i++) + fwrite(sector, 1, sizeof(sector), op); + return 0; +} +*/ + +const int kSim2eHeaderLen = 15; +static const char* kSim2eID = "SIMSYSTEM_HDV"; + +/* + * Test for a virtual hard-drive image. This is either a "raw" unadorned + * image, or one with a 15-byte "SimIIe" header on it. + */ +DIError +WrapperSim2eHDV::Test(GenericFD* pGFD, di_off_t wrappedLength) +{ + char buf[kSim2eHeaderLen]; + + WMSG0("Testing for Sim2e HDV\n"); + + if (wrappedLength < 512 || + ((wrappedLength - kSim2eHeaderLen) % 4096) != 0) + { + return kDIErrGeneric; + } + + pGFD->Rewind(); + + if (pGFD->Read(buf, sizeof(buf)) != kDIErrNone) + return kDIErrGeneric; + + if (strncmp(buf, kSim2eID, strlen(kSim2eID)) == 0) + return kDIErrNone; + else + return kDIErrGeneric; +} + +/* + * These are always ProDOS volumes. + */ +DIError +WrapperSim2eHDV::Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, + di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, + DiskImg::SectorOrder* pOrder, short* pDiskVolNum, + LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD) +{ + *pLength = wrappedLength - kSim2eHeaderLen; + *pPhysical = DiskImg::kPhysicalFormatSectors; + *pOrder = DiskImg::kSectorOrderProDOS; + + *ppNewGFD = new GFDGFD; + return ((GFDGFD*)*ppNewGFD)->Open(pGFD, kSim2eHeaderLen, readOnly); +} + +/* + * Initialize fields for a new file. + */ +DIError +WrapperSim2eHDV::Create(di_off_t length, DiskImg::PhysicalFormat physical, + DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, + di_off_t* pWrappedLength, GenericFD** pDataFD) +{ + unsigned char header[kSim2eHeaderLen]; + long blocks = (long) (length / 512); + + assert(physical == DiskImg::kPhysicalFormatSectors); + assert(order == DiskImg::kSectorOrderProDOS); + + if (blocks < 4 || blocks > 65536) { + WMSG1(" Sim2e invalid blocks %ld\n", blocks); + return kDIErrInvalidArg; + } + if (blocks == 65536) // 32MB volumes are actually 31.9 + blocks = 65535; + + memcpy(header, kSim2eID, strlen(kSim2eID)); + header[13] = (unsigned char) blocks; + header[14] = (unsigned char) ((blocks & 0xff00) >> 8); + DIError dierr = pWrapperGFD->Write(header, kSim2eHeaderLen); + if (dierr != kDIErrNone) { + WMSG1(" Sim2eHDV header write failed (err=%d)\n", dierr); + return dierr; + } + + *pWrappedLength = length + kSim2eHeaderLen; + + *pDataFD = new GFDGFD; + return ((GFDGFD*)*pDataFD)->Open(pWrapperGFD, kSim2eHeaderLen, false); +} + +/* + * We only use GFDGFD, so there's nothing to do here. + */ +DIError +WrapperSim2eHDV::Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, + di_off_t dataLen, di_off_t* pWrappedLen) +{ + return kDIErrNone; +} + + +/* + * =========================================================================== + * TrackStar .app images + * =========================================================================== + */ + +/* + * File format: + * $0000 track 0 data + * $1a00 track 1 data + * $3400 track 2 data + * ... + * $3f600 track 39 data + * + * Each track consists of: + * $0000 Text description of disk contents (same on every track), in low + * ASCII, padded out with spaces ($20) + * $002e Start of zeroed-out header field + * $0080 $00 (indicates end of data when reading from end??) + * $0081 Raw nibble data (hi bit set), written backwards + * $19fe Start offset of track data + * + * Take the start offset, add 128, and walk backward until you find a + * value with the high bit clear. If the start offset is zero, start + * scanning from $19fd backward. (This approach courtesty Gerald Ryckman.) + * + * My take: the "offset" actually indicates the length of data, and the + * $00 is there to simplify somebody's algorithm. If the offset is zero + * it means the track couldn't be analyzed successfully, so a raw dump has + * been provided. Tracks 35-39 on most Apple II disks have zero length, + * but occasionally one analyzes "successfully" with some horribly truncated + * length. + * + * I'm going to assert that byte $81 be zero and that nothing else has the + * high bit clear until you hit the end of valid data. + * + * Because the nibbles are stored in reverse order, it's easiest to unpack + * the tracks to local buffers, then re-pack them when saving the file. + */ + +/* + * Test to see if this is a TrackStar 5.25" disk image. + * + * While the image format supports variable-length nibble tracks, it uses + * fixed-length fields to store them. Each track is stored in 6656 bytes, + * but has a 129-byte header and a 2-byte footer (max of 6525). + * + * Images may be 40-track (5.25") or 80-track (5.25" disk with half-track + * stepping). The latter is useful in some circumstances for handling + * copy-protected disks. We don't have a half-track interface, so we just + * ignore the odd-numbered tracks. + * + * There is currently no way for the API to set the number of tracks. + */ +/*static*/ DIError +WrapperTrackStar::Test(GenericFD* pGFD, di_off_t wrappedLength) +{ + DIError dierr = kDIErrNone; + WMSG0("Testing for TrackStar\n"); + int numTracks; + + /* check the length */ + if (wrappedLength == 6656*40) + numTracks = 40; + else if (wrappedLength == 6656*80) + numTracks = 80; + else + return kDIErrGeneric; + + WMSG1(" Checking for %d-track image\n", numTracks); + + /* verify each track */ + unsigned char trackBuf[kFileTrackStorageLen]; + pGFD->Rewind(); + for (int trk = 0; trk < numTracks; trk++) { + dierr = pGFD->Read(trackBuf, sizeof(trackBuf)); + if (dierr != kDIErrNone) + goto bail; + dierr = VerifyTrack(trk, trackBuf); + if (dierr != kDIErrNone) + goto bail; + } + WMSG0(" TrackStar tracks verified\n"); + +bail: + return dierr; +} + +/* + * Check the format. + */ +/*static*/ DIError +WrapperTrackStar::VerifyTrack(int track, const unsigned char* trackBuf) +{ + unsigned int dataLen; + + if (trackBuf[0x80] != 0) { + WMSG1(" TrackStar track=%d found nonzero at 129\n", track); + return kDIErrGeneric; + } + + dataLen = GetShortLE(trackBuf + 0x19fe); + if (dataLen > kMaxTrackLen) { + WMSG3(" TrackStar track=%d len=%d exceeds max (%d)\n", + track, dataLen, kMaxTrackLen); + return kDIErrGeneric; + } + if (dataLen == 0) + dataLen = kMaxTrackLen; + + unsigned int i; + for (i = 0; i < dataLen; i++) { + if ((trackBuf[0x81 + i] & 0x80) == 0) { + WMSG3(" TrackStar track=%d found invalid data 0x%02x at %d\n", + track, trackBuf[0x81+i], i); + return kDIErrGeneric; + } + } + + if (track == 0) { + WMSG1(" TrackStar msg='%s'\n", trackBuf); + } + + return kDIErrNone; +} + +/* + * Fill in some details. + */ +DIError +WrapperTrackStar::Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, + di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, + DiskImg::SectorOrder* pOrder, short* pDiskVolNum, + LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD) +{ + WMSG0("Prepping for TrackStar\n"); + DIError dierr = kDIErrNone; + + if (wrappedLength == kFileTrackStorageLen * 40) + fImageTracks = 40; + else if (wrappedLength == kFileTrackStorageLen * 80) + fImageTracks = 80; + else + return kDIErrInternal; + + dierr = Unpack(pGFD, ppNewGFD); + if (dierr != kDIErrNone) + return dierr; + + *pLength = kTrackStarNumTracks * kTrackAllocSize; + *pPhysical = DiskImg::kPhysicalFormatNib525_Var; + *pOrder = DiskImg::kSectorOrderPhysical; + + return dierr; +} + +/* + * Unpack reverse-order nibbles from "pGFD" to a new memory buffer + * created in "*ppNewGFD". + */ +DIError +WrapperTrackStar::Unpack(GenericFD* pGFD, GenericFD** ppNewGFD) +{ + DIError dierr; + GFDBuffer* pNewGFD = nil; + unsigned char* buf = nil; + + pGFD->Rewind(); + + buf = new unsigned char[kTrackStarNumTracks * kTrackAllocSize]; + if (buf == nil) { + dierr = kDIErrMalloc; + goto bail; + } + + pNewGFD = new GFDBuffer; + if (pNewGFD == nil) { + dierr = kDIErrMalloc; + goto bail; + } + dierr = pNewGFD->Open(buf, kTrackStarNumTracks * kTrackAllocSize, + true, false, false); + if (dierr != kDIErrNone) + goto bail; + buf = nil; // now owned by pNewGFD; + + dierr = UnpackDisk(pGFD, pNewGFD); + if (dierr != kDIErrNone) + goto bail; + + *ppNewGFD = pNewGFD; + pNewGFD = nil; // now owned by caller + +bail: + delete[] buf; + delete pNewGFD; + return dierr; +} + +/* + * Unpack a TrackStar image. This is mainly just copying bytes around. The + * nibble code is perfectly happy with odd-sized tracks. However, we want + * to be able to find a particular track without having to do a lookup. So, + * we just block out 40 sets of 6656-byte tracks. + * + * The resultant image will always have 40 tracks. On an 80-track image + * we skip the odd ones. + * + * The bytes are stored in reverse order, so we need to unpack them to a + * separate buffer. + * + * This fills out "fNibbleTrackInfo". + */ +DIError +WrapperTrackStar::UnpackDisk(GenericFD* pGFD, GenericFD* pNewGFD) +{ + DIError dierr = kDIErrNone; + unsigned char inBuf[kFileTrackStorageLen]; + unsigned char outBuf[kTrackAllocSize]; + int i, trk; + + assert(kTrackStarNumTracks <= kMaxNibbleTracks525); + + pGFD->Rewind(); + pNewGFD->Rewind(); + + /* we don't currently support half-tracks */ + fNibbleTrackInfo.numTracks = kTrackStarNumTracks; + for (trk = 0; trk < kTrackStarNumTracks; trk++) { + unsigned int dataLen; + + fNibbleTrackInfo.offset[trk] = trk * kTrackAllocSize; + + /* these were verified earlier, so assume data is okay */ + dierr = pGFD->Read(inBuf, sizeof(inBuf)); + if (dierr != kDIErrNone) + goto bail; + + dataLen = GetShortLE(inBuf + 0x19fe); + if (dataLen == 0) + dataLen = kMaxTrackLen; + assert(dataLen <= kMaxTrackLen); + assert(dataLen <= sizeof(outBuf)); + + fNibbleTrackInfo.length[trk] = dataLen; + + memset(outBuf, 0x11, sizeof(outBuf)); + for (i = 0; i < (int) dataLen; i++) + outBuf[i] = inBuf[128+dataLen-i]; + + pNewGFD->Write(outBuf, sizeof(outBuf)); + + if (fImageTracks == 2*kTrackStarNumTracks) { + /* skip the odd-numbered tracks */ + dierr = pGFD->Read(inBuf, sizeof(inBuf)); + if (dierr != kDIErrNone) + goto bail; + } + } + +bail: + return dierr; +} + + +/* + * Initialize stuff for a new file. There's no file header or other + * goodies, so we leave "pWrapperGFD" and "pWrappedLength" alone. + */ +DIError +WrapperTrackStar::Create(di_off_t length, DiskImg::PhysicalFormat physical, + DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, + di_off_t* pWrappedLength, GenericFD** pDataFD) +{ + assert(length == kTrackLenTrackStar525 * kTrackCount525 || + length == kTrackLenTrackStar525 * kTrackStarNumTracks); + assert(physical == DiskImg::kPhysicalFormatNib525_Var); + assert(order == DiskImg::kSectorOrderPhysical); + + DIError dierr; + unsigned char* buf = nil; + int numTracks = (int) (length / kTrackLenTrackStar525); + int i; + + /* + * Set up the track offset and length table. We use the maximum + * data length (kTrackLenTrackStar525) for each. The nibble write + * routine will alter the length field as appropriate. + */ + fNibbleTrackInfo.numTracks = numTracks; + assert(fNibbleTrackInfo.numTracks <= kMaxNibbleTracks525); + for (i = 0; i < numTracks; i++) { + fNibbleTrackInfo.offset[i] = kTrackLenTrackStar525 * i; + fNibbleTrackInfo.length[i] = kTrackLenTrackStar525; + } + + /* + * Create a blank chunk of memory for the image. + */ + buf = new unsigned char[(int) length]; + if (buf == nil) { + dierr = kDIErrMalloc; + goto bail; + } + + GFDBuffer* pNewGFD; + pNewGFD = new GFDBuffer; + dierr = pNewGFD->Open(buf, length, true, false, false); + if (dierr != kDIErrNone) { + delete pNewGFD; + goto bail; + } + *pDataFD = pNewGFD; + buf = nil; // now owned by pNewGFD; + + // can't set *pWrappedLength yet + +bail: + delete[] buf; + return dierr; +} + +/* + * Write the stored data into TrackStar format. + * + * The source data is in "pDataGFD" in a layout described by fNibbleTrackInfo. + * We need to create the new file in "pWrapperGFD". + */ +DIError +WrapperTrackStar::Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, + di_off_t dataLen, di_off_t* pWrappedLen) +{ + DIError dierr = kDIErrNone; + + assert(dataLen == kTrackLenTrackStar525 * kTrackCount525 || + dataLen == kTrackLenTrackStar525 * kTrackStarNumTracks); + assert(kTrackLenTrackStar525 <= kMaxTrackLen); + + pDataGFD->Rewind(); + + unsigned char writeBuf[kFileTrackStorageLen]; + unsigned char dataBuf[kTrackLenTrackStar525]; + int track, trackLen; + + for (track = 0; track < kTrackStarNumTracks; track++) { + if (track < fNibbleTrackInfo.numTracks) { + dierr = pDataGFD->Read(dataBuf, kTrackLenTrackStar525); + if (dierr != kDIErrNone) + goto bail; + trackLen = fNibbleTrackInfo.length[track]; + assert(fNibbleTrackInfo.offset[track] == kTrackLenTrackStar525 * track); + } else { + WMSG1(" TrackStar faking track %d\n", track); + memset(dataBuf, 0xff, sizeof(dataBuf)); + trackLen = kMaxTrackLen; + } + + memset(writeBuf, 0x80, sizeof(writeBuf)); // not strictly necessary + memset(writeBuf, 0x20, kCommentFieldLen); + memset(writeBuf+kCommentFieldLen, 0x00, 0x81-kCommentFieldLen); + + const char* comment; + if (fStorageName != NULL && *fStorageName != '\0') + comment = fStorageName; + else + comment = "(created by CiderPress)"; + if (strlen(comment) > kCommentFieldLen) + memcpy(writeBuf, comment, kCommentFieldLen); + else + memcpy(writeBuf, comment, strlen(comment)); + + int i; + for (i = 0; i < trackLen; i++) { + // If we write a value here with the high bit clear, we will + // reject the file when we try to open it. So, we force the + // high bit on here, on the assumption that the nibble data + // we've been handled is otherwise good. + //writeBuf[0x81+i] = dataBuf[trackLen - i -1]; + writeBuf[0x81+i] = dataBuf[trackLen - i -1] | 0x80; + } + + if (trackLen == kMaxTrackLen) + PutShortLE(writeBuf + 0x19fe, 0); + else + PutShortLE(writeBuf + 0x19fe, (unsigned short) trackLen); + + dierr = pWrapperGFD->Write(writeBuf, sizeof(writeBuf)); + if (dierr != kDIErrNone) + goto bail; + } + + *pWrappedLen = pWrapperGFD->Tell(); + assert(*pWrappedLen == kFileTrackStorageLen * kTrackStarNumTracks); + +bail: + return dierr; +} + +void +WrapperTrackStar::SetNibbleTrackLength(int track, int length) +{ + assert(track >= 0); + assert(length > 0 && length <= kMaxTrackLen); + assert(track < fNibbleTrackInfo.numTracks); + + WMSG2(" TrackStar: set length of track %d to %d\n", track, length); + fNibbleTrackInfo.length[track] = length; +} + + +/* + * =========================================================================== + * FDI (Formatted Disk Image) format + * =========================================================================== + */ + +/* + * The format is described in detail in documents on the "disk2fdi" web site. + * + * FDI is currently unique in that it can (and often does) store nibble + * images of 3.5" disks. Rather than add an understanding of nibblized + * 3.5" disks to DiskImg, I've chosen to present it as a simple 800K + * ProDOS disk image. The only flaw in the scheme is that we have to + * keep track of the bad blocks in a parallel data structure. + */ + +/*static*/ const char* WrapperFDI::kFDIMagic = "Formatted Disk Image file\r\n"; + +/* + * Test to see if this is an FDI disk image. + */ +/*static*/ DIError +WrapperFDI::Test(GenericFD* pGFD, di_off_t wrappedLength) +{ + DIError dierr = kDIErrNone; + unsigned char headerBuf[kMinHeaderLen]; + FDIHeader hdr; + + WMSG0("Testing for FDI\n"); + + pGFD->Rewind(); + dierr = pGFD->Read(headerBuf, sizeof(headerBuf)); + if (dierr != kDIErrNone) + goto bail; + + UnpackHeader(headerBuf, &hdr); + if (strcmp(hdr.signature, kFDIMagic) != 0) { + WMSG0("FDI: FDI signature not found\n"); + return kDIErrGeneric; + } + if (hdr.version < kMinVersion) { + WMSG1("FDI: bad version 0x%.04x\n", hdr.version); + return kDIErrGeneric; + } + +bail: + return dierr; +} + +/* + * Unpack a 512-byte buffer with the FDI header into its components. + */ +/*static*/ void +WrapperFDI::UnpackHeader(const unsigned char* headerBuf, FDIHeader* pHdr) +{ + memcpy(pHdr->signature, &headerBuf[0], kSignatureLen); + pHdr->signature[kSignatureLen] = '\0'; + memcpy(pHdr->creator, &headerBuf[27], kCreatorLen); + pHdr->creator[kCreatorLen] = '\0'; + memcpy(pHdr->comment, &headerBuf[59], kCommentLen); + pHdr->comment[kCommentLen] = '\0'; + + pHdr->version = GetShortBE(&headerBuf[140]); + pHdr->lastTrack = GetShortBE(&headerBuf[142]); + pHdr->lastHead = headerBuf[144]; + pHdr->type = headerBuf[145]; + pHdr->rotSpeed = headerBuf[146]; + pHdr->flags = headerBuf[147]; + pHdr->tpi = headerBuf[148]; + pHdr->headWidth = headerBuf[149]; + pHdr->reserved = GetShortBE(&headerBuf[150]); +} + +/* + * Dump the contents of an FDI header. + */ +/*static*/ void +WrapperFDI::DumpHeader(const FDIHeader* pHdr) +{ + static const char* kTypes[] = { + "8\"", "5.25\"", "3.5\"", "3\"" + }; + static const char* kTPI[] = { + "48", "67", "96", "100", "135", "192" + }; + + WMSG0(" FDI header contents:\n"); + WMSG1(" signature: '%s'\n", pHdr->signature); + WMSG1(" creator : '%s'\n", pHdr->creator); + WMSG1(" comment : '%s'\n", pHdr->comment); + WMSG2(" version : %d.%d\n", pHdr->version >> 8, pHdr->version & 0xff); + WMSG1(" lastTrack: %d\n", pHdr->lastTrack); + WMSG1(" lastHead : %d\n", pHdr->lastHead); + WMSG2(" type : %d (%s)\n", pHdr->type, + (/*pHdr->type >= 0 &&*/ pHdr->type < NELEM(kTypes)) ? + kTypes[pHdr->type] : "???"); + WMSG1(" rotSpeed : %drpm\n", pHdr->rotSpeed + 128); + WMSG1(" flags : 0x%02x\n", pHdr->flags); + WMSG2(" tpi : %d (%s)\n", pHdr->tpi, + (/*pHdr->tpi >= 0 &&*/ pHdr->tpi < NELEM(kTPI)) ? + kTPI[pHdr->tpi] : "???"); + WMSG2(" headWidth: %d (%s)\n", pHdr->headWidth, + (/*pHdr->headWidth >= 0 &&*/ pHdr->headWidth < NELEM(kTPI)) ? + kTPI[pHdr->headWidth] : "???"); + WMSG1(" reserved : %d\n", pHdr->reserved); +} + +/* + * Unpack the disk to heap storage. + */ +DIError +WrapperFDI::Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, + di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, + DiskImg::SectorOrder* pOrder, short* pDiskVolNum, + LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD) +{ + WMSG0("Prepping for FDI\n"); + DIError dierr = kDIErrNone; + FDIHeader hdr; + + pGFD->Rewind(); + dierr = pGFD->Read(fHeaderBuf, sizeof(fHeaderBuf)); + if (dierr != kDIErrNone) + goto bail; + + UnpackHeader(fHeaderBuf, &hdr); + if (strcmp(hdr.signature, kFDIMagic) != 0) + return kDIErrGeneric; + DumpHeader(&hdr); + + /* + * There are two formats that we're interested in, 3.5" and 5.25". They + * are handled differently within CiderPress, so we split here. + * + * Sometimes disk2fdi finds extra tracks. No Apple II hardware ever + * went past 40 on 5.25" disks, but we'll humor the software and allow + * images with up to 50. Ditto for 3.5" disks, which should always + * have 80 double-sided tracks. + */ + if (hdr.type == kDiskType525) { + WMSG0("FDI: decoding 5.25\" disk\n"); + if (hdr.lastHead != 0 || hdr.lastTrack >= kMaxNibbleTracks525 + 10) { + WMSG2("FDI: bad params head=%d ltrack=%d\n", + hdr.lastHead, hdr.lastTrack); + dierr = kDIErrUnsupportedImageFeature; + goto bail; + } + if (hdr.lastTrack >= kMaxNibbleTracks525) { + WMSG2("FDI: reducing hdr.lastTrack from %d to %d\n", + hdr.lastTrack, kMaxNibbleTracks525-1); + hdr.lastTrack = kMaxNibbleTracks525-1; + } + + /* + * Unpack to a series of variable-length nibble tracks. The data + * goes into ppNewGFD, and a table of track info goes into + * fNibbleTrackInfo. + */ + dierr = Unpack525(pGFD, ppNewGFD, hdr.lastTrack+1, hdr.lastHead+1); + if (dierr != kDIErrNone) + return dierr; + + *pLength = kMaxNibbleTracks525 * kTrackAllocSize; + *pPhysical = DiskImg::kPhysicalFormatNib525_Var; + *pOrder = DiskImg::kSectorOrderPhysical; + } else if (hdr.type == kDiskType35) { + WMSG0("FDI: decoding 3.5\" disk\n"); + if (hdr.lastHead != 1 || hdr.lastTrack >= kMaxNibbleTracks35 + 10) { + WMSG2("FDI: bad params head=%d ltrack=%d\n", + hdr.lastHead, hdr.lastTrack); + dierr = kDIErrUnsupportedImageFeature; + goto bail; + } + if (hdr.lastTrack >= kMaxNibbleTracks35) { + WMSG2("FDI: reducing hdr.lastTrack from %d to %d\n", + hdr.lastTrack, kMaxNibbleTracks35-1); + hdr.lastTrack = kMaxNibbleTracks35-1; + } + + /* + * Unpack to 800K of 512-byte ProDOS-order blocks, with a + * "bad block" map. + */ + dierr = Unpack35(pGFD, ppNewGFD, hdr.lastTrack+1, hdr.lastHead+1, + ppBadBlockMap); + if (dierr != kDIErrNone) + return dierr; + + *pLength = 800 * 1024; + *pPhysical = DiskImg::kPhysicalFormatSectors; + *pOrder = DiskImg::kSectorOrderProDOS; + } else { + WMSG0("FDI: unsupported disk type\n"); + dierr = kDIErrUnsupportedImageFeature; + goto bail; + } + +bail: + return dierr; +} + +/* + * Unpack pulse timing values to nibbles. + */ +DIError +WrapperFDI::Unpack525(GenericFD* pGFD, GenericFD** ppNewGFD, int numCyls, + int numHeads) +{ + DIError dierr = kDIErrNone; + GFDBuffer* pNewGFD = nil; + unsigned char* buf = nil; + int numTracks; + + numTracks = numCyls * numHeads; + if (numTracks < kMaxNibbleTracks525) + numTracks = kMaxNibbleTracks525; + + pGFD->Rewind(); + + buf = new unsigned char[numTracks * kTrackAllocSize]; + if (buf == nil) { + dierr = kDIErrMalloc; + goto bail; + } + + pNewGFD = new GFDBuffer; + if (pNewGFD == nil) { + dierr = kDIErrMalloc; + goto bail; + } + dierr = pNewGFD->Open(buf, numTracks * kTrackAllocSize, + true, false, false); + if (dierr != kDIErrNone) + goto bail; + buf = nil; // now owned by pNewGFD; + + dierr = UnpackDisk525(pGFD, pNewGFD, numCyls, numHeads); + if (dierr != kDIErrNone) + goto bail; + + *ppNewGFD = pNewGFD; + pNewGFD = nil; // now owned by caller + +bail: + delete[] buf; + delete pNewGFD; + return dierr; +} + +/* + * Unpack pulse timing values to fully-decoded blocks. + */ +DIError +WrapperFDI::Unpack35(GenericFD* pGFD, GenericFD** ppNewGFD, int numCyls, + int numHeads, LinearBitmap** ppBadBlockMap) +{ + DIError dierr = kDIErrNone; + GFDBuffer* pNewGFD = nil; + unsigned char* buf = nil; + + pGFD->Rewind(); + + buf = new unsigned char[800 * 1024]; + if (buf == nil) { + dierr = kDIErrMalloc; + goto bail; + } + + pNewGFD = new GFDBuffer; + if (pNewGFD == nil) { + dierr = kDIErrMalloc; + goto bail; + } + dierr = pNewGFD->Open(buf, 800 * 1024, true, false, false); + if (dierr != kDIErrNone) + goto bail; + buf = nil; // now owned by pNewGFD; + + *ppBadBlockMap = new LinearBitmap(1600); + if (*ppBadBlockMap == nil) { + dierr = kDIErrMalloc; + goto bail; + } + + dierr = UnpackDisk35(pGFD, pNewGFD, numCyls, numHeads, *ppBadBlockMap); + if (dierr != kDIErrNone) + goto bail; + + *ppNewGFD = pNewGFD; + pNewGFD = nil; // now owned by caller + +bail: + delete[] buf; + delete pNewGFD; + return dierr; +} + +/* + * Initialize stuff for a new file. There's no file header or other + * goodies, so we leave "pWrapperGFD" and "pWrappedLength" alone. + */ +DIError +WrapperFDI::Create(di_off_t length, DiskImg::PhysicalFormat physical, + DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, + di_off_t* pWrappedLength, GenericFD** pDataFD) +{ + DIError dierr = kDIErrGeneric; // not yet +#if 0 + unsigned char* buf = nil; + int numTracks = (int) (length / kTrackLenTrackStar525); + int i; + + assert(length == kTrackLenTrackStar525 * kTrackCount525 || + length == kTrackLenTrackStar525 * kTrackStarNumTracks); + assert(physical == DiskImg::kPhysicalFormatNib525_Var); + assert(order == DiskImg::kSectorOrderPhysical); + + /* + * Set up the track offset and length table. We use the maximum + * data length (kTrackLenTrackStar525) for each. The nibble write + * routine will alter the length field as appropriate. + */ + fNibbleTrackInfo.numTracks = numTracks; + assert(fNibbleTrackInfo.numTracks <= kMaxNibbleTracks); + for (i = 0; i < numTracks; i++) { + fNibbleTrackInfo.offset[i] = kTrackLenTrackStar525 * i; + fNibbleTrackInfo.length[i] = kTrackLenTrackStar525; + } + + /* + * Create a blank chunk of memory for the image. + */ + buf = new unsigned char[(int) length]; + if (buf == nil) { + dierr = kDIErrMalloc; + goto bail; + } + + GFDBuffer* pNewGFD; + pNewGFD = new GFDBuffer; + dierr = pNewGFD->Open(buf, length, true, false, false); + if (dierr != kDIErrNone) { + delete pNewGFD; + goto bail; + } + *pDataFD = pNewGFD; + buf = nil; // now owned by pNewGFD; + + // can't set *pWrappedLength yet + +bail: + delete[] buf; +#endif + return dierr; +} + +/* + * Write the stored data into FDI format. + * + * The source data is in "pDataGFD" in a layout described by fNibbleTrackInfo. + * We need to create the new file in "pWrapperGFD". + */ +DIError +WrapperFDI::Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, + di_off_t dataLen, di_off_t* pWrappedLen) +{ + DIError dierr = kDIErrGeneric; // not yet + +#if 0 + assert(dataLen == kTrackLenTrackStar525 * kTrackCount525 || + dataLen == kTrackLenTrackStar525 * kTrackStarNumTracks); + assert(kTrackLenTrackStar525 <= kMaxTrackLen); + + pDataGFD->Rewind(); + + unsigned char writeBuf[kFileTrackStorageLen]; + unsigned char dataBuf[kTrackLenTrackStar525]; + int track, trackLen; + + for (track = 0; track < kTrackStarNumTracks; track++) { + if (track < fNibbleTrackInfo.numTracks) { + dierr = pDataGFD->Read(dataBuf, kTrackLenTrackStar525); + if (dierr != kDIErrNone) + goto bail; + trackLen = fNibbleTrackInfo.length[track]; + assert(fNibbleTrackInfo.offset[track] == kTrackLenTrackStar525 * track); + } else { + WMSG1(" TrackStar faking track %d\n", track); + memset(dataBuf, 0xff, sizeof(dataBuf)); + trackLen = kMaxTrackLen; + } + + memset(writeBuf, 0x80, sizeof(writeBuf)); // not strictly necessary + memset(writeBuf, 0x20, kCommentFieldLen); + memset(writeBuf+kCommentFieldLen, 0x00, 0x81-kCommentFieldLen); + + const char* comment; + if (fStorageName != NULL && *fStorageName != '\0') + comment = fStorageName; + else + comment = "(created by CiderPress)"; + if (strlen(comment) > kCommentFieldLen) + memcpy(writeBuf, comment, kCommentFieldLen); + else + memcpy(writeBuf, comment, strlen(comment)); + + int i; + for (i = 0; i < trackLen; i++) + writeBuf[0x81+i] = dataBuf[trackLen - i -1]; + + if (trackLen == kMaxTrackLen) + PutShortLE(writeBuf + 0x19fe, 0); + else + PutShortLE(writeBuf + 0x19fe, (unsigned short) trackLen); + + dierr = pWrapperGFD->Write(writeBuf, sizeof(writeBuf)); + if (dierr != kDIErrNone) + goto bail; + } + + *pWrappedLen = pWrapperGFD->Tell(); + assert(*pWrappedLen == kFileTrackStorageLen * kTrackStarNumTracks); + +bail: +#endif + return dierr; +} + +void +WrapperFDI::SetNibbleTrackLength(int track, int length) +{ + assert(false); // not yet +#if 0 + assert(track >= 0); + assert(length > 0 && length <= kMaxTrackLen); + assert(track < fNibbleTrackInfo.numTracks); + + WMSG2(" FDI: set length of track %d to %d\n", track, length); + fNibbleTrackInfo.length[track] = length; +#endif +} + + +/* + * =========================================================================== + * Unadorned nibble format + * =========================================================================== + */ + +/* + * See if this is unadorned nibble format. + */ +/*static*/ DIError +WrapperUnadornedNibble::Test(GenericFD* pGFD, di_off_t wrappedLength) +{ + WMSG0("Testing for unadorned nibble\n"); + + /* test length */ + if (wrappedLength != kTrackCount525 * kTrackLenNib525 && + wrappedLength != kTrackCount525 * kTrackLenNb2525) + { + return kDIErrGeneric; + } + + /* quick scan for invalid data */ + const int kScanSize = 512; + unsigned char buf[kScanSize]; + + pGFD->Rewind(); + if (pGFD->Read(buf, sizeof(buf)) != kDIErrNone) + return kDIErrGeneric; + + /* + * Make sure this is a nibble image and not just a ProDOS volume that + * happened to get the right number of blocks. The primary test is + * for < 0x80 since there's no way that can be valid, even on a track + * full of junk. + */ + for (int i = 0; i < kScanSize; i++) { + if (buf[i] < 0x80) { + WMSG2(" Disqualifying len=%ld from nibble, byte=0x%02x\n", + (long) wrappedLength, buf[i]); + return kDIErrGeneric; + } else if (buf[i] < 0x96) { + WMSG1(" Warning: funky byte 0x%02x in file\n", buf[i]); + } + } + + return kDIErrNone; +} + +/* + * Prepare unadorned nibble for use. Not much to do here. + */ +DIError +WrapperUnadornedNibble::Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, + di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, + DiskImg::SectorOrder* pOrder, short* pDiskVolNum, + LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD) +{ + WMSG0("Prep for unadorned nibble\n"); + + if (wrappedLength == kTrackCount525 * kTrackLenNib525) { + WMSG0(" Prepping for 6656-byte NIB\n"); + *pPhysical = DiskImg::kPhysicalFormatNib525_6656; + } else if (wrappedLength == kTrackCount525 * kTrackLenNb2525) { + WMSG0(" Prepping for 6384-byte NB2\n"); + *pPhysical = DiskImg::kPhysicalFormatNib525_6384; + } else { + WMSG1(" Unexpected wrappedLength %ld for unadorned nibble\n", + (long) wrappedLength); + assert(false); + } + + *pLength = wrappedLength; + *pOrder = DiskImg::kSectorOrderPhysical; + + *ppNewGFD = new GFDGFD; + return ((GFDGFD*)*ppNewGFD)->Open(pGFD, 0, readOnly); +} + +/* + * Initialize fields for a new file. + */ +DIError +WrapperUnadornedNibble::Create(di_off_t length, DiskImg::PhysicalFormat physical, + DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, + di_off_t* pWrappedLength, GenericFD** pDataFD) +{ + WMSG0("Create unadorned nibble\n"); + + *pWrappedLength = length; + *pDataFD = new GFDGFD; + return ((GFDGFD*)*pDataFD)->Open(pWrapperGFD, 0, false); +} + +/* + * We only use GFDGFD, so there's nothing to do here. + */ +DIError +WrapperUnadornedNibble::Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, + di_off_t dataLen, di_off_t* pWrappedLen) +{ + return kDIErrNone; +} + + +/* + * =========================================================================== + * Unadorned sectors + * =========================================================================== + */ + +/* + * See if this is unadorned sector format. The only way we can really tell + * is by looking at the file size. + * + * The only requirement is that it be a multiple of 512 bytes. This holds + * for all ProDOS volumes and all floppy disk images. We also need to test + * for 13-sector ".d13" images. + * + * It also holds for 35-track 6656-byte unadorned nibble images, so we need + * to test for them first. + */ +/*static*/ DIError +WrapperUnadornedSector::Test(GenericFD* pGFD, di_off_t wrappedLength) +{ + WMSG2("Testing for unadorned sector (wrappedLength=%ld/%lu)\n", + (long) (wrappedLength >> 32), (unsigned long) wrappedLength); + if (wrappedLength >= 1536 && (wrappedLength % 512) == 0) + return kDIErrNone; + if (wrappedLength == kD13Length) // 13-sector image? + return kDIErrNone; + + return kDIErrGeneric; +} + +/* + * Prepare unadorned sector for use. Not much to do here. + */ +DIError +WrapperUnadornedSector::Prep(GenericFD* pGFD, di_off_t wrappedLength, bool readOnly, + di_off_t* pLength, DiskImg::PhysicalFormat* pPhysical, + DiskImg::SectorOrder* pOrder, short* pDiskVolNum, + LinearBitmap** ppBadBlockMap, GenericFD** ppNewGFD) +{ + WMSG0("Prepping for unadorned sector\n"); + assert(wrappedLength > 0); + *pLength = wrappedLength; + *pPhysical = DiskImg::kPhysicalFormatSectors; + //*pOrder = undetermined + + *ppNewGFD = new GFDGFD; + return ((GFDGFD*)*ppNewGFD)->Open(pGFD, 0, readOnly); +} + +/* + * Initialize fields for a new file. + */ +DIError +WrapperUnadornedSector::Create(di_off_t length, DiskImg::PhysicalFormat physical, + DiskImg::SectorOrder order, short dosVolumeNum, GenericFD* pWrapperGFD, + di_off_t* pWrappedLength, GenericFD** pDataFD) +{ + WMSG0("Create unadorned sector\n"); + + *pWrappedLength = length; + *pDataFD = new GFDGFD; + return ((GFDGFD*)*pDataFD)->Open(pWrapperGFD, 0, false); +} + +/* + * We only use GFDGFD, so there's nothing to do here. + */ +DIError +WrapperUnadornedSector::Flush(GenericFD* pWrapperGFD, GenericFD* pDataGFD, + di_off_t dataLen, di_off_t* pWrappedLen) +{ + return kDIErrNone; +} diff --git a/diskimg/MacPart.cpp b/diskimg/MacPart.cpp new file mode 100644 index 0000000..07ce94b --- /dev/null +++ b/diskimg/MacPart.cpp @@ -0,0 +1,479 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * The "MacPart" DiskFS is a container class for multiple ProDOS and HFS + * volumes. It represents a partitioned disk device, such as a hard + * drive or CD-ROM. + */ +#include "StdAfx.h" +#include "DiskImgPriv.h" + + +const int kBlkSize = 512; +const int kDDRBlock = 0; // Driver Descriptor Record block +const int kPartMapStart = 1; // start of partition map + + +/* + * Format of DDR (block 0). + */ +typedef struct DiskFSMacPart::DriverDescriptorRecord { + unsigned short sbSig; // {device signature} + unsigned short sbBlkSize; // {block size of the device} + unsigned long sbBlkCount; // {number of blocks on the device} + unsigned short sbDevType; // {reserved} + unsigned short sbDevId; // {reserved} + unsigned long sbData; // {reserved} + unsigned short sbDrvrCount; // {number of driver descriptor entries} + unsigned short hiddenPad; // implicit in specification + unsigned long ddBlock; // {first driver's starting block} + unsigned short ddSize; // {size of the driver, in 512-byte blocks} + unsigned short ddType; // {operating system type (MacOS = 1)} + unsigned short ddPad[242]; // {additional drivers, if any} +} DriverDescriptorRecord; + +/* + * Format of partition map blocks. The partition map is an array of these. + */ +typedef struct DiskFSMacPart::PartitionMap { + unsigned short pmSig; // {partition signature} + unsigned short pmSigPad; // {reserved} + unsigned long pmMapBlkCnt; // {number of blocks in partition map} + unsigned long pmPyPartStart; // {first physical block of partition} + unsigned long pmPartBlkCnt; // {number of blocks in partition} + unsigned char pmPartName[32]; // {partition name} + unsigned char pmParType[32]; // {partition type} + unsigned long pmLgDataStart; // {first logical block of data area} + unsigned long pmDataCnt; // {number of blocks in data area} + unsigned long pmPartStatus; // {partition status information} + unsigned long pmLgBootStart; // {first logical block of boot code} + unsigned long pmBootSize; // {size of boot code, in bytes} + unsigned long pmBootAddr; // {boot code load address} + unsigned long pmBootAddr2; // {reserved} + unsigned long pmBootEntry; // {boot code entry point} + unsigned long pmBootEntry2; // {reserved} + unsigned long pmBootCksum; // {boot code checksum} + unsigned char pmProcessor[16]; // {processor type} + unsigned short pmPad[188]; // {reserved} +} PartitionMap; + + +/* + * Figure out if this is a Macintosh-style partition. + * + * The "imageOrder" parameter has no use here, because (in the current + * version) embedded parent volumes are implicitly ProDOS-ordered. + * + * It would be difficult to guess the block order based on the partition + * structure, because the partition map entries can appear in any order. + */ +/*static*/ DIError +DiskFSMacPart::TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder) +{ + DIError dierr = kDIErrNone; + unsigned char blkBuf[kBlkSize]; + DriverDescriptorRecord ddr; + long pmMapBlkCnt; + + assert(sizeof(PartitionMap) == kBlkSize); + assert(sizeof(DriverDescriptorRecord) == kBlkSize); + + /* check the DDR block */ + dierr = pImg->ReadBlockSwapped(kDDRBlock, blkBuf, imageOrder, + DiskImg::kSectorOrderProDOS); + if (dierr != kDIErrNone) + goto bail; + + UnpackDDR(blkBuf, &ddr); + + if (ddr.sbSig != kDDRSignature) { + dierr = kDIErrFilesystemNotFound; + goto bail; + } + if (ddr.sbBlkSize != kBlkSize || ddr.sbBlkCount == 0) { + if (ddr.sbBlkSize == 0 && ddr.sbBlkCount == 0) { + /* + * This is invalid, but it's the way floptical images formatted + * by the C.V.Tech format utilities look. + */ + WMSG0(" MacPart NOTE: found zeroed-out DDR, continuing anyway\n"); + } else if (ddr.sbBlkSize == kBlkSize && ddr.sbBlkCount == 0) { + /* + * This showed up on a disc, so handle it too. + */ + WMSG0(" MacPart NOTE: found partially-zeroed-out DDR, continuing\n"); + } else { + WMSG2(" MacPart found 'ER' signature but blkSize=%d blkCount=%ld\n", + ddr.sbBlkSize, ddr.sbBlkCount); + dierr = kDIErrFilesystemNotFound; + goto bail; + } + } + DumpDDR(&ddr); + + /* make sure block 1 is a partition */ + dierr = pImg->ReadBlockSwapped(kPartMapStart, blkBuf, imageOrder, + DiskImg::kSectorOrderProDOS); + if (dierr != kDIErrNone) + goto bail; + + if (GetShortBE(&blkBuf[0x00]) != kPartitionSignature) { + WMSG0(" MacPart partition signature not found in first part block\n"); + dierr = kDIErrFilesystemNotFound; + goto bail; + } + pmMapBlkCnt = GetLongBE(&blkBuf[0x04]); + if (pmMapBlkCnt <= 0 || pmMapBlkCnt > 256) { + WMSG1(" MacPart unreasonable pmMapBlkCnt value %ld\n", + pmMapBlkCnt); + dierr = kDIErrFilesystemNotFound; + goto bail; + } + + /* could test the rest -- might fix "imageOrder", might not -- but + the format is pretty unambiguous, and we don't care about the order */ + + // success! + WMSG1(" MacPart partition map block count = %ld\n", pmMapBlkCnt); + +bail: + return dierr; +} + +/* + * Unpack a DDR disk block into a DDR data structure. + */ +/*static*/ void +DiskFSMacPart::UnpackDDR(const unsigned char* buf, + DriverDescriptorRecord* pDDR) +{ + pDDR->sbSig = GetShortBE(&buf[0x00]); + pDDR->sbBlkSize = GetShortBE(&buf[0x02]); + pDDR->sbBlkCount = GetLongBE(&buf[0x04]); + pDDR->sbDevType = GetShortBE(&buf[0x08]); + pDDR->sbDevId = GetShortBE(&buf[0x0a]); + pDDR->sbData = GetLongBE(&buf[0x0c]); + pDDR->sbDrvrCount = GetShortBE(&buf[0x10]); + pDDR->hiddenPad = GetShortBE(&buf[0x12]); + pDDR->ddBlock = GetLongBE(&buf[0x14]); + pDDR->ddSize = GetShortBE(&buf[0x18]); + pDDR->ddType = GetShortBE(&buf[0x1a]); + + int i; + for (i = 0; i < (int) NELEM(pDDR->ddPad); i++) { + pDDR->ddPad[i] = GetShortBE(&buf[0x1c] + i * sizeof(pDDR->ddPad[0])); + } + assert(0x1c + i * sizeof(pDDR->ddPad[0]) == (unsigned int) kBlkSize); +} + +/* + * Debug: dump the contents of the DDR. + */ +/*static*/ void +DiskFSMacPart::DumpDDR(const DriverDescriptorRecord* pDDR) +{ + WMSG0(" MacPart driver descriptor record\n"); + WMSG3(" sbSig=0x%04x sbBlkSize=%d sbBlkCount=%ld\n", + pDDR->sbSig, pDDR->sbBlkSize, pDDR->sbBlkCount); + WMSG4(" sbDevType=%d sbDevId=%d sbData=%ld sbDrvrCount=%d\n", + pDDR->sbDevType, pDDR->sbDevId, pDDR->sbData, pDDR->sbDrvrCount); + WMSG4(" (pad=%d) ddBlock=%ld ddSize=%d ddType=%d\n", + pDDR->hiddenPad, pDDR->ddBlock, pDDR->ddSize, pDDR->ddType); +} + +/* + * Unpack a partition map disk block into a partition map data structure. + */ +/*static*/ void +DiskFSMacPart::UnpackPartitionMap(const unsigned char* buf, + PartitionMap* pMap) +{ + pMap->pmSig = GetShortBE(&buf[0x00]); + pMap->pmSigPad = GetShortBE(&buf[0x02]); + pMap->pmMapBlkCnt = GetLongBE(&buf[0x04]); + pMap->pmPyPartStart = GetLongBE(&buf[0x08]); + pMap->pmPartBlkCnt = GetLongBE(&buf[0x0c]); + memcpy(pMap->pmPartName, &buf[0x10], sizeof(pMap->pmPartName)); + pMap->pmPartName[sizeof(pMap->pmPartName)-1] = '\0'; + memcpy(pMap->pmParType, &buf[0x30], sizeof(pMap->pmParType)); + pMap->pmParType[sizeof(pMap->pmParType)-1] = '\0'; + pMap->pmLgDataStart = GetLongBE(&buf[0x50]); + pMap->pmDataCnt = GetLongBE(&buf[0x54]); + pMap->pmPartStatus = GetLongBE(&buf[0x58]); + pMap->pmLgBootStart = GetLongBE(&buf[0x5c]); + pMap->pmBootSize = GetLongBE(&buf[0x60]); + pMap->pmBootAddr = GetLongBE(&buf[0x64]); + pMap->pmBootAddr2 = GetLongBE(&buf[0x68]); + pMap->pmBootEntry = GetLongBE(&buf[0x6c]); + pMap->pmBootEntry2 = GetLongBE(&buf[0x70]); + pMap->pmBootCksum = GetLongBE(&buf[0x74]); + memcpy((char*) pMap->pmProcessor, &buf[0x78], sizeof(pMap->pmProcessor)); + pMap->pmProcessor[sizeof(pMap->pmProcessor)-1] = '\0'; + + int i; + for (i = 0; i < (int) NELEM(pMap->pmPad); i++) { + pMap->pmPad[i] = GetShortBE(&buf[0x88] + i * sizeof(pMap->pmPad[0])); + } + assert(0x88 + i * sizeof(pMap->pmPad[0]) == (unsigned int) kBlkSize); +} + +/* + * Debug: dump the contents of the partition map. + */ +/*static*/ void +DiskFSMacPart::DumpPartitionMap(long block, const PartitionMap* pMap) +{ + WMSG1(" MacPart partition map: block=%ld\n", block); + WMSG3(" pmSig=0x%04x (pad=0x%04x) pmMapBlkCnt=%ld\n", + pMap->pmSig, pMap->pmSigPad, pMap->pmMapBlkCnt); + WMSG2(" pmPartName='%s' pmParType='%s'\n", + pMap->pmPartName, pMap->pmParType); + WMSG2(" pmPyPartStart=%ld pmPartBlkCnt=%ld\n", + pMap->pmPyPartStart, pMap->pmPartBlkCnt); + WMSG2(" pmLgDataStart=%ld pmDataCnt=%ld\n", + pMap->pmLgDataStart, pMap->pmDataCnt); + WMSG1(" pmPartStatus=%ld\n", + pMap->pmPartStatus); + WMSG2(" pmLgBootStart=%ld pmBootSize=%ld\n", + pMap->pmLgBootStart, pMap->pmBootSize); + WMSG4(" pmBootAddr=%ld pmBootAddr2=%ld pmBootEntry=%ld pmBootEntry2=%ld\n", + pMap->pmBootAddr, pMap->pmBootAddr2, + pMap->pmBootEntry, pMap->pmBootEntry2); + WMSG2(" pmBootCksum=%ld pmProcessor='%s'\n", + pMap->pmBootCksum, pMap->pmProcessor); +} + + +/* + * Open up a sub-volume. + */ +DIError +DiskFSMacPart::OpenSubVolume(const PartitionMap* pMap) +{ + DIError dierr = kDIErrNone; + DiskFS* pNewFS = nil; + DiskImg* pNewImg = nil; + long startBlock, numBlocks; + bool tweaked = false; + + assert(pMap != nil); + startBlock = pMap->pmPyPartStart; + numBlocks = pMap->pmPartBlkCnt; + + WMSG4("Adding '%s' (%s) %ld +%ld\n", + pMap->pmPartName, pMap->pmParType, startBlock, numBlocks); + + if (startBlock > fpImg->GetNumBlocks()) { + WMSG2("MacPart start block out of range (%ld vs %ld)\n", + startBlock, fpImg->GetNumBlocks()); + return kDIErrBadPartition; + } + if (startBlock + numBlocks > fpImg->GetNumBlocks()) { + WMSG2("MacPart partition too large (%ld vs %ld avail)\n", + numBlocks, fpImg->GetNumBlocks() - startBlock); + fpImg->AddNote(DiskImg::kNoteInfo, + "Reduced partition '%s' (%s) from %ld blocks to %ld.\n", + pMap->pmPartName, pMap->pmParType, numBlocks, + fpImg->GetNumBlocks() - startBlock); + numBlocks = fpImg->GetNumBlocks() - startBlock; + tweaked = true; + } + + pNewImg = new DiskImg; + if (pNewImg == nil) { + dierr = kDIErrMalloc; + goto bail; + } + + /* + * If "tweaked" is true, we want to make the volume read-only, so that the + * volume copier doesn't stomp on it (on the off chance we've got it + * wrong). However, that won't stop the volume copier from stomping on + * the entire thing, so we really need to change *all* members of the + * diskimg tree to be read-only. This seems counter-productive though. + * + * So far the only actual occurrence of tweakedness was from the first + * Apple "develop" CD-ROM, which had a bad Apple_Extra partition on the + * end. + */ + + dierr = pNewImg->OpenImage(fpImg, startBlock, numBlocks); + if (dierr != kDIErrNone) { + WMSG3(" MacPartSub: OpenImage(%ld,%ld) failed (err=%d)\n", + startBlock, numBlocks, dierr); + goto bail; + } + + //WMSG2(" +++ CFFASub: new image has ro=%d (parent=%d)\n", + // pNewImg->GetReadOnly(), pImg->GetReadOnly()); + + /* the partition is typed; currently no way to give hints to analyzer */ + dierr = pNewImg->AnalyzeImage(); + if (dierr != kDIErrNone) { + WMSG1(" MacPartSub: analysis failed (err=%d)\n", dierr); + goto bail; + } + + /* we allow unrecognized partitions */ + if (pNewImg->GetFSFormat() == DiskImg::kFormatUnknown || + pNewImg->GetSectorOrder() == DiskImg::kSectorOrderUnknown) + { + WMSG2(" MacPartSub (%ld,%ld): unable to identify filesystem\n", + startBlock, numBlocks); + DiskFSUnknown* pUnknownFS = new DiskFSUnknown; + if (pUnknownFS == nil) { + dierr = kDIErrInternal; + goto bail; + } + pUnknownFS->SetVolumeInfo((const char*)pMap->pmParType); + pNewFS = pUnknownFS; + pNewImg->AddNote(DiskImg::kNoteInfo, "Partition name='%s' type='%s'.", + pMap->pmPartName, pMap->pmParType); + } else { + /* open a DiskFS for the sub-image */ + WMSG2(" MacPartSub (%ld,%ld) analyze succeeded!\n", startBlock, numBlocks); + pNewFS = pNewImg->OpenAppropriateDiskFS(true); + if (pNewFS == nil) { + WMSG0(" MacPartSub: OpenAppropriateDiskFS failed\n"); + dierr = kDIErrUnsupportedFSFmt; + goto bail; + } + } + + /* we encapsulate arbitrary stuff, so encourage child to scan */ + pNewFS->SetScanForSubVolumes(kScanSubEnabled); + + /* + * Load the files from the sub-image. When doing our initial tests, + * or when loading data for the volume copier, we don't want to dig + * into our sub-volumes, just figure out what they are and where. + * + * If "initialize" fails, the sub-volume won't get added to the list. + * It's important that a failure at this stage doesn't cause the whole + * thing to fall over. + */ + InitMode initMode; + if (GetScanForSubVolumes() == kScanSubContainerOnly) + initMode = kInitHeaderOnly; + else + initMode = kInitFull; + dierr = pNewFS->Initialize(pNewImg, initMode); + if (dierr != kDIErrNone) { + WMSG1(" MacPartSub: error %d reading list of files from disk\n", dierr); + goto bail; + } + + /* add it to the list */ + AddSubVolumeToList(pNewImg, pNewFS); + pNewImg = nil; + pNewFS = nil; + +bail: + delete pNewFS; + delete pNewImg; + return dierr; +} + +/* + * Check to see if this is a MacPart volume. + */ +/*static*/ DIError +DiskFSMacPart::TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency) +{ + if (pImg->GetNumBlocks() < kMinInterestingBlocks) + return kDIErrFilesystemNotFound; + if (pImg->GetIsEmbedded()) // don't look for partitions inside + return kDIErrFilesystemNotFound; + + /* assume ProDOS -- shouldn't matter, since it's embedded */ + if (TestImage(pImg, DiskImg::kSectorOrderProDOS) == kDIErrNone) { + *pFormat = DiskImg::kFormatMacPart; + *pOrder = DiskImg::kSectorOrderProDOS; + return kDIErrNone; + } + + WMSG0(" FS didn't find valid MacPart\n"); + return kDIErrFilesystemNotFound; +} + + +/* + * Prep the MacPart "container" for use. + */ +DIError +DiskFSMacPart::Initialize(void) +{ + DIError dierr = kDIErrNone; + + WMSG1("MacPart initializing (scanForSub=%d)\n", fScanForSubVolumes); + + /* seems pointless *not* to, but we just do what we're told */ + if (fScanForSubVolumes != kScanSubDisabled) { + dierr = FindSubVolumes(); + if (dierr != kDIErrNone) + return dierr; + } + + /* blank out the volume usage map */ + SetVolumeUsageMap(); + + return dierr; +} + + +/* + * Find the various sub-volumes and open them. + * + * Because the partitions are explicitly typed, we don't need to probe + * their contents. But we do anyway. + */ +DIError +DiskFSMacPart::FindSubVolumes(void) +{ + DIError dierr = kDIErrNone; + unsigned char buf[kBlkSize]; + PartitionMap map; + int i, numMapBlocks; + + dierr = fpImg->ReadBlock(kPartMapStart, buf); + if (dierr != kDIErrNone) + goto bail; + UnpackPartitionMap(buf, &map); + numMapBlocks = map.pmMapBlkCnt; + + for (i = 0; i < numMapBlocks; i++) { + if (i != 0) { + dierr = fpImg->ReadBlock(kPartMapStart+i, buf); + if (dierr != kDIErrNone) + goto bail; + UnpackPartitionMap(buf, &map); + } + DumpPartitionMap(kPartMapStart+i, &map); + + dierr = OpenSubVolume(&map); + if (dierr != kDIErrNone) { + if (dierr == kDIErrCancelled) + goto bail; + DiskFS* pNewFS = nil; + DiskImg* pNewImg = nil; + WMSG1(" MacPart failed opening sub-volume %d\n", i); + dierr = CreatePlaceholder(map.pmPyPartStart, map.pmPartBlkCnt, + (const char*)map.pmPartName, (const char*)map.pmParType, + &pNewImg, &pNewFS); + if (dierr == kDIErrNone) { + AddSubVolumeToList(pNewImg, pNewFS); + } else { + WMSG1(" MacPart unable to create placeholder (err=%d)\n", + dierr); + break; // something's wrong -- bail out with error + } + } + } + +bail: + return dierr; +} diff --git a/diskimg/Makefile b/diskimg/Makefile new file mode 100644 index 0000000..f52ca4e --- /dev/null +++ b/diskimg/Makefile @@ -0,0 +1,49 @@ +# +# DiskImg makefile for Linux. +# +SHELL = /bin/sh +CC = gcc +CXX = g++ +AR = ar +OPT = -g -D_DEBUG +#-DEXCISE_GPL_CODE +#OPT = -g -O2 +GCC_FLAGS = -Wall -Wwrite-strings -Wpointer-arith -Wshadow +# -Wstrict-prototypes +CXXFLAGS = $(OPT) $(GCC_FLAGS) -D_FILE_OFFSET_BITS=64 + +SRCS = ASPI.cpp CFFA.cpp Container.cpp CPM.cpp DDD.cpp DiskFS.cpp \ + DiskImg.cpp DIUtil.cpp DOS33.cpp DOSImage.cpp FAT.cpp FDI.cpp \ + FocusDrive.cpp \GenericFD.cpp Global.cpp HFS.cpp \ + ImageWrapper.cpp MacPart.cpp MicroDrive.cpp Nibble.cpp \ + Nibble35.cpp OuterWrapper.cpp OzDOS.cpp Pascal.cpp ProDOS.cpp \ + RDOS.cpp TwoImg.cpp UNIDOS.cpp VolumeUsage.cpp Win32BlockIO.cpp +OBJS = ASPI.o CFFA.o Container.o CPM.o DDD.o DiskFS.o \ + DiskImg.o DIUtil.o DOS33.o DOSImage.o FDI.o \ + FocusDrive.o FAT.o GenericFD.o Global.o HFS.o \ + ImageWrapper.o MacPart.o MicroDrive.o Nibble.o \ + Nibble35.o OuterWrapper.o OzDOS.o Pascal.o ProDOS.o \ + RDOS.o TwoImg.o UNIDOS.o VolumeUsage.o Win32BlockIO.o + +STATIC_PRODUCT = libdiskimg.a +PRODUCT = $(STATIC_PRODUCT) + +all: $(PRODUCT) + @true + +$(STATIC_PRODUCT): $(OBJS) + -rm -f $(STATIC_PRODUCT) + $(AR) rcv $@ $(OBJS) + +clean: + -rm -f *.o core + -rm -f $(STATIC_PRODUCT) + -rm -f Makefile.bak + +tags:: + @ctags -R --totals * + +depend: + makedepend -- $(CFLAGS) -- $(SRCS) + +# DO NOT DELETE THIS LINE -- make depend depends on it. diff --git a/diskimg/MicroDrive.cpp b/diskimg/MicroDrive.cpp new file mode 100644 index 0000000..d3e3f8f --- /dev/null +++ b/diskimg/MicroDrive.cpp @@ -0,0 +1,404 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * The "MicroDrive" DiskFS is a container class for multiple ProDOS and HFS + * volumes. It represents a partitioned disk device, such as a hard + * drive or CF card, that has been formatted for use with ///SHH Systeme's + * MicroDrive card for the Apple II. + */ +#include "StdAfx.h" +#include "DiskImgPriv.h" + + +const int kBlkSize = 512; +const int kPartMapBlock = 0; // partition map lives here +const unsigned int kPartSizeMask = 0x00ffffff; + + +/* + * Format of partition map. It resides in the first 256 bytes of block 0. + * All values are in little-endian order. + * + * The layout was discovered through reverse-engineering. Additional notes: + * + From Joachim Lange: + + Below, this is the configuration block as it is used in all + MicroDrive cards. Please verify that my ID shortcut can be + found at offset 0, otherwise the partition info is not + valid. Most of the other parms are not useful, some are + historic and not useful anymore. As a second security + measure, verify that the first partition starts at + absolute block 256. This is also a fixed value used in all + MicroDrive cards. Of course the partition size is not two + bytes long but three (not four), the 4th byte is used for + switching drives in a two-drive configuration. So, for + completeness, when reading partition sizes, perform a + partitionLength[..] & 0x00FFFFFF, or at least issue a + warning that something may be wrong. The offset + (partitionStart) could reach into the 4th byte. + I have attached the config block in a zip file because + the mailer would probably re-format the source text. + */ +const int kMaxNumParts = 8; +typedef struct DiskFSMicroDrive::PartitionMap { + unsigned short magic; // partition signature + unsigned short cylinders; // #of cylinders + unsigned short reserved1; // ?? + unsigned short heads; // #of heads/cylinder + unsigned short sectors; // #of sectors/track + unsigned short reserved2; // ?? + unsigned char numPart1; // #of partitions in first chunk + unsigned char numPart2; // #of partitions in second chunk + unsigned char reserved3[10]; // bytes 0x0e-0x17 + unsigned short romVersion; // IIgs ROM01 or ROM03 + unsigned char reserved4[6]; // bytes 0x1a-0x1f + unsigned long partitionStart1[kMaxNumParts]; // bytes 0x20-0x3f + unsigned long partitionLength1[kMaxNumParts]; // bytes 0x40-0x5f + unsigned char reserved5[32]; // bytes 0x60-0x7f + unsigned long partitionStart2[kMaxNumParts]; // bytes 0x80-0x9f + unsigned long partitionLength2[kMaxNumParts]; // bytes 0xa0-0xbf + + unsigned char padding[320]; +} PartitionMap; + + +/* + * Figure out if this is a MicroDrive partition. + * + * The "imageOrder" parameter has no use here, because (in the current + * version) embedded parent volumes are implicitly ProDOS-ordered. + */ +/*static*/ DIError +DiskFSMicroDrive::TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder) +{ + DIError dierr = kDIErrNone; + unsigned char blkBuf[kBlkSize]; + int partCount1, partCount2; + + assert(sizeof(PartitionMap) == kBlkSize); + + /* + * See if block 0 is a MicroDrive partition map. + */ + dierr = pImg->ReadBlockSwapped(kPartMapBlock, blkBuf, imageOrder, + DiskImg::kSectorOrderProDOS); + if (dierr != kDIErrNone) + goto bail; + + if (GetShortLE(&blkBuf[0x00]) != kPartitionSignature) { + WMSG0(" MicroDrive partition signature not found in first part block\n"); + dierr = kDIErrFilesystemNotFound; + goto bail; + } + /* might assert that partCount2 be zero unless partCount1 == 8? */ + partCount1 = blkBuf[0x0c]; + partCount2 = blkBuf[0x0d]; + if (partCount1 == 0 || partCount1 > kMaxNumParts || + partCount2 > kMaxNumParts) + { + WMSG2(" MicroDrive unreasonable partCount values %d/%d\n", + partCount1, partCount2); + dierr = kDIErrFilesystemNotFound; + goto bail; + } + + /* consider testing other fields */ + + // success! + WMSG2(" MicroDrive partition map count = %d/%d\n", partCount1, partCount2); + +bail: + return dierr; +} + + +/* + * Unpack a partition map block into a partition map data structure. + */ +/*static*/ void +DiskFSMicroDrive::UnpackPartitionMap(const unsigned char* buf, + PartitionMap* pMap) +{ + pMap->magic = GetShortLE(&buf[0x00]); + pMap->cylinders = GetShortLE(&buf[0x02]); + pMap->reserved1 = GetShortLE(&buf[0x04]); + pMap->heads = GetShortLE(&buf[0x06]); + pMap->sectors = GetShortLE(&buf[0x08]); + pMap->reserved2 = GetShortLE(&buf[0x0a]); + pMap->numPart1 = buf[0x0c]; + pMap->numPart2 = buf[0x0d]; + memcpy(pMap->reserved3, &buf[0x0e], sizeof(pMap->reserved3)); + pMap->romVersion = GetShortLE(&buf[0x18]); + memcpy(pMap->reserved4, &buf[0x1a], sizeof(pMap->reserved4)); + + for (int i = 0; i < kMaxNumParts; i++) { + pMap->partitionStart1[i] = GetLongLE(&buf[0x20] + i * 4); + pMap->partitionLength1[i] = GetLongLE(&buf[0x40] + i * 4) & kPartSizeMask; + pMap->partitionStart2[i] = GetLongLE(&buf[0x80] + i * 4); + pMap->partitionLength2[i] = GetLongLE(&buf[0xa0] + i * 4) & kPartSizeMask; + } + memcpy(pMap->reserved5, &buf[0x60], sizeof(pMap->reserved5)); + memcpy(pMap->padding, &buf[0x80], sizeof(pMap->padding)); +} + +/* + * Debug: dump the contents of the partition map. + */ +/*static*/ void +DiskFSMicroDrive::DumpPartitionMap(const PartitionMap* pMap) +{ + WMSG0(" MicroDrive partition map:\n"); + WMSG4(" cyls=%d res1=%d heads=%d sects=%d\n", + pMap->cylinders, pMap->reserved1, pMap->heads, pMap->sectors); + WMSG3(" res2=%d numPart1=%d numPart2=%d\n", + pMap->reserved2, pMap->numPart1, pMap->numPart2); + WMSG1(" romVersion=ROM%02d\n", pMap->romVersion); + + int i, parts; + + parts = pMap->numPart1; + assert(parts <= kMaxNumParts); + for (i = 0; i < parts; i++) { + WMSG3(" %2d: startLBA=%8ld length=%ld\n", + i, pMap->partitionStart1[i], pMap->partitionLength1[i]); + } + parts = pMap->numPart2; + assert(parts <= kMaxNumParts); + for (i = 0; i < parts; i++) { + WMSG3(" %2d: startLBA=%8ld length=%ld\n", + i+8, pMap->partitionStart2[i], pMap->partitionLength2[i]); + } +} + + +/* + * Open up a sub-volume. + */ +DIError +DiskFSMicroDrive::OpenSubVolume(long startBlock, long numBlocks) +{ + DIError dierr = kDIErrNone; + DiskFS* pNewFS = nil; + DiskImg* pNewImg = nil; + //bool tweaked = false; + + WMSG2("Adding %ld +%ld\n", startBlock, numBlocks); + + if (startBlock > fpImg->GetNumBlocks()) { + WMSG2("MicroDrive start block out of range (%ld vs %ld)\n", + startBlock, fpImg->GetNumBlocks()); + return kDIErrBadPartition; + } + if (startBlock + numBlocks > fpImg->GetNumBlocks()) { + WMSG2("MicroDrive partition too large (%ld vs %ld avail)\n", + numBlocks, fpImg->GetNumBlocks() - startBlock); + fpImg->AddNote(DiskImg::kNoteInfo, + "Reduced partition from %ld blocks to %ld.\n", + numBlocks, fpImg->GetNumBlocks() - startBlock); + numBlocks = fpImg->GetNumBlocks() - startBlock; + //tweaked = true; + } + + pNewImg = new DiskImg; + if (pNewImg == nil) { + dierr = kDIErrMalloc; + goto bail; + } + + dierr = pNewImg->OpenImage(fpImg, startBlock, numBlocks); + if (dierr != kDIErrNone) { + WMSG3(" MicroDriveSub: OpenImage(%ld,%ld) failed (err=%d)\n", + startBlock, numBlocks, dierr); + goto bail; + } + + //WMSG2(" +++ CFFASub: new image has ro=%d (parent=%d)\n", + // pNewImg->GetReadOnly(), pImg->GetReadOnly()); + + /* figure out what the format is */ + dierr = pNewImg->AnalyzeImage(); + if (dierr != kDIErrNone) { + WMSG1(" MicroDriveSub: analysis failed (err=%d)\n", dierr); + goto bail; + } + + /* we allow unrecognized partitions */ + if (pNewImg->GetFSFormat() == DiskImg::kFormatUnknown || + pNewImg->GetSectorOrder() == DiskImg::kSectorOrderUnknown) + { + WMSG2(" MicroDriveSub (%ld,%ld): unable to identify filesystem\n", + startBlock, numBlocks); + DiskFSUnknown* pUnknownFS = new DiskFSUnknown; + if (pUnknownFS == nil) { + dierr = kDIErrInternal; + goto bail; + } + //pUnknownFS->SetVolumeInfo((const char*)pMap->pmParType); + pNewFS = pUnknownFS; + //pNewImg->AddNote(DiskImg::kNoteInfo, "Partition name='%s' type='%s'.", + // pMap->pmPartName, pMap->pmParType); + } else { + /* open a DiskFS for the sub-image */ + WMSG2(" MicroDriveSub (%ld,%ld) analyze succeeded!\n", startBlock, numBlocks); + pNewFS = pNewImg->OpenAppropriateDiskFS(true); + if (pNewFS == nil) { + WMSG0(" MicroDriveSub: OpenAppropriateDiskFS failed\n"); + dierr = kDIErrUnsupportedFSFmt; + goto bail; + } + } + + /* we encapsulate arbitrary stuff, so encourage child to scan */ + pNewFS->SetScanForSubVolumes(kScanSubEnabled); + + /* + * Load the files from the sub-image. When doing our initial tests, + * or when loading data for the volume copier, we don't want to dig + * into our sub-volumes, just figure out what they are and where. + * + * If "initialize" fails, the sub-volume won't get added to the list. + * It's important that a failure at this stage doesn't cause the whole + * thing to fall over. + */ + InitMode initMode; + if (GetScanForSubVolumes() == kScanSubContainerOnly) + initMode = kInitHeaderOnly; + else + initMode = kInitFull; + dierr = pNewFS->Initialize(pNewImg, initMode); + if (dierr != kDIErrNone) { + WMSG1(" MicroDriveSub: error %d reading list of files from disk", dierr); + goto bail; + } + + /* add it to the list */ + AddSubVolumeToList(pNewImg, pNewFS); + pNewImg = nil; + pNewFS = nil; + +bail: + delete pNewFS; + delete pNewImg; + return dierr; +} + +/* + * Check to see if this is a MicroDrive volume. + */ +/*static*/ DIError +DiskFSMicroDrive::TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency) +{ + if (pImg->GetNumBlocks() < kMinInterestingBlocks) + return kDIErrFilesystemNotFound; + if (pImg->GetIsEmbedded()) // don't look for partitions inside + return kDIErrFilesystemNotFound; + + /* assume ProDOS -- shouldn't matter, since it's embedded */ + if (TestImage(pImg, DiskImg::kSectorOrderProDOS) == kDIErrNone) { + *pFormat = DiskImg::kFormatMicroDrive; + *pOrder = DiskImg::kSectorOrderProDOS; + return kDIErrNone; + } + + WMSG0(" FS didn't find valid MicroDrive\n"); + return kDIErrFilesystemNotFound; +} + + +/* + * Prep the MicroDrive "container" for use. + */ +DIError +DiskFSMicroDrive::Initialize(void) +{ + DIError dierr = kDIErrNone; + + WMSG1("MicroDrive initializing (scanForSub=%d)\n", fScanForSubVolumes); + + /* seems pointless *not* to, but we just do what we're told */ + if (fScanForSubVolumes != kScanSubDisabled) { + dierr = FindSubVolumes(); + if (dierr != kDIErrNone) + return dierr; + } + + /* blank out the volume usage map */ + SetVolumeUsageMap(); + + return dierr; +} + + +/* + * Find the various sub-volumes and open them. + */ +DIError +DiskFSMicroDrive::FindSubVolumes(void) +{ + DIError dierr = kDIErrNone; + unsigned char buf[kBlkSize]; + PartitionMap map; + int i; + + dierr = fpImg->ReadBlock(kPartMapBlock, buf); + if (dierr != kDIErrNone) + goto bail; + UnpackPartitionMap(buf, &map); + DumpPartitionMap(&map); + + /* first part of the table */ + for (i = 0; i < map.numPart1; i++) { + dierr = OpenVol(i, + map.partitionStart1[i], map.partitionLength1[i]); + if (dierr != kDIErrNone) + goto bail; + } + + /* second part of the table */ + for (i = 0; i < map.numPart2; i++) { + dierr = OpenVol(i + kMaxNumParts, + map.partitionStart2[i], map.partitionLength2[i]); + if (dierr != kDIErrNone) + goto bail; + } + +bail: + return dierr; +} + +/* + * Open the volume. If it fails, open a placeholder instead. (If *that* + * fails, return with an error.) + */ +DIError +DiskFSMicroDrive::OpenVol(int idx, long startBlock, long numBlocks) +{ + DIError dierr; + + dierr = OpenSubVolume(startBlock, numBlocks); + if (dierr != kDIErrNone) { + if (dierr == kDIErrCancelled) + goto bail; + DiskFS* pNewFS = nil; + DiskImg* pNewImg = nil; + + WMSG1(" MicroDrive failed opening sub-volume %d\n", idx); + dierr = CreatePlaceholder(startBlock, numBlocks, NULL, NULL, + &pNewImg, &pNewFS); + if (dierr == kDIErrNone) { + AddSubVolumeToList(pNewImg, pNewFS); + } else { + WMSG1(" MicroDrive unable to create placeholder (err=%d)\n", + dierr); + // fall out with error + } + } + +bail: + return dierr; +} diff --git a/diskimg/Nibble.cpp b/diskimg/Nibble.cpp new file mode 100644 index 0000000..9f28235 --- /dev/null +++ b/diskimg/Nibble.cpp @@ -0,0 +1,1046 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * DiskImg nibblized read/write functions. + */ +#include "StdAfx.h" +#include "DiskImgPriv.h" + +/* define this for verbose output */ +//#define NIB_VERBOSE_DEBUG + + +/* + * =========================================================================== + * Nibble encoding and decoding + * =========================================================================== + */ + +/*static*/ unsigned char DiskImg::kDiskBytes53[32] = { + 0xab, 0xad, 0xae, 0xaf, 0xb5, 0xb6, 0xb7, 0xba, + 0xbb, 0xbd, 0xbe, 0xbf, 0xd6, 0xd7, 0xda, 0xdb, + 0xdd, 0xde, 0xdf, 0xea, 0xeb, 0xed, 0xee, 0xef, + 0xf5, 0xf6, 0xf7, 0xfa, 0xfb, 0xfd, 0xfe, 0xff +}; +/*static*/ unsigned char DiskImg::kDiskBytes62[64] = { + 0x96, 0x97, 0x9a, 0x9b, 0x9d, 0x9e, 0x9f, 0xa6, + 0xa7, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb2, 0xb3, + 0xb4, 0xb5, 0xb6, 0xb7, 0xb9, 0xba, 0xbb, 0xbc, + 0xbd, 0xbe, 0xbf, 0xcb, 0xcd, 0xce, 0xcf, 0xd3, + 0xd6, 0xd7, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde, + 0xdf, 0xe5, 0xe6, 0xe7, 0xe9, 0xea, 0xeb, 0xec, + 0xed, 0xee, 0xef, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, + 0xf7, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff +}; +/*static*/ unsigned char DiskImg::kInvDiskBytes53[256]; // all values are 0-31 +/*static*/ unsigned char DiskImg::kInvDiskBytes62[256]; // all values are 0-63 + +/* + * Compute tables to convert disk bytes back to values. + * + * Should be called once, at DLL initialization time. + */ +/*static*/ void +DiskImg::CalcNibbleInvTables(void) +{ + unsigned int i; + + memset(kInvDiskBytes53, kInvInvalidValue, sizeof(kInvDiskBytes53)); + for (i = 0; i < sizeof(kDiskBytes53); i++) { + assert(kDiskBytes53[i] >= 0x96); + kInvDiskBytes53[kDiskBytes53[i]] = i; + } + + memset(kInvDiskBytes62, kInvInvalidValue, sizeof(kInvDiskBytes62)); + for (i = 0; i < sizeof(kDiskBytes62); i++) { + assert(kDiskBytes62[i] >= 0x96); + kInvDiskBytes62[kDiskBytes62[i]] = i; + } +} + +/* + * Find the start of the data field of a sector in nibblized data. + * + * Returns the index start on success or -1 on failure. + */ +int +DiskImg::FindNibbleSectorStart(const CircularBufferAccess& buffer, int track, + int sector, const NibbleDescr* pNibbleDescr, int* pVol) +{ + const int kMaxDataReach = 48; // fairly arbitrary + //DIError dierr; + long trackLen = buffer.GetSize(); + + assert(sector >= 0 && sector < 16); + + int i; + + for (i = 0; i < trackLen; i++) { + bool foundAddr = false; + + if (pNibbleDescr->special == kNibbleSpecialSkipFirstAddrByte) { + if (/*buffer[i] == pNibbleDescr->addrProlog[0] &&*/ + buffer[i+1] == pNibbleDescr->addrProlog[1] && + buffer[i+2] == pNibbleDescr->addrProlog[2]) + { + foundAddr = true; + } + } else { + if (buffer[i] == pNibbleDescr->addrProlog[0] && + buffer[i+1] == pNibbleDescr->addrProlog[1] && + buffer[i+2] == pNibbleDescr->addrProlog[2]) + { + foundAddr = true; + } + } + + if (foundAddr) { + //i += 3; + + /* found the address header, decode the address */ + short hdrVol, hdrTrack, hdrSector, hdrChksum; + DecodeAddr(buffer, i+3, &hdrVol, &hdrTrack, &hdrSector, + &hdrChksum); + + if (pNibbleDescr->addrVerifyTrack && track != hdrTrack) { + WMSG3(" Track mismatch (T=%d) got T=%d,S=%d\n", + track, hdrTrack, hdrSector); + continue; + } + + if (pNibbleDescr->addrVerifyChecksum) { + if ((pNibbleDescr->addrChecksumSeed ^ + hdrVol ^ hdrTrack ^ hdrSector ^ hdrChksum) != 0) + { + WMSG4(" Addr checksum mismatch (want T=%d,S=%d, got " + "T=%d,S=%d)\n", + track, sector, hdrTrack, hdrSector); + continue; + } + } + + i += 3; + + int j; + for (j = 0; j < pNibbleDescr->addrEpilogVerifyCount; j++) { + if (buffer[i+8+j] != pNibbleDescr->addrEpilog[j]) { + //WMSG3(" Bad epilog byte %d (%02x vs %02x)\n", + // j, buffer[i+8+j], pNibbleDescr->addrEpilog[j]); + break; + } + } + if (j != pNibbleDescr->addrEpilogVerifyCount) + continue; + +#ifdef NIB_VERBOSE_DEBUG + WMSG4(" Good header, T=%d,S=%d (looking for T=%d,S=%d)\n", + hdrTrack, hdrSector, track, sector); +#endif + + if (pNibbleDescr->special == kNibbleSpecialMuse) { + /* e.g. original Castle Wolfenstein */ + if (track > 2) { + if ((hdrSector & 0x01) != 0) + continue; + hdrSector /= 2; + } + } + + if (sector != hdrSector) + continue; + + /* + * Scan forward and look for data prolog. We want to limit + * the reach of our search so we don't blunder into the data + * field of the next sector. + */ + for (j = 0; j < kMaxDataReach; j++) { + if (buffer[i + j] == pNibbleDescr->dataProlog[0] && + buffer[i + j +1] == pNibbleDescr->dataProlog[1] && + buffer[i + j +2] == pNibbleDescr->dataProlog[2]) + { + *pVol = hdrVol; + return buffer.Normalize(i + j + 3); + } + } + } + } + +#ifdef NIB_VERBOSE_DEBUG + WMSG2(" Couldn't find T=%d,S=%d\n", track, sector); +#endif + return -1; +} + +/* + * Decode the values in the address field. + */ +void +DiskImg::DecodeAddr(const CircularBufferAccess& buffer, int offset, + short* pVol, short* pTrack, short* pSector, short* pChksum) +{ + //unsigned int vol, track, sector, chksum; + + *pVol = ConvFrom44(buffer[offset], buffer[offset+1]); + *pTrack = ConvFrom44(buffer[offset+2], buffer[offset+3]); + *pSector = ConvFrom44(buffer[offset+4], buffer[offset+5]); + *pChksum = ConvFrom44(buffer[offset+6], buffer[offset+7]); +} + +/* + * Decode the sector pointed to by "pData" and described by "pNibbleDescr". + * This invokes the appropriate function (e.g. 5&3 or 6&2) to decode the + * data into a 256-byte sector. + */ +DIError +DiskImg::DecodeNibbleData(const CircularBufferAccess& buffer, int idx, + unsigned char* sctBuf, const NibbleDescr* pNibbleDescr) +{ + switch (pNibbleDescr->encoding) { + case kNibbleEnc62: + return DecodeNibble62(buffer, idx, sctBuf, pNibbleDescr); + case kNibbleEnc53: + return DecodeNibble53(buffer, idx, sctBuf, pNibbleDescr); + default: + assert(false); + return kDIErrInternal; + } +} + +/* + * Encode the sector pointed to by "pData" and described by "pNibbleDescr". + * This invokes the appropriate function (e.g. 5&3 or 6&2) to encode the + * data from a 256-byte sector. + */ +void +DiskImg::EncodeNibbleData(const CircularBufferAccess& buffer, int idx, + const unsigned char* sctBuf, const NibbleDescr* pNibbleDescr) const +{ + switch (pNibbleDescr->encoding) { + case kNibbleEnc62: + EncodeNibble62(buffer, idx, sctBuf, pNibbleDescr); + break; + case kNibbleEnc53: + EncodeNibble53(buffer, idx, sctBuf, pNibbleDescr); + break; + default: + assert(false); + break; + } +} + +/* + * Decode 6&2 encoding. + */ +DIError +DiskImg::DecodeNibble62(const CircularBufferAccess& buffer, int idx, + unsigned char* sctBuf, const NibbleDescr* pNibbleDescr) +{ + unsigned char twos[kChunkSize62 * 3]; // 258 + int chksum = pNibbleDescr->dataChecksumSeed; + unsigned char decodedVal; + int i; + + /* + * Pull the 342 bytes out, convert them from disk bytes to 6-bit + * values, and arrange them into a DOS-like pair of buffers. + */ + for (i = 0; i < kChunkSize62; i++) { + decodedVal = kInvDiskBytes62[buffer[idx++]]; + if (decodedVal == kInvInvalidValue) + return kDIErrInvalidDiskByte; + assert(decodedVal < sizeof(kDiskBytes62)); + + chksum ^= decodedVal; + twos[i] = + ((chksum & 0x01) << 1) | ((chksum & 0x02) >> 1); + twos[i + kChunkSize62] = + ((chksum & 0x04) >> 1) | ((chksum & 0x08) >> 3); + twos[i + kChunkSize62*2] = + ((chksum & 0x10) >> 3) | ((chksum & 0x20) >> 5); + } + + for (i = 0; i < 256; i++) { + decodedVal = kInvDiskBytes62[buffer[idx++]]; + if (decodedVal == kInvInvalidValue) + return kDIErrInvalidDiskByte; + assert(decodedVal < sizeof(kDiskBytes62)); + + chksum ^= decodedVal; + sctBuf[i] = (chksum << 2) | twos[i]; + } + + /* + * Grab the 343rd byte (the checksum byte) and see if we did this + * right. + */ + //printf("Dec checksum value is 0x%02x\n", chksum); + decodedVal = kInvDiskBytes62[buffer[idx++]]; + if (decodedVal == kInvInvalidValue) + return kDIErrInvalidDiskByte; + assert(decodedVal < sizeof(kDiskBytes62)); + chksum ^= decodedVal; + + if (pNibbleDescr->dataVerifyChecksum && chksum != 0) { + WMSG0(" NIB bad data checksum\n"); + return kDIErrBadChecksum; + } + return kDIErrNone; +} + +/* + * Encode 6&2 encoding. + */ +void +DiskImg::EncodeNibble62(const CircularBufferAccess& buffer, int idx, + const unsigned char* sctBuf, const NibbleDescr* pNibbleDescr) const +{ + unsigned char top[256]; + unsigned char twos[kChunkSize62]; + int twoPosn, twoShift; + int i; + + memset(twos, 0, sizeof(twos)); + + twoShift = 0; + for (i = 0, twoPosn = kChunkSize62-1; i < 256; i++) { + unsigned int val = sctBuf[i]; + top[i] = val >> 2; + twos[twoPosn] |= ((val & 0x01) << 1 | (val & 0x02) >> 1) << twoShift; + + if (twoPosn == 0) { + twoPosn = kChunkSize62; + twoShift += 2; + } + twoPosn--; + } + + int chksum = pNibbleDescr->dataChecksumSeed; + for (i = kChunkSize62-1; i >= 0; i--) { + assert(twos[i] < sizeof(kDiskBytes62)); + buffer[idx++] = kDiskBytes62[twos[i] ^ chksum]; + chksum = twos[i]; + } + + for (i = 0; i < 256; i++) { + assert(top[i] < sizeof(kDiskBytes62)); + buffer[idx++] = kDiskBytes62[top[i] ^ chksum]; + chksum = top[i]; + } + + //printf("Enc checksum value is 0x%02x\n", chksum); + buffer[idx++] = kDiskBytes62[chksum]; +} + +/* + * Decode 5&3 encoding. + */ +DIError +DiskImg::DecodeNibble53(const CircularBufferAccess& buffer, int idx, + unsigned char* sctBuf, const NibbleDescr* pNibbleDescr) +{ + unsigned char base[256]; + unsigned char threes[kThreeSize]; + int chksum = pNibbleDescr->dataChecksumSeed; + unsigned char decodedVal; + int i; + + /* + * Pull the 410 bytes out, convert them from disk bytes to 5-bit + * values, and arrange them into a DOS-like pair of buffers. + */ + for (i = kThreeSize-1; i >= 0; i--) { + decodedVal = kInvDiskBytes53[buffer[idx++]]; + if (decodedVal == kInvInvalidValue) + return kDIErrInvalidDiskByte; + assert(decodedVal < sizeof(kDiskBytes53)); + + chksum ^= decodedVal; + threes[i] = chksum; + } + + for (i = 0; i < 256; i++) { + decodedVal = kInvDiskBytes53[buffer[idx++]]; + if (decodedVal == kInvInvalidValue) + return kDIErrInvalidDiskByte; + assert(decodedVal < sizeof(kDiskBytes53)); + + chksum ^= decodedVal; + base[i] = (chksum << 3); + } + + /* + * Grab the 411th byte (the checksum byte) and see if we did this + * right. + */ + //printf("Dec checksum value is 0x%02x\n", chksum); + decodedVal = kInvDiskBytes53[buffer[idx++]]; + if (decodedVal == kInvInvalidValue) + return kDIErrInvalidDiskByte; + assert(decodedVal < sizeof(kDiskBytes53)); + chksum ^= decodedVal; + + if (pNibbleDescr->dataVerifyChecksum && chksum != 0) { + WMSG1(" NIB bad data checksum (0x%02x)\n", chksum); + return kDIErrBadChecksum; + } + + /* + * Convert this pile of stuff into 256 data bytes. + */ + unsigned char* bufPtr; + + bufPtr = sctBuf; + for (i = kChunkSize53-1; i >= 0; i--) { + int three1, three2, three3, three4, three5; + + three1 = threes[i]; + three2 = threes[kChunkSize53 + i]; + three3 = threes[kChunkSize53*2 + i]; + three4 = (three1 & 0x02) << 1 | (three2 & 0x02) | (three3 & 0x02) >> 1; + three5 = (three1 & 0x01) << 2 | (three2 & 0x01) << 1 | (three3 & 0x01); + + *bufPtr++ = base[i] | ((three1 >> 2) & 0x07); + *bufPtr++ = base[kChunkSize53 + i] | ((three2 >> 2) & 0x07); + *bufPtr++ = base[kChunkSize53*2 + i] | ((three3 >> 2) & 0x07); + *bufPtr++ = base[kChunkSize53*3 + i] | (three4 & 0x07); + *bufPtr++ = base[kChunkSize53*4 + i] | (three5 & 0x07); + } + assert(bufPtr == sctBuf + 255); + + /* + * Convert the very last byte, which is handled specially. + */ + *bufPtr = base[255] | (threes[kThreeSize-1] & 0x07); + + return kDIErrNone; +} + +/* + * Encode 5&3 encoding. + */ +void +DiskImg::EncodeNibble53(const CircularBufferAccess& buffer, int idx, + const unsigned char* sctBuf, const NibbleDescr* pNibbleDescr) const +{ + unsigned char top[kChunkSize53 * 5 +1]; // (255 / 0xff) +1 + unsigned char threes[kChunkSize53 * 3 +1]; // (153 / 0x99) +1 + int i, chunk; + + /* + * Split the bytes into sections. + */ + chunk = kChunkSize53-1; + for (i = 0; i < (int) sizeof(top)-1; i += 5) { + int three1, three2, three3, three4, three5; + + three1 = *sctBuf++; + three2 = *sctBuf++; + three3 = *sctBuf++; + three4 = *sctBuf++; + three5 = *sctBuf++; + + top[chunk] = three1 >> 3; + top[chunk + kChunkSize53*1] = three2 >> 3; + top[chunk + kChunkSize53*2] = three3 >> 3; + top[chunk + kChunkSize53*3] = three4 >> 3; + top[chunk + kChunkSize53*4] = three5 >> 3; + + threes[chunk] = + (three1 & 0x07) << 2 | (three4 & 0x04) >> 1 | (three5 & 0x04) >> 2; + threes[chunk + kChunkSize53*1] = + (three2 & 0x07) << 2 | (three4 & 0x02) | (three5 & 0x02) >> 1; + threes[chunk + kChunkSize53*2] = + (three3 & 0x07) << 2 | (three4 & 0x01) << 1 | (three5 & 0x01); + + chunk--; + } + assert(chunk == -1); + + /* + * Handle the last byte. + */ + int val; + val = *sctBuf++; + top[255] = val >> 3; + threes[kThreeSize-1] = val & 0x07; + + /* + * Write the bytes. + */ + int chksum = pNibbleDescr->dataChecksumSeed; + for (i = sizeof(threes)-1; i >= 0; i--) { + assert(threes[i] < sizeof(kDiskBytes53)); + buffer[idx++] = kDiskBytes53[threes[i] ^ chksum]; + chksum = threes[i]; + } + + for (i = 0; i < 256; i++) { + assert(top[i] < sizeof(kDiskBytes53)); + buffer[idx++] = kDiskBytes53[top[i] ^ chksum]; + chksum = top[i]; + } + + //printf("Enc checksum value is 0x%02x\n", chksum); + buffer[idx++] = kDiskBytes53[chksum]; +} + + +/* + * =========================================================================== + * Higher-level functions + * =========================================================================== + */ + +/* + * Dump some bytes as hex values into a string. + * + * "buf" must be able to hold (num * 3) characters. + */ +static void +DumpBytes(const unsigned char* bytes, unsigned int num, char* buf) +{ + sprintf(buf, "%02x", bytes[0]); + buf += 2; + + for (int i = 1; i < (int) num; i++) { + sprintf(buf, " %02x", bytes[i]); + buf += 3; + } + + *buf = '\0'; +} + +static inline const char* +VerifyStr(bool val) +{ + return val ? "verify" : "ignore"; +} + +/* + * Dump the contents of a NibbleDescr struct. + */ +void +DiskImg::DumpNibbleDescr(const NibbleDescr* pNibDescr) const +{ + char outBuf1[48]; + char outBuf2[48]; + const char* encodingStr; + + switch (pNibDescr->encoding) { + case kNibbleEnc62: encodingStr = "6&2"; break; + case kNibbleEnc53: encodingStr = "5&3"; break; + case kNibbleEnc44: encodingStr = "4&4"; break; + default: encodingStr = "???"; break; + } + + WMSG1("NibbleDescr '%s':\n", pNibDescr->description); + WMSG1(" Nibble encoding is %s\n", encodingStr); + DumpBytes(pNibDescr->addrProlog, sizeof(pNibDescr->addrProlog), outBuf1); + DumpBytes(pNibDescr->dataProlog, sizeof(pNibDescr->dataProlog), outBuf2); + WMSG2(" Addr prolog: %s Data prolog: %s\n", outBuf1, outBuf2); + DumpBytes(pNibDescr->addrEpilog, sizeof(pNibDescr->addrEpilog), outBuf1); + DumpBytes(pNibDescr->dataEpilog, sizeof(pNibDescr->dataEpilog), outBuf2); + WMSG4(" Addr epilog: %s (%d) Data epilog: %s (%d)\n", + outBuf1, pNibDescr->addrEpilogVerifyCount, + outBuf2, pNibDescr->dataEpilogVerifyCount); + WMSG2(" Addr checksum: %s Data checksum: %s\n", + VerifyStr(pNibDescr->addrVerifyChecksum), + VerifyStr(pNibDescr->dataVerifyChecksum)); + WMSG2(" Addr checksum seed: 0x%02x Data checksum seed: 0x%02x\n", + pNibDescr->addrChecksumSeed, pNibDescr->dataChecksumSeed); + WMSG1(" Addr check track: %s\n", + VerifyStr(pNibDescr->addrVerifyTrack)); +} + + +/* + * Load a nibble track into our track buffer. + */ +DIError +DiskImg::LoadNibbleTrack(long track, long* pTrackLen) +{ + DIError dierr = kDIErrNone; + long offset; + assert(track >= 0 && track < kMaxNibbleTracks525); + + *pTrackLen = GetNibbleTrackLength(track); + offset = GetNibbleTrackOffset(track); + assert(*pTrackLen > 0); + assert(offset >= 0); + + if (track == fNibbleTrackLoaded) { +#ifdef NIB_VERBOSE_DEBUG + WMSG1(" DI track %d already loaded\n", track); +#endif + return kDIErrNone; + } else { + WMSG1(" DI loading track %ld\n", track); + } + + /* invalidate in case we fail with partial read */ + fNibbleTrackLoaded = -1; + + /* alloc track buffer if needed */ + if (fNibbleTrackBuf == nil) { + fNibbleTrackBuf = new unsigned char[kTrackAllocSize]; + if (fNibbleTrackBuf == nil) + return kDIErrMalloc; + } + + /* + * Read the entire track into memory. + */ + dierr = CopyBytesOut(fNibbleTrackBuf, offset, *pTrackLen); + if (dierr != kDIErrNone) + return dierr; + + fNibbleTrackLoaded = track; + + return dierr; +} + +/* + * Save the track buffer back to disk. + */ +DIError +DiskImg::SaveNibbleTrack(void) +{ + if (fNibbleTrackLoaded < 0) { + WMSG0("ERROR: tried to save track without loading it first\n"); + return kDIErrInternal; + } + assert(fNibbleTrackBuf != nil); + + DIError dierr = kDIErrNone; + long trackLen = GetNibbleTrackLength(fNibbleTrackLoaded); + long offset = GetNibbleTrackOffset(fNibbleTrackLoaded); + + /* write the track to fpDataGFD */ + dierr = CopyBytesIn(fNibbleTrackBuf, offset, trackLen); + return dierr; +} + + +/* + * Count up the number of readable sectors found on this track, and + * return it. If "pVol" is non-nil, return the volume number from + * one of the readable sectors. + */ +int +DiskImg::TestNibbleTrack(int track, const NibbleDescr* pNibbleDescr, + int* pVol) +{ + long trackLen; + int count = 0; + + assert(track >= 0 && track < kTrackCount525); + assert(pNibbleDescr != nil); + + if (LoadNibbleTrack(track, &trackLen) != kDIErrNone) { + WMSG0(" DI FindNibbleSectorStart: LoadNibbleTrack failed\n"); + return 0; + } + + CircularBufferAccess buffer(fNibbleTrackBuf, trackLen); + + int i, sectorIdx; + for (i = 0; i < pNibbleDescr->numSectors; i++) { + int vol; + sectorIdx = FindNibbleSectorStart(buffer, track, i, pNibbleDescr, &vol); + if (sectorIdx >= 0) { + if (pVol != nil) + *pVol = vol; + + unsigned char sctBuf[256]; + if (DecodeNibbleData(buffer, sectorIdx, sctBuf, pNibbleDescr) == kDIErrNone) + count++; + } + } + + WMSG3(" Tests on track=%d with '%s' returning count=%d\n", + track, pNibbleDescr->description, count); + + return count; +} + +/* + * Analyze the nibblized track data. + * + * On entry: + * fPhysical indicates the appropriate nibble format + * + * On exit: + * fpNibbleDescr points to the most-likely-to-succeed NibbleDescr + * fDOSVolumeNum holds a volume number from one of the tracks + * fNumTracks holds the number of tracks on the disk + */ +DIError +DiskImg::AnalyzeNibbleData(void) +{ + assert(IsNibbleFormat(fPhysical)); + + if (fPhysical == kPhysicalFormatNib525_Var) { + /* TrackStar can have up to 40 */ + fNumTracks = fpImageWrapper->GetNibbleNumTracks(); + assert(fNumTracks > 0); + } else { + /* fixed-length formats (.nib, .nb2) are always 35 tracks */ + fNumTracks = kTrackCount525; + } + + /* + * Try to read sectors from tracks 1, 16, 17, and 26. If we can get + * at least 13 out of 16 (or 10 out of 13) on three out of four tracks, + * we have a winner. + */ + int i, good, goodTracks; + int protoVol = kVolumeNumNotSet; + + for (i = 0; i < fNumNibbleDescrEntries; i++) { + if (fpNibbleDescrTable[i].numSectors == 0) { + /* uninitialized "custom" entry */ + WMSG1(" Skipping '%s'\n", fpNibbleDescrTable[i].description); + continue; + } + WMSG1(" Trying '%s'\n", fpNibbleDescrTable[i].description); + goodTracks = 0; + + good = TestNibbleTrack(1, &fpNibbleDescrTable[i], nil); + if (good > fpNibbleDescrTable[i].numSectors - 4) + goodTracks++; + good = TestNibbleTrack(16, &fpNibbleDescrTable[i], nil); + if (good > fpNibbleDescrTable[i].numSectors - 4) + goodTracks++; + good = TestNibbleTrack(17, &fpNibbleDescrTable[i], &protoVol); + if (good > fpNibbleDescrTable[i].numSectors - 4) + goodTracks++; + good = TestNibbleTrack(26, &fpNibbleDescrTable[i], nil); + if (good > fpNibbleDescrTable[i].numSectors - 4) + goodTracks++; + + if (goodTracks >= 3) { + WMSG3(" Looks like '%s' (%d-sector), vol=%d\n", + fpNibbleDescrTable[i].description, + fpNibbleDescrTable[i].numSectors, protoVol); + fpNibbleDescr = &fpNibbleDescrTable[i]; + fDOSVolumeNum = protoVol; + break; + } + } + if (i == fNumNibbleDescrEntries) { + WMSG0("AnalyzeNibbleData did not find matching NibbleDescr\n"); + return kDIErrBadNibbleSectors; + } + + return kDIErrNone; +} + + +/* + * Read a sector from a nibble image. + * + * While fNumTracks is valid, fNumSectPerTrack is a little flaky, because + * in theory each track could be formatted differently. + */ +DIError +DiskImg::ReadNibbleSector(long track, int sector, void* buf, + const NibbleDescr* pNibbleDescr) +{ + if (pNibbleDescr == nil) { + /* disk has no recognizable sectors */ + WMSG0(" DI ReadNibbleSector: pNibbleDescr is nil, returning failure\n"); + return kDIErrBadNibbleSectors; + } + if (sector >= pNibbleDescr->numSectors) { + /* e.g. trying to read sector 14 on a 13-sector disk */ + WMSG0(" DI ReadNibbleSector: bad sector number request\n"); + return kDIErrInvalidSector; + } + + assert(pNibbleDescr != nil); + assert(IsNibbleFormat(fPhysical)); + assert(track >= 0 && track < GetNumTracks()); + assert(sector >= 0 && sector < pNibbleDescr->numSectors); + + DIError dierr = kDIErrNone; + long trackLen; + int sectorIdx, vol; + + dierr = LoadNibbleTrack(track, &trackLen); + if (dierr != kDIErrNone) { + WMSG1(" DI ReadNibbleSector: LoadNibbleTrack %ld failed\n", track); + return dierr; + } + + CircularBufferAccess buffer(fNibbleTrackBuf, trackLen); + sectorIdx = FindNibbleSectorStart(buffer, track, sector, pNibbleDescr, + &vol); + if (sectorIdx < 0) + return kDIErrSectorUnreadable; + + dierr = DecodeNibbleData(buffer, sectorIdx, (unsigned char*) buf, + pNibbleDescr); + + return dierr; +} + +/* + * Write a sector to a nibble image. + */ +DIError +DiskImg::WriteNibbleSector(long track, int sector, const void* buf, + const NibbleDescr* pNibbleDescr) +{ + assert(pNibbleDescr != nil); + assert(IsNibbleFormat(fPhysical)); + assert(track >= 0 && track < GetNumTracks()); + assert(sector >= 0 && sector < pNibbleDescr->numSectors); + assert(!fReadOnly); + + DIError dierr = kDIErrNone; + long trackLen; + int sectorIdx, vol; + + dierr = LoadNibbleTrack(track, &trackLen); + if (dierr != kDIErrNone) { + WMSG1(" DI ReadNibbleSector: LoadNibbleTrack %ld failed\n", track); + return dierr; + } + + CircularBufferAccess buffer(fNibbleTrackBuf, trackLen); + sectorIdx = FindNibbleSectorStart(buffer, track, sector, pNibbleDescr, + &vol); + if (sectorIdx < 0) + return kDIErrSectorUnreadable; + + EncodeNibbleData(buffer, sectorIdx, (unsigned char*) buf, pNibbleDescr); + + dierr = SaveNibbleTrack(); + if (dierr != kDIErrNone) { + WMSG1(" DI ReadNibbleSector: SaveNibbleTrack %ld failed\n", track); + return dierr; + } + + return dierr; +} + + +/* + * Get the contents of the nibble track. + * + * "buf" must be able to hold kTrackAllocSize bytes. + */ +DIError +DiskImg::ReadNibbleTrack(long track, unsigned char* buf, long* pTrackLen) +{ + DIError dierr; + + dierr = LoadNibbleTrack(track, pTrackLen); + if (dierr != kDIErrNone) { + WMSG1(" DI ReadNibbleTrack: LoadNibbleTrack %ld failed\n", track); + return dierr; + } + + memcpy(buf, fNibbleTrackBuf, *pTrackLen); + return kDIErrNone; +} + +/* + * Set the contents of a nibble track. + * + * NOTE: This currently does the wrong thing when converting from .nb2 to + * .nib. Fixed-length formats shouldn't be allowed to interact. Figure + * this out someday. For now, the higher-level code prevents it. + */ +DIError +DiskImg::WriteNibbleTrack(long track, const unsigned char* buf, long trackLen) +{ + DIError dierr; + long oldTrackLen; + + /* load the track to set the "current track" stuff */ + dierr = LoadNibbleTrack(track, &oldTrackLen); + if (dierr != kDIErrNone) { + WMSG1(" DI WriteNibbleTrack: LoadNibbleTrack %ld failed\n", track); + return dierr; + } + + if (trackLen > GetNibbleTrackAllocLength()) { + WMSG2("ERROR: tried to write too-long track len (%ld vs %d)\n", + trackLen, GetNibbleTrackAllocLength()); + return kDIErrInvalidArg; + } + + if (trackLen < oldTrackLen) // pad out any extra space + memset(fNibbleTrackBuf, 0xff, oldTrackLen); + memcpy(fNibbleTrackBuf, buf, trackLen); + fpImageWrapper->SetNibbleTrackLength(track, trackLen); + + dierr = SaveNibbleTrack(); + if (dierr != kDIErrNone) { + WMSG1(" DI ReadNibbleSector: SaveNibbleTrack %ld failed\n", track); + return dierr; + } + + return kDIErrNone; +} + + +/* + * Create a blank nibble image, using fpNibbleDescr as the template. + * Sets "fLength". + * + * Tracks are written the same way regardless of actual track length (be + * it 6656, 6384, or variable-length). Anything longer than 6384 just has + * more padding at the end of the track. + * + * The format looks like this: + * Gap one (48 self-sync bytes) + * For each sector: + * Address field (14 bytes, e.g. d5aa96 vol track sect chksum deaaeb) + * Gap two (six self-sync bytes) + * Data field (6 header bytes, 1 checksum byte, and 342 or 410 data bytes) + * Gap three (27 self-sync bytes) + * + * 48 + (14 + 6 + (6 + 1 + 342) + 27) * 16 = 6384 + * 48 + (14 + 6 + (6 + 1 + 410) + 27) * 13 = 6080 + */ +DIError +DiskImg::FormatNibbles(GenericFD* pGFD) const +{ + assert(fHasNibbles); + assert(GetNumTracks() > 0); + + DIError dierr = kDIErrNone; + unsigned char trackBuf[kTrackAllocSize]; + /* these should be the same except for var-len images */ + long trackAllocLen = GetNibbleTrackAllocLength(); + long trackLen = GetNibbleTrackFormatLength(); + int track; + + assert(trackLen > 0); + pGFD->Rewind(); + + /* + * If we don't have sector access, take a shortcut and just fill the + * entire image with 0xff. + */ + if (!fHasSectors) { + memset(trackBuf, 0xff, trackLen); + for (track = 0; track < GetNumTracks(); track++) { + /* write the track to the GFD */ + dierr = pGFD->Write(trackBuf, trackAllocLen); + if (dierr != kDIErrNone) + return dierr; + fpImageWrapper->SetNibbleTrackLength(track, trackAllocLen); + } + + return kDIErrNone; + } + + + assert(fHasSectors); + assert(fpNibbleDescr != nil); + assert(fpNibbleDescr->numSectors == GetNumSectPerTrack()); + assert(fpNibbleDescr->encoding == kNibbleEnc53 || + fpNibbleDescr->encoding == kNibbleEnc62); + assert(fDOSVolumeNum != kVolumeNumNotSet); + + /* + * Create a prototype sector. The data for a sector full of zeroes + * is exactly the same; only the address header changes. + */ + unsigned char sampleSource[256]; + unsigned char sampleBuf[512]; // must hold 5&3 and 6&2 + CircularBufferAccess sample(sampleBuf, 512); + long dataLen; + + if (fpNibbleDescr->encoding == kNibbleEnc53) + dataLen = 410 +1; + else + dataLen = 342 +1; + + memset(sampleSource, 0, sizeof(sampleSource)); + EncodeNibbleData(sample, 0, sampleSource, fpNibbleDescr); + + /* + * For each track in the image, "format" the expected number of + * sectors, then write the data to the GFD. + */ + for (track = 0; track < GetNumTracks(); track++) { + //WMSG1("Formatting track %d\n", track); + unsigned char* trackPtr = trackBuf; + + /* + * Fill with "self-sync" bytes. + */ + memset(trackBuf, 0xff, trackAllocLen); + + /* gap one */ + trackPtr += 48; + + for (int sector = 0; sector < fpNibbleDescr->numSectors; sector++) { + /* + * Write address field. + */ + unsigned short hdrTrack, hdrSector, hdrVol, hdrChksum; + hdrTrack = track; + hdrSector = sector; + hdrVol = fDOSVolumeNum; + *trackPtr++ = fpNibbleDescr->addrProlog[0]; + *trackPtr++ = fpNibbleDescr->addrProlog[1]; + *trackPtr++ = fpNibbleDescr->addrProlog[2]; + *trackPtr++ = Conv44(hdrVol, true); + *trackPtr++ = Conv44(hdrVol, false); + *trackPtr++ = Conv44(hdrTrack, true); + *trackPtr++ = Conv44(hdrTrack, false); + *trackPtr++ = Conv44(hdrSector, true); + *trackPtr++ = Conv44(hdrSector, false); + hdrChksum = fpNibbleDescr->addrChecksumSeed ^ + hdrVol ^ hdrTrack ^ hdrSector; + *trackPtr++ = Conv44(hdrChksum, true); + *trackPtr++ = Conv44(hdrChksum, false); + *trackPtr++ = fpNibbleDescr->addrEpilog[0]; + *trackPtr++ = fpNibbleDescr->addrEpilog[1]; + *trackPtr++ = fpNibbleDescr->addrEpilog[2]; + + /* gap two */ + trackPtr += 6; + + /* + * Write data field. + */ + *trackPtr++ = fpNibbleDescr->dataProlog[0]; + *trackPtr++ = fpNibbleDescr->dataProlog[1]; + *trackPtr++ = fpNibbleDescr->dataProlog[2]; + memcpy(trackPtr, sampleBuf, dataLen); + trackPtr += dataLen; + *trackPtr++ = fpNibbleDescr->dataEpilog[0]; + *trackPtr++ = fpNibbleDescr->dataEpilog[1]; + *trackPtr++ = fpNibbleDescr->dataEpilog[2]; + + /* gap three */ + trackPtr += 27; + } + + assert(trackPtr - trackBuf == 6384 || + trackPtr - trackBuf == 6080); + + /* + * Write the track to the GFD. + */ + dierr = pGFD->Write(trackBuf, trackAllocLen); + if (dierr != kDIErrNone) + break; + + /* on a variable-length image, reduce track len to match */ + fpImageWrapper->SetNibbleTrackLength(track, trackLen); + } + + return dierr; +} + diff --git a/diskimg/Nibble35.cpp b/diskimg/Nibble35.cpp new file mode 100644 index 0000000..b1a1283 --- /dev/null +++ b/diskimg/Nibble35.cpp @@ -0,0 +1,557 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * GCR nibble image support for 3.5" disks. + * + * Each track has between 8 and 12 512-byte sectors. The encoding is similar + * to but different from that used on 5.25" disks. + * + * THOUGHT: this is currently designed for unpacking all blocks from a track. + * We really ought to allow the user to view the track in nibble form, which + * means reworking the interface to be more like the 5.25" nibble stuff. We + * should present it as a block interface rather than track/sector; the code + * here can convert the block # to track/sector, and just provide a raw + * interface for the nibble track viewer. + */ +#include "StdAfx.h" +#include "DiskImgPriv.h" + +/* +Physical sector layout: + ++00 self-sync 0xff pattern (36 10-bit bytes, or 45 8-bit bytes) ++36 addr prolog (0xd5 0xaa 0x96) ++39 6&2enc track number (0-79 mod 63) ++40 6&2enc sector number (0-N) ++41 6&2enc side (0x00 or 0x20, ORed with 0x01 for tracks >= 64) ++42 6&2enc format (0x22, 0x24, others?) ++43 6&2enc checksum (track ^ sector ^ side ^ format) ++44 addr epilog (0xde 0xaa) ++46 self-sync 0xff (6 10-bit bytes) ++52 data prolog (0xd5 0xaa 0xad) ++55 6&2enc sector number (another copy) ++56 6&2enc nibblized data (699 bytes) ++755 checksum, 3 bytes 6&2 encoded as 4 bytes ++759 data epilog (0xde 0xaa) ++761 0xff (pad byte) + +Some sources say it starts with 42 10-bit self-sync bytes instead of 36. +*/ + +/* + * Basic disk geometry. + */ +const int kCylindersPerDisk = 80; +const int kHeadsPerCylinder = 2; +const int kMaxSectorsPerTrack = 12; +const int kSectorSize35 = 524; // 512 data bytes + 12 tag bytes +const int kTagBytesLen = 12; +const int kDataChecksumLen = 3; +const int kChunkSize35 = 175; // ceil(524 / 3) +const int kOffsetToChecksum = 699; +const int kNibblizedOutputLen = (kOffsetToChecksum + 4); +const int kMaxDataReach = 48; // should only be 6 bytes */ + +enum { + kAddrProlog0 = 0xd5, + kAddrProlog1 = 0xaa, + kAddrProlog2 = 0x96, + kAddrEpilog0 = 0xde, + kAddrEpilog1 = 0xaa, + + kDataProlog0 = 0xd5, + kDataProlog1 = 0xaa, + kDataProlog2 = 0xad, + kDataEpilog0 = 0xde, + kDataEpilog1 = 0xaa, +}; + +/* + * There are 12 sectors per track for the first 16 cylinders, 11 sectors + * per track for the next 16, and so on until we're down to 8 per track. + */ +/*static*/ int +DiskImg::SectorsPerTrack35(int cylinder) +{ + return kMaxSectorsPerTrack - (cylinder / 16); +} + +/* + * Convert cylinder/head/sector to a block number on a 3.5" disk. + */ +/*static*/ int +DiskImg::CylHeadSect35ToBlock(int cyl, int head, int sect) +{ + int i, block; + + assert(cyl >= 0 && cyl < kCylindersPerDisk); + assert(head >= 0 && head < kHeadsPerCylinder); + assert(sect >= 0 && sect < SectorsPerTrack35(cyl)); + + block = 0; + for (i = 0; i < cyl; i++) + block += SectorsPerTrack35(i) * kHeadsPerCylinder; + if (head) + block += SectorsPerTrack35(i); + block += sect; + + //WMSG4("Nib35: c/h/s %d/%d/%d --> block %d\n", cyl, head, sect, block); + assert(block >= 0 && block < 1600); + return block; +} + +/* + * Unpack a nibble track. + * + * "outputBuf" must be able to hold 512 * 12 sectors of decoded sector data. + */ +/*static*/ DIError +DiskImg::UnpackNibbleTrack35(const unsigned char* nibbleBuf, + long nibbleLen, unsigned char* outputBuf, int cyl, int head, + LinearBitmap* pBadBlockMap) +{ + CircularBufferAccess buffer(nibbleBuf, nibbleLen); + bool foundSector[kMaxSectorsPerTrack]; + unsigned char sectorBuf[kSectorSize35]; + unsigned char readSum[kDataChecksumLen]; + unsigned char calcSum[kDataChecksumLen]; + int i; + + memset(&foundSector, 0, sizeof(foundSector)); + + i = 0; + while (i < nibbleLen) { + int sector; + + i = FindNextSector35(buffer, i, cyl, head, §or); + if (i < 0) + break; + + assert(sector >= 0 && sector < SectorsPerTrack35(cyl)); + if (foundSector[sector]) { + WMSG3("Nib35: WARNING: found two copies of sect %d on cyl=%d head=%d\n", + sector, cyl, head); + } else { + memset(sectorBuf, 0xa9, sizeof(sectorBuf)); + if (DecodeNibbleSector35(buffer, i, sectorBuf, readSum, calcSum)) + { + /* successfully decoded sector, copy data & verify checksum */ + foundSector[sector] = true; + memcpy(outputBuf + kBlockSize * sector, + sectorBuf + kTagBytesLen, kBlockSize); + + if (calcSum[0] != readSum[0] || + calcSum[1] != readSum[1] || + calcSum[2] != readSum[2]) + { + WMSG2("Nib35: checksum mismatch: 0x%06x vs. 0x%06x\n", + calcSum[0] << 16 | calcSum[1] << 8 | calcSum[2], + readSum[0] << 16 | readSum[1] << 8 | readSum[2]); + WMSG4("Nib35: marking cyl=%d head=%d sect=%d (block=%d)\n", + cyl, head, sector, + CylHeadSect35ToBlock(cyl, head, sector)); + pBadBlockMap->Set(CylHeadSect35ToBlock(cyl, head, sector)); + } + } + } + } + + /* + * Check to see if we have all our parts. Anything missing sets + * a flag in the "bad block" map. + */ + for (i = SectorsPerTrack35(cyl)-1; i >= 0; i--) { + if (!foundSector[i]) { + WMSG4("Nib35: didn't find cyl=%d head=%d sect=%d (block=%d)\n", + cyl, head, i, CylHeadSect35ToBlock(cyl, head, i)); + pBadBlockMap->Set(CylHeadSect35ToBlock(cyl, head, i)); + } + + /* + // DEBUG test + if ((cyl == 0 || cyl == 12 || cyl == 79) && + (head == (cyl & 0x01)) && + (i == 1 || i == 7)) + { + WMSG4("DEBUG: setting bad %d/%d/%d (%d)\n", + cyl, head, i, CylHeadSect35ToBlock(cyl, head, i)); + pBadBlockMap->Set(CylHeadSect35ToBlock(cyl, head, i)); + } + */ + } + + return kDIErrNone; // maybe return an error if nothing found? +} + +/* + * Returns the offset of the next sector, or -1 if we went off the end. + */ +/*static*/ int +DiskImg::FindNextSector35(const CircularBufferAccess& buffer, int start, + int cyl, int head, int* pSector) +{ + int end = buffer.GetSize(); + int i; + + for (i = start; i < end; i++) { + bool foundAddr = false; + + if (buffer[i] == kAddrProlog0 && + buffer[i+1] == kAddrProlog1 && + buffer[i+2] == kAddrProlog2) + { + foundAddr = true; + } + + if (foundAddr) { + /* decode the address field */ + int trackNum, sectNum, side, format, checksum; + + trackNum = kInvDiskBytes62[buffer[i+3]]; + sectNum = kInvDiskBytes62[buffer[i+4]]; + side = kInvDiskBytes62[buffer[i+5]]; + format = kInvDiskBytes62[buffer[i+6]]; + checksum = kInvDiskBytes62[buffer[i+7]]; + if (trackNum == kInvInvalidValue || + sectNum == kInvInvalidValue || + side == kInvInvalidValue || + format == kInvInvalidValue || + checksum == kInvInvalidValue) + { + WMSG0("Nib35: garbled address header found\n"); + continue; + } + //WMSG5(" Nib35: got addr: track=%2d sect=%2d side=%d format=%d sum=0x%02x\n", + // trackNum, sectNum, side, format, checksum); + if (side != ((head * 0x20) | (cyl >> 6))) { + WMSG3("Nib35: unexpected value for side: %d on cyl=%d head=%d\n", + side, cyl, head); + } + if (sectNum >= SectorsPerTrack35(cyl)) { + WMSG2("Nib35: invalid value for sector: %d (cyl=%d)\n", + sectNum, cyl); + continue; + } + /* format seems to be 0x22 or 0x24 */ + if (checksum != (trackNum ^ sectNum ^ side ^ format)) { + WMSG2("Nib35: unexpected checksum: 0x%02x vs. 0x%02x\n", + checksum, trackNum ^ sectNum ^ side ^ format); + continue; + } + + /* check the epilog bytes */ + if (buffer[i+8] != kAddrEpilog0 || + buffer[i+9] != kAddrEpilog1) + { + WMSG0("Nib35: invalid address epilog\n"); + /* maybe we allow this anyway? */ + } + + *pSector = sectNum; + return i+10; // move past address field + } + } + + return -1; +} + +/* + * Unpack a 524-byte sector from a 3.5" disk. Start with "start" pointed + * in the general vicinity of the data prolog bytes. + * + * "sectorBuf" must hold at least kSectorSize35 bytes. It will be filled + * with the decoded data. + * "readChecksum" and "calcChecksum" must each hold at least kDataChecksumLen + * bytes. The former holds the checksum read from the sector, the latter + * holds the checksum computed from the data. + * + * The 4 to 3 conversion is pretty straightforward. The checksum is + * a little crazy. + * + * Returns "true" if all goes well, "false" if there is a problem. Does + * not return false on a checksum mismatch -- it's up to the caller to + * verify the checksum if desired. + */ +/*static*/ bool +DiskImg::DecodeNibbleSector35(const CircularBufferAccess& buffer, int start, + unsigned char* sectorBuf, unsigned char* readChecksum, + unsigned char* calcChecksum) +{ + const int kMaxDataReach35 = 48; // fairly arbitrary + unsigned char* sectorBufStart = sectorBuf; + unsigned char part0[kChunkSize35], part1[kChunkSize35], part2[kChunkSize35]; + unsigned int chk0, chk1, chk2; + unsigned char val, nib0, nib1, nib2, twos; + int i, off; + + /* + * Find the start of the actual data. Adjust "start" to point at it. + */ + for (off = start; off < start + kMaxDataReach35; off++) { + if (buffer[off] == kDataProlog0 && + buffer[off+1] == kDataProlog1 && + buffer[off+2] == kDataProlog2) + { + start = off + 4; // 3 prolog bytes + sector number + break; + } + } + if (off == start + kMaxDataReach35) { + WMSG0("nib25: could not find start of data field\n"); + return false; + } + + /* + * Assemble 8-bit bytes from 6&2 encoded values. + */ + off = start; + for (i = 0; i < kChunkSize35; i++) { + twos = kInvDiskBytes62[buffer[off++]]; + nib0 = kInvDiskBytes62[buffer[off++]]; + nib1 = kInvDiskBytes62[buffer[off++]]; + if (i != kChunkSize35-1) + nib2 = kInvDiskBytes62[buffer[off++]]; + else + nib2 = 0; + + if (twos == kInvInvalidValue || + nib0 == kInvInvalidValue || + nib1 == kInvInvalidValue || + nib2 == kInvInvalidValue) + { + // junk found + WMSG1("Nib25: found invalid disk byte in sector data at %d\n", + off - start); + WMSG4(" (one of 0x%02x 0x%02x 0x%02x 0x%02x)\n", + buffer[off-4], buffer[off-3], buffer[off-2], buffer[off-1]); + return false; + //if (twos == kInvInvalidValue) + // twos = 0; + //if (nib0 == kInvInvalidValue) + // nib0 = 0; + //if (nib1 == kInvInvalidValue) + // nib1 = 0; + //if (nib2 == kInvInvalidValue) + // nib2 = 0; + } + + part0[i] = nib0 | ((twos << 2) & 0xc0); + part1[i] = nib1 | ((twos << 4) & 0xc0); + part2[i] = nib2 | ((twos << 6) & 0xc0); + } + assert(off == start + kOffsetToChecksum); + + chk0 = chk1 = chk2 = 0; + i = 0; + while (true) { + chk0 = (chk0 & 0xff) << 1; + if (chk0 & 0x0100) + chk0++; + + val = part0[i] ^ chk0; + chk2 += val; + if (chk0 & 0x0100) { + chk2++; + chk0 &= 0xff; + } + *sectorBuf++ = val; + + val = part1[i] ^ chk2; + chk1 += val; + if (chk2 > 0xff) { + chk1++; + chk2 &= 0xff; + } + *sectorBuf++ = val; + + if (sectorBuf - sectorBufStart == 524) + break; + + val = part2[i] ^ chk1; + chk0 += val; + if (chk1 > 0xff) { + chk0++; + chk1 &= 0xff; + } + *sectorBuf++ = val; + + i++; + assert(i < kChunkSize35); + //WMSG2("i = %d, diff=%d\n", i, sectorBuf - sectorBufStart); + } + + calcChecksum[0] = chk0; + calcChecksum[1] = chk1; + calcChecksum[2] = chk2; + + if (!UnpackChecksum35(buffer, off, readChecksum)) { + WMSG0("Nib35: failure reading checksum\n"); + readChecksum[0] = calcChecksum[0] ^ 0xff; // force a failure + return false; + } + off += 4; // skip past checksum bytes + + if (buffer[off] != kDataEpilog0 || buffer[off+1] != kDataEpilog1) { + WMSG0("nib25: WARNING: data epilog not found\n"); + // allow it, if the checksum matches + } + +//#define TEST_ENC_35 +#ifdef TEST_ENC_35 + { + unsigned char nibBuf[kNibblizedOutputLen]; + memset(nibBuf, 0xcc, sizeof(nibBuf)); + + /* encode what we just decoded */ + EncodeNibbleSector35(sectorBufStart, nibBuf); + /* compare it to the original */ + for (i = 0; i < kNibblizedOutputLen; i++) { + if (buffer[start + i] != nibBuf[i]) { + /* + * The very last "twos" entry may have undefined bits when + * written by a real drive. Peel it apart and ignore the + * two flaky bits. + */ + if (i == 696) { + unsigned char val1, val2; + val1 = kInvDiskBytes62[buffer[start + i]]; + val2 = kInvDiskBytes62[nibBuf[i]]; + if ((val1 & 0xfc) != (val2 & 0xfc)) { + WMSG5("Nib35 DEBUG: output differs at byte %d" + " (0x%02x vs 0x%02x / 0x%02x vs 0x%02x)\n", + i, buffer[start+i], nibBuf[i], val1, val2); + } + } else { + // note: checksum is 699-702 + WMSG3("Nib35 DEBUG: output differs at byte %d (0x%02x vs 0x%02x)\n", + i, buffer[start+i], nibBuf[i]); + } + } + } + } +#endif /*TEST_ENC_35*/ + + return true; +} + +/* + * Unpack the 6&2 encoded 3-byte checksum at the end of a sector. + * + * "offset" should point to the first byte of the checksum. + * + * Returns "true" if all goes well, "false" otherwise. + */ +/*static*/ bool +DiskImg::UnpackChecksum35(const CircularBufferAccess& buffer, int offset, + unsigned char* checksumBuf) +{ + unsigned char nib0, nib1, nib2, twos; + + twos = kInvDiskBytes62[buffer[offset++]]; + nib2 = kInvDiskBytes62[buffer[offset++]]; + nib1 = kInvDiskBytes62[buffer[offset++]]; + nib0 = kInvDiskBytes62[buffer[offset++]]; + + if (twos == kInvInvalidValue || + nib0 == kInvInvalidValue || + nib1 == kInvInvalidValue || + nib2 == kInvInvalidValue) + { + WMSG0("nib25: found invalid disk byte in checksum\n"); + return false; + } + + checksumBuf[0] = nib0 | ((twos << 6) & 0xc0); + checksumBuf[1] = nib1 | ((twos << 4) & 0xc0); + checksumBuf[2] = nib2 | ((twos << 2) & 0xc0); + return true; +} + +/* + * Encode 524 bytes of sector data into 699 bytes of 6&2 nibblized data + * plus a 4-byte checksum. + * + * "outBuf" must be able to hold kNibblizedOutputLen bytes. + */ +/*static*/ void +DiskImg::EncodeNibbleSector35(const unsigned char* sectorData, + unsigned char* outBuf) +{ + const unsigned char* sectorDataStart = sectorData; + unsigned char* outBufStart = outBuf; + unsigned char part0[kChunkSize35], part1[kChunkSize35], part2[kChunkSize35]; + unsigned int chk0, chk1, chk2; + unsigned char val, twos; + int i; + + /* + * Compute checksum and split the input into 3 pieces. + */ + i = 0; + chk0 = chk1 = chk2 = 0; + while (true) { + chk0 = (chk0 & 0xff) << 1; + if (chk0 & 0x0100) + chk0++; + + val = *sectorData++; + chk2 += val; + if (chk0 & 0x0100) { + chk2++; + chk0 &= 0xff; + } + part0[i] = (val ^ chk0) & 0xff; + + val = *sectorData++; + chk1 += val; + if (chk2 > 0xff) { + chk1++; + chk2 &= 0xff; + } + part1[i] = (val ^ chk2) & 0xff; + + if (sectorData - sectorDataStart == 524) + break; + + val = *sectorData++; + chk0 += val; + if (chk1 > 0xff) { + chk0++; + chk1 &= 0xff; + } + part2[i] = (val ^ chk1) & 0xff; + i++; + } + part2[kChunkSize35-1] = 0; // gets merged into the "twos" + + assert(i == kChunkSize35-1); + + /* + * Output the nibble data. + */ + for (i = 0; i < kChunkSize35; i++) { + twos = ((part0[i] & 0xc0) >> 2) | + ((part1[i] & 0xc0) >> 4) | + ((part2[i] & 0xc0) >> 6); + + *outBuf++ = kDiskBytes62[twos]; + *outBuf++ = kDiskBytes62[part0[i] & 0x3f]; + *outBuf++ = kDiskBytes62[part1[i] & 0x3f]; + if (i != kChunkSize35 -1) + *outBuf++ = kDiskBytes62[part2[i] & 0x3f]; + } + + /* + * Output the checksum. + */ + twos = ((chk0 & 0xc0) >> 6) | ((chk1 & 0xc0) >> 4) | ((chk2 & 0xc0) >> 2); + *outBuf++ = kDiskBytes62[twos]; + *outBuf++ = kDiskBytes62[chk2 & 0x3f]; + *outBuf++ = kDiskBytes62[chk1 & 0x3f]; + *outBuf++ = kDiskBytes62[chk0 & 0x3f]; + + assert(outBuf - outBufStart == kNibblizedOutputLen); +} diff --git a/diskimg/OuterWrapper.cpp b/diskimg/OuterWrapper.cpp new file mode 100644 index 0000000..04e0dbb --- /dev/null +++ b/diskimg/OuterWrapper.cpp @@ -0,0 +1,1535 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Code for handling "outer wrappers" like ZIP and gzip. + * + * TODO: for safety, these should compress into a temp file and then rename + * the temp file over the original. The current implementation just + * truncates the open file descriptor or reopens the original file. Both + * risk data loss if the program or system crashes while the data is being + * written to disk. + */ +#include "StdAfx.h" +#include "DiskImgPriv.h" +#define DEF_MEM_LEVEL 8 // normally in zutil.h + + +/* + * =========================================================================== + * OuterGzip + * =========================================================================== + */ + +/* + * Test to see if this is a gzip file. + * + * This test is pretty weak, so we shouldn't even be looking at this + * unless the file ends in ".gz". A better test would scan the entire + * header. + * + * Would be nice to just gzopen the file, but unfortunately that tries + * to be "helpful" and reads the file whether it's in gz format or not. + * Some days I could do with a little less "help". + */ +/*static*/ DIError +OuterGzip::Test(GenericFD* pGFD, di_off_t outerLength) +{ + const int kGzipMagic = 0x8b1f; // 0x1f 0x8b + unsigned short magic, magicBuf; + const char* imagePath; + + WMSG0("Testing for gzip\n"); + + /* don't need this here, but we will later on */ + imagePath = pGFD->GetPathName(); + if (imagePath == nil) { + WMSG0("Can't test gzip on non-file\n"); + return kDIErrNotSupported; + } + + pGFD->Rewind(); + + if (pGFD->Read(&magicBuf, 2) != kDIErrNone) + return kDIErrGeneric; + magic = GetShortLE((unsigned char*) &magicBuf); + + if (magic == kGzipMagic) + return kDIErrNone; + else + return kDIErrGeneric; +} + + +/* + * The gzip file format has a length embedded in the footer, but + * unfortunately there is no interface to access it. So, we have + * to keep reading until we run out of data, extending the buffer + * to accommodate the new data each time. (We could also just read + * the footer directly, but that requires that there are no garbage + * bytes at the end of the file, which is a real concern on some FTP sites.) + * + * Start out by trying sizes that we think will work (140K, 800K), + * then grow quickly. + * + * The largest possible ProDOS image is 32MB, but it's possible to + * have an HFS volume or a partitioned image larger than that. We currently + * cap the limit to avoid nasty behavior when encountering really + * large .gz files. This isn't great -- we ought to support extracting + * to a temp file, or allowing the caller to specify what the largest + * size they can handle is. + */ +DIError +OuterGzip::ExtractGzipImage(gzFile gzfp, char** pBuf, di_off_t* pLength) +{ + DIError dierr = kDIErrNone; + const int kMinEmpty = 256 * 1024; + const int kStartSize = 141 * 1024; + const int kNextSize1 = 801 * 1024; + const int kNextSize2 = 1024 * 1024; + const int kMaxIncr = 4096 * 1024; + const int kAbsoluteMax = kMaxUncompressedSize; + char* buf = nil; + char* newBuf = nil; + long curSize, maxSize; + + assert(gzfp != nil); + assert(pBuf != nil); + assert(pLength != nil); + + curSize = 0; + maxSize = kStartSize; + + buf = new char[maxSize]; + if (buf == nil) { + dierr = kDIErrMalloc; + goto bail; + } + + while (1) { + long len; + + /* + * Try to fill the buffer. + * + * It appears that zlib v1.1.4 was more tolerant of certain kinds + * of broken archives than v1.2.1. Both give you a pile of data + * on the first read, with no error reported, but the next read + * attempt returns with z_err=-3 (Z_DATA_ERROR) and z_eof set. I'm + * not sure exactly what the flaw is, but I'm guessing something + * got lopped off the end of the archives. gzip v1.3.3 won't touch + * them either. + * + * It would be easy enough to access them, if they were accessible. + * Unfortunately the implementation is buried. Instead, we do + * a quick test against known unadorned floppy disk sizes to see + * if we can salvage the contents. (Our read attempts are all + * slightly *over* the standard disk sizes, so if it comes back right + * on one we're *probably* okay.) + */ + len = gzread(gzfp, buf + curSize, maxSize - curSize); + if (len < 0) { + WMSG1(" ExGZ Call to gzread failed, errno=%d\n", errno); + if (curSize == 140*1024 || curSize == 800*1024) { + WMSG0("WARNING: accepting damaged gzip file\n"); + fWrapperDamaged = true; + break; // sleazy, but currently necessary + } + dierr = kDIErrReadFailed; + goto bail; + } else if (len == 0) { + /* EOF reached */ + break; + } else if (len < (maxSize - curSize)) { + /* we've probably reached the end, but we can't be sure, + so let's go around again */ + WMSG2(" ExGZ gzread(%ld) returned %ld, letting it ride\n", + maxSize - curSize, len); + curSize += len; + } else { + /* update buffer, and grow it if it's not big enough */ + curSize += len; + WMSG2(" max=%ld cur=%ld\n", maxSize, curSize); + if (maxSize - curSize < kMinEmpty) { + /* not enough room, grow it */ + + if (maxSize == kStartSize) + maxSize = kNextSize1; + else if (maxSize == kNextSize1) + maxSize = kNextSize2; + else { + if (maxSize < kMaxIncr) + maxSize = maxSize * 2; + else + maxSize += kMaxIncr; + } + + newBuf = new char[maxSize]; + if (newBuf == nil) { + WMSG1(" ExGZ failed buffer alloc (%ld)\n", + maxSize); + dierr = kDIErrMalloc; + goto bail; + } + + memcpy(newBuf, buf, curSize); + delete[] buf; + buf = newBuf; + newBuf = nil; + + WMSG1(" ExGZ grew buffer to %ld\n", maxSize); + } else { + /* don't need to grow buffer yet */ + WMSG3(" ExGZ read %ld bytes, cur=%ld max=%ld\n", + len, curSize, maxSize); + } + } + assert(curSize < maxSize); + + if (curSize > kAbsoluteMax) { + WMSG0(" ExGZ excessive size, probably not a disk image\n"); + dierr = kDIErrTooBig; // close enough + goto bail; + } + } + + if (curSize + (1024*1024) < maxSize) { + /* shrink it down so it fits */ + WMSG2(" Down-sizing buffer from %ld to %ld\n", maxSize, curSize); + newBuf = new char[curSize]; + if (newBuf == nil) + goto bail; + memcpy(newBuf, buf, curSize); + delete[] buf; + buf = newBuf; + newBuf = nil; + } + + *pBuf = buf; + *pLength = curSize; + WMSG1(" ExGZ final size = %ld\n", curSize); + + buf = nil; + +bail: + delete[] buf; + delete[] newBuf; + return dierr; +} + +/* + * Open the archive, and extract the disk image into a memory buffer. + */ +DIError +OuterGzip::Load(GenericFD* pOuterGFD, di_off_t outerLength, bool readOnly, + di_off_t* pWrapperLength, GenericFD** ppWrapperGFD) +{ + DIError dierr = kDIErrNone; + GFDBuffer* pNewGFD = nil; + char* buf = nil; + di_off_t length = -1; + const char* imagePath; + gzFile gzfp = nil; + + imagePath = pOuterGFD->GetPathName(); + if (imagePath == nil) { + assert(false); // should've been caught in Test + return kDIErrNotSupported; + } + + gzfp = gzopen(imagePath, "rb"); // use "readOnly" here + if (gzfp == nil) { // DON'T retry RO -- should be done at higher level? + WMSG1("gzopen failed, errno=%d\n", errno); + dierr = kDIErrGeneric; + goto bail; + } + + dierr = ExtractGzipImage(gzfp, &buf, &length); + if (dierr != kDIErrNone) + goto bail; + + /* + * Everything is going well. Now we substitute a memory-based GenericFD + * for the existing GenericFD. + */ + pNewGFD = new GFDBuffer; + dierr = pNewGFD->Open(buf, length, true, false, readOnly); + if (dierr != kDIErrNone) + goto bail; + buf = nil; // now owned by pNewGFD; + + /* + * Success! + */ + assert(dierr == kDIErrNone); + *ppWrapperGFD = pNewGFD; + pNewGFD = nil; + + *pWrapperLength = length; + +bail: + if (dierr != kDIErrNone) { + delete pNewGFD; + } + if (gzfp != nil) + gzclose(gzfp); + return dierr; +} + +/* + * Save the contents of "pWrapperGFD" to the file pointed to by + * "pOuterGFD". + * + * "pOuterGFD" isn't disturbed (same as Load). All we want is to get the + * filename and then do everything through gzio. + */ +DIError +OuterGzip::Save(GenericFD* pOuterGFD, GenericFD* pWrapperGFD, + di_off_t wrapperLength) +{ + DIError dierr = kDIErrNone; + const char* imagePath; + gzFile gzfp = nil; + + WMSG1(" GZ save (wrapperLen=%ld)\n", (long) wrapperLength); + assert(wrapperLength > 0); + + /* + * Reopen the file. + */ + imagePath = pOuterGFD->GetPathName(); + if (imagePath == nil) { + assert(false); // should've been caught long ago + return kDIErrNotSupported; + } + + gzfp = gzopen(imagePath, "wb"); + if (gzfp == nil) { + WMSG1("gzopen for write failed, errno=%d\n", errno); + dierr = kDIErrGeneric; + goto bail; + } + + char buf[16384]; + size_t actual; + long written, totalWritten; + + pWrapperGFD->Rewind(); + + totalWritten = 0; + while (wrapperLength > 0) { + dierr = pWrapperGFD->Read(buf, sizeof(buf), &actual); + if (dierr == kDIErrEOF) { + dierr = kDIErrNone; + break; + } + if (dierr != kDIErrNone) { + WMSG1("Error reading source GFD during gzip save (err=%d)\n",dierr); + goto bail; + } + assert(actual > 0); + + written = gzwrite(gzfp, buf, actual); + if (written == 0) { + WMSG1("Failed writing %d bytes to gzio\n", actual); + dierr = kDIErrGeneric; + goto bail; + } + + totalWritten += written; + wrapperLength -= actual; + } + assert(wrapperLength == 0); // not expecting any slop + + WMSG1(" GZ wrote %ld bytes\n", totalWritten); + + /* + * Success! + */ + assert(dierr == kDIErrNone); + +bail: + if (gzfp != nil) + gzclose(gzfp); + return dierr; +} + + +/* + * =========================================================================== + * OuterZip + * =========================================================================== + */ + +/* + * Test to see if this is a ZIP archive. + */ +/*static*/ DIError +OuterZip::Test(GenericFD* pGFD, di_off_t outerLength) +{ + DIError dierr = kDIErrNone; + CentralDirEntry cde; + + WMSG0("Testing for zip\n"); + dierr = ReadCentralDir(pGFD, outerLength, &cde); + if (dierr != kDIErrNone) + goto bail; + + /* + * Make sure it's a compression method we support. + */ + if (cde.fCompressionMethod != kCompressStored && + cde.fCompressionMethod != kCompressDeflated) + { + WMSG1(" ZIP compression method %d not supported\n", + cde.fCompressionMethod); + dierr = kDIErrGeneric; + goto bail; + } + + /* + * Limit the size to something reasonable. + */ + if (cde.fUncompressedSize < 512 || + cde.fUncompressedSize > kMaxUncompressedSize) + { + WMSG1(" ZIP uncompressed size %lu is outside range\n", + cde.fUncompressedSize); + dierr = kDIErrGeneric; + goto bail; + } + + assert(dierr == kDIErrNone); + +bail: + return dierr; +} + +/* + * Open the archive, and extract the disk image into a memory buffer. + */ +DIError +OuterZip::Load(GenericFD* pOuterGFD, di_off_t outerLength, bool readOnly, + di_off_t* pWrapperLength, GenericFD** ppWrapperGFD) +{ + DIError dierr = kDIErrNone; + GFDBuffer* pNewGFD = nil; + CentralDirEntry cde; + unsigned char* buf = nil; + di_off_t length = -1; + const char* pExt; + + dierr = ReadCentralDir(pOuterGFD, outerLength, &cde); + if (dierr != kDIErrNone) + goto bail; + + if (cde.fFileNameLength > 0) { + pExt = FindExtension((const char*) cde.fFileName, kZipFssep); + if (pExt != nil) { + assert(*pExt == '.'); + SetExtension(pExt+1); + + WMSG1("OuterZip using extension '%s'\n", GetExtension()); + } + + SetStoredFileName((const char*) cde.fFileName); + } + + dierr = ExtractZipEntry(pOuterGFD, &cde, &buf, &length); + if (dierr != kDIErrNone) + goto bail; + + /* + * Everything is going well. Now we substitute a memory-based GenericFD + * for the existing GenericFD. + */ + pNewGFD = new GFDBuffer; + dierr = pNewGFD->Open(buf, length, true, false, readOnly); + if (dierr != kDIErrNone) + goto bail; + buf = nil; // now owned by pNewGFD; + + /* + * Success! + */ + assert(dierr == kDIErrNone); + *ppWrapperGFD = pNewGFD; + pNewGFD = nil; + + *pWrapperLength = length; + +bail: + if (dierr != kDIErrNone) { + delete pNewGFD; + } + return dierr; +} + +/* + * Save the contents of "pWrapperGFD" to the file pointed to by + * "pOuterGFD". + */ +DIError +OuterZip::Save(GenericFD* pOuterGFD, GenericFD* pWrapperGFD, + di_off_t wrapperLength) +{ + DIError dierr = kDIErrNone; + LocalFileHeader lfh; + CentralDirEntry cde; + EndOfCentralDir eocd; + di_off_t lfhOffset; + + WMSG1(" ZIP save (wrapperLen=%ld)\n", (long) wrapperLength); + assert(wrapperLength > 0); + + dierr = pOuterGFD->Rewind(); + if (dierr != kDIErrNone) + goto bail; + dierr = pOuterGFD->Truncate(); + if (dierr != kDIErrNone) + goto bail; + + dierr = pWrapperGFD->Rewind(); + if (dierr != kDIErrNone) + goto bail; + + lfhOffset = pOuterGFD->Tell(); // always 0 with only one file + + /* + * Don't store an empty filename. Some applications, e.g. Info-ZIP's + * "unzip", get confused. Ideally the DiskImg image creation code + * will have set the actual filename, with an extension that matches + * the file contents. + */ + if (fStoredFileName == nil || fStoredFileName[0] == '\0') + SetStoredFileName("disk"); + + /* + * Write the ZIP local file header. We don't have file lengths or + * CRCs yet, so we have to go back and fill those in later. + */ + lfh.fVersionToExtract = kDefaultVersion; +#if NO_ZIP_COMPRESS + lfh.fGPBitFlag = 0; + lfh.fCompressionMethod = 0; +#else + lfh.fGPBitFlag = 0x0002; // indicates maximum compression used + lfh.fCompressionMethod = 8; // when compressionMethod == deflate +#endif + GetMSDOSTime(&lfh.fLastModFileDate, &lfh.fLastModFileTime); + lfh.SetFileName(fStoredFileName); + dierr = lfh.Write(pOuterGFD); + if (dierr != kDIErrNone) + goto bail; + + /* + * Write the compressed data. + */ + unsigned long crc; + di_off_t compressedLen; + if (lfh.fCompressionMethod == kCompressDeflated) { + dierr = DeflateGFDToGFD(pOuterGFD, pWrapperGFD, wrapperLength, + &compressedLen, &crc); + if (dierr != kDIErrNone) + goto bail; + } else if (lfh.fCompressionMethod == kCompressStored) { + dierr = GenericFD::CopyFile(pOuterGFD, pWrapperGFD, wrapperLength, + &crc); + if (dierr != kDIErrNone) + goto bail; + compressedLen = wrapperLength; + } else { + assert(false); + dierr = kDIErrInternal; + goto bail; + } + + /* + * Go back and take care of the local file header stuff. + * + * It's not supposed to be necessary, but some utilities (WinZip, + * Info-ZIP) get bent out of shape if these aren't set and the data + * is compressed. They seem okay with it when the file isn't + * compressed. I don't understand this behavior, but writing the + * local file header is easy enough. + */ + lfh.fCRC32 = crc; + lfh.fCompressedSize = (unsigned long) compressedLen; + lfh.fUncompressedSize = (unsigned long) wrapperLength; + + di_off_t curPos; + curPos = pOuterGFD->Tell(); + dierr = pOuterGFD->Seek(lfhOffset, kSeekSet); + if (dierr != kDIErrNone) + goto bail; + dierr = lfh.Write(pOuterGFD); + if (dierr != kDIErrNone) + goto bail; + dierr = pOuterGFD->Seek(curPos, kSeekSet); + if (dierr != kDIErrNone) + goto bail; + + di_off_t cdeStart, cdeFinish; + cdeStart = pOuterGFD->Tell(); + + /* + * Write the central dir entry. This is largely just a copy of the + * data in the local file header (and in fact some utilities will + * get rather bent out of shape if the two don't match exactly). + */ + cde.fVersionMadeBy = kDefaultVersion; + cde.fVersionToExtract = lfh.fVersionToExtract; + cde.fGPBitFlag = lfh.fGPBitFlag; + cde.fCompressionMethod = lfh.fCompressionMethod; + cde.fLastModFileDate = lfh.fLastModFileDate; + cde.fLastModFileTime = lfh.fLastModFileTime; + cde.fCRC32 = lfh.fCRC32; + cde.fCompressedSize = lfh.fCompressedSize; + cde.fUncompressedSize = lfh.fUncompressedSize; + assert(lfh.fExtraFieldLength == 0 && cde.fExtraFieldLength == 0); + cde.fExternalAttrs = 0x81b60020; // matches what WinZip does + cde.fLocalHeaderRelOffset = (unsigned long) lfhOffset; + cde.SetFileName(fStoredFileName); + dierr = cde.Write(pOuterGFD); + if (dierr != kDIErrNone) + goto bail; + + cdeFinish = pOuterGFD->Tell(); + + /* + * Write the end-of-central-dir stuff. + */ + eocd.fNumEntries = 1; + eocd.fTotalNumEntries = 1; + eocd.fCentralDirSize = (unsigned long) (cdeFinish - cdeStart); + eocd.fCentralDirOffset = (unsigned long) cdeStart; + assert(eocd.fCentralDirSize >= EndOfCentralDir::kEOCDLen); + dierr = eocd.Write(pOuterGFD); + if (dierr != kDIErrNone) + goto bail; + + /* + * Success! + */ + assert(dierr == kDIErrNone); + +bail: + return dierr; +} + + +/* + * Track the name of the file stored in the ZIP archive. + */ +void +OuterZip::SetStoredFileName(const char* name) +{ + delete[] fStoredFileName; + fStoredFileName = StrcpyNew(name); +} + + +/* + * Find the central directory and read the contents. + * + * We currently only support archives with a single entry. + * + * The fun thing about ZIP archives is that they may or may not be + * readable from start to end. In some cases, notably for archives + * that were written to stdout, the only length information is in the + * central directory at the end of the file. + * + * Of course, the central directory can be followed by a variable-length + * comment field, so we have to scan through it backwards. The comment + * is at most 64K, plus we have 18 bytes for the end-of-central-dir stuff + * itself, plus apparently sometimes people throw random junk on the end + * just for the fun of it. + * + * This is all a little wobbly. If the wrong value ends up in the EOCD + * area, we're hosed. This appears to be the way that the Info-ZIP guys + * do it though, so we're in pretty good company if this fails. + */ +/*static*/ DIError +OuterZip::ReadCentralDir(GenericFD* pGFD, di_off_t outerLength, + CentralDirEntry* pDirEntry) +{ + DIError dierr = kDIErrNone; + EndOfCentralDir eocd; + unsigned char* buf = nil; + di_off_t seekStart; + long readAmount; + int i; + + /* too small to be a ZIP archive? */ + if (outerLength < EndOfCentralDir::kEOCDLen + 4) + return kDIErrGeneric; + + buf = new unsigned char[kMaxEOCDSearch]; + if (buf == nil) { + dierr = kDIErrMalloc; + goto bail; + } + + if (outerLength > kMaxEOCDSearch) { + seekStart = outerLength - kMaxEOCDSearch; + readAmount = kMaxEOCDSearch; + } else { + seekStart = 0; + readAmount = (long) outerLength; + } + dierr = pGFD->Seek(seekStart, kSeekSet); + if (dierr != kDIErrNone) + goto bail; + + /* read the last part of the file into the buffer */ + dierr = pGFD->Read(buf, readAmount); + if (dierr != kDIErrNone) + goto bail; + + /* find the end-of-central-dir magic */ + for (i = readAmount - 4; i >= 0; i--) { + if (buf[i] == 0x50 && + GetLongLE(&buf[i]) == EndOfCentralDir::kSignature) + { + WMSG1("+++ Found EOCD at buf+%d\n", i); + break; + } + } + if (i < 0) { + WMSG0("+++ EOCD not found, not ZIP\n"); + dierr = kDIErrGeneric; + goto bail; + } + + /* extract eocd values */ + dierr = eocd.ReadBuf(buf + i, readAmount - i); + if (dierr != kDIErrNone) + goto bail; + eocd.Dump(); + + if (eocd.fDiskNumber != 0 || eocd.fDiskWithCentralDir != 0 || + eocd.fNumEntries != 1 || eocd.fTotalNumEntries != 1) + { + WMSG0(" Probable ZIP archive has more than one member\n"); + dierr = kDIErrFileArchive; + goto bail; + } + + /* + * So far so good. "fCentralDirSize" is the size in bytes of the + * central directory, so we can just seek back that far to find it. + * We can also seek forward fCentralDirOffset bytes from the + * start of the file. + * + * We're not guaranteed to have the rest of the central dir in the + * buffer, nor are we guaranteed that the central dir will have any + * sort of convenient size. We need to skip to the start of it and + * read the header, then the other goodies. + * + * The only thing we really need right now is the file comment, which + * we're hoping to preserve. + */ + dierr = pGFD->Seek(eocd.fCentralDirOffset, kSeekSet); + if (dierr != kDIErrNone) + goto bail; + + /* + * Read the central dir entry. + */ + dierr = pDirEntry->Read(pGFD); + if (dierr != kDIErrNone) + goto bail; + + pDirEntry->Dump(); + + { + unsigned char checkBuf[4]; + dierr = pGFD->Read(checkBuf, 4); + if (dierr != kDIErrNone) + goto bail; + if (GetLongLE(checkBuf) != EndOfCentralDir::kSignature) { + WMSG0("CDE read check failed\n"); + assert(false); + dierr = kDIErrGeneric; + goto bail; + } + WMSG0("+++ CDE read check passed\n"); + } + +bail: + delete[] buf; + return dierr; +} + + +/* + * The central directory tells us where to find the local header. We + * have to skip over that to get to the start of the data.s + */ +DIError +OuterZip::ExtractZipEntry(GenericFD* pOuterGFD, CentralDirEntry* pCDE, + unsigned char** pBuf, di_off_t* pLength) +{ + DIError dierr = kDIErrNone; + LocalFileHeader lfh; + unsigned char* buf = nil; + + /* seek to the start of the local header */ + dierr = pOuterGFD->Seek(pCDE->fLocalHeaderRelOffset, kSeekSet); + if (dierr != kDIErrNone) + goto bail; + + /* + * Read the local file header, mainly as a way to get past it. There + * are legitimate reasons why the size fields and filename might be + * empty, so we really don't want to depend on any data in the LFH. + * We just need to find where the data starts. + */ + dierr = lfh.Read(pOuterGFD); + if (dierr != kDIErrNone) + goto bail; + lfh.Dump(); + + /* we should now be pointing at the data */ + WMSG1("File offset is 0x%08lx\n", (long) pOuterGFD->Tell()); + + buf = new unsigned char[pCDE->fUncompressedSize]; + if (buf == nil) { + /* a very real possibility */ + WMSG1(" ZIP unable to allocate buffer of %lu bytes\n", + pCDE->fUncompressedSize); + dierr = kDIErrMalloc; + goto bail; + } + + /* unpack or copy the data */ + if (pCDE->fCompressionMethod == kCompressDeflated) { + dierr = InflateGFDToBuffer(pOuterGFD, pCDE->fCompressedSize, + pCDE->fUncompressedSize, buf); + if (dierr != kDIErrNone) + goto bail; + } else if (pCDE->fCompressionMethod == kCompressStored) { + dierr = pOuterGFD->Read(buf, pCDE->fUncompressedSize); + if (dierr != kDIErrNone) + goto bail; + } else { + assert(false); + dierr = kDIErrInternal; + goto bail; + } + + /* check the CRC32 */ + unsigned long crc; + crc = crc32(0L, Z_NULL, 0); + crc = crc32(crc, buf, pCDE->fUncompressedSize); + + if (crc == pCDE->fCRC32) { + WMSG0("+++ ZIP CRCs match\n"); + } else { + WMSG2("ZIP CRC mismatch: inflated crc32=0x%08lx, stored=0x%08lx\n", + crc, pCDE->fCRC32); + dierr = kDIErrBadChecksum; + goto bail; + } + + *pBuf = buf; + *pLength = pCDE->fUncompressedSize; + + buf = nil; + +bail: + delete[] buf; + return dierr; +} + +/* + * Uncompress data from "pOuterGFD" to "buf". + * + * "buf" must be able to hold "uncompSize" bytes. + */ +DIError +OuterZip::InflateGFDToBuffer(GenericFD* pGFD, unsigned long compSize, + unsigned long uncompSize, unsigned char* buf) +{ + DIError dierr = kDIErrNone; + const unsigned long kReadBufSize = 65536; + unsigned char* readBuf = nil; + z_stream zstream; + int zerr; + unsigned long compRemaining; + + readBuf = new unsigned char[kReadBufSize]; + if (readBuf == nil) { + dierr = kDIErrMalloc; + goto bail; + } + compRemaining = compSize; + + /* + * Initialize the zlib stream. + */ + memset(&zstream, 0, sizeof(zstream)); + zstream.zalloc = Z_NULL; + zstream.zfree = Z_NULL; + zstream.opaque = Z_NULL; + zstream.next_in = nil; + zstream.avail_in = 0; + zstream.next_out = buf; + zstream.avail_out = uncompSize; + zstream.data_type = Z_UNKNOWN; + + /* + * Use the undocumented "negative window bits" feature to tell zlib + * that there's no zlib header waiting for it. + */ + zerr = inflateInit2(&zstream, -MAX_WBITS); + if (zerr != Z_OK) { + dierr = kDIErrInternal; + if (zerr == Z_VERSION_ERROR) { + WMSG1("Installed zlib is not compatible with linked version (%s)\n", + ZLIB_VERSION); + } else { + WMSG1("Call to inflateInit2 failed (zerr=%d)\n", zerr); + } + goto bail; + } + + /* + * Loop while we have data. + */ + do { + unsigned long getSize; + + /* read as much as we can */ + if (zstream.avail_in == 0) { + getSize = (compRemaining > kReadBufSize) ? + kReadBufSize : compRemaining; + WMSG2("+++ reading %ld bytes (%ld left)\n", getSize, + compRemaining); + + dierr = pGFD->Read(readBuf, getSize); + if (dierr != kDIErrNone) { + WMSG0("inflate read failed\n"); + goto z_bail; + } + + compRemaining -= getSize; + + zstream.next_in = readBuf; + zstream.avail_in = getSize; + } + + /* uncompress the data */ + zerr = inflate(&zstream, Z_NO_FLUSH); + if (zerr != Z_OK && zerr != Z_STREAM_END) { + dierr = kDIErrInternal; + WMSG1("zlib inflate call failed (zerr=%d)\n", zerr); + goto z_bail; + } + + /* output buffer holds all, so no need to write the output */ + } while (zerr == Z_OK); + + assert(zerr == Z_STREAM_END); /* other errors should've been caught */ + + if (zstream.total_out != uncompSize) { + dierr = kDIErrBadCompressedData; + WMSG2("Size mismatch on inflated file (%ld vs %ld)\n", + zstream.total_out, uncompSize); + goto z_bail; + } + +z_bail: + inflateEnd(&zstream); /* free up any allocated structures */ + +bail: + delete[] readBuf; + return dierr; +} + + +/* + * Get the current date/time, in MS-DOS format. + */ +void +OuterZip::GetMSDOSTime(unsigned short* pDate, unsigned short* pTime) +{ +#if 0 + /* this gets gmtime; we want localtime */ + SYSTEMTIME sysTime; + FILETIME fileTime; + ::GetSystemTime(&sysTime); + ::SystemTimeToFileTime(&sysTime, &fileTime); + ::FileTimeToDosDateTime(&fileTime, pDate, pTime); + //WMSG3("+++ Windows date: %04x %04x %d\n", *pDate, *pTime, + // (*pTime >> 11) & 0x1f); +#endif + + time_t now = time(nil); + DOSTime(now, pDate, pTime); + //WMSG3("+++ Our date : %04x %04x %d\n", *pDate, *pTime, + // (*pTime >> 11) & 0x1f); +} + +/* + * Convert a time_t to MS-DOS date and time values. + */ +void +OuterZip::DOSTime(time_t when, unsigned short* pDate, unsigned short* pTime) +{ + time_t even; + + *pDate = *pTime = 0; + + struct tm* ptm; + + /* round up to an even number of seconds */ + even = (time_t)(((unsigned long)(when) + 1) & (~1)); + + /* expand */ + ptm = localtime(&even); + + int year; + year = ptm->tm_year; + if (year < 80) + year = 80; + + *pDate = (year - 80) << 9 | (ptm->tm_mon+1) << 5 | ptm->tm_mday; + *pTime = ptm->tm_hour << 11 | ptm->tm_min << 5 | ptm->tm_sec >> 1; +} + + +/* + * Compress "length" bytes of data from "pSrc" to "pDst". + */ +DIError +OuterZip::DeflateGFDToGFD(GenericFD* pDst, GenericFD* pSrc, di_off_t srcLen, + di_off_t* pCompLength, unsigned long* pCRC) +{ + DIError dierr = kDIErrNone; + const unsigned long kBufSize = 32768; + unsigned char* inBuf = nil; + unsigned char* outBuf = nil; + z_stream zstream; + unsigned long crc; + int zerr; + + /* + * Create an input buffer and an output buffer. + */ + inBuf = new unsigned char[kBufSize]; + outBuf = new unsigned char[kBufSize]; + if (inBuf == nil || outBuf == nil) { + dierr = kDIErrMalloc; + goto bail; + } + + /* + * Initialize the zlib stream. + */ + memset(&zstream, 0, sizeof(zstream)); + zstream.zalloc = Z_NULL; + zstream.zfree = Z_NULL; + zstream.opaque = Z_NULL; + zstream.next_in = nil; + zstream.avail_in = 0; + zstream.next_out = outBuf; + zstream.avail_out = kBufSize; + zstream.data_type = Z_UNKNOWN; + + zerr = deflateInit2(&zstream, Z_BEST_COMPRESSION, + Z_DEFLATED, -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY); + if (zerr != Z_OK) { + dierr = kDIErrInternal; + if (zerr == Z_VERSION_ERROR) { + WMSG1("Installed zlib is not compatible with linked version (%s)\n", + ZLIB_VERSION); + } else { + WMSG1("Call to deflateInit2 failed (zerr=%d)\n", zerr); + } + goto bail; + } + + crc = crc32(0L, Z_NULL, 0); + + /* + * Loop while we have data. + */ + do { + long getSize; + int flush; + + /* only read if the input is empty */ + if (zstream.avail_in == 0 && srcLen) { + getSize = (srcLen > kBufSize) ? kBufSize : (long) srcLen; + WMSG1("+++ reading %ld bytes\n", getSize); + + dierr = pSrc->Read(inBuf, getSize); + if (dierr != kDIErrNone) { + WMSG0("deflate read failed\n"); + goto z_bail; + } + + srcLen -= getSize; + + crc = crc32(crc, inBuf, getSize); + + zstream.next_in = inBuf; + zstream.avail_in = getSize; + } + + if (srcLen == 0) + flush = Z_FINISH; /* tell zlib that we're done */ + else + flush = Z_NO_FLUSH; /* more to come! */ + + zerr = deflate(&zstream, flush); + if (zerr != Z_OK && zerr != Z_STREAM_END) { + WMSG1("zlib deflate call failed (zerr=%d)\n", zerr); + dierr = kDIErrInternal; + goto z_bail; + } + + /* write when we're full or when we're done */ + if (zstream.avail_out == 0 || + (zerr == Z_STREAM_END && zstream.avail_out != kBufSize)) + { + WMSG1("+++ writing %d bytes\n", zstream.next_out - outBuf); + dierr = pDst->Write(outBuf, zstream.next_out - outBuf); + if (dierr != kDIErrNone) { + WMSG0("write failed in deflate\n"); + goto z_bail; + } + + zstream.next_out = outBuf; + zstream.avail_out = kBufSize; + } + } while (zerr == Z_OK); + + assert(zerr == Z_STREAM_END); /* other errors should've been caught */ + + *pCompLength = zstream.total_out; + *pCRC = crc; + +z_bail: + deflateEnd(&zstream); /* free up any allocated structures */ + +bail: + delete[] inBuf; + delete[] outBuf; + + return dierr; +} + +/* + * Set the "fExtension" field. + */ +void +OuterZip::SetExtension(const char* ext) +{ + delete[] fExtension; + fExtension = StrcpyNew(ext); +} + + +/* + * =================================== + * OuterZip::LocalFileHeader + * =================================== + */ + +/* + * Read a local file header. + * + * On entry, "pGFD" points to the signature at the start of the header. + * On exit, "pGFD" points to the start of data. + */ +DIError +OuterZip::LocalFileHeader::Read(GenericFD* pGFD) +{ + DIError dierr = kDIErrNone; + unsigned char buf[kLFHLen]; + + dierr = pGFD->Read(buf, kLFHLen); + if (dierr != kDIErrNone) + goto bail; + + if (GetLongLE(&buf[0x00]) != kSignature) { + WMSG0(" ZIP: whoops: didn't find expected signature\n"); + assert(false); + return kDIErrGeneric; + } + + fVersionToExtract = GetShortLE(&buf[0x04]); + fGPBitFlag = GetShortLE(&buf[0x06]); + fCompressionMethod = GetShortLE(&buf[0x08]); + fLastModFileTime = GetShortLE(&buf[0x0a]); + fLastModFileDate = GetShortLE(&buf[0x0c]); + fCRC32 = GetLongLE(&buf[0x0e]); + fCompressedSize = GetLongLE(&buf[0x12]); + fUncompressedSize = GetLongLE(&buf[0x16]); + fFileNameLength = GetShortLE(&buf[0x1a]); + fExtraFieldLength = GetShortLE(&buf[0x1c]); + + /* grab filename */ + if (fFileNameLength != 0) { + assert(fFileName == nil); + fFileName = new unsigned char[fFileNameLength+1]; + if (fFileName == nil) { + dierr = kDIErrMalloc; + goto bail; + } else { + dierr = pGFD->Read(fFileName, fFileNameLength); + fFileName[fFileNameLength] = '\0'; + } + if (dierr != kDIErrNone) + goto bail; + } + + dierr = pGFD->Seek(fExtraFieldLength, kSeekCur); + if (dierr != kDIErrNone) + goto bail; + +bail: + return dierr; +} + +/* + * Write a local file header. + */ +DIError +OuterZip::LocalFileHeader::Write(GenericFD* pGFD) +{ + DIError dierr = kDIErrNone; + unsigned char buf[kLFHLen]; + + PutLongLE(&buf[0x00], kSignature); + PutShortLE(&buf[0x04], fVersionToExtract); + PutShortLE(&buf[0x06], fGPBitFlag); + PutShortLE(&buf[0x08], fCompressionMethod); + PutShortLE(&buf[0x0a], fLastModFileTime); + PutShortLE(&buf[0x0c], fLastModFileDate); + PutLongLE(&buf[0x0e], fCRC32); + PutLongLE(&buf[0x12], fCompressedSize); + PutLongLE(&buf[0x16], fUncompressedSize); + PutShortLE(&buf[0x1a], fFileNameLength); + PutShortLE(&buf[0x1c], fExtraFieldLength); + + dierr = pGFD->Write(buf, kLFHLen); + if (dierr != kDIErrNone) + goto bail; + + /* write filename */ + if (fFileNameLength != 0) { + dierr = pGFD->Write(fFileName, fFileNameLength); + if (dierr != kDIErrNone) + goto bail; + } + assert(fExtraFieldLength == 0); + +bail: + return dierr; +} + +/* + * Change the filename field. + */ +void +OuterZip::LocalFileHeader::SetFileName(const char* name) +{ + delete[] fFileName; + fFileName = nil; + fFileNameLength = 0; + + if (name != nil) { + fFileNameLength = strlen(name); + fFileName = new unsigned char[fFileNameLength+1]; + if (fFileName == nil) { + WMSG1("Malloc failure in SetFileName %u\n", fFileNameLength); + fFileName = nil; + fFileNameLength = 0; + } else { + memcpy(fFileName, name, fFileNameLength); + fFileName[fFileNameLength] = '\0'; + WMSG1("+++ OuterZip LFH filename set to '%s'\n", fFileName); + } + } +} + +/* + * Dump the contents of a LocalFileHeader object. + */ +void +OuterZip::LocalFileHeader::Dump(void) const +{ + WMSG0(" LocalFileHeader contents:\n"); + WMSG3(" versToExt=%u gpBits=0x%04x compression=%u\n", + fVersionToExtract, fGPBitFlag, fCompressionMethod); + WMSG3(" modTime=0x%04x modDate=0x%04x crc32=0x%08lx\n", + fLastModFileTime, fLastModFileDate, fCRC32); + WMSG2(" compressedSize=%lu uncompressedSize=%lu\n", + fCompressedSize, fUncompressedSize); + WMSG2(" filenameLen=%u extraLen=%u\n", + fFileNameLength, fExtraFieldLength); +} + + +/* + * =================================== + * OuterZip::CentralDirEntry + * =================================== + */ + +/* + * Read the central dir entry that appears next in the file. + * + * On entry, "pGFD" should be positioned on the signature bytes for the + * entry. On exit, "pGFD" will point at the signature word for the next + * entry or for the EOCD. + */ +DIError +OuterZip::CentralDirEntry::Read(GenericFD* pGFD) +{ + DIError dierr = kDIErrNone; + unsigned char buf[kCDELen]; + + dierr = pGFD->Read(buf, kCDELen); + if (dierr != kDIErrNone) + goto bail; + + if (GetLongLE(&buf[0x00]) != kSignature) { + WMSG0(" ZIP: whoops: didn't find expected signature\n"); + assert(false); + return kDIErrGeneric; + } + + fVersionMadeBy = GetShortLE(&buf[0x04]); + fVersionToExtract = GetShortLE(&buf[0x06]); + fGPBitFlag = GetShortLE(&buf[0x08]); + fCompressionMethod = GetShortLE(&buf[0x0a]); + fLastModFileTime = GetShortLE(&buf[0x0c]); + fLastModFileDate = GetShortLE(&buf[0x0e]); + fCRC32 = GetLongLE(&buf[0x10]); + fCompressedSize = GetLongLE(&buf[0x14]); + fUncompressedSize = GetLongLE(&buf[0x18]); + fFileNameLength = GetShortLE(&buf[0x1c]); + fExtraFieldLength = GetShortLE(&buf[0x1e]); + fFileCommentLength = GetShortLE(&buf[0x20]); + fDiskNumberStart = GetShortLE(&buf[0x22]); + fInternalAttrs = GetShortLE(&buf[0x24]); + fExternalAttrs = GetLongLE(&buf[0x26]); + fLocalHeaderRelOffset = GetLongLE(&buf[0x2a]); + + /* grab filename */ + if (fFileNameLength != 0) { + assert(fFileName == nil); + fFileName = new unsigned char[fFileNameLength+1]; + if (fFileName == nil) { + dierr = kDIErrMalloc; + goto bail; + } else { + dierr = pGFD->Read(fFileName, fFileNameLength); + fFileName[fFileNameLength] = '\0'; + } + if (dierr != kDIErrNone) + goto bail; + } + + /* skip over "extra field" */ + dierr = pGFD->Seek(fExtraFieldLength, kSeekCur); + if (dierr != kDIErrNone) + goto bail; + + /* grab comment, if any */ + if (fFileCommentLength != 0) { + assert(fFileComment == nil); + fFileComment = new unsigned char[fFileCommentLength+1]; + if (fFileComment == nil) { + dierr = kDIErrMalloc; + goto bail; + } else { + dierr = pGFD->Read(fFileComment, fFileCommentLength); + fFileComment[fFileCommentLength] = '\0'; + } + if (dierr != kDIErrNone) + goto bail; + } + +bail: + return dierr; +} + +/* + * Write a central dir entry. + */ +DIError +OuterZip::CentralDirEntry::Write(GenericFD* pGFD) +{ + DIError dierr = kDIErrNone; + unsigned char buf[kCDELen]; + + PutLongLE(&buf[0x00], kSignature); + PutShortLE(&buf[0x04], fVersionMadeBy); + PutShortLE(&buf[0x06], fVersionToExtract); + PutShortLE(&buf[0x08], fGPBitFlag); + PutShortLE(&buf[0x0a], fCompressionMethod); + PutShortLE(&buf[0x0c], fLastModFileTime); + PutShortLE(&buf[0x0e], fLastModFileDate); + PutLongLE(&buf[0x10], fCRC32); + PutLongLE(&buf[0x14], fCompressedSize); + PutLongLE(&buf[0x18], fUncompressedSize); + PutShortLE(&buf[0x1c], fFileNameLength); + PutShortLE(&buf[0x1e], fExtraFieldLength); + PutShortLE(&buf[0x20], fFileCommentLength); + PutShortLE(&buf[0x22], fDiskNumberStart); + PutShortLE(&buf[0x24], fInternalAttrs); + PutLongLE(&buf[0x26], fExternalAttrs); + PutLongLE(&buf[0x2a], fLocalHeaderRelOffset); + + dierr = pGFD->Write(buf, kCDELen); + if (dierr != kDIErrNone) + goto bail; + + /* write filename */ + if (fFileNameLength != 0) { + dierr = pGFD->Write(fFileName, fFileNameLength); + if (dierr != kDIErrNone) + goto bail; + } + assert(fExtraFieldLength == 0); + assert(fFileCommentLength == 0); + +bail: + return dierr; +} + +/* + * Change the filename field. + */ +void +OuterZip::CentralDirEntry::SetFileName(const char* name) +{ + delete[] fFileName; + fFileName = nil; + fFileNameLength = 0; + + if (name != nil) { + fFileNameLength = strlen(name); + fFileName = new unsigned char[fFileNameLength+1]; + if (fFileName == nil) { + WMSG1("Malloc failure in SetFileName %u\n", fFileNameLength); + fFileName = nil; + fFileNameLength = 0; + } else { + memcpy(fFileName, name, fFileNameLength); + fFileName[fFileNameLength] = '\0'; + WMSG1("+++ OuterZip CDE filename set to '%s'\n", fFileName); + } + } +} + + +/* + * Dump the contents of a CentralDirEntry object. + */ +void +OuterZip::CentralDirEntry::Dump(void) const +{ + WMSG0(" CentralDirEntry contents:\n"); + WMSG4(" versMadeBy=%u versToExt=%u gpBits=0x%04x compression=%u\n", + fVersionMadeBy, fVersionToExtract, fGPBitFlag, fCompressionMethod); + WMSG3(" modTime=0x%04x modDate=0x%04x crc32=0x%08lx\n", + fLastModFileTime, fLastModFileDate, fCRC32); + WMSG2(" compressedSize=%lu uncompressedSize=%lu\n", + fCompressedSize, fUncompressedSize); + WMSG3(" filenameLen=%u extraLen=%u commentLen=%u\n", + fFileNameLength, fExtraFieldLength, fFileCommentLength); + WMSG4(" diskNumStart=%u intAttr=0x%04x extAttr=0x%08lx relOffset=%lu\n", + fDiskNumberStart, fInternalAttrs, fExternalAttrs, + fLocalHeaderRelOffset); + + if (fFileName != nil) { + WMSG1(" filename: '%s'\n", fFileName); + } + if (fFileComment != nil) { + WMSG1(" comment: '%s'\n", fFileComment); + } +} + + +/* + * =================================== + * OuterZip::EndOfCentralDir + * =================================== + */ + +/* + * Read the end-of-central-dir fields. + * + * "buf" should be positioned at the EOCD signature. + */ +DIError +OuterZip::EndOfCentralDir::ReadBuf(const unsigned char* buf, int len) +{ + if (len < kEOCDLen) { + /* looks like ZIP file got truncated */ + WMSG2(" Zip EOCD: expected >= %d bytes, found %d\n", + kEOCDLen, len); + return kDIErrBadArchiveStruct; + } + + if (GetLongLE(&buf[0x00]) != kSignature) + return kDIErrInternal; + + fDiskNumber = GetShortLE(&buf[0x04]); + fDiskWithCentralDir = GetShortLE(&buf[0x06]); + fNumEntries = GetShortLE(&buf[0x08]); + fTotalNumEntries = GetShortLE(&buf[0x0a]); + fCentralDirSize = GetLongLE(&buf[0x0c]); + fCentralDirOffset = GetLongLE(&buf[0x10]); + fCommentLen = GetShortLE(&buf[0x14]); + + return kDIErrNone; +} + +/* + * Write an end-of-central-directory section. + */ +DIError +OuterZip::EndOfCentralDir::Write(GenericFD* pGFD) +{ + DIError dierr = kDIErrNone; + unsigned char buf[kEOCDLen]; + + PutLongLE(&buf[0x00], kSignature); + PutShortLE(&buf[0x04], fDiskNumber); + PutShortLE(&buf[0x06], fDiskWithCentralDir); + PutShortLE(&buf[0x08], fNumEntries); + PutShortLE(&buf[0x0a], fTotalNumEntries); + PutLongLE(&buf[0x0c], fCentralDirSize); + PutLongLE(&buf[0x10], fCentralDirOffset); + PutShortLE(&buf[0x14], fCommentLen); + + dierr = pGFD->Write(buf, kEOCDLen); + if (dierr != kDIErrNone) + goto bail; + + assert(fCommentLen == 0); + +bail: + return dierr; +} +/* + * Dump the contents of an EndOfCentralDir object. + */ +void +OuterZip::EndOfCentralDir::Dump(void) const +{ + WMSG0(" EndOfCentralDir contents:\n"); + WMSG4(" diskNum=%u diskWCD=%u numEnt=%u totalNumEnt=%u\n", + fDiskNumber, fDiskWithCentralDir, fNumEntries, fTotalNumEntries); + WMSG3(" centDirSize=%lu centDirOff=%lu commentLen=%u\n", + fCentralDirSize, fCentralDirOffset, fCommentLen); +} diff --git a/diskimg/OzDOS.cpp b/diskimg/OzDOS.cpp new file mode 100644 index 0000000..837089d --- /dev/null +++ b/diskimg/OzDOS.cpp @@ -0,0 +1,325 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Implementation of DiskFSOzDOS class. + * + * It would make life MUCH EASIER to have the DiskImg recognize this as + * a file format and just rearrange the blocks into linear order for us, + * but unfortunately that's not going to happen. + */ +#include "StdAfx.h" +#include "DiskImgPriv.h" + + +/* + * =========================================================================== + * DiskFSOzDOS + * =========================================================================== + */ + +const int kExpectedNumBlocks = 1600; +const int kExpectedTracks = 50; // 50 tracks of 32 sectors == 400K +const int kExpectedSectors = 32; +const int kVTOCTrack = 17; +const int kVTOCSector = 0; +const int kSctSize = 256; + +const int kCatalogEntrySize = 0x23; // length in bytes of catalog entries +const int kCatalogEntriesPerSect = 7; // #of entries per catalog sector +const int kMaxTSPairs = 0x7a; // 122 entries for 256-byte sectors +const int kTSOffset = 0x0c; // first T/S entry in a T/S list + +const int kMaxTSIterations = 32; +const int kMaxCatalogIterations = 64; + + +/* + * Read a track/sector, adjusting for 32-sector disks being treated as + * if they were 16-sector. + */ +static DIError +ReadTrackSectorAdjusted(DiskImg* pImg, int track, int sector, + int sectorOffset, unsigned char* buf, DiskImg::SectorOrder imageOrder) +{ + track *= 4; + sector = sector * 2 + sectorOffset; + while (sector >= 16) { + track++; + sector -= 16; + } + return pImg->ReadTrackSectorSwapped(track, sector, buf, imageOrder, + DiskImg::kSectorOrderDOS); +} + +/* + * Test for presence of 400K OzDOS 3.3 volumes. + */ +static DIError +TestImageHalf(DiskImg* pImg, int sectorOffset, DiskImg::SectorOrder imageOrder) +{ + DIError dierr = kDIErrNone; + unsigned char sctBuf[kSctSize]; + int numTracks, numSectors; + int catTrack, catSect; + int foundGood = 0; + int iterations = 0; + + assert(sectorOffset == 0 || sectorOffset == 1); + + dierr = ReadTrackSectorAdjusted(pImg, kVTOCTrack, kVTOCSector, + sectorOffset, sctBuf, imageOrder); + if (dierr != kDIErrNone) + goto bail; + + catTrack = sctBuf[0x01]; + catSect = sctBuf[0x02]; + numTracks = sctBuf[0x34]; + numSectors = sctBuf[0x35]; + + if (!(sctBuf[0x27] == kMaxTSPairs) || + /*!(sctBuf[0x36] == 0 && sctBuf[0x37] == 1) ||*/ // bytes per sect + !(numTracks == kExpectedTracks) || + !(numSectors == 32) || + !(catTrack < numTracks && catSect < numSectors) || + 0) + { + WMSG1(" OzDOS header test %d failed\n", sectorOffset); + dierr = kDIErrFilesystemNotFound; + goto bail; + } + + /* + * Walk through the catalog track to try to figure out ordering. + */ + while (catTrack != 0 && catSect != 0 && iterations < kMaxCatalogIterations) + { + dierr = ReadTrackSectorAdjusted(pImg, catTrack, catSect, + sectorOffset, sctBuf, imageOrder); + if (dierr != kDIErrNone) + goto bail_ok; /* allow it if not fully broken */ + + if (sctBuf[1] == catTrack && sctBuf[2] == catSect-1) + foundGood++; + + catTrack = sctBuf[1]; + catSect = sctBuf[2]; + iterations++; // watch for infinite loops + } + if (iterations >= kMaxCatalogIterations) { + dierr = kDIErrDirectoryLoop; + goto bail; + } + +bail_ok: + WMSG3(" OzDOS foundGood=%d off=%d swap=%d\n", foundGood, sectorOffset, + imageOrder); + /* foundGood hits 3 even when swap is wrong */ + if (foundGood > 4) + dierr = kDIErrNone; + else + dierr = kDIErrFilesystemNotFound; + +bail: + return dierr; +} + +/* + * Test both of the DOS partitions. + */ +static DIError +TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder) +{ + DIError dierr; + + WMSG1(" OzDOS checking first half (swap=%d)\n", imageOrder); + dierr = TestImageHalf(pImg, 0, imageOrder); + if (dierr != kDIErrNone) + return dierr; + + WMSG1(" OzDOS checking second half (swap=%d)\n", imageOrder); + dierr = TestImageHalf(pImg, 1, imageOrder); + if (dierr != kDIErrNone) + return dierr; + + return kDIErrNone; +} + +/* + * Test to see if the image is a OzDOS volume. + */ +/*static*/ DIError +DiskFSOzDOS::TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency) +{ + /* only on 800K disks (at the least, insist on numTracks being even) */ + if (pImg->GetNumBlocks() != kExpectedNumBlocks) + return kDIErrFilesystemNotFound; + + /* if a value is specified, try that first -- useful for OverrideFormat */ + if (*pOrder != DiskImg::kSectorOrderUnknown) { + if (TestImage(pImg, *pOrder) == kDIErrNone) { + WMSG0(" OzDOS accepted FirstTry value\n"); + return kDIErrNone; + } + } + + DiskImg::SectorOrder ordering[DiskImg::kSectorOrderMax]; + + DiskImg::GetSectorOrderArray(ordering, *pOrder); + + for (int i = 0; i < DiskImg::kSectorOrderMax; i++) { + if (ordering[i] == DiskImg::kSectorOrderUnknown) + continue; + if (TestImage(pImg, ordering[i]) == kDIErrNone) { + *pOrder = ordering[i]; + *pFormat = DiskImg::kFormatOzDOS; + return kDIErrNone; + } + } + + WMSG0(" OzDOS didn't find valid FS\n"); + return kDIErrFilesystemNotFound; +} + +#if 0 +/* + * Test to see if the image is a 'wide' (32-sector) DOS3.3 volume, i.e. + * half of a OzDOS volume. + */ +/*static*/ DIError +DiskFS::TestOzWideDOS33(const DiskImg* pImg, DiskImg::SectorOrder* pOrder) +{ + DIError dierr = kDIErrNone; + + /* only on 400K disks (at the least, insist on numTracks being even) */ + if (pImg->GetNumBlocks() != kExpectedNumBlocks/2) + return kDIErrFilesystemNotFound; + + /* if a value is specified, try that first -- useful for OverrideFormat */ + if (*pOrder != DiskImg::kSectorOrderUnknown) { + if (TestImageHalf(pImg, 0, *pOrder) == kDIErrNone) { + WMSG0(" WideDOS accepted FirstTry value\n"); + return kDIErrNone; + } + } + + if (TestImageHalf(pImg, 0, DiskImg::kSectorOrderDOS) == kDIErrNone) { + *pOrder = DiskImg::kSectorOrderDOS; + } else if (TestImageHalf(pImg, 0, DiskImg::kSectorOrderProDOS) == kDIErrNone) { + *pOrder = DiskImg::kSectorOrderProDOS; + } else if (TestImageHalf(pImg, 0, DiskImg::kSectorOrderPhysical) == kDIErrNone) { + *pOrder = DiskImg::kSectorOrderPhysical; + } else { + WMSG0(" FS didn't find valid 'wide' DOS3.3\n"); + return kDIErrFilesystemNotFound; + } + + return kDIErrNone; +} +#endif + +/* + * Set up our sub-volumes. + */ +DIError +DiskFSOzDOS::Initialize(void) +{ + DIError dierr = kDIErrNone; + + if (fScanForSubVolumes != kScanSubDisabled) { + dierr = OpenSubVolume(0); + if (dierr != kDIErrNone) + return dierr; + + dierr = OpenSubVolume(1); + if (dierr != kDIErrNone) + return dierr; + } else { + WMSG0(" OzDOS not scanning for sub-volumes\n"); + } + + SetVolumeUsageMap(); + + return kDIErrNone; +} + +/* + * Open up one of the DOS 3.3 sub-volumes. + */ +DIError +DiskFSOzDOS::OpenSubVolume(int idx) +{ + DIError dierr = kDIErrNone; + DiskFS* pNewFS = nil; + DiskImg* pNewImg = nil; + + pNewImg = new DiskImg; + if (pNewImg == nil) { + dierr = kDIErrMalloc; + goto bail; + } + + // open the full 800K; SetPairedSectors cuts it in half + dierr = pNewImg->OpenImage(fpImg, 0, 0, + 2 * kExpectedTracks * kExpectedSectors); + if (dierr != kDIErrNone) { + WMSG3(" OzSub: OpenImage(%d,0,%d) failed (err=%d)\n", + 0, 2 * kExpectedTracks * kExpectedSectors, dierr); + goto bail; + } + + assert(idx == 0 || idx == 1); + pNewImg->SetPairedSectors(true, 1-idx); + + WMSG1(" OzSub: testing for recognizable volume in idx=%d\n", idx); + dierr = pNewImg->AnalyzeImage(); + if (dierr != kDIErrNone) { + WMSG1(" OzSub: analysis failed (err=%d)\n", dierr); + goto bail; + } + + if (pNewImg->GetFSFormat() == DiskImg::kFormatUnknown || + pNewImg->GetSectorOrder() == DiskImg::kSectorOrderUnknown) + { + WMSG0(" OzSub: unable to identify filesystem\n"); + dierr = kDIErrUnsupportedFSFmt; + goto bail; + } + + /* open a DiskFS for the sub-image */ + WMSG1(" UNISub %d succeeded!\n", idx); + pNewFS = pNewImg->OpenAppropriateDiskFS(); + if (pNewFS == nil) { + WMSG0(" OzSub: OpenAppropriateDiskFS failed\n"); + dierr = kDIErrUnsupportedFSFmt; + goto bail; + } + + /* load the files from the sub-image */ + dierr = pNewFS->Initialize(pNewImg, kInitFull); + if (dierr != kDIErrNone) { + WMSG1(" OzSub: error %d reading list of files from disk", dierr); + goto bail; + } + + /* if this really is DOS 3.3, override the "volume name" */ + if (pNewImg->GetFSFormat() == DiskImg::kFormatDOS33) { + DiskFSDOS33* pDOS = (DiskFSDOS33*) pNewFS; /* eek, a downcast */ + pDOS->SetDiskVolumeNum(idx+1); + } + + /* + * Success, add it to the sub-volume list. + */ + AddSubVolumeToList(pNewImg, pNewFS); + +bail: + if (dierr != kDIErrNone) { + delete pNewFS; + delete pNewImg; + } + return dierr; +} diff --git a/diskimg/Pascal.cpp b/diskimg/Pascal.cpp new file mode 100644 index 0000000..63548ae --- /dev/null +++ b/diskimg/Pascal.cpp @@ -0,0 +1,1907 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Implementation of DiskFSPascal class. + * + * Currently each file may only be open once. + */ +#include "StdAfx.h" +#include "DiskImgPriv.h" + + +/* + * =========================================================================== + * DiskFSPascal + * =========================================================================== + */ + +const int kBlkSize = 512; +const int kVolHeaderBlock = 2; // first directory block +const int kMaxCatalogIterations = 64; // should be short, linear catalog +const int kHugeDir = 32; +static const char* kInvalidNameChars = "$=?,[#:"; + + +/* + * See if this looks like a Pascal volume. + * + * We test a few fields in the volume directory for validity. + */ +static DIError +TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder) +{ + DIError dierr = kDIErrNone; + unsigned char blkBuf[512]; + unsigned char volName[DiskFSPascal::kMaxVolumeName+1]; + + dierr = pImg->ReadBlockSwapped(kVolHeaderBlock, blkBuf, imageOrder, + DiskImg::kSectorOrderProDOS); + if (dierr != kDIErrNone) + goto bail; + + + if (!(blkBuf[0x00] == 0 && blkBuf[0x01] == 0) || + !(blkBuf[0x04] == 0 && blkBuf[0x05] == 0) || + !(blkBuf[0x06] > 0 && blkBuf[0x06] <= DiskFSPascal::kMaxVolumeName) || + 0) + { + dierr = kDIErrFilesystemNotFound; + goto bail; + } + + /* volume name length is good, check the name itself */ + /* (this may be overly restrictive, but it's probably good to be) */ + memset(volName, 0, sizeof(volName)); + memcpy(volName, &blkBuf[0x07], blkBuf[0x06]); + if (!DiskFSPascal::IsValidVolumeName((const char*) volName)) + return kDIErrFilesystemNotFound; + +bail: + return dierr; +} + +/* + * Test to see if the image is a Pascal disk. + */ +/*static*/ DIError +DiskFSPascal::TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency) +{ + DiskImg::SectorOrder ordering[DiskImg::kSectorOrderMax]; + + DiskImg::GetSectorOrderArray(ordering, *pOrder); + + for (int i = 0; i < DiskImg::kSectorOrderMax; i++) { + if (ordering[i] == DiskImg::kSectorOrderUnknown) + continue; + if (TestImage(pImg, ordering[i]) == kDIErrNone) { + *pOrder = ordering[i]; + *pFormat = DiskImg::kFormatPascal; + return kDIErrNone; + } + } + + WMSG0(" Pascal didn't find valid FS\n"); + return kDIErrFilesystemNotFound; +} + +/* + * Get things rolling. + * + * Since we're assured that this is a valid disk, errors encountered from here + * on out must be handled somehow, possibly by claiming that the disk is + * completely full and has no files on it. + */ +DIError +DiskFSPascal::Initialize(void) +{ + DIError dierr = kDIErrNone; + + fDiskIsGood = false; // hosed until proven innocent + fEarlyDamage = false; + + fVolumeUsage.Create(fpImg->GetNumBlocks()); + + dierr = LoadVolHeader(); + if (dierr != kDIErrNone) + goto bail; + DumpVolHeader(); + + dierr = ProcessCatalog(); + if (dierr != kDIErrNone) + goto bail; + + dierr = ScanFileUsage(); + if (dierr != kDIErrNone) { + /* this might not be fatal; just means that *some* files are bad */ + goto bail; + } + + fDiskIsGood = CheckDiskIsGood(); + + fVolumeUsage.Dump(); + + //A2File* pFile; + //pFile = GetNextFile(nil); + //while (pFile != nil) { + // pFile->Dump(); + // pFile = GetNextFile(pFile); + //} + +bail: + return dierr; +} + +/* + * Read some interesting fields from the volume header. + */ +DIError +DiskFSPascal::LoadVolHeader(void) +{ + DIError dierr = kDIErrNone; + unsigned char blkBuf[kBlkSize]; + int nameLen, maxFiles; + + dierr = fpImg->ReadBlock(kVolHeaderBlock, blkBuf); + if (dierr != kDIErrNone) + goto bail; + + /* vol header is same size as dir entry, but different layout */ + fStartBlock = GetShortLE(&blkBuf[0x00]); + assert(fStartBlock == 0); // verified in "TestImage" + fNextBlock = GetShortLE(&blkBuf[0x02]); + assert(GetShortLE(&blkBuf[0x04]) == 0); // type + nameLen = blkBuf[0x06] & 0x07; + memcpy(fVolumeName, &blkBuf[0x07], nameLen); + fVolumeName[nameLen] = '\0'; + fTotalBlocks = GetShortLE(&blkBuf[0x0e]); + fNumFiles = GetShortLE(&blkBuf[0x10]); + fAccessWhen = GetShortLE(&blkBuf[0x12]); // time of last access + fDateSetWhen = GetShortLE(&blkBuf[0x14]); // most recent date set + fStuff1 = GetShortLE(&blkBuf[0x16]); // filler + fStuff2 = GetShortLE(&blkBuf[0x18]); // filler + + if (fTotalBlocks != fpImg->GetNumBlocks()) { + // saw this most recently on a 40-track .APP image; not a problem + WMSG2(" Pascal WARNING: total (%u) != img (%ld)\n", + fTotalBlocks, fpImg->GetNumBlocks()); + } + + /* + * Sanity checks. + */ + if (fNextBlock > 34) { + // directory really shouldn't be more than 6; I'm being generous + fpImg->AddNote(DiskImg::kNoteWarning, + "Pascal directory is too big (%d blocks); trimming.", + fNextBlock - fStartBlock); + } + + /* max #of file entries, including the vol dir header */ + maxFiles = ((fNextBlock - kVolHeaderBlock) * kBlkSize) / kDirectoryEntryLen; + if (fNumFiles > maxFiles-1) { + fpImg->AddNote(DiskImg::kNoteWarning, + "Pascal fNumFiles (%d) exceeds max files (%d); trimming.\n", + fNumFiles, maxFiles-1); + fEarlyDamage = true; + } + + SetVolumeID(); + +bail: + return dierr; +} + +/* + * Set the volume ID field. + */ +void +DiskFSPascal::SetVolumeID(void) +{ + sprintf(fVolumeID, "Pascal %s:", fVolumeName); +} + +/* + * Dump what we pulled out of the volume header. + */ +void +DiskFSPascal::DumpVolHeader(void) +{ + time_t access, dateSet; + + WMSG1(" Pascal volume header for '%s'\n", fVolumeName); + WMSG2(" startBlock=%d nextBlock=%d\n", + fStartBlock, fNextBlock); + WMSG4(" totalBlocks=%d numFiles=%d access=0x%04x dateSet=0x%04x\n", + fTotalBlocks, fNumFiles, fAccessWhen, fDateSetWhen); + + access = A2FilePascal::ConvertPascalDate(fAccessWhen); + dateSet = A2FilePascal::ConvertPascalDate(fDateSetWhen); + WMSG1(" -->access %.24s\n", ctime(&access)); + WMSG1(" -->dateSet %.24s\n", ctime(&dateSet)); + + //WMSG2("Unconvert access=0x%04x dateSet=0x%04x\n", + // A2FilePascal::ConvertPascalDate(access), + // A2FilePascal::ConvertPascalDate(dateSet)); +} + + +/* + * Read the catalog from the disk. + * + * No distinction is made for block boundaries, so we want to slurp the + * entire thing into memory. + * + * Sets "fDirectory". + */ +DIError +DiskFSPascal::LoadCatalog(void) +{ + DIError dierr = kDIErrNone; + unsigned char* dirPtr; + int block, numBlocks; + + assert(fDirectory == nil); + + numBlocks = fNextBlock - kVolHeaderBlock; + if (numBlocks <= 0 || numBlocks > kHugeDir) { + dierr = kDIErrBadDiskImage; + goto bail; + } + + fDirectory = new unsigned char[kBlkSize * numBlocks]; + if (fDirectory == nil) { + dierr = kDIErrMalloc; + goto bail; + } + + block = kVolHeaderBlock; + dirPtr = fDirectory; + while (numBlocks--) { + dierr = fpImg->ReadBlock(block, dirPtr); + if (dierr != kDIErrNone) + goto bail; + + block++; + dirPtr += kBlkSize; + } + +bail: + if (dierr != kDIErrNone) { + delete[] fDirectory; + fDirectory = nil; + } + return dierr; +} + +/* + * Write our copy of the catalog back out to disk. + */ +DIError +DiskFSPascal::SaveCatalog(void) +{ + DIError dierr = kDIErrNone; + unsigned char* dirPtr; + int block, numBlocks; + + assert(fDirectory != nil); + + numBlocks = fNextBlock - kVolHeaderBlock; + block = kVolHeaderBlock; + dirPtr = fDirectory; + while (numBlocks--) { + dierr = fpImg->WriteBlock(block, dirPtr); + if (dierr != kDIErrNone) + goto bail; + + block++; + dirPtr += kBlkSize; + } + +bail: + return dierr; +} + +/* + * Free the catalog storage. + */ +void +DiskFSPascal::FreeCatalog(void) +{ + delete[] fDirectory; + fDirectory = nil; +} + + +/* + * Process the catalog into A2File structures. + */ +DIError +DiskFSPascal::ProcessCatalog(void) +{ + DIError dierr = kDIErrNone; + int i, nameLen; + A2FilePascal* pFile; + const unsigned char* dirPtr; + unsigned short prevNextBlock = fNextBlock; + + dierr = LoadCatalog(); + if (dierr != kDIErrNone) + goto bail; + + dirPtr = fDirectory + kDirectoryEntryLen; // skip vol dir entry + for (i = 0; i < fNumFiles; i++) { + pFile = new A2FilePascal(this); + + pFile->fStartBlock = GetShortLE(&dirPtr[0x00]); + pFile->fNextBlock = GetShortLE(&dirPtr[0x02]); + pFile->fFileType = (A2FilePascal::FileType) GetShortLE(&dirPtr[0x04]); + nameLen = dirPtr[0x06] & 0x0f; + memcpy(pFile->fFileName, &dirPtr[0x07], nameLen); + pFile->fFileName[nameLen] = '\0'; + pFile->fBytesRemaining = GetShortLE(&dirPtr[0x16]); + pFile->fModWhen = GetShortLE(&dirPtr[0x18]); + + /* check bytesRem before setting length field */ + if (pFile->fBytesRemaining > kBlkSize) { + WMSG2(" Pascal found strange bytesRem %u on '%s', trimming\n", + pFile->fBytesRemaining, pFile->fFileName); + pFile->fBytesRemaining = kBlkSize; + pFile->SetQuality(A2File::kQualitySuspicious); + } + + pFile->fLength = pFile->fBytesRemaining + + (pFile->fNextBlock - pFile->fStartBlock -1) * kBlkSize; + + /* + * Check values. + */ + if (pFile->fStartBlock == pFile->fNextBlock) { + WMSG1(" Pascal found zero-block file '%s'\n", pFile->fFileName); + pFile->SetQuality(A2File::kQualityDamaged); + } + if (pFile->fStartBlock < prevNextBlock) { + WMSG3(" Pascal start of '%s' (%d) overlaps previous end (%d)\n", + pFile->fFileName, pFile->fStartBlock, prevNextBlock); + pFile->SetQuality(A2File::kQualityDamaged); + } + + if (pFile->fNextBlock > fpImg->GetNumBlocks()) { + WMSG3(" Pascal invalid 'next' block %d (max %ld) '%s'\n", + pFile->fNextBlock, fpImg->GetNumBlocks(), pFile->fFileName); + pFile->fStartBlock = pFile->fNextBlock = 0; + pFile->fLength = 0; + pFile->SetQuality(A2File::kQualityDamaged); + } else if (pFile->fNextBlock > fTotalBlocks) { + WMSG3(" Pascal 'next' block %d exceeds max (%d) '%s'\n", + pFile->fNextBlock, fTotalBlocks, pFile->fFileName); + pFile->SetQuality(A2File::kQualitySuspicious); + } + + //pFile->Dump(); + AddFileToList(pFile); + + dirPtr += kDirectoryEntryLen; + prevNextBlock = pFile->fNextBlock; + } + +bail: + FreeCatalog(); + return dierr; +} + + +/* + * Create the volume usage map. Since UCSD Pascal volumes have neither + * in-use maps nor index blocks, this is pretty straightforward. + */ +DIError +DiskFSPascal::ScanFileUsage(void) +{ + int block; + + /* start with the boot blocks */ + SetBlockUsage(0, VolumeUsage::kChunkPurposeSystem); + SetBlockUsage(1, VolumeUsage::kChunkPurposeSystem); + + for (block = kVolHeaderBlock; block < fNextBlock; block++) { + SetBlockUsage(block, VolumeUsage::kChunkPurposeVolumeDir); + } + + A2FilePascal* pFile; + pFile = (A2FilePascal*) GetNextFile(nil); + while (pFile != nil) { + for (block = pFile->fStartBlock; block < pFile->fNextBlock; block++) + SetBlockUsage(block, VolumeUsage::kChunkPurposeUserData); + + pFile = (A2FilePascal*) GetNextFile(pFile); + } + + return kDIErrNone; +} + +/* + * Update an entry in the volume usage map. + */ +void +DiskFSPascal::SetBlockUsage(long block, VolumeUsage::ChunkPurpose purpose) +{ + VolumeUsage::ChunkState cstate; + + fVolumeUsage.GetChunkState(block, &cstate); + if (cstate.isUsed) { + cstate.purpose = VolumeUsage::kChunkPurposeConflict; + WMSG1(" Pascal conflicting uses for bl=%ld\n", block); + } else { + cstate.isUsed = true; + cstate.isMarkedUsed = true; + cstate.purpose = purpose; + } + fVolumeUsage.SetChunkState(block, &cstate); +} + + +/* + * Test a string for validity as a Pascal volume name. + * + * Volume names can only be 7 characters long, but otherwise obey the same + * rules as file names. + */ +/*static*/ bool +DiskFSPascal::IsValidVolumeName(const char* name) +{ + if (name == nil) { + assert(false); + return false; + } + if (strlen(name) > kMaxVolumeName) + return false; + return IsValidFileName(name); +} + +/* + * Test a string for validity as a Pascal file name. + * + * Filenames can be up to 15 characters long. All characters are valid. + * However, the system filer gets bent out of shape if you use spaces, + * control characters, any of the wildcards ($=?), or filer meta-characters + * (,[#:). It also converts all alpha characters to upper case, but we can + * take care of that later. + */ +/*static*/ bool +DiskFSPascal::IsValidFileName(const char* name) +{ + assert(name != nil); + + if (name[0] == '\0') + return false; + if (strlen(name) > A2FilePascal::kMaxFileName) + return false; + + /* must be A-Z 0-9 '.' */ + while (*name != '\0') { + if (*name <= 0x20 || *name >= 0x7f) // no space, del, or ctrl + return false; + //if (*name >= 'a' && *name <= 'z') // no lower case + // return false; + if (strchr(kInvalidNameChars, *name) != nil) // filer metacharacters + return false; + + name++; + } + + return true; +} + +/* + * Put a Pascal filesystem image on the specified DiskImg. + */ +DIError +DiskFSPascal::Format(DiskImg* pDiskImg, const char* volName) +{ + DIError dierr = kDIErrNone; + unsigned char blkBuf[kBlkSize]; + long formatBlocks; + + if (!IsValidVolumeName(volName)) + return kDIErrInvalidArg; + + /* set fpImg so calls that rely on it will work; we un-set it later */ + assert(fpImg == nil); + SetDiskImg(pDiskImg); + + WMSG0(" Pascal formatting disk image\n"); + + /* write ProDOS-style blocks */ + dierr = fpImg->OverrideFormat(fpImg->GetPhysicalFormat(), + DiskImg::kFormatGenericProDOSOrd, fpImg->GetSectorOrder()); + if (dierr != kDIErrNone) + goto bail; + + formatBlocks = pDiskImg->GetNumBlocks(); + if (formatBlocks != 280 && formatBlocks != 1600) { + WMSG1(" Pascal: rejecting format req blocks=%ld\n", formatBlocks); + assert(false); + return kDIErrInvalidArg; + } + + /* + * We should now zero out the disk blocks, but this is done automatically + * on new disk images, so there's no need to do it here. + */ +// dierr = fpImg->ZeroImage(); + WMSG0(" Pascal (not zeroing blocks)\n"); + + /* + * Start by writing blocks 0 and 1 (the boot blocks). The file + * APPLE3:FORMATTER.DATA holds images for 3.5" and 5.25" disks. + */ + dierr = WriteBootBlocks(); + if (dierr != kDIErrNone) + goto bail; + + /* + * Write the disk volume entry. + */ + memset(blkBuf, 0, sizeof(blkBuf)); + PutShortLE(&blkBuf[0x00], 0); // start block + PutShortLE(&blkBuf[0x02], 6); // next block + PutShortLE(&blkBuf[0x04], 0); // "file" type + blkBuf[0x06] = strlen(volName); + memcpy(&blkBuf[0x07], volName, strlen(volName)); + PutShortLE(&blkBuf[0x0e], (unsigned short) pDiskImg->GetNumBlocks()); + PutShortLE(&blkBuf[0x10], 0); // num files + PutShortLE(&blkBuf[0x12], 0); // last access date + PutShortLE(&blkBuf[0x14], 0xa87b); // last date set (Nov 7 1984) + dierr = fpImg->WriteBlock(kVolHeaderBlock, blkBuf); + if (dierr != kDIErrNone) { + WMSG2(" Format: block %d write failed (err=%d)\n", + kVolHeaderBlock, dierr); + goto bail; + } + + /* check our work, and set some object fields, by reading what we wrote */ + dierr = LoadVolHeader(); + if (dierr != kDIErrNone) { + WMSG1(" GLITCH: couldn't read header we just wrote (err=%d)\n", dierr); + goto bail; + } + + +bail: + SetDiskImg(nil); // shouldn't really be set by us + return dierr; +} + +/* + * Blocks 0 and 1 of a 5.25" bootable Pascal disk, formatted by + * APPLE3:FORMATTER from Pascal v1.3. + */ +const unsigned char gPascal525Block0[] = { + 0x01, 0xe0, 0x70, 0xb0, 0x04, 0xe0, 0x40, 0xb0, 0x39, 0xbd, 0x88, 0xc0, + 0x20, 0x20, 0x08, 0xa2, 0x00, 0xbd, 0x25, 0x08, 0x09, 0x80, 0x20, 0xfd, + 0xfb, 0xe8, 0xe0, 0x1d, 0xd0, 0xf3, 0xf0, 0xfe, 0xa9, 0x0a, 0x4c, 0x24, + 0xfc, 0x4d, 0x55, 0x53, 0x54, 0x20, 0x42, 0x4f, 0x4f, 0x54, 0x20, 0x46, + 0x52, 0x4f, 0x4d, 0x20, 0x53, 0x4c, 0x4f, 0x54, 0x20, 0x34, 0x2c, 0x20, + 0x35, 0x20, 0x4f, 0x52, 0x20, 0x36, 0x8a, 0x85, 0x43, 0x4a, 0x4a, 0x4a, + 0x4a, 0x09, 0xc0, 0x85, 0x0d, 0xa9, 0x5c, 0x85, 0x0c, 0xad, 0x00, 0x08, + 0xc9, 0x06, 0xb0, 0x0a, 0x69, 0x02, 0x8d, 0x00, 0x08, 0xe6, 0x3d, 0x6c, + 0x0c, 0x00, 0xa9, 0x00, 0x8d, 0x78, 0x04, 0xa9, 0x0a, 0x85, 0x0e, 0xa9, + 0x80, 0x85, 0x3f, 0x85, 0x11, 0xa9, 0x00, 0x85, 0x10, 0xa9, 0x08, 0x85, + 0x02, 0xa9, 0x02, 0x85, 0x0f, 0xa9, 0x00, 0x20, 0x4c, 0x09, 0xa2, 0x4e, + 0xa0, 0x06, 0xb1, 0x10, 0xd9, 0x39, 0x09, 0xf0, 0x2b, 0x18, 0xa5, 0x10, + 0x69, 0x1a, 0x85, 0x10, 0x90, 0x02, 0xe6, 0x11, 0xca, 0xd0, 0xe9, 0xc6, + 0x0e, 0xd0, 0xcc, 0x20, 0x20, 0x08, 0xa6, 0x43, 0xbd, 0x88, 0xc0, 0xa2, + 0x00, 0xbd, 0x2a, 0x09, 0x09, 0x80, 0x20, 0xfd, 0xfb, 0xe8, 0xe0, 0x15, + 0xd0, 0xf3, 0xf0, 0xfe, 0xc8, 0xc0, 0x13, 0xd0, 0xc9, 0xad, 0x81, 0xc0, + 0xad, 0x81, 0xc0, 0xa9, 0xd0, 0x85, 0x3f, 0xa9, 0x30, 0x85, 0x02, 0xa0, + 0x00, 0xb1, 0x10, 0x85, 0x0f, 0xc8, 0xb1, 0x10, 0x20, 0x4c, 0x09, 0xad, + 0x89, 0xc0, 0xa9, 0xd0, 0x85, 0x3f, 0xa9, 0x10, 0x85, 0x02, 0xa0, 0x00, + 0xb1, 0x10, 0x18, 0x69, 0x18, 0x85, 0x0f, 0xc8, 0xb1, 0x10, 0x69, 0x00, + 0x20, 0x4c, 0x09, 0xa5, 0x43, 0xc9, 0x50, 0xf0, 0x08, 0x90, 0x1a, 0xad, + 0x80, 0xc0, 0x6c, 0xf8, 0xff, 0xa2, 0x00, 0x8e, 0xc4, 0xfe, 0xe8, 0x8e, + 0xc6, 0xfe, 0xe8, 0x8e, 0xb6, 0xfe, 0xe8, 0x8e, 0xb8, 0xfe, 0x4c, 0xfb, + 0x08, 0xa2, 0x00, 0x8e, 0xc0, 0xfe, 0xe8, 0x8e, 0xc2, 0xfe, 0xa2, 0x04, + 0x8e, 0xb6, 0xfe, 0xe8, 0x8e, 0xb8, 0xfe, 0x4c, 0xfb, 0x08, 0x4e, 0x4f, + 0x20, 0x46, 0x49, 0x4c, 0x45, 0x20, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, + 0x2e, 0x41, 0x50, 0x50, 0x4c, 0x45, 0x20, 0x0c, 0x53, 0x59, 0x53, 0x54, + 0x45, 0x4d, 0x2e, 0x41, 0x50, 0x50, 0x4c, 0x45, 0x4a, 0x08, 0xa5, 0x0f, + 0x29, 0x07, 0x0a, 0x85, 0x00, 0xa5, 0x0f, 0x28, 0x6a, 0x4a, 0x4a, 0x85, + 0xf0, 0xa9, 0x00, 0x85, 0x3e, 0x4c, 0x78, 0x09, 0xa6, 0x02, 0xf0, 0x22, + 0xc6, 0x02, 0xe6, 0x3f, 0xe6, 0x00, 0xa5, 0x00, 0x49, 0x10, 0xd0, 0x04, + 0x85, 0x00, 0xe6, 0xf0, 0xa4, 0x00, 0xb9, 0x8b, 0x09, 0x85, 0xf1, 0xa2, + 0x00, 0xe4, 0x02, 0xf0, 0x05, 0x20, 0x9b, 0x09, 0x90, 0xda, 0x60, 0x00, + 0x02, 0x04, 0x06, 0x08, 0x0a, 0x0c, 0x0e, 0x01, 0x03, 0x05, 0x07, 0x09, + 0x0b, 0x0d, 0x0f, 0xa6, 0x43, 0xa5, 0xf0, 0x0a, 0x0e, 0x78, 0x04, 0x20, + 0xa3, 0x0a, 0x4e, 0x78, 0x04, 0x20, 0x47, 0x0a, 0xb0, 0xfb, 0xa4, 0x2e, + 0x8c, 0x78, 0x04, 0xc4, 0xf0, 0xd0, 0xe6, 0xa5, 0x2d, 0xc5, 0xf1, 0xd0, + 0xec, 0x20, 0xdf, 0x09, 0xb0, 0xe7, 0x20, 0xc7, 0x09, 0x18, 0x60, 0xa0, + 0x00, 0xa2, 0x56, 0xca, 0x30, 0xfb, 0xb9, 0x00, 0x02, 0x5e, 0x00, 0x03, + 0x2a, 0x5e, 0x00, 0x03, 0x2a, 0x91, 0x3e, 0xc8, 0xd0, 0xed, 0x60, 0xa0, + 0x20, 0x88, 0xf0, 0x61, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0x49, 0xd5, 0xd0, + 0xf4, 0xea, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0xc9, 0xaa, 0xd0, 0xf2, 0xa0, + 0x56, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0xc9, 0xad +}; +const unsigned char gPascal525Block1[] = { + 0xd0, 0xe7, 0xa9, 0x00, 0x88, 0x84, 0x26, 0xbc, 0x8c, 0xc0, 0x10, 0xfb, + 0x59, 0xd6, 0x02, 0xa4, 0x26, 0x99, 0x00, 0x03, 0xd0, 0xee, 0x84, 0x26, + 0xbc, 0x8c, 0xc0, 0x10, 0xfb, 0x59, 0xd6, 0x02, 0xa4, 0x26, 0x99, 0x00, + 0x02, 0xc8, 0xd0, 0xee, 0xbc, 0x8c, 0xc0, 0x10, 0xfb, 0xd9, 0xd6, 0x02, + 0xd0, 0x13, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0xc9, 0xde, 0xd0, 0x0a, 0xea, + 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0xc9, 0xaa, 0xf0, 0x5c, 0x38, 0x60, 0xa0, + 0xfc, 0x84, 0x26, 0xc8, 0xd0, 0x04, 0xe6, 0x26, 0xf0, 0xf3, 0xbd, 0x8c, + 0xc0, 0x10, 0xfb, 0xc9, 0xd5, 0xd0, 0xf0, 0xea, 0xbd, 0x8c, 0xc0, 0x10, + 0xfb, 0xc9, 0xaa, 0xd0, 0xf2, 0xa0, 0x03, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, + 0xc9, 0x96, 0xd0, 0xe7, 0xa9, 0x00, 0x85, 0x27, 0xbd, 0x8c, 0xc0, 0x10, + 0xfb, 0x2a, 0x85, 0x26, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0x25, 0x26, 0x99, + 0x2c, 0x00, 0x45, 0x27, 0x88, 0x10, 0xe7, 0xa8, 0xd0, 0xb7, 0xbd, 0x8c, + 0xc0, 0x10, 0xfb, 0xc9, 0xde, 0xd0, 0xae, 0xea, 0xbd, 0x8c, 0xc0, 0x10, + 0xfb, 0xc9, 0xaa, 0xd0, 0xa4, 0x18, 0x60, 0x86, 0x2b, 0x85, 0x2a, 0xcd, + 0x78, 0x04, 0xf0, 0x48, 0xa9, 0x00, 0x85, 0x26, 0xad, 0x78, 0x04, 0x85, + 0x27, 0x38, 0xe5, 0x2a, 0xf0, 0x37, 0xb0, 0x07, 0x49, 0xff, 0xee, 0x78, + 0x04, 0x90, 0x05, 0x69, 0xfe, 0xce, 0x78, 0x04, 0xc5, 0x26, 0x90, 0x02, + 0xa5, 0x26, 0xc9, 0x0c, 0xb0, 0x01, 0xa8, 0x20, 0xf4, 0x0a, 0xb9, 0x15, + 0x0b, 0x20, 0x04, 0x0b, 0xa5, 0x27, 0x29, 0x03, 0x0a, 0x05, 0x2b, 0xaa, + 0xbd, 0x80, 0xc0, 0xb9, 0x21, 0x0b, 0x20, 0x04, 0x0b, 0xe6, 0x26, 0xd0, + 0xbf, 0x20, 0x04, 0x0b, 0xad, 0x78, 0x04, 0x29, 0x03, 0x0a, 0x05, 0x2b, + 0xaa, 0xbd, 0x81, 0xc0, 0xa6, 0x2b, 0x60, 0xea, 0xa2, 0x11, 0xca, 0xd0, + 0xfd, 0xe6, 0x46, 0xd0, 0x02, 0xe6, 0x47, 0x38, 0xe9, 0x01, 0xd0, 0xf0, + 0x60, 0x01, 0x30, 0x28, 0x24, 0x20, 0x1e, 0x1d, 0x1c, 0x1c, 0x1c, 0x1c, + 0x1c, 0x70, 0x2c, 0x26, 0x22, 0x1f, 0x1e, 0x1d, 0x1c, 0x1c, 0x1c, 0x1c, + 0x1c, 0x20, 0x43, 0x4f, 0x50, 0x59, 0x52, 0x49, 0x47, 0x48, 0x54, 0x20, + 0x41, 0x50, 0x50, 0x4c, 0x45, 0x20, 0x43, 0x4f, 0x4d, 0x50, 0x55, 0x54, + 0x45, 0x52, 0x2c, 0x20, 0x49, 0x4e, 0x43, 0x2e, 0x2c, 0x20, 0x31, 0x39, + 0x38, 0x34, 0x2c, 0x20, 0x31, 0x39, 0x38, 0x35, 0x20, 0x43, 0x2e, 0x4c, + 0x45, 0x55, 0x4e, 0x47, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x68, 0x03, 0x00, 0x00, 0x02, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbb +}; + +/* + * Block 0 of a 3.5" bootable Pascal disk, formatted by + * APPLE3:FORMATTER from Pascal v1.3. Block 1 is zeroed out. + */ +unsigned char gPascal35Block0[] = { + 0x01, 0xe0, 0x70, 0xb0, 0x04, 0xe0, 0x40, 0xb0, 0x39, 0xbd, 0x88, 0xc0, + 0x20, 0x20, 0x08, 0xa2, 0x00, 0xbd, 0x25, 0x08, 0x09, 0x80, 0x20, 0xfd, + 0xfb, 0xe8, 0xe0, 0x1d, 0xd0, 0xf3, 0xf0, 0xfe, 0xa9, 0x0a, 0x4c, 0x24, + 0xfc, 0x4d, 0x55, 0x53, 0x54, 0x20, 0x42, 0x4f, 0x4f, 0x54, 0x20, 0x46, + 0x52, 0x4f, 0x4d, 0x20, 0x53, 0x4c, 0x4f, 0x54, 0x20, 0x34, 0x2c, 0x20, + 0x35, 0x20, 0x4f, 0x52, 0x20, 0x36, 0x8a, 0x85, 0x43, 0x4a, 0x4a, 0x4a, + 0x4a, 0x09, 0xc0, 0x85, 0x15, 0x8d, 0x5d, 0x09, 0xa9, 0x00, 0x8d, 0x78, + 0x04, 0x85, 0x14, 0xa9, 0x0a, 0x85, 0x0e, 0xa9, 0x80, 0x85, 0x13, 0x85, + 0x11, 0xa9, 0x00, 0x85, 0x10, 0x85, 0x0b, 0xa9, 0x02, 0x85, 0x0a, 0xa9, + 0x04, 0x85, 0x02, 0x20, 0x40, 0x09, 0xa2, 0x4e, 0xa0, 0x06, 0xb1, 0x10, + 0xd9, 0x2d, 0x09, 0xf0, 0x2b, 0x18, 0xa5, 0x10, 0x69, 0x1a, 0x85, 0x10, + 0x90, 0x02, 0xe6, 0x11, 0xca, 0xd0, 0xe9, 0xc6, 0x0e, 0xd0, 0xcc, 0x20, + 0x20, 0x08, 0xa6, 0x43, 0xbd, 0x88, 0xc0, 0xa2, 0x00, 0xbd, 0x1e, 0x09, + 0x09, 0x80, 0x20, 0xfd, 0xfb, 0xe8, 0xe0, 0x15, 0xd0, 0xf3, 0xf0, 0xfe, + 0xc8, 0xc0, 0x13, 0xd0, 0xc9, 0xad, 0x83, 0xc0, 0xad, 0x83, 0xc0, 0xa9, + 0xd0, 0x85, 0x13, 0xa0, 0x00, 0xb1, 0x10, 0x85, 0x0a, 0xc8, 0xb1, 0x10, + 0x85, 0x0b, 0xa9, 0x18, 0x85, 0x02, 0x20, 0x40, 0x09, 0xad, 0x8b, 0xc0, + 0xa9, 0xd0, 0x85, 0x13, 0xa0, 0x00, 0xb1, 0x10, 0x18, 0x69, 0x18, 0x85, + 0x0a, 0xc8, 0xb1, 0x10, 0x69, 0x00, 0x85, 0x0b, 0xa9, 0x08, 0x85, 0x02, + 0x20, 0x40, 0x09, 0xa5, 0x43, 0xc9, 0x50, 0xf0, 0x08, 0x90, 0x1a, 0xad, + 0x80, 0xc0, 0x6c, 0xf8, 0xff, 0xa2, 0x00, 0x8e, 0xc4, 0xfe, 0xe8, 0x8e, + 0xc6, 0xfe, 0xe8, 0x8e, 0xb6, 0xfe, 0xe8, 0x8e, 0xb8, 0xfe, 0x4c, 0xef, + 0x08, 0xa2, 0x00, 0x8e, 0xc0, 0xfe, 0xe8, 0x8e, 0xc2, 0xfe, 0xa2, 0x04, + 0x8e, 0xb6, 0xfe, 0xe8, 0x8e, 0xb8, 0xfe, 0x4c, 0xef, 0x08, 0x4e, 0x4f, + 0x20, 0x46, 0x49, 0x4c, 0x45, 0x20, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, + 0x2e, 0x41, 0x50, 0x50, 0x4c, 0x45, 0x20, 0x0c, 0x53, 0x59, 0x53, 0x54, + 0x45, 0x4d, 0x2e, 0x41, 0x50, 0x50, 0x4c, 0x45, 0xa9, 0x01, 0x85, 0x42, + 0xa0, 0xff, 0xb1, 0x14, 0x8d, 0x5c, 0x09, 0xa9, 0x00, 0x85, 0x44, 0xa5, + 0x13, 0x85, 0x45, 0xa5, 0x0a, 0x85, 0x46, 0xa5, 0x0b, 0x85, 0x47, 0x20, + 0x00, 0x00, 0x90, 0x03, 0x4c, 0x5b, 0x08, 0xc6, 0x02, 0xf0, 0x0c, 0xe6, + 0x13, 0xe6, 0x13, 0xe6, 0x0a, 0xd0, 0xdc, 0xe6, 0x0b, 0xd0, 0xd8, 0x60, + 0x20, 0x43, 0x4f, 0x50, 0x59, 0x52, 0x49, 0x47, 0x48, 0x54, 0x20, 0x41, + 0x50, 0x50, 0x4c, 0x45, 0x20, 0x43, 0x4f, 0x4d, 0x50, 0x55, 0x54, 0x45, + 0x52, 0x2c, 0x20, 0x49, 0x4e, 0x43, 0x2e, 0x2c, 0x20, 0x31, 0x39, 0x38, + 0x34, 0x2c, 0x20, 0x31, 0x39, 0x38, 0x35, 0x20, 0x43, 0x2e, 0x4c, 0x45, + 0x55, 0x4e, 0x47, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xb0, 0x01, 0x00, 0x00, 0x02, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +/* + * Write the Pascal boot blocks onto the disk image. + */ +DIError +DiskFSPascal::WriteBootBlocks(void) +{ + DIError dierr; + unsigned char block0[512]; + unsigned char block1[512]; + bool is525 = false; + + assert(fpImg->GetHasBlocks()); + if (fpImg->GetNumBlocks() == 280) + is525 = true; + else if (fpImg->GetNumBlocks() == 1600) + is525 = false; + else { + WMSG1(" Pascal boot blocks for blocks=%ld unknown\n", + fpImg->GetNumBlocks()); + return kDIErrInternal; + } + + if (is525) { + memcpy(block0, gPascal525Block0, sizeof(block0)); + memcpy(block1, gPascal525Block1, sizeof(block1)); + } else { + memcpy(block0, gPascal35Block0, sizeof(block0)); + memset(block1, 0, sizeof(block1)); + } + + dierr = fpImg->WriteBlock(0, block0); + if (dierr != kDIErrNone) { + WMSG1(" WriteBootBlocks: block0 write failed (err=%d)\n", dierr); + return dierr; + } + dierr = fpImg->WriteBlock(1, block1); + if (dierr != kDIErrNone) { + WMSG1(" WriteBootBlocks: block1 write failed (err=%d)\n", dierr); + return dierr; + } + + return kDIErrNone; +} + + +/* + * Scan for damaged files and conflicting file allocation entries. + * + * Appends some entries to the DiskImg notes, so this should only be run + * once per DiskFS. + * + * Returns "true" if disk appears to be perfect, "false" otherwise. + */ +bool +DiskFSPascal::CheckDiskIsGood(void) +{ + //DIError dierr; + bool result = true; + + if (fEarlyDamage) + result = false; + + /* (don't need to check to see if the boot blocks or disk catalog are + marked in use -- the directory is defined by the set of blocks + in the volume header) */ + + /* + * Scan for "damaged" or "suspicious" files diagnosed earlier. + */ + bool damaged, suspicious; + ScanForDamagedFiles(&damaged, &suspicious); + + if (damaged) { + fpImg->AddNote(DiskImg::kNoteWarning, + "One or more files are damaged."); + result = false; + } else if (suspicious) { + fpImg->AddNote(DiskImg::kNoteWarning, + "One or more files look suspicious."); + result = false; + } + + return result; +} + +/* + * Run through the list of files and count up the free blocks. + */ +DIError +DiskFSPascal::GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const +{ + A2FilePascal* pFile; + long freeBlocks = 0; + unsigned short prevNextBlock = fNextBlock; + + pFile = (A2FilePascal*) GetNextFile(nil); + while (pFile != nil) { + freeBlocks += pFile->fStartBlock - prevNextBlock; + prevNextBlock = pFile->fNextBlock; + + pFile = (A2FilePascal*) GetNextFile(pFile); + } + freeBlocks += fTotalBlocks - prevNextBlock; + + *pTotalUnits = fTotalBlocks; + *pFreeUnits = freeBlocks; + *pUnitSize = kBlockSize; + + return kDIErrNone; +} + + +/* + * Normalize a Pascal path. Used when adding files from DiskArchive. + * + * "*pNormalizedBufLen" is used to pass in the length of the buffer and + * pass out the length of the string (should the buffer prove inadequate). + */ +DIError +DiskFSPascal::NormalizePath(const char* path, char fssep, + char* normalizedBuf, int* pNormalizedBufLen) +{ + DIError dierr = kDIErrNone; + char tmpBuf[A2FilePascal::kMaxFileName+1]; + int len; + + DoNormalizePath(path, fssep, tmpBuf); + len = strlen(tmpBuf)+1; + + if (*pNormalizedBufLen < len) + dierr = kDIErrDataOverrun; + else + strcpy(normalizedBuf, tmpBuf); + *pNormalizedBufLen = len; + + return dierr; +} + +/* + * Normalize a Pascal pathname. Lower case becomes upper case, invalid + * characters get stripped. + * + * "outBuf" must be able to hold kMaxFileName+1 characters. + */ +void +DiskFSPascal::DoNormalizePath(const char* name, char fssep, char* outBuf) +{ + char* outp = outBuf; + char* cp; + + /* throw out leading pathname, if any */ + if (fssep != '\0') { + cp = strrchr(name, fssep); + if (cp != nil) + name = cp+1; + } + + while (*name != '\0' && (outp - outBuf) < A2FilePascal::kMaxFileName) { + if (*name > 0x20 && *name < 0x7f && + strchr(kInvalidNameChars, *name) == nil) + { + *outp++ = toupper(*name); + } + + name++; + } + + *outp = '\0'; + + if (*outBuf == '\0') { + /* nothing left */ + strcpy(outBuf, "BLANK"); + } +} + + +/* + * Create an empty file. It doesn't look like pascal normally allows you + * to create a zero-block file, so we create a 1-block file and set the + * "data in last block" field to zero. + * + * We don't know how big the file will be, so we can't do a "best fit" + * algorithm for placement. Instead, we just put it in the largest + * available free space. + * + * NOTE: the Pascal system will auto-delete zero-byte files. It expects a + * brand-new 1-block file to have a "bytes remaining" of 512. The files + * we create here are expected to be written to, not used as filler, so + * this behavior is actually a *good* thing. + */ +DIError +DiskFSPascal::CreateFile(const CreateParms* pParms, A2File** ppNewFile) +{ + DIError dierr = kDIErrNone; + const bool createUnique = (GetParameter(kParm_CreateUnique) != 0); + char normalName[A2FilePascal::kMaxFileName+1]; + A2FilePascal* pNewFile = nil; + + if (fpImg->GetReadOnly()) + return kDIErrAccessDenied; + if (!fDiskIsGood) + return kDIErrBadDiskImage; + + assert(pParms != nil); + assert(pParms->pathName != nil); + assert(pParms->storageType == A2FileProDOS::kStorageSeedling); + WMSG1(" Pascal ---v--- CreateFile '%s'\n", pParms->pathName); + + /* compute maxFiles, which includes the vol dir header */ + int maxFiles = + ((fNextBlock - kVolHeaderBlock) * kBlkSize) / kDirectoryEntryLen; + if (fNumFiles >= maxFiles-1) { + WMSG1("Pascal volume directory full (%d entries)\n", fNumFiles); + return kDIErrVolumeDirFull; + } + + *ppNewFile = nil; + + DoNormalizePath(pParms->pathName, pParms->fssep, normalName); + + /* + * See if the file already exists. + * + * If "create unique" is set, we append digits until the name doesn't + * match any others. The name will be modified in place. + */ + if (createUnique) { + MakeFileNameUnique(normalName); + } else { + if (GetFileByName(normalName) != nil) { + WMSG1(" Pascal create: normalized name '%s' already exists\n", + normalName); + dierr = kDIErrFileExists; + goto bail; + } + } + + /* + * Find the largest gap in the file space. + * + * We get an index pointer and A2File pointer to the previous entry. If + * the blank space is at the head of the list, prevIdx will be zero and + * pPrevFile will be nil. + */ + A2FilePascal* pPrevFile; + int prevIdx; + + dierr = FindLargestFreeArea(&prevIdx, &pPrevFile); + if (dierr != kDIErrNone) + goto bail; + assert(prevIdx >= 0); + + /* + * Make a new entry. + */ + time_t now; + pNewFile = new A2FilePascal(this); + if (pNewFile == nil) { + dierr = kDIErrMalloc; + goto bail; + } + if (pPrevFile == nil) + pNewFile->fStartBlock = fNextBlock; + else + pNewFile->fStartBlock = pPrevFile->fNextBlock; + pNewFile->fNextBlock = pNewFile->fStartBlock +1; // alloc 1 block + pNewFile->fFileType = A2FilePascal::ConvertFileType(pParms->fileType); + memset(pNewFile->fFileName, 0, A2FilePascal::kMaxFileName); + strcpy(pNewFile->fFileName, normalName); + pNewFile->fBytesRemaining = 0; + now = time(nil); + pNewFile->fModWhen = A2FilePascal::ConvertPascalDate(now); + + pNewFile->fLength = 0; + + /* + * Make a hole. + */ + dierr = LoadCatalog(); + if (dierr != kDIErrNone) + goto bail; + + if (fNumFiles > prevIdx) { + WMSG1(" Pascal sliding last %d entries down a slot\n", + fNumFiles - prevIdx); + memmove(fDirectory + (prevIdx+2) * kDirectoryEntryLen, + fDirectory + (prevIdx+1) * kDirectoryEntryLen, + (fNumFiles - prevIdx) * kDirectoryEntryLen); + } + + /* + * Fill the hole. + */ + unsigned char* dirPtr; + dirPtr = fDirectory + (prevIdx+1) * kDirectoryEntryLen; + PutShortLE(&dirPtr[0x00], pNewFile->fStartBlock); + PutShortLE(&dirPtr[0x02], pNewFile->fNextBlock); + PutShortLE(&dirPtr[0x04], (unsigned short) pNewFile->fFileType); + dirPtr[0x06] = (unsigned char) strlen(pNewFile->fFileName); + memcpy(&dirPtr[0x07], pNewFile->fFileName, A2FilePascal::kMaxFileName); + PutShortLE(&dirPtr[0x16], pNewFile->fBytesRemaining); + PutShortLE(&dirPtr[0x18], pNewFile->fModWhen); + + /* + * Update the #of files. + */ + fNumFiles++; + PutShortLE(&fDirectory[0x10], fNumFiles); + + /* + * Flush. + */ + dierr = SaveCatalog(); + if (dierr != kDIErrNone) + goto bail; + + /* + * Add to the linear file list. + */ + InsertFileInList(pNewFile, pPrevFile); + + *ppNewFile = pNewFile; + pNewFile = nil; + +bail: + delete pNewFile; + FreeCatalog(); + return dierr; +} + +/* + * Make the name pointed to by "fileName" unique. The name should already + * be FS-normalized, and be in a buffer that can hold at least kMaxFileName+1 + * bytes. + * + * (This is nearly identical to the code in the ProDOS implementation. I'd + * like to make it a general DiskFS function, but making the loop condition + * work requires setting up callbacks, which isn't hard here but is a little + * annoying in ProDOS because of the subdir buffer. So it's cut & paste + * for now.) + * + * Returns an error on failure, which should be impossible. + */ +DIError +DiskFSPascal::MakeFileNameUnique(char* fileName) +{ + assert(fileName != nil); + assert(strlen(fileName) <= A2FilePascal::kMaxFileName); + + if (GetFileByName(fileName) == nil) + return kDIErrNone; + + WMSG1(" Pascal found duplicate of '%s', making unique\n", fileName); + + int nameLen = strlen(fileName); + int dotOffset=0, dotLen=0; + char dotBuf[kMaxExtensionLen+1]; + + /* ensure the result will be null-terminated */ + memset(fileName + nameLen, 0, (A2FilePascal::kMaxFileName - nameLen) +1); + + /* + * If this has what looks like a filename extension, grab it. We want + * to preserve ".gif", ".c", etc. + */ + const char* cp = strrchr(fileName, '.'); + if (cp != nil) { + int tmpOffset = cp - fileName; + if (tmpOffset > 0 && nameLen - tmpOffset <= kMaxExtensionLen) { + WMSG1(" Pascal (keeping extension '%s')\n", cp); + assert(strlen(cp) <= kMaxExtensionLen); + strcpy(dotBuf, cp); + dotOffset = tmpOffset; + dotLen = nameLen - dotOffset; + } + } + + const int kMaxDigits = 999; + int digits = 0; + int digitLen; + int copyOffset; + char digitBuf[4]; + do { + if (digits == kMaxDigits) + return kDIErrFileExists; + digits++; + + /* not the most efficient way to do this, but it'll do */ + sprintf(digitBuf, "%d", digits); + digitLen = strlen(digitBuf); + if (nameLen + digitLen > A2FilePascal::kMaxFileName) + copyOffset = A2FilePascal::kMaxFileName - dotLen - digitLen; + else + copyOffset = nameLen - dotLen; + memcpy(fileName + copyOffset, digitBuf, digitLen); + if (dotLen != 0) + memcpy(fileName + copyOffset + digitLen, dotBuf, dotLen); + } while (GetFileByName(fileName) != nil); + + WMSG1(" Pascal converted to unique name: %s\n", fileName); + + return kDIErrNone; +} + +/* + * Find the largest chunk of free space on the disk. + * + * Returns the index to the directory entry of the file immediately before + * the chunk (where 0 is the directory header), and the corresponding + * A2File entry. + * + * If there's no free space left, returns kDIErrDiskFull. + */ +DIError +DiskFSPascal::FindLargestFreeArea(int *pPrevIdx, A2FilePascal** ppPrevFile) +{ + A2FilePascal* pFile; + A2FilePascal* pPrevFile; + unsigned short prevNextBlock = fNextBlock; + int gapSize, maxGap, maxIndex, idx; + + maxIndex = -1; + maxGap = 0; + idx = 0; + *ppPrevFile = pPrevFile = nil; + + pFile = (A2FilePascal*) GetNextFile(nil); + while (pFile != nil) { + gapSize = pFile->fStartBlock - prevNextBlock; + if (gapSize > maxGap) { + maxGap = gapSize; + maxIndex = idx; + *ppPrevFile = pPrevFile; + } + + idx++; + prevNextBlock = pFile->fNextBlock; + pPrevFile = pFile; + pFile = (A2FilePascal*) GetNextFile(pFile); + } + + gapSize = fTotalBlocks - prevNextBlock; + if (gapSize > maxGap) { + maxGap = gapSize; + maxIndex = idx; + *ppPrevFile = pPrevFile; + } + + WMSG3("Pascal largest gap after entry %d '%s' (size=%d)\n", + maxIndex, + *ppPrevFile != nil ? (*ppPrevFile)->GetPathName() : "(root)", + maxGap); + *pPrevIdx = maxIndex; + + if (maxIndex < 0) + return kDIErrDiskFull; + return kDIErrNone; +} + + +/* + * Delete a file. Because Pascal doesn't have a block allocation map, this + * is a simple matter of crunching the directory entry out. + */ +DIError +DiskFSPascal::DeleteFile(A2File* pGenericFile) +{ + DIError dierr = kDIErrNone; + A2FilePascal* pFile = (A2FilePascal*) pGenericFile; + unsigned char* pEntry; + int dirLen, offsetToNextEntry; + + if (pGenericFile == nil) { + assert(false); + return kDIErrInvalidArg; + } + + if (fpImg->GetReadOnly()) + return kDIErrAccessDenied; + if (!fDiskIsGood) + return kDIErrBadDiskImage; + if (pGenericFile->IsFileOpen()) + return kDIErrFileOpen; + + WMSG1(" Pascal deleting '%s'\n", pFile->GetPathName()); + + dierr = LoadCatalog(); + if (dierr != kDIErrNone) + goto bail; + + pEntry = FindDirEntry(pFile); + if (pEntry == nil) { + assert(false); + dierr = kDIErrInternal; + goto bail; + } + dirLen = (fNumFiles+1) * kDirectoryEntryLen; + offsetToNextEntry = (pEntry - fDirectory) + kDirectoryEntryLen; + if (dirLen == offsetToNextEntry) { + WMSG0("+++ removing last entry\n"); + } else { + memmove(pEntry, pEntry+kDirectoryEntryLen, dirLen - offsetToNextEntry); + } + + assert(fNumFiles > 0); + fNumFiles--; + PutShortLE(&fDirectory[0x10], fNumFiles); + + dierr = SaveCatalog(); + if (dierr != kDIErrNone) + goto bail; + + /* + * Remove the A2File* from the list. + */ + DeleteFileFromList(pFile); + +bail: + FreeCatalog(); + return dierr; +} + +/* + * Rename a file. + */ +DIError +DiskFSPascal::RenameFile(A2File* pGenericFile, const char* newName) +{ + DIError dierr = kDIErrNone; + A2FilePascal* pFile = (A2FilePascal*) pGenericFile; + char normalName[A2FilePascal::kMaxFileName+1]; + unsigned char* pEntry; + + if (pFile == nil || newName == nil) + return kDIErrInvalidArg; + if (!IsValidFileName(newName)) + return kDIErrInvalidArg; + if (fpImg->GetReadOnly()) + return kDIErrAccessDenied; + if (!fDiskIsGood) + return kDIErrBadDiskImage; + /* not strictly necessary, but watch sanity check in Close/FindDirEntry */ + if (pGenericFile->IsFileOpen()) + return kDIErrFileOpen; + + DoNormalizePath(newName, '\0', normalName); + + WMSG2(" Pascal renaming '%s' to '%s'\n", pFile->GetPathName(), normalName); + + dierr = LoadCatalog(); + if (dierr != kDIErrNone) + goto bail; + + pEntry = FindDirEntry(pFile); + if (pEntry == nil) { + assert(false); + dierr = kDIErrInternal; + goto bail; + } + + pEntry[0x06] = strlen(normalName); + memcpy(&pEntry[0x07], normalName, A2FilePascal::kMaxFileName); + strcpy(pFile->fFileName, normalName); + + dierr = SaveCatalog(); + if (dierr != kDIErrNone) + goto bail; + +bail: + FreeCatalog(); + return dierr; +} + +/* + * Set file info. + * + * Pascal does not have an aux type or access flags. It has a file type, + * but we don't allow the full range of ProDOS types. Attempting to change + * to an unsupported type results in "PDA" being used. + */ +DIError +DiskFSPascal::SetFileInfo(A2File* pGenericFile, long fileType, long auxType, + long accessFlags) +{ + DIError dierr = kDIErrNone; + A2FilePascal* pFile = (A2FilePascal*) pGenericFile; + unsigned char* pEntry; + + if (pFile == nil) + return kDIErrInvalidArg; + if (fpImg->GetReadOnly()) + return kDIErrAccessDenied; + if (!fDiskIsGood) + return kDIErrBadDiskImage; + + WMSG2("Pascal SetFileInfo '%s' fileType=0x%04lx\n", + pFile->GetPathName(), fileType); + + dierr = LoadCatalog(); + if (dierr != kDIErrNone) + goto bail; + + pEntry = FindDirEntry(pFile); + if (pEntry == nil) { + assert(false); + dierr = kDIErrInternal; + goto bail; + } + + A2FilePascal::FileType newType; + + newType = A2FilePascal::ConvertFileType(fileType); + PutShortLE(&pEntry[0x04], (unsigned short) newType); + + dierr = SaveCatalog(); + if (dierr != kDIErrNone) + goto bail; + + /* update our local copy */ + pFile->fFileType = newType; + +bail: + FreeCatalog(); + return dierr; +} + +/* + * Change the Pascal volume name. + */ +DIError +DiskFSPascal::RenameVolume(const char* newName) +{ + DIError dierr = kDIErrNone; + char normalName[A2FilePascal::kMaxFileName+1]; + + if (!IsValidVolumeName(newName)) + return kDIErrInvalidArg; + if (fpImg->GetReadOnly()) + return kDIErrAccessDenied; + + DoNormalizePath(newName, '\0', normalName); + + dierr = LoadCatalog(); + if (dierr != kDIErrNone) + goto bail; + + fDirectory[0x06] = strlen(normalName); + memcpy(&fDirectory[0x07], normalName, fDirectory[0x06]); + strcpy(fVolumeName, normalName); + + SetVolumeID(); + + dierr = SaveCatalog(); + if (dierr != kDIErrNone) + goto bail; + +bail: + FreeCatalog(); + return dierr; +} + + +/* + * Find "pFile" in "fDirectory". + */ +unsigned char* +DiskFSPascal::FindDirEntry(A2FilePascal* pFile) +{ + unsigned char* ptr; + int i; + + assert(fDirectory != nil); + + ptr = fDirectory; // volume header; first iteration skips over it + for (i = 0; i < fNumFiles; i++) { + ptr += kDirectoryEntryLen; + + if (GetShortLE(&ptr[0x00]) == pFile->fStartBlock) { + if (memcmp(&ptr[0x07], pFile->fFileName, ptr[0x06]) != 0) { + assert(false); + WMSG2("name/block mismatch on '%s' %d\n", + pFile->GetPathName(), pFile->fStartBlock); + return nil; + } + return ptr; + } + } + + return nil; +} + + +/* + * =========================================================================== + * A2FilePascal + * =========================================================================== + */ + +/* + * Convert Pascal file type to ProDOS file type. + */ +long +A2FilePascal::GetFileType(void) const +{ + switch (fFileType) { + case kTypeUntyped: return 0x00; // NON + case kTypeXdsk: return 0x01; // BAD (was 0xf2 in v1.2.2) + case kTypeCode: return 0x02; // PCD + case kTypeText: return 0x03; // PTX + case kTypeInfo: return 0xf3; // no idea + case kTypeData: return 0x05; // PDA + case kTypeGraf: return 0xf4; // no idea + case kTypeFoto: return 0x08; // FOT + case kTypeSecurdir: return 0xf5; // no idea + default: + WMSG1("Pascal WARNING: found invalid file type %d\n", fFileType); + return 0; + } +} + +/* + * Convert a ProDOS file type to a Pascal file type. + */ +/*static*/ A2FilePascal::FileType +A2FilePascal::ConvertFileType(long prodosType) +{ + FileType newType; + + switch (prodosType) { + case 0x00: newType = kTypeUntyped; break; // NON + case 0x01: newType = kTypeXdsk; break; // BAD + case 0x02: newType = kTypeCode; break; // PCD + case 0x03: newType = kTypeText; break; // PTX + case 0xf3: newType = kTypeInfo; break; // ? + case 0x05: newType = kTypeData; break; // PDA + case 0xf4: newType = kTypeGraf; break; // ? + case 0x08: newType = kTypeFoto; break; // FOT + case 0xf5: newType = kTypeSecurdir; break; // ? + default: newType = kTypeData; break; // PDA for generic + } + + return newType; +} + + +/* + * Convert from Pascal compact date format to a time_t. + * + * Format yyyyyyydddddmmmm + * Month 0..12 (0 indicates invalid date) + * Day 0..31 + * Year 0..100 (1900-1999; 100 will be rejected) + * + * We follow the ProDOS protocol of "year < 40 == 1900 + year". We could + * probably make that 1970, but the time_t epoch ends before then. + * + * The Pascal Filer uses a special date with the year 100 in it to indicate + * file updates in progress. If the system comes up and sees a file with + * the year 100, it will assume that the file was created shortly before the + * system crashed, and will remove the file. + */ +/*static*/ time_t +A2FilePascal::ConvertPascalDate(PascalDate pascalDate) +{ + int year, month, day; + + month = pascalDate & 0x0f; + if (!month) + return 0; + day = (pascalDate >> 4) & 0x1f; + year = (pascalDate >> 9) & 0x7f; + if (year == 100) { + // ought to mark the file as "suspicious"? + WMSG0("Pascal WARNING: date with year=100\n"); + } + if (year < 40) + year += 100; + + struct tm tmbuf; + time_t when; + + tmbuf.tm_sec = 0; + tmbuf.tm_min = 0; + tmbuf.tm_hour = 0; + tmbuf.tm_mday = day; + tmbuf.tm_mon = month-1; + tmbuf.tm_year = year; + tmbuf.tm_wday = 0; + tmbuf.tm_yday = 0; + tmbuf.tm_isdst = -1; // let it figure DST and time zone + when = mktime(&tmbuf); + + if (when == (time_t) -1) + when = 0; + return when; +} + +/* + * Convert a time_t to a Pascal-format date. + * + * CiderPress uses kDateInvalid==-1 and kDateNone==-2. + */ +/*static*/ A2FilePascal::PascalDate +A2FilePascal::ConvertPascalDate(time_t unixDate) +{ + unsigned long date, year; + struct tm* ptm; + + if (unixDate == 0 || unixDate == -1 || unixDate == -2) + return 0; + + ptm = localtime(&unixDate); + if (ptm == nil) + return 0; // must've been invalid or unspecified + + year = ptm->tm_year; // years since 1900 + if (year >= 100) + year -= 100; + if (year < 0 || year >= 100) { + WMSG2("WHOOPS: got year %lu from %d\n", year, ptm->tm_year); + year = 70; + } + date = year << 9 | (ptm->tm_mon+1) | ptm->tm_mday << 4; + return (PascalDate) date; +} + + +/* + * Return the file modification date. + */ +time_t +A2FilePascal::GetModWhen(void) const +{ + return ConvertPascalDate(fModWhen); +} + +/* + * Dump the contents of the A2File structure. + */ +void +A2FilePascal::Dump(void) const +{ + WMSG1("A2FilePascal '%s'\n", fFileName); + WMSG3(" start=%d next=%d type=%d\n", + fStartBlock, fNextBlock, fFileType); + WMSG2(" bytesRem=%d modWhen=0x%04x\n", + fBytesRemaining, fModWhen); +} + +/* + * Not a whole lot to do, since there's no fancy index blocks. + */ +DIError +A2FilePascal::Open(A2FileDescr** ppOpenFile, bool readOnly, + bool rsrcFork /*=false*/) +{ + A2FDPascal* pOpenFile = nil; + + if (!readOnly) { + if (fpDiskFS->GetDiskImg()->GetReadOnly()) + return kDIErrAccessDenied; + if (fpDiskFS->GetFSDamaged()) + return kDIErrBadDiskImage; + } + if (fpOpenFile != nil) + return kDIErrAlreadyOpen; + if (rsrcFork) + return kDIErrForkNotFound; + + pOpenFile = new A2FDPascal(this); + + pOpenFile->fOffset = 0; + pOpenFile->fOpenEOF = fLength; + pOpenFile->fOpenBlocksUsed = fNextBlock - fStartBlock; + pOpenFile->fModified = false; + + fpOpenFile = pOpenFile; + *ppOpenFile = pOpenFile; + + return kDIErrNone; +} + + +/* + * =========================================================================== + * A2FDPascal + * =========================================================================== + */ + +/* + * Read a chunk of data from the current offset. + */ +DIError +A2FDPascal::Read(void* buf, size_t len, size_t* pActual) +{ + WMSG3(" Pascal reading %d bytes from '%s' (offset=%ld)\n", + len, fpFile->GetPathName(), (long) fOffset); + + A2FilePascal* pFile = (A2FilePascal*) fpFile; + + /* don't allow them to read past the end of the file */ + if (fOffset + (long)len > fOpenEOF) { + if (pActual == nil) + return kDIErrDataUnderrun; + len = (size_t) (fOpenEOF - fOffset); + } + if (pActual != nil) + *pActual = len; + long incrLen = len; + + DIError dierr = kDIErrNone; + unsigned char blkBuf[kBlkSize]; + long block = pFile->fStartBlock + (long) (fOffset / kBlkSize); + int bufOffset = (long) (fOffset % kBlkSize); // (& 0x01ff) + size_t thisCount; + + if (len == 0) + return kDIErrNone; + assert(fOpenEOF != 0); + + while (len) { + assert(block >= pFile->fStartBlock && block < pFile->fNextBlock); + + dierr = pFile->GetDiskFS()->GetDiskImg()->ReadBlock(block, blkBuf); + if (dierr != kDIErrNone) { + WMSG1(" Pascal error reading file '%s'\n", pFile->fFileName); + return dierr; + } + thisCount = kBlkSize - bufOffset; + if (thisCount > len) + thisCount = len; + + memcpy(buf, blkBuf + bufOffset, thisCount); + len -= thisCount; + buf = (char*)buf + thisCount; + + bufOffset = 0; + block++; + } + + fOffset += incrLen; + + return dierr; +} + +/* + * Write data at the current offset. + * + * We make the customary assumptions here: we're writing to a brand-new file, + * and writing all data in one shot. On a Pascal disk, that makes this + * process almost embarrassingly simple. + */ +DIError +A2FDPascal::Write(const void* buf, size_t len, size_t* pActual) +{ + DIError dierr = kDIErrNone; + A2FilePascal* pFile = (A2FilePascal*) fpFile; + DiskFSPascal* pDiskFS = (DiskFSPascal*) fpFile->GetDiskFS(); + unsigned char blkBuf[kBlkSize]; + size_t origLen = len; + + WMSG2(" DOS Write len=%u %s\n", len, pFile->GetPathName()); + + if (len >= 0x01000000) { // 16MB + assert(false); + return kDIErrInvalidArg; + } + + assert(fOffset == 0); // big simplifying assumption + assert(fOpenEOF == 0); // another one + assert(fOpenBlocksUsed == 1); + assert(buf != nil); + + /* + * Verify that there's enough room between this file and the next to + * hold the contents of the file. + */ + long blocksNeeded, blocksAvail; + A2FilePascal* pNextFile; + pNextFile = (A2FilePascal*) pDiskFS->GetNextFile(pFile); + if (pNextFile == nil) + blocksAvail = pDiskFS->GetTotalBlocks() - pFile->fStartBlock; + else + blocksAvail = pNextFile->fStartBlock - pFile->fStartBlock; + + blocksNeeded = (len + kBlkSize -1) / kBlkSize; + WMSG4("Pascal write '%s' %d bytes: avail=%ld needed=%ld\n", + pFile->GetPathName(), len, blocksAvail, blocksNeeded); + if (blocksAvail < blocksNeeded) + return kDIErrDiskFull; + + /* + * Write the data. + */ + long block; + block = pFile->fStartBlock; + while (len != 0) { + if (len >= (size_t) kBlkSize) { + /* full block write */ + dierr = pDiskFS->GetDiskImg()->WriteBlock(block, buf); + if (dierr != kDIErrNone) + goto bail; + + len -= kBlkSize; + buf = (unsigned char*) buf + kBlkSize; + } else { + /* partial block write */ + memset(blkBuf, 0, sizeof(blkBuf)); + memcpy(blkBuf, buf, len); + dierr = pDiskFS->GetDiskImg()->WriteBlock(block, blkBuf); + if (dierr != kDIErrNone) + goto bail; + + len = 0; + } + + block++; + } + + /* + * Update FD state. + */ + fOpenBlocksUsed = blocksNeeded; + fOpenEOF = origLen; + fOffset = origLen; + fModified = true; + +bail: + return dierr; +} + +/* + * Seek to a new offset. + */ +DIError +A2FDPascal::Seek(di_off_t offset, DIWhence whence) +{ + //di_off_t fileLen = ((A2FilePascal*) fpFile)->fLength; + + switch (whence) { + case kSeekSet: + if (offset < 0 || offset > fOpenEOF) + return kDIErrInvalidArg; + fOffset = offset; + break; + case kSeekEnd: + if (offset > 0 || offset < -fOpenEOF) + return kDIErrInvalidArg; + fOffset = fOpenEOF + offset; + break; + case kSeekCur: + if (offset < -fOffset || + offset >= (fOpenEOF - fOffset)) + { + return kDIErrInvalidArg; + } + fOffset += offset; + break; + default: + assert(false); + return kDIErrInvalidArg; + } + + assert(fOffset >= 0 && fOffset <= fOpenEOF); + return kDIErrNone; +} + +/* + * Return current offset. + */ +di_off_t +A2FDPascal::Tell(void) +{ + return fOffset; +} + +/* + * Release file state, and tell our parent to destroy us. + * + * Most applications don't check the value of "Close", or call it from a + * destructor, so we call CloseDescr whether we succeed or not. + */ +DIError +A2FDPascal::Close(void) +{ + DIError dierr = kDIErrNone; + DiskFSPascal* pDiskFS = (DiskFSPascal*) fpFile->GetDiskFS(); + + if (fModified) { + A2FilePascal* pFile = (A2FilePascal*) fpFile; + unsigned char* pEntry; + + dierr = pDiskFS->LoadCatalog(); + if (dierr != kDIErrNone) + goto bail; + + /* + * Update our internal copies of stuff. + */ + pFile->fLength = fOpenEOF; + pFile->fNextBlock = pFile->fStartBlock + (unsigned short) fOpenBlocksUsed; + pFile->fModWhen = A2FilePascal::ConvertPascalDate(time(nil)); + + /* + * Update the "next block" value and the length-in-last-block. We + * have to scan through the directory to find our entry, rather + * than remember an offset at "open" time, on the off chance that + * somebody created or deleted a file after we were opened. + */ + pEntry = pDiskFS->FindDirEntry(pFile); + if (pEntry == nil) { + // we deleted an open file? + assert(false); + dierr = kDIErrInternal; + goto bail; + } + unsigned short bytesInLastBlock; + bytesInLastBlock = (unsigned short)pFile->fLength % kBlkSize; + if (bytesInLastBlock == 0) + bytesInLastBlock = 512; // exactly filled out last block + + PutShortLE(&pEntry[0x02], pFile->fNextBlock); + PutShortLE(&pEntry[0x16], bytesInLastBlock); + PutShortLE(&pEntry[0x18], pFile->fModWhen); + + dierr = pDiskFS->SaveCatalog(); + if (dierr != kDIErrNone) + goto bail; + } + +bail: + pDiskFS->FreeCatalog(); + fpFile->CloseDescr(this); + return dierr; +} + +/* + * Return the #of sectors/blocks in the file. + */ +long +A2FDPascal::GetSectorCount(void) const +{ + A2FilePascal* pFile = (A2FilePascal*) fpFile; + return (pFile->fNextBlock - pFile->fStartBlock) * 2; +} +long +A2FDPascal::GetBlockCount(void) const +{ + A2FilePascal* pFile = (A2FilePascal*) fpFile; + return pFile->fNextBlock - pFile->fStartBlock; +} + +/* + * Return the Nth track/sector in this file. + */ +DIError +A2FDPascal::GetStorage(long sectorIdx, long* pTrack, long* pSector) const +{ + A2FilePascal* pFile = (A2FilePascal*) fpFile; + long pascalIdx = sectorIdx / 2; + long pascalBlock = pFile->fStartBlock + pascalIdx; + if (pascalBlock >= pFile->fNextBlock) + return kDIErrInvalidIndex; + + /* sparse blocks not possible on Pascal volumes */ + BlockToTrackSector(pascalBlock, (sectorIdx & 0x01) != 0, pTrack, pSector); + return kDIErrNone; +} +/* + * Return the Nth 512-byte block in this file. + */ +DIError +A2FDPascal::GetStorage(long blockIdx, long* pBlock) const +{ + A2FilePascal* pFile = (A2FilePascal*) fpFile; + long pascalBlock = pFile->fStartBlock + blockIdx; + if (pascalBlock >= pFile->fNextBlock) + return kDIErrInvalidIndex; + + *pBlock = pascalBlock; + assert(*pBlock < pFile->GetDiskFS()->GetDiskImg()->GetNumBlocks()); + return kDIErrNone; +} diff --git a/diskimg/ProDOS.cpp b/diskimg/ProDOS.cpp new file mode 100644 index 0000000..945cbb3 --- /dev/null +++ b/diskimg/ProDOS.cpp @@ -0,0 +1,5185 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Implementation of DiskFSProDOS class. + * + * We currently only allow one fork to be open at a time, and each file may + * only be opened once. + * + * BUG: does not keep VolumeUsage up to date. + */ +#include "StdAfx.h" +#include "DiskImgPriv.h" + +// disable Y2K+ dates when testing w/ProSel-16 vol rep (newer ProSel is OK) +//#define OLD_PRODOS_DATES + +#if defined(OLD_PRODOS_DATES) && !(defined(_DEBUG) || defined(_DEBUG_LOG)) +# error "don't set OLD_PRODOS_DATES for production" +#endif + + +/* + * =========================================================================== + * DiskFSProDOS + * =========================================================================== + */ + +const int kBlkSize = 512; +const int kVolHeaderBlock = 2; // block where Volume Header resides +const int kVolDirExpectedNumBlocks = 4; // customary #of volume header blocks +const int kMinReasonableBlocks = 16; // min size for ProDOS volume +const int kExpectedBitmapStart = 6; // block# where vol bitmap should start +const int kMaxCatalogIterations = 1024; // theoretical max is 32768? +const int kMaxDirectoryDepth = 64; // not sure what ProDOS limit is +const int kEntriesPerBlock = 0x0d; // expected value for entries per blk +const int kEntryLength = 0x27; // expected value for dir entry len +const int kTypeDIR = 0x0f; + + +/* + * Directory header. All fields not marked as "only for subdirs" also apply + * to the volume directory header. + */ +//namespace DiskImgLib { + +typedef struct DiskFSProDOS::DirHeader { + unsigned char storageType; + char dirName[A2FileProDOS::kMaxFileName+1]; + DiskFSProDOS::ProDate createWhen; + unsigned char version; + unsigned char minVersion; + unsigned char access; + unsigned char entryLength; + unsigned char entriesPerBlock; + unsigned short fileCount; + /* the rest are only for subdirs */ + unsigned short parentPointer; + unsigned char parentEntry; + unsigned char parentEntryLength; +} DirHeader; + +//}; // namespace DiskImgLib + + +/* + * See if this looks like a ProDOS volume. + * + * We test a few fields in the volume directory header for validity. + */ +static DIError +TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder) +{ + DIError dierr = kDIErrNone; + unsigned char blkBuf[kBlkSize]; + int volDirEntryLength; + int volDirEntriesPerBlock; + + dierr = pImg->ReadBlockSwapped(kVolHeaderBlock, blkBuf, imageOrder, + DiskImg::kSectorOrderProDOS); + if (dierr != kDIErrNone) + goto bail; + + volDirEntryLength = blkBuf[0x23]; + volDirEntriesPerBlock = blkBuf[0x24]; + + + if (!(blkBuf[0x00] == 0 && blkBuf[0x01] == 0) || + !((blkBuf[0x04] & 0xf0) == 0xf0) || + !((blkBuf[0x04] & 0x0f) != 0) || + !(volDirEntryLength * volDirEntriesPerBlock <= kBlkSize) || + !(blkBuf[0x05] >= 'A' && blkBuf[0x05] <= 'Z') || + 0) + { + dierr = kDIErrFilesystemNotFound; + goto bail; + } + +bail: + return dierr; +} + +/* + * Test to see if the image is a ProDOS disk. + */ +/*static*/ DIError +DiskFSProDOS::TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency) +{ + DiskImg::SectorOrder ordering[DiskImg::kSectorOrderMax]; + + DiskImg::GetSectorOrderArray(ordering, *pOrder); + + for (int i = 0; i < DiskImg::kSectorOrderMax; i++) { + if (ordering[i] == DiskImg::kSectorOrderUnknown) + continue; + if (TestImage(pImg, ordering[i]) == kDIErrNone) { + *pOrder = ordering[i]; + *pFormat = DiskImg::kFormatProDOS; + return kDIErrNone; + } + } + + WMSG0(" ProDOS didn't find valid FS\n"); + return kDIErrFilesystemNotFound; +} + +/* + * Get things rolling. + * + * Since we're assured that this is a valid disk, errors encountered from here + * on out must be handled somehow, possibly by claiming that the disk has + * no files on it. + */ +DIError +DiskFSProDOS::Initialize(InitMode initMode) +{ + DIError dierr = kDIErrNone; + char msg[kMaxVolumeName + 32]; + + fDiskIsGood = false; // hosed until proven innocent + fEarlyDamage = false; + + /* + * NOTE: we'd probably be better off with fTotalBlocks, since that's how + * big the disk *thinks* it is, especially on a CFFA or MacPart subvol. + * However, we know that the image block count is the absolute maximum, + * so while it may not be a tight bound it is an upper bound. + */ + fVolumeUsage.Create(fpImg->GetNumBlocks()); + + dierr = LoadVolHeader(); + if (dierr != kDIErrNone) + goto bail; + DumpVolHeader(); + + dierr = ScanVolBitmap(); + if (dierr != kDIErrNone) + goto bail; + + if (initMode == kInitHeaderOnly) { + WMSG0(" ProDOS - headerOnly set, skipping file load\n"); + goto bail; + } + + sprintf(msg, "Scanning %s", fVolumeName); + if (!fpImg->UpdateScanProgress(msg)) { + WMSG0(" ProDOS cancelled by user\n"); + dierr = kDIErrCancelled; + goto bail; + } + + /* volume dir is guaranteed to come first; if not, we need a lookup func */ + A2FileProDOS* pVolumeDir; + pVolumeDir = (A2FileProDOS*) GetNextFile(nil); + + dierr = RecursiveDirAdd(pVolumeDir, kVolHeaderBlock, "", 0); + if (dierr != kDIErrNone) { + WMSG0(" ProDOS RecursiveDirAdd failed\n"); + goto bail; + } + + sprintf(msg, "Processing %s", fVolumeName); + if (!fpImg->UpdateScanProgress(msg)) { + WMSG0(" ProDOS cancelled by user\n"); + dierr = kDIErrCancelled; + goto bail; + } + + dierr = ScanFileUsage(); + if (dierr != kDIErrNone) { + if (dierr == kDIErrCancelled) + goto bail; + + /* this might not be fatal; just means that *some* files are bad */ + WMSG1("WARNING: ScanFileUsage returned err=%d\n", dierr); + dierr = kDIErrNone; + fpImg->AddNote(DiskImg::kNoteWarning, + "Some errors were encountered while scanning files."); + fEarlyDamage = true; // make sure we know it's damaged + } + + fDiskIsGood = CheckDiskIsGood(); + + if (fScanForSubVolumes != kScanSubDisabled) + (void) ScanForSubVolumes(); + + if (fpImg->GetNumBlocks() <= 1600) + fVolumeUsage.Dump(); + +// A2File* pFile; +// pFile = GetNextFile(nil); +// while (pFile != nil) { +// pFile->Dump(); +// pFile = GetNextFile(pFile); +// } + +bail: + return dierr; +} + +/* + * Read some interesting fields from the volume header. + * + * The "test" function verified certain things, e.g. the storage type + * is $f and the volume name length is nonzero. + */ +DIError +DiskFSProDOS::LoadVolHeader(void) +{ + DIError dierr = kDIErrNone; + unsigned char blkBuf[kBlkSize]; + int nameLen; + + dierr = fpImg->ReadBlock(kVolHeaderBlock, blkBuf); + if (dierr != kDIErrNone) + goto bail; + + //fPrevBlock = GetShortLE(&blkBuf[0x00]); + //fNextBlock = GetShortLE(&blkBuf[0x02]); + nameLen = blkBuf[0x04] & 0x0f; + memcpy(fVolumeName, &blkBuf[0x05], nameLen); + fVolumeName[nameLen] = '\0'; + // 0x14-15 reserved + // undocumented: GS/OS writes the modification date to 0x16-19 + fModWhen = GetLongLE(&blkBuf[0x16]); + // undocumented: GS/OS uses 0x1a-1b for lower-case handling (see below) + fCreateWhen = GetLongLE(&blkBuf[0x1c]); + //fVersion = blkBuf[0x20]; + if (blkBuf[0x21] != 0) { + /* + * We don't care about the MIN_VERSION field, but it looks like GS/OS + * rejects anything with a nonzero value here. We want to add a note + * about it. + */ + fpImg->AddNote(DiskImg::kNoteInfo, + "Volume header has nonzero min_version; could confuse GS/OS."); + } + fAccess = blkBuf[0x22]; + //fEntryLength = blkBuf[0x23]; + //fEntriesPerBlock = blkBuf[0x24]; + fVolDirFileCount = GetShortLE(&blkBuf[0x25]); + fBitMapPointer = GetShortLE(&blkBuf[0x27]); + fTotalBlocks = GetShortLE(&blkBuf[0x29]); + + if (blkBuf[0x1b] & 0x80) { + /* + * Handle lower-case conversion; see GS/OS tech note #8. Unlike + * filenames, volume names are not allowed to contain spaces. If + * they try it we just ignore them. + * + * Technote 8 doesn't actually talk about volume names. By + * experimentation the field was discovered at offset 0x1a from + * the start of the block, which is marked as "reserved" in Beneath + * Apple ProDOS. + */ + unsigned short lcFlags = GetShortLE(&blkBuf[0x1a]); + + GenerateLowerCaseName(fVolumeName, fVolumeName, lcFlags, false); + } + + if (fTotalBlocks <= kVolHeaderBlock) { + /* incr to min; don't use max, or bitmap count may be too large */ + WMSG1(" ProDOS found tiny fTotalBlocks (%d), increasing to minimum\n", + fTotalBlocks); + fpImg->AddNote(DiskImg::kNoteWarning, + "ProDOS filesystem blockcount (%d) too small, setting to %d.", + fTotalBlocks, kMinReasonableBlocks); + fTotalBlocks = kMinReasonableBlocks; + fEarlyDamage = true; + } else if (fTotalBlocks != fpImg->GetNumBlocks()) { + if (fTotalBlocks != 65535 || fpImg->GetNumBlocks() != 65536) { + WMSG2(" ProDOS WARNING: total (%u) != img (%ld)\n", + fTotalBlocks, fpImg->GetNumBlocks()); + // could AddNote here, but not really necessary + } + + /* + * For safety (esp. vol bitmap read), constrain fTotalBlocks. We might + * consider not doing this for ".hdv", which can start small and then + * expand as files are added. (Check "fExpanded".) + */ + if (fTotalBlocks > fpImg->GetNumBlocks()) { + fpImg->AddNote(DiskImg::kNoteWarning, + "ProDOS filesystem blockcount (%d) exceeds disk image blocks (%ld).", + fTotalBlocks, fpImg->GetNumBlocks()); + fTotalBlocks = (unsigned short) fpImg->GetNumBlocks(); + fEarlyDamage = true; + } + } + + /* + * Test for funky volume bitmap pointer. Some disks (e.g. /RAM and + * ProSel-16) truncate the volume directory to eke a little more storage + * out of a disk. There's nothing wrong with that, but we don't want to + * try to use a volume bitmap pointer of zero or 0xffff, because it's + * probably garbage. + */ + if (fBitMapPointer != kExpectedBitmapStart) { + if (fBitMapPointer <= kVolHeaderBlock || + fBitMapPointer > kExpectedBitmapStart) + { + fpImg->AddNote(DiskImg::kNoteWarning, + "Volume bitmap pointer (%d) is probably invalid.", + fBitMapPointer); + fBitMapPointer = 6; // just fix it and hope for the best + fEarlyDamage = true; + } else { + fpImg->AddNote(DiskImg::kNoteInfo, + "Unusual volume bitmap start (%d).", fBitMapPointer); + // try it and see + } + } + + SetVolumeID(); + + /* + * Create a "magic" directory entry for the volume directory. + * + * Normally these values are pulled out of the file entry in the parent + * directory. Here, we synthesize them from the volume dir header. + */ + A2FileProDOS* pFile; + pFile = new A2FileProDOS(this); + if (pFile == nil) { + dierr = kDIErrMalloc; + goto bail; + } + + A2FileProDOS::DirEntry* pEntry; + pEntry = &pFile->fDirEntry; + + int foundStorage; + foundStorage = (blkBuf[0x04] & 0xf0) >> 4; + if (foundStorage != A2FileProDOS::kStorageVolumeDirHeader) { + WMSG1(" ProDOS WARNING: unexpected vol dir file type %d\n", + pEntry->storageType); + /* keep going */ + } + pEntry->storageType = A2FileProDOS::kStorageVolumeDirHeader; + strcpy(pEntry->fileName, fVolumeName); + //nameLen = blkBuf[0x04] & 0x0f; + //memcpy(pEntry->fileName, &blkBuf[0x05], nameLen); + //pEntry->fileName[nameLen] = '\0'; + pFile->SetPathName(":", pEntry->fileName); + pEntry->fileName[nameLen] = '\0'; + pEntry->fileType = kTypeDIR; + pEntry->keyPointer = kVolHeaderBlock; + pEntry->blocksUsed = kVolDirExpectedNumBlocks; + pEntry->eof = kVolDirExpectedNumBlocks * 512; + pEntry->createWhen = GetLongLE(&blkBuf[0x1c]); + pEntry->version = blkBuf[0x20]; + pEntry->minVersion = blkBuf[0x21]; + pEntry->access = blkBuf[0x22]; + pEntry->auxType = 0; +// if (blkBuf[0x20] >= 5) + pEntry->modWhen = GetLongLE(&blkBuf[0x16]); + pEntry->headerPointer = 0; + + pFile->fSparseDataEof = pEntry->eof; + pFile->fSparseRsrcEof = -1; + + AddFileToList(pFile); + +bail: + return dierr; +} + +/* + * Set the volume ID field. + */ +void +DiskFSProDOS::SetVolumeID(void) +{ + sprintf(fVolumeID, "ProDOS /%s", fVolumeName); +} + + +/* + * Dump what we pulled out of the volume header. + */ +void +DiskFSProDOS::DumpVolHeader(void) +{ + WMSG1(" ProDOS volume header for '%s'\n", fVolumeName); + WMSG4(" CreateWhen=0x%08lx access=0x%02x bitmap=%d totalbl=%d\n", + fCreateWhen, fAccess, fBitMapPointer, fTotalBlocks); + + time_t when; + when = A2FileProDOS::ConvertProDate(fCreateWhen); + WMSG1(" CreateWhen is %.24s\n", ctime(&when)); + + //WMSG4(" prev=%d next=%d bitmap=%d total=%d\n", + // fPrevBlock, fNextBlock, fBitMapPointer, fTotalBlocks); + //WMSG2(" create date=0x%08lx access=0x%02x\n", fCreateWhen, fAccess); + //WMSG4(" version=%d minVersion=%d entryLen=%d epb=%d\n", + // fVersion, fMinVersion, fEntryLength, fEntriesPerBlock); + //WMSG1(" volume dir fileCount=%d\n", fFileCount); +} + + +/* + * Load the disk's volume bitmap into the object's "fBlockUseMap" pointer. + * + * Does not attempt to analyze the data. + */ +DIError +DiskFSProDOS::LoadVolBitmap(void) +{ + DIError dierr = kDIErrNone; + int bitBlock, numBlocks; + + if (fBitMapPointer <= kVolHeaderBlock) + return kDIErrBadDiskImage; + if (fTotalBlocks <= kVolHeaderBlock) + return kDIErrBadDiskImage; + + /* should not already be allocated */ + assert(fBlockUseMap == nil); + delete[] fBlockUseMap; // just in case + + bitBlock = fBitMapPointer; + + numBlocks = GetNumBitmapBlocks(); // based on fTotalBlocks + assert(numBlocks > 0); + + fBlockUseMap = new unsigned char[kBlkSize * numBlocks]; + if (fBlockUseMap == nil) + return kDIErrMalloc; + + while (numBlocks--) { + dierr = fpImg->ReadBlock(bitBlock + numBlocks, + fBlockUseMap + kBlkSize * numBlocks); + if (dierr != kDIErrNone) { + delete[] fBlockUseMap; + fBlockUseMap = nil; + return dierr; + } + } + + return kDIErrNone; +} + +/* + * Save our copy of the volume bitmap. + */ +DIError +DiskFSProDOS::SaveVolBitmap(void) +{ + DIError dierr = kDIErrNone; + int bitBlock, numBlocks; + + if (fBlockUseMap == nil) { + assert(false); + return kDIErrNotReady; + } + assert(fBitMapPointer > kVolHeaderBlock); + assert(fTotalBlocks > kVolHeaderBlock); + + bitBlock = fBitMapPointer; + + numBlocks = GetNumBitmapBlocks(); + assert(numBlocks > 0); + + while (numBlocks--) { + dierr = fpImg->WriteBlock(bitBlock + numBlocks, + fBlockUseMap + kBlkSize * numBlocks); + if (dierr != kDIErrNone) + return dierr; + } + + return kDIErrNone; +} + +/* + * Throw away the volume bitmap, discarding any unsaved changes. + * + * It's okay to call this if the bitmap isn't loaded. + */ +void +DiskFSProDOS::FreeVolBitmap(void) +{ + delete[] fBlockUseMap; + fBlockUseMap = nil; +} + +/* + * Examine the volume bitmap, setting fields in the VolumeUsage map + * as appropriate. + */ +DIError +DiskFSProDOS::ScanVolBitmap(void) +{ + DIError dierr; + + dierr = LoadVolBitmap(); + if (dierr != kDIErrNone) { + WMSG1(" ProDOS failed to load volume bitmap (err=%d)\n", dierr); + return dierr; + } + + assert(fBlockUseMap != nil); + + /* mark the boot blocks as system */ + SetBlockUsage(0, VolumeUsage::kChunkPurposeSystem); + SetBlockUsage(1, VolumeUsage::kChunkPurposeSystem); + + /* mark the bitmap blocks as system */ + int i; + for (i = GetNumBitmapBlocks(); i > 0; i--) + SetBlockUsage(fBitMapPointer + i -1, VolumeUsage::kChunkPurposeSystem); + + /* + * Set the "isMarkedUsed" flag in VolumeUsage for all used blocks. + */ + VolumeUsage::ChunkState cstate; + + long block = 0; + long numBytes = (fTotalBlocks + 7) / 8; + for (i = 0; i < numBytes; i++) { + unsigned char val = fBlockUseMap[i]; + + for (int j = 0; j < 8; j++) { + if (!(val & 0x80)) { + /* block is in use, mark it */ + if (fVolumeUsage.GetChunkState(block, &cstate) != kDIErrNone) + { + assert(false); + // keep going, I guess + } + cstate.isMarkedUsed = true; + fVolumeUsage.SetChunkState(block, &cstate); + } + val <<= 1; + block++; + + if (block >= fTotalBlocks) + break; + } + if (block >= fTotalBlocks) + break; + } + + FreeVolBitmap(); + return dierr; +} + +/* + * Generate an empty block use map. + */ +DIError +DiskFSProDOS::CreateEmptyBlockMap(void) +{ + DIError dierr; + + /* load from disk; this is just to allocate the data structures */ + dierr = LoadVolBitmap(); + if (dierr != kDIErrNone) + return dierr; + + /* + * Set the bits, block by block. Not the most efficient way, but it's + * fast enough, and it exercises the standard set of functions. + */ + long block; + long firstEmpty = + kVolHeaderBlock + kVolDirExpectedNumBlocks + GetNumBitmapBlocks(); + for (block = 0; block < firstEmpty; block++) + SetBlockUseEntry(block, true); + for ( ; block < fTotalBlocks; block++) + SetBlockUseEntry(block, false); + + dierr = SaveVolBitmap(); + FreeVolBitmap(); + if (dierr != kDIErrNone) + return dierr; + + return kDIErrNone; +} + +/* + * Get the state of an entry in the block use map. + * + * Returns "true" if it's in use, "false" otherwise. + */ +bool +DiskFSProDOS::GetBlockUseEntry(long block) const +{ + assert(block >= 0 && block < fTotalBlocks); + assert(fBlockUseMap != nil); + + int offset; + unsigned char mask; + + offset = block / 8; + mask = 0x80 >> (block & 0x07); + if (fBlockUseMap[offset] & mask) + return false; + else + return true; +} + +/* + * Change the state of an entry in the block use map. + */ +void +DiskFSProDOS::SetBlockUseEntry(long block, bool inUse) +{ + assert(block >= 0 && block < fTotalBlocks); + assert(fBlockUseMap != nil); + + if (block == 0 && !inUse) { + // shouldn't happen + assert(false); + } + + int offset; + unsigned char mask; + + offset = block / 8; + mask = 0x80 >> (block & 0x07); + if (!inUse) + fBlockUseMap[offset] |= mask; + else + fBlockUseMap[offset] &= ~mask; +} + +/* + * Check for entries in the block use map past the point where they should be. + * + * Returns "true" if bogus entries were found, "false" if all is well. + */ +bool +DiskFSProDOS::ScanForExtraEntries(void) const +{ + assert(fBlockUseMap != nil); + + int offset, endOffset; + + /* sloppy: we're not checking for excess bits within last byte */ + offset = (fTotalBlocks / 8) +1; + endOffset = GetNumBitmapBlocks() * kBlkSize; + + while (offset < endOffset) { + if (fBlockUseMap[offset] != 0) { + WMSG2(" ProDOS found bogus bitmap junk 0x%02x at offset=%d\n", + fBlockUseMap[offset], offset); + return true; + } + offset++; + } + return false; +} + +/* + * Allocate a new block on a ProDOS volume. + * + * Only touches the in-memory copy. + * + * Returns the block number (0-65535) on success or -1 on failure. + */ +long +DiskFSProDOS::AllocBlock(void) +{ + assert(fBlockUseMap != nil); + +#if 0 // whoa... this is REALLY slow + /* + * Run through the entire set of blocks until we find one that's not + * allocated. We could probably make this faster by scanning bytes and + * then shifting bits, but this is easier and fast enough. + * + * We don't scan block 0 because (a) it should never be available and + * (b) it has a special meaning in some circumstances. We could probably + * start at kVolHeaderBlock+kVolHeaderNumBlocks. + */ + long block; + for (block = kVolHeaderBlock; block < fTotalBlocks; block++) { + if (!GetBlockUseEntry(block)) { + SetBlockUseEntry(block, true); + return block; + } + } +#endif + + int offset; + int maxOffset = (fTotalBlocks + 7) / 8; + + for (offset = 0; offset < maxOffset; offset++) { + if (fBlockUseMap[offset] != 0) { + /* got one, figure out which */ + int subBlock = 0; + unsigned char uch = fBlockUseMap[offset]; + while ((uch & 0x80) == 0) { + subBlock++; + uch <<= 1; + } + + long block = offset * 8 + subBlock; + assert(!GetBlockUseEntry(block)); + SetBlockUseEntry(block, true); + if (block == 0 || block == 1) { + WMSG0("PRODOS: GLITCH: rejecting alloc of block 0\n"); + continue; + } + return block; + } + } + + WMSG0("ProDOS: NOTE: AllocBlock just failed!\n"); + return -1; +} + +/* + * Tally up the number of free blocks. + */ +DIError +DiskFSProDOS::GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, + int* pUnitSize) const +{ + DIError dierr; + long block, freeBlocks; + freeBlocks = 0; + + dierr = const_cast(this)->LoadVolBitmap(); + if (dierr != kDIErrNone) + return dierr; + + for (block = 0; block < fTotalBlocks; block++) { + if (!GetBlockUseEntry(block)) + freeBlocks++; + } + + *pTotalUnits = fTotalBlocks; + *pFreeUnits = freeBlocks; + *pUnitSize = kBlockSize; + + const_cast(this)->FreeVolBitmap(); + return kDIErrNone; +} + + +/* + * Update an entry in the VolumeUsage map. + * + * The VolumeUsage map spans the range of blocks + */ +void +DiskFSProDOS::SetBlockUsage(long block, VolumeUsage::ChunkPurpose purpose) +{ + VolumeUsage::ChunkState cstate; + + fVolumeUsage.GetChunkState(block, &cstate); + if (cstate.isUsed) { + cstate.purpose = VolumeUsage::kChunkPurposeConflict; + WMSG1(" ProDOS conflicting uses for bl=%ld\n", block); + } else { + cstate.isUsed = true; + cstate.purpose = purpose; + } + fVolumeUsage.SetChunkState(block, &cstate); +} + +/* + * Pass in the number of the first block of the directory. + * + * Start with "pParent" set to the magic entry for the volume dir. + */ +DIError +DiskFSProDOS::RecursiveDirAdd(A2File* pParent, unsigned short dirBlock, + const char* basePath, int depth) +{ + DIError dierr = kDIErrNone; + DirHeader header; + unsigned char blkBuf[kBlkSize]; + int numEntries, iterations, foundCount; + bool first; + + /* if we get too deep, assume it's a loop */ + if (depth > kMaxDirectoryDepth) { + dierr = kDIErrDirectoryLoop; + goto bail; + } + + + if (dirBlock < kVolHeaderBlock || dirBlock >= fpImg->GetNumBlocks()) { + WMSG1(" ProDOS ERROR: directory block %u out of range\n", dirBlock); + dierr = kDIErrInvalidBlock; + goto bail; + } + + numEntries = 1; + iterations = 0; + foundCount = 0; + first = true; + + while (dirBlock && iterations < kMaxCatalogIterations) { + dierr = fpImg->ReadBlock(dirBlock, blkBuf); + if (dierr != kDIErrNone) + goto bail; + if (pParent->IsVolumeDirectory()) + SetBlockUsage(dirBlock, VolumeUsage::kChunkPurposeVolumeDir); + else + SetBlockUsage(dirBlock, VolumeUsage::kChunkPurposeSubdir); + + if (first) { + /* this is the directory header entry */ + dierr = GetDirHeader(blkBuf, &header); + if (dierr != kDIErrNone) + goto bail; + numEntries = header.fileCount; + //WMSG1(" ProDOS got dir header numEntries = %d\n", numEntries); + } + + /* slurp the entries out of this block */ + dierr = SlurpEntries(pParent, &header, blkBuf, first, &foundCount, + basePath, dirBlock, depth); + if (dierr != kDIErrNone) + goto bail; + + dirBlock = GetShortLE(&blkBuf[0x02]); + if (dirBlock != 0 && + (dirBlock < 2 || dirBlock >= fpImg->GetNumBlocks())) + { + WMSG2(" ProDOS ERROR: invalid dir link block %u in base='%s'\n", + dirBlock, basePath); + dierr = kDIErrInvalidBlock; + goto bail; + } + first = false; + iterations++; + } + if (iterations == kMaxCatalogIterations) { + WMSG0(" ProDOS subdir iteration count exceeded\n"); + dierr = kDIErrDirectoryLoop; + goto bail; + } + if (foundCount != numEntries) { + /* not significant; just means somebody isn't updating correctly */ + WMSG3(" ProDOS WARNING: numEntries=%d foundCount=%d in base='%s'\n", + numEntries, foundCount, basePath); + } + +bail: + return dierr; +} + +/* + * Slurp the entries out of a single ProDOS directory block. + * + * Recursively calls RecursiveDirAdd for directories. + * + * "*pFound" is increased by the number of valid entries found in this block. + */ +DIError +DiskFSProDOS::SlurpEntries(A2File* pParent, const DirHeader* pHeader, + const unsigned char* blkBuf, bool skipFirst, int* pCount, + const char* basePath, unsigned short thisBlock, int depth) +{ + DIError dierr = kDIErrNone; + int entriesThisBlock = pHeader->entriesPerBlock; + const unsigned char* entryBuf; + A2FileProDOS* pFile; + + int idx = 0; + entryBuf = &blkBuf[0x04]; + if (skipFirst) { + entriesThisBlock--; + entryBuf += pHeader->entryLength; + idx++; + } + + for ( ; entriesThisBlock > 0 ; + entriesThisBlock--, idx++, entryBuf += pHeader->entryLength) + { + if (entryBuf >= blkBuf + kBlkSize) { + WMSG0(" ProDOS whoops, just walked out of dirent buffer\n"); + return kDIErrBadDirectory; + } + + if ((entryBuf[0x00] & 0xf0) == A2FileProDOS::kStorageDeleted) { + /* skip deleted entries */ + continue; + } + + pFile = new A2FileProDOS(this); + if (pFile == nil) { + dierr = kDIErrMalloc; + goto bail; + } + + A2FileProDOS::DirEntry* pEntry; + pEntry = &pFile->fDirEntry; + A2FileProDOS::InitDirEntry(pEntry, entryBuf); + + pFile->SetParent(pParent); + pFile->fParentDirBlock = thisBlock; + pFile->fParentDirIdx = idx; + + pFile->SetPathName(basePath, pEntry->fileName); + + if (pEntry->keyPointer <= kVolHeaderBlock) { + WMSG2("ProDOS invalid key pointer %d on '%s'\n", + pEntry->keyPointer, pFile->GetPathName()); + pFile->SetQuality(A2File::kQualityDamaged); + } else + if (pEntry->storageType == A2FileProDOS::kStorageExtended) { + dierr = ReadExtendedInfo(pFile); + if (dierr != kDIErrNone) { + pFile->SetQuality(A2File::kQualityDamaged); + dierr = kDIErrNone; + } + } + + //pFile->Dump(); + AddFileToList(pFile); + (*pCount)++; + + if (!fpImg->UpdateScanProgress(nil)) { + WMSG0(" ProDOS cancelled by user\n"); + dierr = kDIErrCancelled; + goto bail; + } + + if (pEntry->storageType == A2FileProDOS::kStorageDirectory) { + // don't need to check for kStorageVolumeDirHeader here + dierr = RecursiveDirAdd(pFile, pEntry->keyPointer, + pFile->GetPathName(), depth+1); + if (dierr != kDIErrNone) { + if (dierr == kDIErrCancelled) + goto bail; + + /* mark subdir as damaged and keep going */ + pFile->SetQuality(A2File::kQualityDamaged); + dierr = kDIErrNone; + } + } + } + +bail: + return dierr; +} + + +/* + * Pull the directory header out of the first block of a directory. + */ +DIError +DiskFSProDOS::GetDirHeader(const unsigned char* blkBuf, DirHeader* pHeader) +{ + int nameLen; + + pHeader->storageType = (blkBuf[0x04] & 0xf0) >> 4; + if (pHeader->storageType != A2FileProDOS::kStorageSubdirHeader && + pHeader->storageType != A2FileProDOS::kStorageVolumeDirHeader) + { + WMSG1(" ProDOS WARNING: subdir header has wrong storage type (%d)\n", + pHeader->storageType); + /* keep going... might be bad idea */ + } + nameLen = blkBuf[0x04] & 0x0f; + memcpy(pHeader->dirName, &blkBuf[0x05], nameLen); + pHeader->dirName[nameLen] = '\0'; + pHeader->createWhen = GetLongLE(&blkBuf[0x1c]); + pHeader->version = blkBuf[0x20]; + pHeader->minVersion = blkBuf[0x21]; + pHeader->access = blkBuf[0x22]; + pHeader->entryLength = blkBuf[0x23]; + pHeader->entriesPerBlock = blkBuf[0x24]; + pHeader->fileCount = GetShortLE(&blkBuf[0x25]); + pHeader->parentPointer = GetShortLE(&blkBuf[0x27]); + pHeader->parentEntry = blkBuf[0x29]; + pHeader->parentEntryLength = blkBuf[0x2a]; + + if (pHeader->entryLength * pHeader->entriesPerBlock > kBlkSize || + pHeader->entryLength * pHeader->entriesPerBlock == 0) + { + WMSG2(" ProDOS invalid subdir header: entryLen=%d, entriesPerBlock=%d\n", + pHeader->entryLength, pHeader->entriesPerBlock); + return kDIErrBadDirectory; + } + + return kDIErrNone; +} + +/* + * Read the information from the key block of an extended file. + * + * There's some "HFS Finder information" stuffed into the key block + * right after the data fork info, but I'm planning to ignore that. + */ +DIError +DiskFSProDOS::ReadExtendedInfo(A2FileProDOS* pFile) +{ + DIError dierr = kDIErrNone; + unsigned char blkBuf[kBlkSize]; + + dierr = fpImg->ReadBlock(pFile->fDirEntry.keyPointer, blkBuf); + if (dierr != kDIErrNone) { + WMSG1(" ProDOS ReadExtendedInfo: unable to read key block %d\n", + pFile->fDirEntry.keyPointer); + goto bail; + } + + pFile->fExtData.storageType = blkBuf[0x0000] & 0x0f; + pFile->fExtData.keyBlock = GetShortLE(&blkBuf[0x0001]); + pFile->fExtData.blocksUsed = GetShortLE(&blkBuf[0x0003]); + pFile->fExtData.eof = GetLongLE(&blkBuf[0x0005]); + pFile->fExtData.eof &= 0x00ffffff; + + pFile->fExtRsrc.storageType = blkBuf[0x0100] & 0x0f; + pFile->fExtRsrc.keyBlock = GetShortLE(&blkBuf[0x0101]); + pFile->fExtRsrc.blocksUsed = GetShortLE(&blkBuf[0x0103]); + pFile->fExtRsrc.eof = GetLongLE(&blkBuf[0x0105]); + pFile->fExtRsrc.eof &= 0x00ffffff; + + if (pFile->fExtData.keyBlock <= kVolHeaderBlock || + pFile->fExtRsrc.keyBlock <= kVolHeaderBlock) + { + WMSG2(" ProDOS ReadExtendedInfo: found bad extended key blocks %d/%d\n", + pFile->fExtData.keyBlock, pFile->fExtRsrc.keyBlock); + return kDIErrBadFile; + } + +bail: + return dierr; +} + +/* + * Scan all of the files on the disk, reading their block usage into the + * volume usage map. This is important for detecting damage, and makes + * later accesses easier. + * + * As a side-effect, we set the "sparse" length for the file. + */ +DIError +DiskFSProDOS::ScanFileUsage(void) +{ + DIError dierr = kDIErrNone; + A2FileProDOS* pFile; + long blockCount, indexCount, sparseCount; + unsigned short* blockList = nil; + unsigned short* indexList = nil; + + pFile = (A2FileProDOS*) GetNextFile(nil); + while (pFile != nil) { + if (!fpImg->UpdateScanProgress(nil)) { + WMSG0(" ProDOS cancelled by user\n"); + dierr = kDIErrCancelled; + goto bail; + } + + //pFile->Dump(); + if (pFile->GetQuality() == A2File::kQualityDamaged) + goto skip; + + if (pFile->fDirEntry.storageType == A2FileProDOS::kStorageExtended) { + /* resource fork */ + if (!A2FileProDOS::IsRegularFile(pFile->fExtRsrc.storageType)) { + /* not expecting to find a directory here, but it happens */ + dierr = kDIErrBadFile; + } else { + dierr = pFile->LoadBlockList(pFile->fExtRsrc.storageType, + pFile->fExtRsrc.keyBlock, pFile->fExtRsrc.eof, + &blockCount, &blockList, &indexCount, &indexList); + } + if (dierr != kDIErrNone) { + WMSG1(" ProDOS skipping scan rsrc '%s'\n", + pFile->fDirEntry.fileName); + pFile->SetQuality(A2File::kQualityDamaged); + goto skip; + } + ScanBlockList(blockCount, blockList, indexCount, indexList, + &sparseCount); + pFile->fSparseRsrcEof = + (di_off_t) pFile->fExtRsrc.eof - sparseCount * kBlkSize; + //WMSG3(" SparseCount %d rsrcEof %d '%s'\n", + // sparseCount, pFile->fSparseRsrcEof, pFile->fDirEntry.fileName); + delete[] blockList; + blockList = nil; + delete[] indexList; + indexList = nil; + + /* data fork */ + if (!A2FileProDOS::IsRegularFile(pFile->fExtRsrc.storageType)) { + dierr = kDIErrBadFile; + } else { + dierr = pFile->LoadBlockList(pFile->fExtData.storageType, + pFile->fExtData.keyBlock, pFile->fExtData.eof, + &blockCount, &blockList, &indexCount, &indexList); + } + if (dierr != kDIErrNone) { + WMSG1(" ProDOS skipping scan data '%s'\n", + pFile->fDirEntry.fileName); + pFile->SetQuality(A2File::kQualityDamaged); + goto skip; + } + ScanBlockList(blockCount, blockList, indexCount, indexList, + &sparseCount); + pFile->fSparseDataEof = + (di_off_t) pFile->fExtData.eof - sparseCount * kBlkSize; + //WMSG3(" SparseCount %d dataEof %d '%s'\n", + // sparseCount, pFile->fSparseDataEof, pFile->fDirEntry.fileName); + delete[] blockList; + blockList = nil; + delete[] indexList; + indexList = nil; + + /* mark the extended key block as in-use */ + SetBlockUsage(pFile->fDirEntry.keyPointer, + VolumeUsage::kChunkPurposeFileStruct); + } else if (pFile->fDirEntry.storageType == A2FileProDOS::kStorageDirectory || + pFile->fDirEntry.storageType == A2FileProDOS::kStorageVolumeDirHeader) + { + /* we already got these during the recursive descent */ + /* (could do them here if we used "fake" directory entry + for volume dir to lead off the recursion) */ + goto skip; + } else if (pFile->fDirEntry.storageType == A2FileProDOS::kStorageSeedling || + pFile->fDirEntry.storageType == A2FileProDOS::kStorageSapling || + pFile->fDirEntry.storageType == A2FileProDOS::kStorageTree) + { + /* standard file */ + dierr = pFile->LoadBlockList(pFile->fDirEntry.storageType, + pFile->fDirEntry.keyPointer, pFile->fDirEntry.eof, + &blockCount, &blockList, &indexCount, &indexList); + if (dierr != kDIErrNone) { + WMSG1(" ProDOS skipping scan '%s'\n", + pFile->fDirEntry.fileName); + pFile->SetQuality(A2File::kQualityDamaged); + goto skip; + } + ScanBlockList(blockCount, blockList, indexCount, indexList, + &sparseCount); + pFile->fSparseDataEof = + (di_off_t) pFile->fDirEntry.eof - sparseCount * kBlkSize; + //WMSG4(" +++ sparseCount=%ld blockCount=%ld sparseDataEof=%ld '%s'\n", + // sparseCount, blockCount, (long) pFile->fSparseDataEof, + // pFile->fDirEntry.fileName); + + delete[] blockList; + blockList = nil; + delete[] indexList; + indexList = nil; + } else { + WMSG2(" ProDOS found weird storage type %d on '%s', ignoring\n", + pFile->fDirEntry.storageType, pFile->fDirEntry.fileName); + pFile->SetQuality(A2File::kQualityDamaged); + } + + /* + * A completely empty file written as zero blocks (as opposed to simply + * having its EOF extended, e.g. "sparse seedlings") will have zero data + * blocks but possibly an EOF that doesn't land on 512 bytes. This can + * result in a slightly negative "sparse length", which we trim to zero + * here. + */ + //if (stricmp(pFile->fDirEntry.fileName, "EMPTY.SPARSE.R") == 0) + // WMSG0("wahoo\n"); + if (pFile->fSparseDataEof < 0) + pFile->fSparseDataEof = 0; + if (pFile->fSparseRsrcEof < 0) + pFile->fSparseRsrcEof = 0; + +skip: + pFile = (A2FileProDOS*) GetNextFile(pFile); + } + + dierr = kDIErrNone; + +bail: + return dierr; +} + +/* + * Scan a block list into the volume usage map. + */ +void +DiskFSProDOS::ScanBlockList(long blockCount, unsigned short* blockList, + long indexCount, unsigned short* indexList, long* pSparseCount) +{ + assert(blockList != nil); + assert(indexCount == 0 || indexList != nil); + assert(pSparseCount != nil); + + *pSparseCount = 0; + + int i; + for (i = 0; i < blockCount; i++) { + if (blockList[i] != 0) { + SetBlockUsage(blockList[i], VolumeUsage::kChunkPurposeUserData); + } else { + (*pSparseCount)++; // sparse data block + } + } + + for (i = 0; i < indexCount; i++) { + if (indexList[i] != 0) { + SetBlockUsage(indexList[i], VolumeUsage::kChunkPurposeFileStruct); + } // else sparse index block + } +} + + +/* + * ProDOS disks may contain other filesystems. The typical DOS-in-ProDOS + * strategy involves marking a bunch of blocks at the end of the disc as + * "in use" without creating a file to go along with them. + * + * We look for certain types of embedded volume by looking for disk + * usage patterns and then testing those with the standard disk testing + * facilities. + */ +DIError +DiskFSProDOS::ScanForSubVolumes(void) +{ + DIError dierr = kDIErrNone; + VolumeUsage::ChunkState cstate; + int firstBlock, matchCount; + int block; + + /* this is guaranteed by constraint in volume header read */ + assert(fTotalBlocks <= fpImg->GetNumBlocks()); + + if (fTotalBlocks != 1600) { + WMSG1(" ProDOS ScanForSub: not 800K disk (%ld)\n", + fpImg->GetNumBlocks()); + return kDIErrNone; // only scan 800K disks + } + + matchCount = 0; + for (block = fTotalBlocks-1; block >= 0; block--) { + if (fVolumeUsage.GetChunkState(block, &cstate) != kDIErrNone) { + assert(false); + return kDIErrGeneric; + } + + if (!cstate.isMarkedUsed || cstate.isUsed) + break; + + matchCount++; + } + firstBlock = block+1; + + WMSG1("MATCH COUNT %d\n", matchCount); + if (matchCount < 35*8) // 280 blocks on 35-track floppy + return kDIErrNone; + //if (matchCount % 8 != 0) { // must have 4K tracks + // WMSG1(" ProDOS ScanForSub: matchCount %d odd number\n", + // matchCount); + // return kDIErrNone; + //} + + /* + * Try #1: this is a single DOS 3.3 volume (200K or less). + */ + if ((matchCount % 8) == 0 && matchCount <= (50*8)) { // max 50 tracks + DiskFS* pNewFS = nil; + DiskImg* pNewImg = nil; + WMSG0(" Sub #1: looking for single DOS volume\n"); + dierr = FindSubVolume(firstBlock, matchCount, &pNewImg, &pNewFS); + if (dierr == kDIErrNone) { + AddSubVolumeToList(pNewImg, pNewFS); + MarkSubVolumeBlocks(firstBlock, matchCount); + return kDIErrNone; + } + } + + + /* + * Try #2: there are multiple 140K DOS 3.3 volumes here. + * + * We may want to override their volume numbers, but it looks like + * DOS Master disks have distinct volume numbers anyway. + */ + const int kBlkCount140 = 140*2; + if ((matchCount % (kBlkCount140)) == 0) { + int i, count; + bool found = false; + + count = matchCount / kBlkCount140; + WMSG1(" Sub #2: looking for %d 140K volumes\n", + matchCount / kBlkCount140); + + for (i = 0; i < count; i++) { + DiskFS* pNewFS = nil; + DiskImg* pNewImg = nil; + WMSG1(" Sub #2: looking for DOS volume at (%d)\n", + firstBlock + i * kBlkCount140); + dierr = FindSubVolume(firstBlock + i * kBlkCount140, + kBlkCount140, &pNewImg, &pNewFS); + if (dierr == kDIErrNone) { + AddSubVolumeToList(pNewImg, pNewFS); + MarkSubVolumeBlocks(firstBlock + i * kBlkCount140, + kBlkCount140); + found = true; + } + } + if (found) + return kDIErrNone; + } + + /* + * Try #3: there are five 160K DOS 3.3 volumes here (which works out + * to exactly 800K). The first DOS volume loses early tracks as + * needed to accommodate the ProDOS directory and up to 28K of + * boot files. + * + * Because the first 160K volume starts at the front of the disk, + * we need to restrict this to non-ProDOS sub-volumes, or we'll see + * a "ghost" volume in the first position. This stuff is going to + * fail if we test for ProDOS before we check for DOS 3.3. + */ + const int kBlkCount160 = 160*2; + if (matchCount == 1537 || matchCount == 1593) { + int i, count; + bool found = false; + + count = 1600 / kBlkCount160; + WMSG1(" Sub #3: looking for %d 160K volumes\n", + matchCount / kBlkCount160); + + for (i = 0; i < count; i++) { + DiskFS* pNewFS = nil; + DiskImg* pNewImg = nil; + WMSG1(" Sub #3: looking for DOS volume at (%d)\n", + i * kBlkCount160); + dierr = FindSubVolume(i * kBlkCount160, + kBlkCount160, &pNewImg, &pNewFS); + if (dierr == kDIErrNone) { + if (pNewImg->GetFSFormat() == DiskImg::kFormatDOS33) { + AddSubVolumeToList(pNewImg, pNewFS); + if (i == 0) + MarkSubVolumeBlocks(firstBlock, kBlkCount160 - firstBlock); + else + MarkSubVolumeBlocks(i * kBlkCount160, kBlkCount160); + } else { + delete pNewFS; + delete pNewImg; + pNewFS = nil; + pNewImg = nil; + } + } + } + if (found) + return kDIErrNone; + } + + return kDIErrNone; +} + +/* + * Look for a sub-volume at the specified location. + * + * On success, "*ppDiskImg" and "*ppDiskFS" are newly-allocated objects + * of the appropriate kind. + */ +DIError +DiskFSProDOS::FindSubVolume(long blockStart, long blockCount, + DiskImg** ppDiskImg, DiskFS** ppDiskFS) +{ + DIError dierr = kDIErrNone; + DiskFS* pNewFS = nil; + DiskImg* pNewImg = nil; + + pNewImg = new DiskImg; + if (pNewImg == nil) { + dierr = kDIErrMalloc; + goto bail; + } + + dierr = pNewImg->OpenImage(fpImg, blockStart, blockCount); + if (dierr != kDIErrNone) { + WMSG3(" Sub: OpenImage(%ld,%ld) failed (err=%d)\n", + blockStart, blockCount, dierr); + goto bail; + } + + dierr = pNewImg->AnalyzeImage(); + if (dierr != kDIErrNone) { + WMSG1(" Sub: analysis failed (err=%d)\n", dierr); + goto bail; + } + + if (pNewImg->GetFSFormat() == DiskImg::kFormatUnknown || + pNewImg->GetSectorOrder() == DiskImg::kSectorOrderUnknown) + { + WMSG0(" Sub: unable to identify filesystem\n"); + dierr = kDIErrFilesystemNotFound; + goto bail; + } + + /* open a DiskFS for the sub-image */ + WMSG0(" Sub DiskImg succeeded, opening DiskFS\n"); + pNewFS = pNewImg->OpenAppropriateDiskFS(); + if (pNewFS == nil) { + WMSG0(" Sub: OpenAppropriateDiskFS failed\n"); + dierr = kDIErrUnsupportedFSFmt; + goto bail; + } + + /* load the files from the sub-image */ + dierr = pNewFS->Initialize(pNewImg, kInitFull); + if (dierr != kDIErrNone) { + WMSG1(" Sub: error %d reading list of files from disk", dierr); + goto bail; + } + +bail: + if (dierr != kDIErrNone) { + delete pNewFS; + delete pNewImg; + } else { + assert(pNewImg != nil && pNewFS != nil); + *ppDiskImg = pNewImg; + *ppDiskFS = pNewFS; + } + return dierr; +} + +/* + * Mark the blocks used by a sub-volume as in-use. + */ +void +DiskFSProDOS::MarkSubVolumeBlocks(long block, long count) +{ + VolumeUsage::ChunkState cstate; + + while (count--) { + if (fVolumeUsage.GetChunkState(block, &cstate) != kDIErrNone) { + assert(false); + return; + } + + assert(cstate.isMarkedUsed && !cstate.isUsed); + cstate.isUsed = true; + cstate.purpose = VolumeUsage::kChunkPurposeEmbedded; + if (fVolumeUsage.SetChunkState(block, &cstate) != kDIErrNone) { + assert(false); + return; + } + + block++; + } +} + + +/* + * Put a ProDOS filesystem image on the specified DiskImg. + */ +DIError +DiskFSProDOS::Format(DiskImg* pDiskImg, const char* volName) +{ + DIError dierr = kDIErrNone; + const bool allowLowerCase = (GetParameter(kParmProDOS_AllowLowerCase) != 0); + unsigned char blkBuf[kBlkSize]; + long formatBlocks; + + if (!IsValidVolumeName(volName)) + return kDIErrInvalidArg; + + /* set fpImg so calls that rely on it will work; we un-set it later */ + assert(fpImg == nil); + SetDiskImg(pDiskImg); + + WMSG0(" ProDOS formatting disk image\n"); + + /* write ProDOS blocks */ + dierr = fpImg->OverrideFormat(fpImg->GetPhysicalFormat(), + DiskImg::kFormatGenericProDOSOrd, fpImg->GetSectorOrder()); + if (dierr != kDIErrNone) + goto bail; + + formatBlocks = pDiskImg->GetNumBlocks(); + if (formatBlocks > 65536) { + WMSG1(" ProDOS: rejecting format req blocks=%ld\n", formatBlocks); + assert(false); + return kDIErrInvalidArg; + } + if (formatBlocks == 65536) { + WMSG0(" ProDOS: trimming FS size from 65536 to 65535\n"); + formatBlocks = 65535; + } + + /* + * We should now zero out the disk blocks, but on a 32MB volume that can + * take a little while. The blocks are zeroed for us when a disk is + * created, so this is really only needed if we're re-formatting an + * existing disk. CiderPress currently doesn't do that, so we're going + * to skip it here. + */ +// dierr = fpImg->ZeroImage(); + WMSG0(" ProDOS (not zeroing blocks)\n"); + + /* + * Start by writing blocks 0 and 1 (the boot blocks). This is done from + * a standard boot block image that happens to be essentially the same + * for all types of disks. (Apparently these blocks are only used when + * booting 5.25" disks?) + */ + dierr = WriteBootBlocks(); + if (dierr != kDIErrNone) + goto bail; + + /* + * Write the four-block disk volume entry. Start by writing the three + * empty ones (which only have the prev/next pointers), and finish by + * writing the first block, which has the volume directory header. + */ + int i; + memset(blkBuf, 0, sizeof(blkBuf)); + for (i = kVolHeaderBlock+1; i < kVolHeaderBlock+kVolDirExpectedNumBlocks; i++) + { + PutShortLE(&blkBuf[0x00], i-1); + if (i == kVolHeaderBlock+kVolDirExpectedNumBlocks-1) + PutShortLE(&blkBuf[0x02], 0); + else + PutShortLE(&blkBuf[0x02], i+1); + + dierr = fpImg->WriteBlock(i, blkBuf); + if (dierr != kDIErrNone) { + WMSG2(" Format: block %d write failed (err=%d)\n", i, dierr); + goto bail; + } + } + + char upperName[A2FileProDOS::kMaxFileName+1]; + unsigned short lcFlags; + time_t now; + + now = time(nil); + + /* + * Compute the lower-case flags, if desired. The test for "allowLowerCase" + * is probably bogus, because in most cases we just got created by the + * DiskImg and the app hasn't had time to set the "allow lower" flag. + * So it defaults to "enabled", which means the app needs to manually + * change the volume name to lower case. + */ + UpperCaseName(upperName, volName); + lcFlags = 0; + if (allowLowerCase) + lcFlags = GenerateLowerCaseBits(upperName, volName, false); + + PutShortLE(&blkBuf[0x00], 0); + PutShortLE(&blkBuf[0x02], kVolHeaderBlock+1); + blkBuf[0x04] = strlen(upperName) | (A2FileProDOS::kStorageVolumeDirHeader << 4); + strncpy((char*) &blkBuf[0x05], upperName, A2FileProDOS::kMaxFileName); + PutLongLE(&blkBuf[0x16], A2FileProDOS::ConvertProDate(now)); + PutShortLE(&blkBuf[0x1a], lcFlags); + PutLongLE(&blkBuf[0x1c], A2FileProDOS::ConvertProDate(now)); + blkBuf[0x20] = 0; // GS/OS uses 5? + /* min_version is zero */ + blkBuf[0x22] = 0xe3; // access (format/rename/backup/write/read) + blkBuf[0x23] = 0x27; // entry_length: always $27 + blkBuf[0x24] = 0x0d; // entries_per_block: always $0d + /* file_count is zero - does not include volume dir */ + PutShortLE(&blkBuf[0x27], kVolHeaderBlock + kVolDirExpectedNumBlocks); // bit_map_pointer + PutShortLE(&blkBuf[0x29], (unsigned short) formatBlocks); // total_blocks + dierr = fpImg->WriteBlock(kVolHeaderBlock, blkBuf); + if (dierr != kDIErrNone) { + WMSG2(" Format: block %d write failed (err=%d)\n", + kVolHeaderBlock, dierr); + goto bail; + } + + /* check our work, and set some object fields, by reading what we wrote */ + dierr = LoadVolHeader(); + if (dierr != kDIErrNone) { + WMSG1(" GLITCH: couldn't read header we just wrote (err=%d)\n", dierr); + goto bail; + } + + /* + * Generate the initial block usage map. The only entries in use are + * right at the start of the disk. When we finish, scan what we just + * created into + */ + CreateEmptyBlockMap(); + + /* don't do this -- assume they're going to call Initialize() later */ + //ScanVolBitmap(); + +bail: + SetDiskImg(nil); // shouldn't really be set by us + return dierr; +} + + +/* + * The standard boot block found on ProDOS disks. The same thing appears + * to be written to both 5.25" and 3.5" disks, with some modifications + * made for HD images. + * + * This is block 0; block 1 is either zeroed out or filled with a repeating + * pattern. + */ +const unsigned char gFloppyBlock0[512] = { + 0x01, 0x38, 0xb0, 0x03, 0x4c, 0x32, 0xa1, 0x86, 0x43, 0xc9, 0x03, 0x08, + 0x8a, 0x29, 0x70, 0x4a, 0x4a, 0x4a, 0x4a, 0x09, 0xc0, 0x85, 0x49, 0xa0, + 0xff, 0x84, 0x48, 0x28, 0xc8, 0xb1, 0x48, 0xd0, 0x3a, 0xb0, 0x0e, 0xa9, + 0x03, 0x8d, 0x00, 0x08, 0xe6, 0x3d, 0xa5, 0x49, 0x48, 0xa9, 0x5b, 0x48, + 0x60, 0x85, 0x40, 0x85, 0x48, 0xa0, 0x63, 0xb1, 0x48, 0x99, 0x94, 0x09, + 0xc8, 0xc0, 0xeb, 0xd0, 0xf6, 0xa2, 0x06, 0xbc, 0x1d, 0x09, 0xbd, 0x24, + 0x09, 0x99, 0xf2, 0x09, 0xbd, 0x2b, 0x09, 0x9d, 0x7f, 0x0a, 0xca, 0x10, + 0xee, 0xa9, 0x09, 0x85, 0x49, 0xa9, 0x86, 0xa0, 0x00, 0xc9, 0xf9, 0xb0, + 0x2f, 0x85, 0x48, 0x84, 0x60, 0x84, 0x4a, 0x84, 0x4c, 0x84, 0x4e, 0x84, + 0x47, 0xc8, 0x84, 0x42, 0xc8, 0x84, 0x46, 0xa9, 0x0c, 0x85, 0x61, 0x85, + 0x4b, 0x20, 0x12, 0x09, 0xb0, 0x68, 0xe6, 0x61, 0xe6, 0x61, 0xe6, 0x46, + 0xa5, 0x46, 0xc9, 0x06, 0x90, 0xef, 0xad, 0x00, 0x0c, 0x0d, 0x01, 0x0c, + 0xd0, 0x6d, 0xa9, 0x04, 0xd0, 0x02, 0xa5, 0x4a, 0x18, 0x6d, 0x23, 0x0c, + 0xa8, 0x90, 0x0d, 0xe6, 0x4b, 0xa5, 0x4b, 0x4a, 0xb0, 0x06, 0xc9, 0x0a, + 0xf0, 0x55, 0xa0, 0x04, 0x84, 0x4a, 0xad, 0x02, 0x09, 0x29, 0x0f, 0xa8, + 0xb1, 0x4a, 0xd9, 0x02, 0x09, 0xd0, 0xdb, 0x88, 0x10, 0xf6, 0x29, 0xf0, + 0xc9, 0x20, 0xd0, 0x3b, 0xa0, 0x10, 0xb1, 0x4a, 0xc9, 0xff, 0xd0, 0x33, + 0xc8, 0xb1, 0x4a, 0x85, 0x46, 0xc8, 0xb1, 0x4a, 0x85, 0x47, 0xa9, 0x00, + 0x85, 0x4a, 0xa0, 0x1e, 0x84, 0x4b, 0x84, 0x61, 0xc8, 0x84, 0x4d, 0x20, + 0x12, 0x09, 0xb0, 0x17, 0xe6, 0x61, 0xe6, 0x61, 0xa4, 0x4e, 0xe6, 0x4e, + 0xb1, 0x4a, 0x85, 0x46, 0xb1, 0x4c, 0x85, 0x47, 0x11, 0x4a, 0xd0, 0xe7, + 0x4c, 0x00, 0x20, 0x4c, 0x3f, 0x09, 0x26, 0x50, 0x52, 0x4f, 0x44, 0x4f, + 0x53, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0xa5, 0x60, + 0x85, 0x44, 0xa5, 0x61, 0x85, 0x45, 0x6c, 0x48, 0x00, 0x08, 0x1e, 0x24, + 0x3f, 0x45, 0x47, 0x76, 0xf4, 0xd7, 0xd1, 0xb6, 0x4b, 0xb4, 0xac, 0xa6, + 0x2b, 0x18, 0x60, 0x4c, 0xbc, 0x09, 0xa9, 0x9f, 0x48, 0xa9, 0xff, 0x48, + 0xa9, 0x01, 0xa2, 0x00, 0x4c, 0x79, 0xf4, 0x20, 0x58, 0xfc, 0xa0, 0x1c, + 0xb9, 0x50, 0x09, 0x99, 0xae, 0x05, 0x88, 0x10, 0xf7, 0x4c, 0x4d, 0x09, + 0xaa, 0xaa, 0xaa, 0xa0, 0xd5, 0xce, 0xc1, 0xc2, 0xcc, 0xc5, 0xa0, 0xd4, + 0xcf, 0xa0, 0xcc, 0xcf, 0xc1, 0xc4, 0xa0, 0xd0, 0xd2, 0xcf, 0xc4, 0xcf, + 0xd3, 0xa0, 0xaa, 0xaa, 0xaa, 0xa5, 0x53, 0x29, 0x03, 0x2a, 0x05, 0x2b, + 0xaa, 0xbd, 0x80, 0xc0, 0xa9, 0x2c, 0xa2, 0x11, 0xca, 0xd0, 0xfd, 0xe9, + 0x01, 0xd0, 0xf7, 0xa6, 0x2b, 0x60, 0xa5, 0x46, 0x29, 0x07, 0xc9, 0x04, + 0x29, 0x03, 0x08, 0x0a, 0x28, 0x2a, 0x85, 0x3d, 0xa5, 0x47, 0x4a, 0xa5, + 0x46, 0x6a, 0x4a, 0x4a, 0x85, 0x41, 0x0a, 0x85, 0x51, 0xa5, 0x45, 0x85, + 0x27, 0xa6, 0x2b, 0xbd, 0x89, 0xc0, 0x20, 0xbc, 0x09, 0xe6, 0x27, 0xe6, + 0x3d, 0xe6, 0x3d, 0xb0, 0x03, 0x20, 0xbc, 0x09, 0xbc, 0x88, 0xc0, 0x60, + 0xa5, 0x40, 0x0a, 0x85, 0x53, 0xa9, 0x00, 0x85, 0x54, 0xa5, 0x53, 0x85, + 0x50, 0x38, 0xe5, 0x51, 0xf0, 0x14, 0xb0, 0x04, 0xe6, 0x53, 0x90, 0x02, + 0xc6, 0x53, 0x38, 0x20, 0x6d, 0x09, 0xa5, 0x50, 0x18, 0x20, 0x6f, 0x09, + 0xd0, 0xe3, 0xa0, 0x7f, 0x84, 0x52, 0x08, 0x28, 0x38, 0xc6, 0x52, 0xf0, + 0xce, 0x18, 0x08, 0x88, 0xf0, 0xf5, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +unsigned char gHDBlock0[] = { + 0x01, 0x38, 0xb0, 0x03, 0x4c, 0x1c, 0x09, 0x78, 0x86, 0x43, 0xc9, 0x03, + 0x08, 0x8a, 0x29, 0x70, 0x4a, 0x4a, 0x4a, 0x4a, 0x09, 0xc0, 0x85, 0x49, + 0xa0, 0xff, 0x84, 0x48, 0x28, 0xc8, 0xb1, 0x48, 0xd0, 0x3a, 0xb0, 0x0e, + 0xa9, 0x03, 0x8d, 0x00, 0x08, 0xe6, 0x3d, 0xa5, 0x49, 0x48, 0xa9, 0x5b, + 0x48, 0x60, 0x85, 0x40, 0x85, 0x48, 0xa0, 0x5e, 0xb1, 0x48, 0x99, 0x94, + 0x09, 0xc8, 0xc0, 0xeb, 0xd0, 0xf6, 0xa2, 0x06, 0xbc, 0x32, 0x09, 0xbd, + 0x39, 0x09, 0x99, 0xf2, 0x09, 0xbd, 0x40, 0x09, 0x9d, 0x7f, 0x0a, 0xca, + 0x10, 0xee, 0xa9, 0x09, 0x85, 0x49, 0xa9, 0x86, 0xa0, 0x00, 0xc9, 0xf9, + 0xb0, 0x2f, 0x85, 0x48, 0x84, 0x60, 0x84, 0x4a, 0x84, 0x4c, 0x84, 0x4e, + 0x84, 0x47, 0xc8, 0x84, 0x42, 0xc8, 0x84, 0x46, 0xa9, 0x0c, 0x85, 0x61, + 0x85, 0x4b, 0x20, 0x27, 0x09, 0xb0, 0x66, 0xe6, 0x61, 0xe6, 0x61, 0xe6, + 0x46, 0xa5, 0x46, 0xc9, 0x06, 0x90, 0xef, 0xad, 0x00, 0x0c, 0x0d, 0x01, + 0x0c, 0xd0, 0x52, 0xa9, 0x04, 0xd0, 0x02, 0xa5, 0x4a, 0x18, 0x6d, 0x23, + 0x0c, 0xa8, 0x90, 0x0d, 0xe6, 0x4b, 0xa5, 0x4b, 0x4a, 0xb0, 0x06, 0xc9, + 0x0a, 0xf0, 0x71, 0xa0, 0x04, 0x84, 0x4a, 0xad, 0x20, 0x09, 0x29, 0x0f, + 0xa8, 0xb1, 0x4a, 0xd9, 0x20, 0x09, 0xd0, 0xdb, 0x88, 0x10, 0xf6, 0xa0, + 0x16, 0xb1, 0x4a, 0x4a, 0x6d, 0x1f, 0x09, 0x8d, 0x1f, 0x09, 0xa0, 0x11, + 0xb1, 0x4a, 0x85, 0x46, 0xc8, 0xb1, 0x4a, 0x85, 0x47, 0xa9, 0x00, 0x85, + 0x4a, 0xa0, 0x1e, 0x84, 0x4b, 0x84, 0x61, 0xc8, 0x84, 0x4d, 0x20, 0x27, + 0x09, 0xb0, 0x35, 0xe6, 0x61, 0xe6, 0x61, 0xa4, 0x4e, 0xe6, 0x4e, 0xb1, + 0x4a, 0x85, 0x46, 0xb1, 0x4c, 0x85, 0x47, 0x11, 0x4a, 0xd0, 0x18, 0xa2, + 0x01, 0xa9, 0x00, 0xa8, 0x91, 0x60, 0xc8, 0xd0, 0xfb, 0xe6, 0x61, 0xea, + 0xea, 0xca, 0x10, 0xf4, 0xce, 0x1f, 0x09, 0xf0, 0x07, 0xd0, 0xd8, 0xce, + 0x1f, 0x09, 0xd0, 0xca, 0x58, 0x4c, 0x00, 0x20, 0x4c, 0x47, 0x09, 0x02, + 0x26, 0x50, 0x52, 0x4f, 0x44, 0x4f, 0x53, 0xa5, 0x60, 0x85, 0x44, 0xa5, + 0x61, 0x85, 0x45, 0x6c, 0x48, 0x00, 0x08, 0x1e, 0x24, 0x3f, 0x45, 0x47, + 0x76, 0xf4, 0xd7, 0xd1, 0xb6, 0x4b, 0xb4, 0xac, 0xa6, 0x2b, 0x18, 0x60, + 0x4c, 0xbc, 0x09, 0x20, 0x58, 0xfc, 0xa0, 0x14, 0xb9, 0x58, 0x09, 0x99, + 0xb1, 0x05, 0x88, 0x10, 0xf7, 0x4c, 0x55, 0x09, 0xd5, 0xce, 0xc1, 0xc2, + 0xcc, 0xc5, 0xa0, 0xd4, 0xcf, 0xa0, 0xcc, 0xcf, 0xc1, 0xc4, 0xa0, 0xd0, + 0xd2, 0xcf, 0xc4, 0xcf, 0xd3, 0xa5, 0x53, 0x29, 0x03, 0x2a, 0x05, 0x2b, + 0xaa, 0xbd, 0x80, 0xc0, 0xa9, 0x2c, 0xa2, 0x11, 0xca, 0xd0, 0xfd, 0xe9, + 0x01, 0xd0, 0xf7, 0xa6, 0x2b, 0x60, 0xa5, 0x46, 0x29, 0x07, 0xc9, 0x04, + 0x29, 0x03, 0x08, 0x0a, 0x28, 0x2a, 0x85, 0x3d, 0xa5, 0x47, 0x4a, 0xa5, + 0x46, 0x6a, 0x4a, 0x4a, 0x85, 0x41, 0x0a, 0x85, 0x51, 0xa5, 0x45, 0x85, + 0x27, 0xa6, 0x2b, 0xbd, 0x89, 0xc0, 0x20, 0xbc, 0x09, 0xe6, 0x27, 0xe6, + 0x3d, 0xe6, 0x3d, 0xb0, 0x03, 0x20, 0xbc, 0x09, 0xbc, 0x88, 0xc0, 0x60, + 0xa5, 0x40, 0x0a, 0x85, 0x53, 0xa9, 0x00, 0x85, 0x54, 0xa5, 0x53, 0x85, + 0x50, 0x38, 0xe5, 0x51, 0xf0, 0x14, 0xb0, 0x04, 0xe6, 0x53, 0x90, 0x02, + 0xc6, 0x53, 0x38, 0x20, 0x6d, 0x09, 0xa5, 0x50, 0x18, 0x20, 0x6f, 0x09, + 0xd0, 0xe3, 0xa0, 0x7f, 0x84, 0x52, 0x08, 0x28, 0x38, 0xc6, 0x52, 0xf0, + 0xce, 0x18, 0x08, 0x88, 0xf0, 0xf5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +/* + * Write the ProDOS boot blocks onto the disk image. + */ +DIError +DiskFSProDOS::WriteBootBlocks(void) +{ + DIError dierr; + unsigned char block0[512]; + unsigned char block1[512]; + bool isHD; + + assert(fpImg->GetHasBlocks()); + + if (fpImg->GetNumBlocks() == 280 || fpImg->GetNumBlocks() == 1600) + isHD = false; + else + isHD = true; + + if (isHD) { + memcpy(block0, gHDBlock0, sizeof(block0)); + // repeating 0x42 0x48 pattern + int i; + unsigned char* ucp; + for (i = 0, ucp = block1; i < (int)sizeof(block1); i++) + *ucp++ = 0x42 + 6 * (i & 0x01); + } else { + memcpy(block0, gFloppyBlock0, sizeof(block0)); + memset(block1, 0, sizeof(block1)); + } + + dierr = fpImg->WriteBlock(0, block0); + if (dierr != kDIErrNone) { + WMSG1(" WriteBootBlocks: block0 write failed (err=%d)\n", dierr); + return dierr; + } + dierr = fpImg->WriteBlock(1, block1); + if (dierr != kDIErrNone) { + WMSG1(" WriteBootBlocks: block1 write failed (err=%d)\n", dierr); + return dierr; + } + + return kDIErrNone; +} + + +/* + * Create a new, empty file. There are three different kinds of files we + * need to be able to handle: + * (1) Standard file. Create the directory entry and an empty "seedling" + * file with one block allocated. It does not appear that "sparse" + * allocation applies to seedlings. + * (2) Extended file. Create the directory entry, the extended key block, + * and allocate one seedling block for each fork. + * (3) Subdirectory. Allocate a block for the subdir and fill in the + * details in the subdir header. + * + * In all cases we need to add a new directory entry as well. + * + * By not flushing the updated block usage map and the updated directory + * block(s) until we're done, we can abort our changes at any time if we + * encounter a damaged sector or run out of disk space. We do need to be + * careful when updating our internal copies of things like file storage + * types and lengths, updating them only after everything else has + * succeeded. + * + * NOTE: if we detect an empty directory holder, "*ppNewFile" does NOT + * end up pointing at a file. + * + * NOTE: kParm_CreateUnique does *not* apply to creating subdirectories. + */ +DIError +DiskFSProDOS::CreateFile(const CreateParms* pParms, A2File** ppNewFile) +{ + DIError dierr = kDIErrNone; + char* normalizedPath = nil; + char* basePath = nil; + char* fileName = nil; + A2FileProDOS* pSubdir = nil; + A2FileDescr* pOpenSubdir = nil; + A2FileProDOS* pNewFile = nil; + unsigned char* subdirBuf = nil; + const bool allowLowerCase = (GetParameter(kParmProDOS_AllowLowerCase) != 0); + const bool createUnique = (GetParameter(kParm_CreateUnique) != 0); + char upperName[A2FileProDOS::kMaxFileName+1]; + char lowerName[A2FileProDOS::kMaxFileName+1]; + + if (fpImg->GetReadOnly()) + return kDIErrAccessDenied; + if (!fDiskIsGood) + return kDIErrBadDiskImage; + + assert(pParms != nil); + assert(pParms->pathName != nil); + assert(pParms->storageType == A2FileProDOS::kStorageSeedling || + pParms->storageType == A2FileProDOS::kStorageExtended || + pParms->storageType == A2FileProDOS::kStorageDirectory); + // kStorageVolumeDirHeader not allowed -- that's created by Format + WMSG1(" ProDOS ---v--- CreateFile '%s'\n", pParms->pathName); + *ppNewFile = nil; + + /* + * Normalize the pathname so that all components are ProDOS-safe + * and separated by ':'. + */ + assert(pParms->pathName != nil); + dierr = DoNormalizePath(pParms->pathName, pParms->fssep, + &normalizedPath); + if (dierr != kDIErrNone) + goto bail; + assert(normalizedPath != nil); + + /* + * Split the base path and filename apart. + */ + char* cp; + cp = strrchr(normalizedPath, A2FileProDOS::kFssep); + if (cp == nil) { + assert(basePath == nil); + fileName = normalizedPath; + } else { + fileName = new char[strlen(cp+1) +1]; + strcpy(fileName, cp+1); + *cp = '\0'; + basePath = normalizedPath; + } + normalizedPath = nil; // either fileName or basePath points here now + + assert(fileName != nil); + //WMSG2(" ProDOS normalized to '%s':'%s'\n", + // basePath == nil ? "" : basePath, fileName); + + /* + * Open the base path. If it doesn't exist, create it recursively. + */ + if (basePath != nil) { + WMSG2(" ProDOS Creating '%s' in '%s'\n", fileName, basePath); + /* open the named subdir, creating it if it doesn't exist */ + pSubdir = (A2FileProDOS*)GetFileByName(basePath); + if (pSubdir == nil) { + WMSG1(" ProDOS Creating subdir '%s'\n", basePath); + A2File* pNewSub; + CreateParms newDirParms; + newDirParms.pathName = basePath; + newDirParms.fssep = A2FileProDOS::kFssep; + newDirParms.storageType = A2FileProDOS::kStorageDirectory; + newDirParms.fileType = kTypeDIR; // 0x0f + newDirParms.auxType = 0; + newDirParms.access = 0xe3; // unlocked, backup bit set + newDirParms.createWhen = newDirParms.modWhen = time(nil); + dierr = this->CreateFile(&newDirParms, &pNewSub); + if (dierr != kDIErrNone) + goto bail; + assert(pNewSub != nil); + + pSubdir = (A2FileProDOS*) pNewSub; + } + + /* + * And now the annoying part. We need to reconstruct basePath out + * of the filenames actually present, rather than relying on the + * argument passed in. That's because some directories might have + * lower-case flags and some might not, and we do case-insensitive + * comparisons. It's not crucial for our inner workings, but the + * linear file list in the DiskFS should have accurate strings. + * (It'll work just fine, but the display might show the wrong values + * for parent directories until they reload the disk.) + * + * On the bright side, we know exactly how long the string needs + * to be, so we can just stomp on it in place. Assuming, of course, + * that the filename created matches up with what the filename + * normalizer came up with, which we can guarantee since (a) everybody + * uses the same normalizer and (b) the "uniqueify" stuff doesn't + * kick in for subdirs because we wouldn't be creating a new subdir + * if it didn't already exist. + * + * This is essentially the same as RegeneratePathName(), but that's + * meant for a situation where the filename already exists. + */ + A2FileProDOS* pBaseDir = pSubdir; + int basePathLen = strlen(basePath); + while (!pBaseDir->IsVolumeDirectory()) { + const char* fixedName = pBaseDir->GetFileName(); + int fixedLen = strlen(fixedName); + if (fixedLen > basePathLen) { + assert(false); + break; + } + assert(basePathLen == fixedLen || + *(basePath + (basePathLen-fixedLen-1)) == kDIFssep); + memcpy(basePath + (basePathLen-fixedLen), fixedName, fixedLen); + basePathLen -= fixedLen+1; + + pBaseDir = (A2FileProDOS*) pBaseDir->GetParent(); + assert(pBaseDir != nil); + } + // check the math + if (pSubdir->IsVolumeDirectory()) + assert(basePathLen == 0); + else + assert(basePathLen == -1); + } else { + /* open the volume directory */ + WMSG1(" ProDOS Creating '%s' in volume dir\n", fileName); + /* volume dir must be first in the list */ + pSubdir = (A2FileProDOS*) GetNextFile(nil); + assert(pSubdir != nil); + assert(pSubdir->IsVolumeDirectory()); + } + if (pSubdir == nil) { + WMSG1(" ProDOS Unable to open subdir '%s'\n", basePath); + dierr = kDIErrFileNotFound; + goto bail; + } + + /* + * Load the block usage map into memory. All changes, to the end of this + * function, are made to the in-memory copy and can be "undone" by simply + * throwing the temporary map away. + */ + dierr = LoadVolBitmap(); + if (dierr != kDIErrNone) + return dierr; + + /* + * Load the subdir or volume dir into memory, and alloc a new directory + * entry. + */ + dierr = pSubdir->Open(&pOpenSubdir, false); + if (dierr != kDIErrNone) + goto bail; + + unsigned char* dirEntryPtr; + long dirLen; + unsigned short dirBlock, dirKeyBlock; + int dirEntrySlot; + dierr = AllocDirEntry(pOpenSubdir, &subdirBuf, &dirLen, &dirEntryPtr, + &dirKeyBlock, &dirEntrySlot, &dirBlock); + if (dierr != kDIErrNone) + goto bail; + + assert(subdirBuf != nil); + assert(dirLen > 0); + assert(dirKeyBlock > 0); + assert(dirEntrySlot >= 0); + assert(dirBlock > 0); + + /* + * Create a copy of the filename with everything in upper case and spaces + * changed to periods. + */ + UpperCaseName(upperName, fileName); + + /* + * Make the name unique within the current directory. This requires + * appending digits until the name doesn't match any others. + * + * The filename buffer ("upperName") must be able to hold kMaxFileName+1 + * chars. It will be modified in place. + */ + if (createUnique && + pParms->storageType != A2FileProDOS::kStorageDirectory) + { + MakeFileNameUnique(subdirBuf, dirLen, upperName); + } else { + /* check to see if it already exists */ + if (NameExistsInDir(subdirBuf, dirLen, upperName)) { + if (pParms->storageType == A2FileProDOS::kStorageDirectory) + dierr = kDIErrDirectoryExists; + else + dierr = kDIErrFileExists; + goto bail; + } + } + + /* + * Allocate file storage and initialize: + * - For directory, a single block with the directory header. + * - For seedling, an empty block. + * - For extended, an extended key block entry and two empty blocks. + */ + long keyBlock; + int blocksUsed; + int newEOF; + keyBlock = -1; + blocksUsed = newEOF = -1; + + dierr = AllocInitialFileStorage(pParms, upperName, dirBlock, + dirEntrySlot, &keyBlock, &blocksUsed, &newEOF); + if (dierr != kDIErrNone) + goto bail; + + assert(blocksUsed > 0); + assert(keyBlock > 0); + assert(newEOF >= 0); + + /* + * Fill out the newly-created directory entry pointed to by "dirEntryPtr". + * + * ProDOS filenames are always stored in upper case. ProDOS 8 v1.8 and + * later allow lower-case names with '.' converting to ' '. We optionally + * set the flags here, using the original file name to decide which parts + * are lower case. (Some parts of the original may have been stomped + * when the name was made unique, so we need to watch for that.) + */ + dirEntryPtr[0x00] = (pParms->storageType << 4) | strlen(upperName); + strncpy((char*) &dirEntryPtr[0x01], upperName, A2FileProDOS::kMaxFileName); + if (pParms->fileType >= 0 && pParms->fileType <= 0xff) + dirEntryPtr[0x10] = (unsigned char) pParms->fileType; + else + dirEntryPtr[0x10] = 0; // HFS long type? + PutShortLE(&dirEntryPtr[0x11], (unsigned short) keyBlock); + PutShortLE(&dirEntryPtr[0x13], blocksUsed); + PutShortLE(&dirEntryPtr[0x15], newEOF); + dirEntryPtr[0x17] = 0; // high byte of EOF + PutLongLE(&dirEntryPtr[0x18], A2FileProDOS::ConvertProDate(pParms->createWhen)); + if (allowLowerCase) { + unsigned short lcBits; + lcBits = GenerateLowerCaseBits(upperName, fileName, false); + GenerateLowerCaseName(upperName, lowerName, lcBits, false); + lowerName[strlen(upperName)] = '\0'; + + PutShortLE(&dirEntryPtr[0x1c], lcBits); + } else { + strcpy(lowerName, upperName); + PutShortLE(&dirEntryPtr[0x1c], 0); // version, min_version + } + dirEntryPtr[0x1e] = pParms->access; + if (pParms->auxType >= 0 && pParms->auxType <= 0xffff) + PutShortLE(&dirEntryPtr[0x1f], (unsigned short) pParms->auxType); + else + PutShortLE(&dirEntryPtr[0x1f], 0); + PutLongLE(&dirEntryPtr[0x21], A2FileProDOS::ConvertProDate(pParms->modWhen)); + PutShortLE(&dirEntryPtr[0x25], dirKeyBlock); + + /* + * Write updated directory. If this succeeds, we can no longer undo + * what we have done by simply bailing. If this fails partway through, + * we might have a corrupted disk, so it's best to ensure that it's not + * going to fail before we call. + * + * Assuming this isn't a nibble image with I/O errors, the only way we + * can really fail is by running out of disk space. The block has been + * pre-allocated, so this should always work. + */ + dierr = pOpenSubdir->Write(subdirBuf, dirLen); + if (dierr != kDIErrNone) { + WMSG1(" ProDOS directory write failed (dirLen=%ld)\n", dirLen); + goto bail; + } + + /* + * Flush updated block usage map. + */ + dierr = SaveVolBitmap(); + if (dierr != kDIErrNone) + goto bail; + + /* + * Success! + * + * Create an A2File entry for this, and add it to the list. The calls + * below will re-process some of what we just created, which is slightly + * inefficient but helps guarantee that we aren't creating bogus data + * structures that won't match what we see when the disk is reloaded. + * + * - Regen or update internal VolumeUsage map?? Throw it away or mark + * it as invalid? + */ + pNewFile = new A2FileProDOS(this); + + A2FileProDOS::DirEntry* pEntry; + pEntry = &pNewFile->fDirEntry; + + A2FileProDOS::InitDirEntry(pEntry, dirEntryPtr); + + pNewFile->fParentDirBlock = dirBlock; + pNewFile->fParentDirIdx = (dirEntrySlot-1) % kEntriesPerBlock; + pNewFile->fSparseDataEof = 0; + pNewFile->fSparseRsrcEof = 0; + + /* + * Get the properly-cased filename for the file list. We already have + * a name in "lowerName", but it doesn't take AppleWorks aux type + * case stuff into account. If necessary, deal with it now. + */ + if (A2FileProDOS::UsesAppleWorksAuxType(pNewFile->fDirEntry.fileType)) { + DiskFSProDOS::GenerateLowerCaseName(pNewFile->fDirEntry.fileName, + lowerName, pNewFile->fDirEntry.auxType, true); + } + pNewFile->SetPathName(basePath == nil ? "" : basePath, lowerName); + + if (pEntry->storageType == A2FileProDOS::kStorageExtended) { + dierr = ReadExtendedInfo(pNewFile); + if (dierr != kDIErrNone) { + WMSG0(" ProDOS GLITCH: readback of extended block failed!\n"); + delete pNewFile; + goto bail; + } + } + + pNewFile->SetParent(pSubdir); + //pNewFile->Dump(); + + /* + * Because we're hierarchical, and we guarantee that the contents of + * subdirectories are grouped together, we must insert the file into an + * appropriate place in the list rather than just throwing it onto the + * end. + * + * The proper location for the new file in the linear list is after the + * previous file in our subdir. If we're the first item in the subdir, + * we get added right after the parent. If not, we need to scan, starting + * from the parent, for an entry in the file list whose key block pointer + * matches that of the previous item in the list. + * + * We wouldn't be this far if the disk were damaged, so we don't have to + * worry too much about weirdness. The directory entry allocator always + * returns the first available, so we know the previous entry is valid. + */ + unsigned char* prevDirEntryPtr; + prevDirEntryPtr = GetPrevDirEntry(subdirBuf, dirEntryPtr); + if (prevDirEntryPtr == nil) { + /* previous entry is volume or subdir header */ + InsertFileInList(pNewFile, pNewFile->GetParent()); + WMSG2("Inserted '%s' after '%s'\n", + pNewFile->GetPathName(), pNewFile->GetParent()->GetPathName()); + } else { + /* dig out the key block pointer and find the matching file */ + unsigned short prevKeyBlock; + assert((prevDirEntryPtr[0x00] & 0xf0) != 0); // verify storage type + prevKeyBlock = GetShortLE(&prevDirEntryPtr[0x11]); + A2File* pPrev; + pPrev = FindFileByKeyBlock(pNewFile->GetParent(), prevKeyBlock); + if (pPrev == nil) { + /* should be impossible! */ + assert(false); + AddFileToList(pNewFile); + } else { + /* insert the new file in the list after the previous file */ + InsertFileInList(pNewFile, pPrev); + } + } +// WMSG0("LIST NOW:\n"); +// DumpFileList(); + + *ppNewFile = pNewFile; + pNewFile = nil; + +bail: + delete pNewFile; + if (pOpenSubdir != nil) + pOpenSubdir->Close(); // writes updated dir entry in parent dir + FreeVolBitmap(); + delete[] normalizedPath; + delete[] subdirBuf; + delete[] fileName; + delete[] basePath; + WMSG1(" ProDOS ---^--- CreateFile '%s' DONE\n", pParms->pathName); + return dierr; +} + +/* + * Run through the DiskFS file list, looking for an entry with a matching + * key block. + */ +A2File* +DiskFSProDOS::FindFileByKeyBlock(A2File* pStart, unsigned short keyBlock) +{ + while (pStart != nil) { + A2FileProDOS* pPro = (A2FileProDOS*) pStart; + + if (pPro->fDirEntry.keyPointer == keyBlock) + return pStart; + + pStart = GetNextFile(pStart); + } + + return nil; +} + +/* + * Allocate the initial storage (key blocks, directory header) for a new file. + * + * Output values are the key block for the new file, the number of blocks + * used, and an EOF value. + * + * "upperName" is the upper-case name for the file. "dirBlock" and + * "dirEntrySlot" refer to the entry in the higher-level directory for this + * file, and are only needed when creating a new subdir (because the first + * entry in a subdir points to its entry in the parent dir). + */ +DIError +DiskFSProDOS::AllocInitialFileStorage(const CreateParms* pParms, + const char* upperName, unsigned short dirBlock, int dirEntrySlot, + long* pKeyBlock, int* pBlocksUsed, int* pNewEOF) +{ + DIError dierr = kDIErrNone; + unsigned char blkBuf[kBlkSize]; + long keyBlock; + int blocksUsed; + int newEOF; + + blocksUsed = -1; + keyBlock = -1; + newEOF = 0; + memset(blkBuf, 0, sizeof(blkBuf)); + + if (pParms->storageType == A2FileProDOS::kStorageSeedling) { + keyBlock = AllocBlock(); + if (keyBlock == -1) { + dierr = kDIErrDiskFull; + goto bail; + } + blocksUsed = 1; + + /* write zeroed block */ + dierr = fpImg->WriteBlock(keyBlock, blkBuf); + if (dierr != kDIErrNone) + goto bail; + } else if (pParms->storageType == A2FileProDOS::kStorageExtended) { + long dataBlock, rsrcBlock; + + dataBlock = AllocBlock(); + rsrcBlock = AllocBlock(); + keyBlock = AllocBlock(); + if (dataBlock < 0 || rsrcBlock < 0 || keyBlock < 0) { + dierr = kDIErrDiskFull; + goto bail; + } + blocksUsed = 3; + newEOF = kBlkSize; + + /* write zeroed block */ + dierr = fpImg->WriteBlock(dataBlock, blkBuf); + if (dierr != kDIErrNone) + goto bail; + dierr = fpImg->WriteBlock(rsrcBlock, blkBuf); + if (dierr != kDIErrNone) + goto bail; + + /* fill in extended key block details */ + blkBuf[0x00] = blkBuf[0x100] = A2FileProDOS::kStorageSeedling; + PutShortLE(&blkBuf[0x01], (unsigned short) dataBlock); + PutShortLE(&blkBuf[0x101], (unsigned short) rsrcBlock); + blkBuf[0x03] = blkBuf[0x103] = 1; // blocks used (lo byte) + /* 3 bytes at 0x05 hold EOF, currently 0 */ + + dierr = fpImg->WriteBlock(keyBlock, blkBuf); + if (dierr != kDIErrNone) + goto bail; + } else if (pParms->storageType == A2FileProDOS::kStorageDirectory) { + keyBlock = AllocBlock(); + if (keyBlock == -1) { + dierr = kDIErrDiskFull; + goto bail; + } + blocksUsed = 1; + newEOF = kBlkSize; + + /* fill in directory header fields */ + // 0x00: prev, set to zero + // 0x02: next, set to zero + blkBuf[0x04] = (A2FileProDOS::kStorageSubdirHeader << 4) | strlen(upperName); + strncpy((char*) &blkBuf[0x05], upperName, A2FileProDOS::kMaxFileName); + blkBuf[0x14] = 0x76; // 0x75 under old P8, 0x76 under GS/OS + PutLongLE(&blkBuf[0x1c], A2FileProDOS::ConvertProDate(pParms->createWhen)); + blkBuf[0x20] = 5; // 0 under 1.0, 3 under v1.4?, 5 under GS/OS + blkBuf[0x21] = 0; + blkBuf[0x22] = pParms->access; + blkBuf[0x23] = kEntryLength; + blkBuf[0x24] = kEntriesPerBlock; + PutShortLE(&blkBuf[0x25], 0); // file count + PutShortLE(&blkBuf[0x27], dirBlock); + blkBuf[0x29] = (unsigned char) dirEntrySlot; + blkBuf[0x2a] = kEntryLength; // the parent dir's entry length + + dierr = fpImg->WriteBlock(keyBlock, blkBuf); + if (dierr != kDIErrNone) + goto bail; + } else { + assert(false); + dierr = kDIErrInternal; + goto bail; + } + + *pKeyBlock = keyBlock; + *pBlocksUsed = blocksUsed; + *pNewEOF = newEOF; + +bail: + return dierr; +} + + +/* + * Scan for damaged files and mysterious or conflicting block usage map + * entries. + * + * Appends some entries to the DiskImg notes, so this should only be run + * once per DiskFS. + * + * This function doesn't set anything; it's effectively "const" except + * that LoadVolBitmap is inherently non-const. + * + * Returns "true" if disk appears to be perfect, "false" otherwise. + */ +bool +DiskFSProDOS::CheckDiskIsGood(void) +{ + DIError dierr; + bool result = true; + int i; + + if (fEarlyDamage) + result = false; + + dierr = LoadVolBitmap(); + if (dierr != kDIErrNone) + goto bail; + + /* + * Check the system blocks to see if any of them are marked as free. + * If so, refuse to write to this disk. + */ + if (!GetBlockUseEntry(0) || !GetBlockUseEntry(1)) { + fpImg->AddNote(DiskImg::kNoteWarning, "Block 0/1 marked as free."); + result = false; + } + for (i = GetNumBitmapBlocks(); i > 0; i--) { + if (!GetBlockUseEntry(fBitMapPointer + i -1)) { + fpImg->AddNote(DiskImg::kNoteWarning, + "One or more bitmap blocks are marked as free."); + result = false; + break; + } + } + + /* + * Check for used blocks that aren't marked in-use. + * + * This requires that VolumeUsage be accurate. Since this function is + * only run during initial startup, any later deviation between VU and + * the block use map is irrelevant. + */ + VolumeUsage::ChunkState cstate; + long blk, notMarked, extraUsed, conflicts; + notMarked = extraUsed = conflicts = 0; + for (blk = 0; blk < fVolumeUsage.GetNumChunks(); blk++) { + dierr = fVolumeUsage.GetChunkState(blk, &cstate); + if (dierr != kDIErrNone) { + fpImg->AddNote(DiskImg::kNoteWarning, + "Internal volume usage error on blk=%ld.", blk); + result = false; + goto bail; + } + + if (cstate.isUsed && !cstate.isMarkedUsed) + notMarked++; + if (!cstate.isUsed && cstate.isMarkedUsed) + extraUsed++; + if (cstate.purpose == VolumeUsage::kChunkPurposeConflict) + conflicts++; + } + if (extraUsed > 0) { + fpImg->AddNote(DiskImg::kNoteInfo, + "%ld block%s marked used but not part of any file.", + extraUsed, extraUsed == 1 ? " is" : "s are"); + // not a problem, really + } + if (notMarked > 0) { + fpImg->AddNote(DiskImg::kNoteWarning, + "%ld block%s used by files but not marked used.", + notMarked, notMarked == 1 ? " is" : "s are"); + result = false; // very bad -- any change could trash files + } + if (conflicts > 0) { + fpImg->AddNote(DiskImg::kNoteWarning, + "%ld block%s used by more than one file.", + conflicts, conflicts == 1 ? " is" : "s are"); + result = false; // kinda bad -- file deletion leads to trouble + } + + /* + * Check for bits set past the end of the actually-needed bits. For + * some reason P8 and GS/OS both examine these bits, and GS/OS will + * freak out completely and claim the disk is unrecognizeable ("would + * you like to format?") if they're set. + */ + if (ScanForExtraEntries()) { + fpImg->AddNote(DiskImg::kNoteWarning, + "Blocks past the end of the disk are marked 'in use' in the" + " volume bitmap."); + /* don't flunk the disk just for this */ + } + + /* + * Scan for "damaged" or "suspicious" files diagnosed earlier. + */ + bool damaged, suspicious; + ScanForDamagedFiles(&damaged, &suspicious); + + if (damaged) { + fpImg->AddNote(DiskImg::kNoteWarning, + "One or more files are damaged."); + result = false; + } else if (suspicious) { + fpImg->AddNote(DiskImg::kNoteWarning, + "One or more files look suspicious."); + result = false; + } + +bail: + FreeVolBitmap(); + return result; +} + + +/* + * Test a string for validity as a ProDOS volume name. Syntax is the same as + * ProDOS file names, but we also disallow spaces. + */ +/*static*/ bool +DiskFSProDOS::IsValidVolumeName(const char* name) +{ + assert((int) A2FileProDOS::kMaxFileName == (int) kMaxVolumeName); + if (!IsValidFileName(name)) + return false; + while (*name != '\0') { + if (*name++ == ' ') + return false; + } + return true; +} + +/* + * Test a string for validity as a ProDOS file name. Names may be 1-15 + * characters long, must start with a letter, and may contain letters and + * digits. + * + * Lower case and spaces (a/k/a lower-case '.') are accepted. Trailing + * spaces are not allowed. + */ +/*static*/ bool +DiskFSProDOS::IsValidFileName(const char* name) +{ + if (name == nil) { + assert(false); + return false; + } + + /* must be 1-15 characters long */ + if (name[0] == '\0') + return false; + if (strlen(name) > A2FileProDOS::kMaxFileName) + return false; + + /* must begin with letter; this also catches zero-length filenames */ + if (toupper(name[0]) < 'A' || toupper(name[0]) > 'Z') + return false; + + /* no trailing spaces */ + if (name[strlen(name)-1] == ' ') + return false; + + /* must be A-Za-z 0-9 '.' ' ' */ + name++; + while (*name != '\0') { + if (!( (toupper(*name) >= 'A' && toupper(*name) <= 'Z') || + (*name >= '0' && *name <= '9') || + (*name == '.') || + (*name == ' ') + )) + { + return false; + } + + name++; + } + + return true; +} + +/* + * Generate lower case flags by comparing "upperName" to "lowerName". + * + * It's okay for "lowerName" to be longer than "upperName". The extra chars + * are just ignored. Similarly, "lowerName" does not need to be + * null-terminated. "lowerName" does need to point to storage with at least + * as many valid bytes as "upperName", though, or we could crash. + * + * Returns the mask to use in a ProDOS dir. If "forAppleWorks" is set to + * "true", the mask is modified for use with an AppleWorks aux type. + */ +/*static*/ unsigned short +DiskFSProDOS::GenerateLowerCaseBits(const char* upperName, + const char* lowerName, bool forAppleWorks) +{ + unsigned short caseMask = 0x8000; + unsigned short caseBit = 0x8000; + int len, i; + char lowch; + + len = strlen(upperName); + assert(len <= A2FileProDOS::kMaxFileName); + + for (i = 0; i < len; i++) { + caseBit >>= 1; + lowch = A2FileProDOS::NameToLower(upperName[i]); + if (lowch == lowerName[i]) + caseMask |= caseBit; + } + + if (forAppleWorks) { + unsigned short adjusted; + caseMask <<= 1; + adjusted = caseMask << 8 | caseMask >> 8; + return adjusted; + } else { + if (caseMask == 0x8000) + return 0; // all upper case, don't freak out pre-v1.8 + else + return caseMask; + } +} + +/* + * Generate the lower-case version of a ProDOS filename, using the supplied + * lower case flags. "lowerName" must be able to hold 15 chars (enough for + * a filename or volname). + * + * The string will NOT be null-terminated, but the output buffer will be padded + * with NULs out to the maximum filename len. This makes it suitable for + * copying directly into directory block buffers. + * + * It's okay to pass the same buffer for "upperName" and "lowerName". + * + * "lcFlags" is either ProDOS directory flags or AppleWorks aux type flags, + * depending on the value of "fromAppleWorks". + */ +/*static*/ void +DiskFSProDOS::GenerateLowerCaseName(const char* upperName, char* lowerName, + unsigned short lcFlags, bool fromAppleWorks) +{ + int nameLen = strlen(upperName); + int bit; + assert(nameLen <= A2FileProDOS::kMaxFileName); + + if (fromAppleWorks) { + /* handle AppleWorks lower-case-in-auxtype */ + unsigned short caseMask = // swap bytes + (lcFlags << 8) | (lcFlags >> 8); + for (bit = 0; bit < nameLen ; bit++) { + if ((caseMask & 0x8000) != 0) + lowerName[bit] = A2FileProDOS::NameToLower(upperName[bit]); + else + lowerName[bit] = upperName[bit]; + caseMask <<= 1; + } + for ( ; bit < A2FileProDOS::kMaxFileName; bit++) + lowerName[bit] = '\0'; + } else { + /* handle lower-case conversion; see GS/OS tech note #8 */ + if (lcFlags != 0 && !(lcFlags & 0x8000)) { + // Should be zero or 0x8000 plus other bits; shouldn't be + // bunch of bits without 0x8000 or 0x8000 by itself. Not + // really a problem, just unexpected. + assert(false); + memcpy(lowerName, upperName, A2FileProDOS::kMaxFileName); + return; + } + for (bit = 0; bit < nameLen; bit++) { + lcFlags <<= 1; + if ((lcFlags & 0x8000) != 0) + lowerName[bit] = A2FileProDOS::NameToLower(upperName[bit]); + else + lowerName[bit] = upperName[bit]; + } + } + for ( ; bit < A2FileProDOS::kMaxFileName; bit++) + lowerName[bit] = '\0'; +} + +/* + * Normalize a ProDOS path. Invokes DoNormalizePath and handles the buffer + * management (if the normalized path doesn't fit in "*pNormalizedBufLen" + * bytes, we set "*pNormalizedBufLen to the required length). + * + * This is invoked from the generalized "add" function in CiderPress, which + * doesn't want to understand the ins and outs of ProDOS pathnames. + */ +DIError +DiskFSProDOS::NormalizePath(const char* path, char fssep, + char* normalizedBuf, int* pNormalizedBufLen) +{ + DIError dierr = kDIErrNone; + char* normalizedPath = nil; + int len; + + assert(pNormalizedBufLen != nil); + assert(normalizedBuf != nil || *pNormalizedBufLen == 0); + + dierr = DoNormalizePath(path, fssep, &normalizedPath); + if (dierr != kDIErrNone) + goto bail; + + assert(normalizedPath != nil); + len = strlen(normalizedPath); + if (normalizedBuf == nil || *pNormalizedBufLen <= len) { + /* too short */ + dierr = kDIErrDataOverrun; + } else { + /* fits */ + strcpy(normalizedBuf, normalizedPath); + } + + *pNormalizedBufLen = len+1; // alloc room for the '\0' + +bail: + delete[] normalizedPath; + return dierr; +} + +/* + * Normalize a ProDOS path. This requires separating each path component + * out, making it ProDOS-compliant, and then putting it back in. + * The fssep could be anything, so we need to change it to kFssep. + * + * We don't try to identify duplicates here. If more than one subdir maps + * to the same thing, then you're just going to end up with lots of files + * in the same subdir. If this is unacceptable then it will have to be + * fixed at a higher level. + * + * Lower-case letters and spaces are left in place. They're expected to + * be removed later. + * + * The caller must delete[] "*pNormalizedPath". + */ +DIError +DiskFSProDOS::DoNormalizePath(const char* path, char fssep, + char** pNormalizedPath) +{ + DIError dierr = kDIErrNone; + char* workBuf = nil; + char* partBuf = nil; + char* outputBuf = nil; + char* start; + char* end; + char* outPtr; + + assert(path != nil); + workBuf = new char[strlen(path)+1]; + partBuf = new char[strlen(path)+1 +1]; // need +1 for prepending letter + outputBuf = new char[strlen(path) * 2]; + if (workBuf == nil || partBuf == nil || outputBuf == nil) { + dierr = kDIErrMalloc; + goto bail; + } + + strcpy(workBuf, path); + outputBuf[0] = '\0'; + + outPtr = outputBuf; + start = workBuf; + while (*start != '\0') { + //char* origStart = start; // need for debug msg + int partIdx; + + if (fssep == '\0') { + end = nil; + } else { + end = strchr(start, fssep); + if (end != nil) + *end = '\0'; + } + partIdx = 0; + + /* + * Skip over everything up to the first letter. If we encounter a + * number or a '\0' first, insert a leading letter. + */ + while (*start != '\0') { + if (toupper(*start) >= 'A' && toupper(*start) <= 'Z') { + partBuf[partIdx++] = *start++; + break; + } + if (*start >= '0' && *start <= '9') { + partBuf[partIdx++] = 'A'; + break; + } + + start++; + } + if (partIdx == 0) + partBuf[partIdx++] = 'Z'; + + /* + * Continue copying, dropping all illegal chars. + */ + while (*start != '\0') { + if ((toupper(*start) >= 'A' && toupper(*start) <= 'Z') || + (*start >= '0' && *start <= '9') || + (*start == '.') || + (*start == ' ') ) + { + partBuf[partIdx++] = *start++; + } else { + start++; + } + } + + /* + * Truncate at 15 chars, preserving anything that looks like a + * filename extension. "partIdx" represents the length of the + * string at this point. "partBuf" holds the string, which we + * want to null-terminate before proceeding. + */ + partBuf[partIdx] = '\0'; + if (partIdx > A2FileProDOS::kMaxFileName) { + const char* pDot = strrchr(partBuf, '.'); + //int DEBUGDOTLEN = pDot - partBuf; + if (pDot != nil && partIdx - (pDot-partBuf) <= kMaxExtensionLen) { + int dotLen = partIdx - (pDot-partBuf); + memmove(partBuf + (A2FileProDOS::kMaxFileName - dotLen), + pDot, dotLen); // don't use memcpy, move might overlap + } + partIdx = A2FileProDOS::kMaxFileName; + } + partBuf[partIdx] = '\0'; + + //WMSG2(" ProDOS Converted component '%s' to '%s'\n", + // origStart, partBuf); + + if (outPtr != outputBuf) + *outPtr++ = A2FileProDOS::kFssep; + strcpy(outPtr, partBuf); + outPtr += partIdx; + + /* + * Continue with next segment. + */ + if (end == nil) + break; + start = end+1; + } + + *outPtr = '\0'; + + WMSG3(" ProDOS Converted path '%s' to '%s' (fssep='%c')\n", + path, outputBuf, fssep); + assert(*outputBuf != '\0'); + + *pNormalizedPath = outputBuf; + outputBuf = nil; + +bail: + delete[] workBuf; + delete[] partBuf; + delete[] outputBuf; + return dierr; +} + +/* + * Create a copy of the filename with everything in upper case and spaces + * changed to periods. + * + * "upperName" must be a buffer that holds at least kMaxFileName+1 characters. + * If "name" is longer than kMaxFileName, it will be truncated. + */ +void +DiskFSProDOS::UpperCaseName(char* upperName, const char* name) +{ + int i; + + for (i = 0; i < A2FileProDOS::kMaxFileName; i++) { + char ch = name[i]; + if (ch == '\0') + break; + else if (ch == ' ') + upperName[i] = '.'; + else + upperName[i] = toupper(ch); + } + + /* null terminate with prejudice -- we memcpy this buffer into subdirs */ + for ( ; i <= A2FileProDOS::kMaxFileName; i++) + upperName[i] = '\0'; +} + +/* + * Allocate a new directory entry. We start by reading the entire thing + * into memory. If the current set of allocated directory blocks is full, + * and we're not operating on the volume dir, we extend the directory. + * + * This just allocates the space; it does not fill in any details, except + * for the prev/next block pointers and the file count in the header. (One + * small exception: if we have to extend the directory, the "prev/next" fields + * of the new block will be filled in.) + * + * The volume in-use block map must be loaded before this is called. If + * this needs to extend the directory, a new block will be allocated. + * + * Returns a pointer to the new entry, and a whole bunch of other stuff: + * "ppDir" gets a pointer to newly-allocated memory with the whole directory + * "pDirLen" is the size of the *ppDir buffer + * "ppDirEntry" gets a memory pointer to the start of the created entry + * "pDirKeyBlock" gets the key block of the directory as a whole + * "pDirEntrySlot" gets the slot number within the directory block (first is 1) + * "pDirBlock" gets the actual block in which the created entry resides + * + * The caller should Write the entire thing to "pOpenSubdir" after filling + * in the new details for the entry. + * + * Possible reasons for failure: disk is out of space, volume dir is out + * of space, pOpenSubdir is screwy. + * + * We guarantee that we will return the first available entry in the current + * directory. + */ +DIError +DiskFSProDOS::AllocDirEntry(A2FileDescr* pOpenSubdir, unsigned char** ppDir, + long* pDirLen, unsigned char** ppDirEntry, unsigned short* pDirKeyBlock, + int* pDirEntrySlot, unsigned short* pDirBlock) +{ + assert(pOpenSubdir != nil); + *ppDirEntry = nil; + *pDirLen = -1; + *pDirKeyBlock = 0; + *pDirEntrySlot = -1; + *pDirBlock = 0; + + DIError dierr = kDIErrNone; + unsigned char* dirBuf = nil; + long dirLen; + A2FileProDOS* pFile; + long newBlock = -1; + + /* + * Load the subdir into memory. + */ + pFile = (A2FileProDOS*) pOpenSubdir->GetFile(); + dirLen = (long) pFile->GetDataLength(); + if (dirLen < 512 || (dirLen % 512) != 0) { + WMSG2(" ProDOS GLITCH: funky dir EOF %ld (quality=%d)\n", + dirLen, pFile->GetQuality()); + dierr = kDIErrBadFile; + goto bail; + } + dirBuf = new unsigned char[dirLen]; + if (dirBuf == nil) { + dierr = kDIErrMalloc; + goto bail; + } + + dierr = pOpenSubdir->Read(dirBuf, dirLen); + if (dierr != kDIErrNone) + goto bail; + + if (dirBuf[0x23] != kEntryLength || + dirBuf[0x24] != kEntriesPerBlock) + { + WMSG1(" ProDOS GLITCH: funky entries per block %d\n", dirBuf[0x24]); + dierr = kDIErrBadDirectory; + goto bail; + } + + /* + * Find the first available entry (storage_type is zero). We need to + * step through this by blocks, because the data is block-oriented. + * If we run off the end of the last block, (re)alloc a new one. + */ + unsigned char* pDirEntry; + int blockIdx; + int entryIdx; + + pDirEntry = nil; // make the compiler happy + entryIdx = -1; // make the compiler happy + + for (blockIdx = 0; blockIdx < dirLen / 512; blockIdx++) { + pDirEntry = dirBuf + 512*blockIdx + 4; // skip 4 bytes of prev/next + + for (entryIdx = 0; entryIdx < kEntriesPerBlock; + entryIdx++, pDirEntry += kEntryLength) + { + if ((pDirEntry[0x00] & 0xf0) == 0) { + WMSG1(" ProDOS Found empty dir entry in slot %d\n", entryIdx); + break; // found one; break out of inner loop + } + } + if (entryIdx < kEntriesPerBlock) + break; // out of outer loop + } + if (blockIdx == dirLen / 512) { + if (((dirBuf[0x04] & 0xf0) >> 4) == A2FileProDOS::kStorageVolumeDirHeader) + { + /* can't extend the volume dir */ + dierr = kDIErrVolumeDirFull; + goto bail; + } + + WMSG0(" ProDOS ran out of directory space, adding another block\n"); + + /* + * Request an unused block from the system. Point the "next" pointer + * in the last block at it, so that when we go to write this dir + * we will know where to put it. + */ + unsigned char* pBlock; + pBlock = dirBuf + 512 * (blockIdx-1); + if (pBlock[0x02] != 0) { + WMSG0(" ProDOS GLITCH: adding to block with nonzero next ptr!\n"); + dierr = kDIErrBadDirectory; + goto bail; + } + + newBlock = AllocBlock(); + if (newBlock < 0) { + dierr = kDIErrDiskFull; + goto bail; + } + + PutShortLE(&pBlock[0x02], (unsigned short) newBlock); // set "next" + + /* + * Extend our memory buffer to hold the new entry. + */ + unsigned char* newSpace = new unsigned char[dirLen + 512]; + if (newSpace == nil) { + dierr = kDIErrMalloc; + goto bail; + } + memcpy(newSpace, dirBuf, dirLen); + memset(newSpace + dirLen, 0, 512); + delete[] dirBuf; + dirBuf = newSpace; + dirLen += 512; + + /* + * Set the "prev" pointer in the new block to point at the last + * block of the existing directory structure. + */ + long lastBlock; + dierr = pOpenSubdir->GetStorage(blockIdx-1, &lastBlock); + if (dierr != kDIErrNone) + goto bail; + pBlock = dirBuf + 512 * blockIdx; + PutShortLE(&pBlock[0x00], (unsigned short) lastBlock); // set "prev" + assert(GetShortLE(&pBlock[0x02]) == 0); // "next" pointer + + /* + * Finally, point pDirEntry at the first entry in the new area. + */ + pDirEntry = pBlock + 4; + entryIdx = 0; + assert(pDirEntry[0x00] == 0x00); + } + + /* + * Success. Update the file count in the header. + */ + unsigned short count; + count = GetShortLE(&dirBuf[0x25]); + count++; + PutShortLE(&dirBuf[0x25], count); + + long whichBlock; + + *ppDir = dirBuf; + *pDirLen = dirLen; + *ppDirEntry = pDirEntry; + *pDirKeyBlock = pFile->fDirEntry.keyPointer; + *pDirEntrySlot = entryIdx +1; + if (blockIdx == ((A2FDProDOS*)pOpenSubdir)->GetBlockCount()) { + /* not yet added to block list, so can't use GetStorage */ + assert(newBlock > 0); + *pDirBlock = (unsigned short) newBlock; + } else { + assert(newBlock < 0); + dierr = pOpenSubdir->GetStorage(blockIdx, &whichBlock); + assert(dierr == kDIErrNone); + *pDirBlock = (unsigned short) whichBlock; + } + dirBuf = nil; + +bail: + delete[] dirBuf; + return dierr; +} + +/* + * Given a pointer to a directory buffer and a pointer to an entry, find the + * previous entry. (This is handy when trying to figure out where to insert + * a new entry into the DiskFS linear file list.) + * + * If the previous entry is the first in the list (i.e. it's a volume or + * subdir header), this returns nil. + * + * This is a little awkward because the directories are chopped up into + * 512-byte blocks, with 13 entries per block (which doesn't completely fill + * the block, leaving gaps we have to skip around). If the previous entry is + * in the same block we can just return (ptr-0x27), but if it's in a previous + * block we need to return the last entry in the previous. + */ +unsigned char* +DiskFSProDOS::GetPrevDirEntry(unsigned char* buf, unsigned char* ptr) +{ + assert(buf != nil); + assert(ptr != nil); + + const int kStartOffset = 4; + + if (ptr == buf + kStartOffset || ptr == buf + kStartOffset + kEntryLength) + return nil; + + while (ptr - buf > 512) + buf += 512; + + assert((ptr - buf - kStartOffset) % kEntryLength == 0); + + if (ptr == buf + kStartOffset) { + /* whoops, went too far */ + buf -= 512; + return buf + kStartOffset + kEntryLength * (kEntriesPerBlock-1); + } else { + return ptr - kEntryLength; + } +} + +/* + * Make the name pointed to by "fileName" unique within the directory + * loaded in "subdirBuf". The name should already be trimmed to 15 chars + * or less and converted to upper-case only, and be in a buffer that can + * hold at least kMaxFileName+1 bytes. + * + * Returns an error on failure, which should only happen if there are a + * large number of files with similar names. + */ +DIError +DiskFSProDOS::MakeFileNameUnique(const unsigned char* dirBuf, long dirLen, + char* fileName) +{ + assert(dirBuf != nil); + assert(dirLen > 0); + assert((dirLen % 512) == 0); + assert(fileName != nil); + assert(strlen(fileName) <= A2FileProDOS::kMaxFileName); + + if (!NameExistsInDir(dirBuf, dirLen, fileName)) + return kDIErrNone; + + WMSG1(" ProDOS found duplicate of '%s', making unique\n", fileName); + + int nameLen = strlen(fileName); + int dotOffset=0, dotLen=0; + char dotBuf[kMaxExtensionLen+1]; + + /* ensure the result will be null-terminated */ + memset(fileName + nameLen, 0, (A2FileProDOS::kMaxFileName - nameLen) +1); + + /* + * If this has what looks like a filename extension, grab it. We want + * to preserve ".gif", ".c", etc., since the filetypes don't necessarily + * do everything we need. + * + * This will tend to screw up the upper/lower case stuff, especially + * since what we think is a '.' might actually be a ' '. We could work + * around this, but it's probably not necessary. + */ + const char* cp = strrchr(fileName, '.'); + if (cp != nil) { + int tmpOffset = cp - fileName; + if (tmpOffset > 0 && nameLen - tmpOffset <= kMaxExtensionLen) { + WMSG1(" ProDOS (keeping extension '%s')\n", cp); + assert(strlen(cp) <= kMaxExtensionLen); + strcpy(dotBuf, cp); + dotOffset = tmpOffset; + dotLen = nameLen - dotOffset; + } + } + + const int kMaxDigits = 999; + int digits = 0; + int digitLen; + int copyOffset; + char digitBuf[4]; + do { + if (digits == kMaxDigits) + return kDIErrFileExists; + digits++; + + /* not the most efficient way to do this, but it'll do */ + sprintf(digitBuf, "%d", digits); + digitLen = strlen(digitBuf); + if (nameLen + digitLen > A2FileProDOS::kMaxFileName) + copyOffset = A2FileProDOS::kMaxFileName - dotLen - digitLen; + else + copyOffset = nameLen - dotLen; + memcpy(fileName + copyOffset, digitBuf, digitLen); + if (dotLen != 0) + memcpy(fileName + copyOffset + digitLen, dotBuf, dotLen); + } while (NameExistsInDir(dirBuf, dirLen, fileName)); + + WMSG1(" ProDOS converted to unique name: %s\n", fileName); + + return kDIErrNone; +} + +/* + * Determine whether the specified file name exists in the raw directory + * buffer. + * + * This should be called with the upper-case-only version of the filename. + */ +bool +DiskFSProDOS::NameExistsInDir(const unsigned char* dirBuf, long dirLen, + const char* fileName) +{ + const unsigned char* pDirEntry; + int blockIdx; + int entryIdx; + int nameLen = strlen(fileName); + + assert(nameLen <= A2FileProDOS::kMaxFileName); + + for (blockIdx = 0; blockIdx < dirLen / 512; blockIdx++) { + pDirEntry = dirBuf + 512*blockIdx + 4; // skip 4 bytes of prev/next + + for (entryIdx = 0; entryIdx < kEntriesPerBlock; + entryIdx++, pDirEntry += kEntryLength) + { + /* skip directory header */ + if (blockIdx == 0 && entryIdx == 0) + continue; + + if ((pDirEntry[0x00] & 0xf0) != 0 && + (pDirEntry[0x00] & 0x0f) == nameLen && + strncmp((char*) &pDirEntry[0x01], fileName, nameLen) == 0) + { + return true; + } + } + } + + return false; +} + + +/* + * Delete a file. + * + * There are three fairly simple steps: (1) mark all blocks used by the file as + * free, (2) set the storage type in the directory entry to 0, and (3) + * decrement the file count in the directory header. We then remove it from + * the DiskFS file list. + * + * We only allow deletion of a subdirectory when the subdir is empty. + */ +DIError +DiskFSProDOS::DeleteFile(A2File* pGenericFile) +{ + DIError dierr = kDIErrNone; + long blockCount = -1; + long indexCount = -1; + unsigned short* blockList = nil; + unsigned short* indexList = nil; + + if (pGenericFile == nil) { + assert(false); + return kDIErrInvalidArg; + } + + if (fpImg->GetReadOnly()) + return kDIErrAccessDenied; + if (!fDiskIsGood) + return kDIErrBadDiskImage; + if (pGenericFile->IsFileOpen()) + return kDIErrFileOpen; + + /* + * If they try to delete all entries, we don't want to spit back a + * failure message over our "fake" volume dir entry. So we just silently + * ignore the request. + */ + if (pGenericFile->IsVolumeDirectory()) { + WMSG0("ProDOS not deleting volume directory\n"); + return kDIErrNone; + } + + A2FileProDOS* pFile = (A2FileProDOS*) pGenericFile; + + WMSG1(" Deleting '%s'\n", pFile->GetPathName()); + + dierr = LoadVolBitmap(); + if (dierr != kDIErrNone) + goto bail; + switch (pFile->fDirEntry.storageType) { + case A2FileProDOS::kStorageExtended: + // handle rsrc fork here, fall out for data fork + dierr = pFile->LoadBlockList( + pFile->fExtRsrc.storageType, + pFile->fExtRsrc.keyBlock, + pFile->fExtRsrc.eof, + &blockCount, &blockList, + &indexCount, &indexList); + if (dierr != kDIErrNone) + goto bail; + FreeBlocks(blockCount, blockList); + if (indexList != nil) // no indices for seedling + FreeBlocks(indexCount, indexList); + delete[] blockList; + delete[] indexList; + indexList = nil; + + // handle the key block "manually" + blockCount = 1; + blockList = new unsigned short[blockCount]; + blockList[0] = pFile->fDirEntry.keyPointer; + FreeBlocks(blockCount, blockList); + delete[] blockList; + blockList = nil; + + dierr = pFile->LoadBlockList( + pFile->fExtData.storageType, + pFile->fExtData.keyBlock, + pFile->fExtData.eof, + &blockCount, &blockList, + &indexCount, &indexList); + break; // fall out + + case A2FileProDOS::kStorageDirectory: + dierr = pFile->LoadDirectoryBlockList( + pFile->fDirEntry.keyPointer, + pFile->fDirEntry.eof, + &blockCount, &blockList); + break; // fall out + + case A2FileProDOS::kStorageSeedling: + case A2FileProDOS::kStorageSapling: + case A2FileProDOS::kStorageTree: + dierr = pFile->LoadBlockList( + pFile->fDirEntry.storageType, + pFile->fDirEntry.keyPointer, + pFile->fDirEntry.eof, + &blockCount, &blockList, + &indexCount, &indexList); + break; // fall out + + default: + WMSG1("ProDOS can't delete unknown storage type %d\n", + pFile->fDirEntry.storageType); + dierr = kDIErrBadDirectory; + break; // fall out + } + + if (dierr != kDIErrNone) + goto bail; + + FreeBlocks(blockCount, blockList); + if (indexList != nil) + FreeBlocks(indexCount, indexList); + + /* + * Update the directory entry. After this point, failure gets ugly. + * + * It might be "proper" to open the subdir file, find the correct entry, + * and write it back, but the A2FileProDOS structure has the directory + * block and entry index stored in it. Makes it a little easier. + */ + unsigned char blkBuf[kBlkSize]; + unsigned char* ptr; + assert(pFile->fParentDirBlock > 0); + assert(pFile->fParentDirIdx >= 0 && + pFile->fParentDirIdx < kEntriesPerBlock); + dierr = fpImg->ReadBlock(pFile->fParentDirBlock, blkBuf); + if (dierr != kDIErrNone) { + WMSG1("ProDOS unable to read directory block %u\n", + pFile->fParentDirBlock); + goto bail; + } + + ptr = blkBuf + 4 + pFile->fParentDirIdx * kEntryLength; + if ((*ptr) >> 4 != pFile->fDirEntry.storageType) { + WMSG2("ProDOS GLITCH: mismatched storage types (%d vs %d)\n", + (*ptr) >> 4, pFile->fDirEntry.storageType); + assert(false); + dierr = kDIErrBadDirectory; + goto bail; + } + ptr[0x00] = 0; // zap both storage type and name length + dierr = fpImg->WriteBlock(pFile->fParentDirBlock, blkBuf); + if (dierr != kDIErrNone) { + WMSG1("ProDOS unable to write directory block %u\n", + pFile->fParentDirBlock); + goto bail; + } + + /* + * Save our updated copy of the volume bitmap to disk. + */ + dierr = SaveVolBitmap(); + if (dierr != kDIErrNone) + goto bail; + + /* + * One last little thing: decrement the file count in the directory + * header. We can find the appropriate place pretty easily because + * we know it's the first block in pFile->fpParent, which for a dir is + * always the block pointed to by the key pointer. + * + * Strictly speaking, failure to update this correctly isn't fatal. I + * doubt most utilities pay any attention to this. Still, it's important + * to keep the filesystem in a consistent state, so we at least must + * report the error. They'll need to run the ProSel volume repair util + * to fix it. + */ + A2FileProDOS* pParent; + unsigned short fileCount; + int storageType; + pParent = (A2FileProDOS*) pFile->GetParent(); + assert(pParent != nil); + assert(pParent->fDirEntry.keyPointer >= kVolHeaderBlock); + dierr = fpImg->ReadBlock(pParent->fDirEntry.keyPointer, blkBuf); + if (dierr != kDIErrNone) { + WMSG1("ProDOS unable to read parent dir block %u\n", + pParent->fDirEntry.keyPointer); + goto bail; + } + ptr = nil; + + storageType = (blkBuf[0x04] & 0xf0) >> 4; + if (storageType != A2FileProDOS::kStorageSubdirHeader && + storageType != A2FileProDOS::kStorageVolumeDirHeader) + { + WMSG1("ProDOS invalid storage type %d in dir header block\n", + storageType); + DebugBreak(); + dierr = kDIErrBadDirectory; + goto bail; + } + fileCount = GetShortLE(&blkBuf[0x25]); + if (fileCount > 0) + fileCount--; + PutShortLE(&blkBuf[0x25], fileCount); + dierr = fpImg->WriteBlock(pParent->fDirEntry.keyPointer, blkBuf); + if (dierr != kDIErrNone) { + WMSG1("ProDOS unable to write parent dir block %u\n", + pParent->fDirEntry.keyPointer); + goto bail; + } + + /* + * Remove the A2File* from the list. + */ + DeleteFileFromList(pFile); + +bail: + FreeVolBitmap(); + delete[] blockList; + delete[] indexList; + return kDIErrNone; +} + +/* + * Mark all of the blocks in the blockList as free. + * + * The in-use map must already be loaded. + */ +DIError +DiskFSProDOS::FreeBlocks(long blockCount, unsigned short* blockList) +{ + VolumeUsage::ChunkState cstate; + int i; + + //WMSG2(" +++ FreeBlocks (blockCount=%d blockList=0x%08lx)\n", + // blockCount, blockList); + assert(blockCount >= 0 && blockCount < 65536); + assert(blockList != nil); + + cstate.isUsed = false; + cstate.isMarkedUsed = false; + cstate.purpose = VolumeUsage::kChunkPurposeUnknown; + + for (i = 0; i < blockCount; i++) { + if (blockList[i] == 0) // expected for "sparse" files + continue; + + if (!GetBlockUseEntry(blockList[i])) { + WMSG1("WARNING: freeing unallocated block %u\n", blockList[i]); + assert(false); // impossible unless disk is "damaged" + } + SetBlockUseEntry(blockList[i], false); + + fVolumeUsage.SetChunkState(blockList[i], &cstate); + } + + return kDIErrNone; +} + + +/* + * Rename a file. + * + * Pass in a pointer to the file and a string with the new filename (just + * the filename, not a pathname -- this function doesn't move files + * between directories). The new name must already be normalized. + * + * Renaming the magic volume directory "file" is not allowed. + * + * Things to note: + * - Renaming subdirs is annoying. The name has to be changed in two + * places, and the "pathname" value cached in A2FileProDOS must be + * updated for all children of the subdir. + * - Must check for duplicates. + * - If it's an AppleWorks file type, we need to change the aux type + * according to the upper/lower case flags. This holds even if the + * "allow lower case" flag is disabled. + */ +DIError +DiskFSProDOS::RenameFile(A2File* pGenericFile, const char* newName) +{ + DIError dierr = kDIErrNone; + A2FileProDOS* pFile = (A2FileProDOS*) pGenericFile; + char upperName[A2FileProDOS::kMaxFileName+1]; + char upperComp[A2FileProDOS::kMaxFileName+1]; + + if (pFile == nil || newName == nil) + return kDIErrInvalidArg; + if (!IsValidFileName(newName)) + return kDIErrInvalidArg; + if (pFile->IsVolumeDirectory()) + return kDIErrInvalidArg; + if (fpImg->GetReadOnly()) + return kDIErrAccessDenied; + if (!fDiskIsGood) + return kDIErrBadDiskImage; + + WMSG2(" ProDOS renaming '%s' to '%s'\n", pFile->GetPathName(), newName); + + /* + * Check for duplicates. We do this by getting the parent subdir and + * running through it looking for an upper-case-converted match. + * + * We start in the list at our parent node, knowing that the kids are + * grouped together after it. However, we can't stop right away, + * because some of the kids might be subdirectories themselves. So we + * will probably run through a significant chunk of the list. + */ + A2File* pParent = pFile->GetParent(); + A2File* pCur; + + UpperCaseName(upperName, newName); + pCur = GetNextFile(pParent); + assert(pCur != nil); // at the very least, pFile is in this dir + while (pCur != nil) { + if (pCur != pFile && pCur->GetParent() == pParent) { + /* one of our siblings; see if the name matches */ + UpperCaseName(upperComp, pCur->GetFileName()); + if (strcmp(upperName, upperComp) == 0) { + WMSG0(" ProDOS rename dup found\n"); + return kDIErrFileExists; + } + } + + pCur = GetNextFile(pCur); + } + + /* + * Grab the directory block and update the filename in the entry. If this + * was a subdir we also need to update its directory header entry. To + * minimize the chances of a partial update, we load both blocks up + * front, modify both, then write them both back. + */ + unsigned char parentDirBuf[kBlkSize]; + unsigned char thisDirBuf[kBlkSize]; + + dierr = fpImg->ReadBlock(pFile->fParentDirBlock, parentDirBuf); + if (dierr != kDIErrNone) + goto bail; + if (pFile->IsDirectory()) { + dierr = fpImg->ReadBlock(pFile->fDirEntry.keyPointer, thisDirBuf); + if (dierr != kDIErrNone) + goto bail; + } + + /* compute lower case flags as needed */ + unsigned short lcFlags, lcAuxType; + bool allowLowerCase, isAW; + + allowLowerCase = GetParameter(kParmProDOS_AllowLowerCase) != 0; + isAW = A2FileProDOS::UsesAppleWorksAuxType((unsigned char)pFile->GetFileType()); + + if (allowLowerCase) + lcFlags = GenerateLowerCaseBits(upperName, newName, false); + else + lcFlags = 0; + if (isAW) + lcAuxType = GenerateLowerCaseBits(upperName, newName, true); + else + lcAuxType = 0; + + /* + * Possible optimization: if "upperName" matches what's in the block on + * disk and the "lcFlags"/"lcAuxType" values match as well, we don't + * need to write the blocks back. + * + * It's difficult to test for this earlier, because we need to do the + * update if (a) they're just changing the capitalization or (b) we're + * changing the capitalization for them because the "allow lower case" + * flag got turned off. + */ + + /* find the right entry, and copy our filename in */ + unsigned char* ptr; + assert(pFile->fParentDirIdx >= 0 && + pFile->fParentDirIdx < kEntriesPerBlock); + ptr = parentDirBuf + 4 + pFile->fParentDirIdx * kEntryLength; + if ((*ptr) >> 4 != pFile->fDirEntry.storageType) { + WMSG2("ProDOS GLITCH: mismatched storage types (%d vs %d)\n", + (*ptr) >> 4, pFile->fDirEntry.storageType); + assert(false); + dierr = kDIErrBadDirectory; + goto bail; + } + ptr[0x00] = (ptr[0x00] & 0xf0) | strlen(upperName); + memcpy(&ptr[0x01], upperName, A2FileProDOS::kMaxFileName); + PutShortLE(&ptr[0x1c], lcFlags); // version/min_version + if (isAW) + PutShortLE(&ptr[0x1f], lcAuxType); + + if (pFile->IsDirectory()) { + ptr = thisDirBuf + 4; + if ((*ptr) >> 4 != A2FileProDOS::kStorageSubdirHeader) { + WMSG1("ProDOS GLITCH: bad storage type in subdir header (%d)\n", + (*ptr) >> 4); + assert(false); + dierr = kDIErrBadDirectory; + goto bail; + } + ptr[0x00] = (ptr[0x00] & 0xf0) | strlen(upperName); + memcpy(&ptr[0x01], upperName, A2FileProDOS::kMaxFileName); + PutShortLE(&ptr[0x1c], lcFlags); // version/min_version + } + + /* write the updated data back to the disk */ + dierr = fpImg->WriteBlock(pFile->fParentDirBlock, parentDirBuf); + if (dierr != kDIErrNone) + goto bail; + if (pFile->IsDirectory()) { + dierr = fpImg->WriteBlock(pFile->fDirEntry.keyPointer, thisDirBuf); + if (dierr != kDIErrNone) + goto bail; + } + + /* + * At this point the ProDOS filesystem is back in a consistent state. + * Everything we do from here on is self-inflicted. + * + * We need to update this entry's A2FileProDOS::fDirEntry.fileName, + * as well as the A2FileProDOS::fPathName. If this was a subdir, then + * we need to update A2FileProDOS::fPathName for all files inside the + * directory (including children of children). + * + * The latter is somewhat awkward, so we just re-acquire the pathname + * for every file on the disk. Less efficient but easier to code. + */ + if (isAW) + GenerateLowerCaseName(upperName, pFile->fDirEntry.fileName, + lcAuxType, true); + else + GenerateLowerCaseName(upperName, pFile->fDirEntry.fileName, + lcFlags, false); + assert(pFile->fDirEntry.fileName[A2FileProDOS::kMaxFileName] == '\0'); + + if (pFile->IsDirectory()) { + /* do all files that come after us */ + pCur = pFile; + while (pCur != nil) { + RegeneratePathName((A2FileProDOS*) pCur); + pCur = GetNextFile(pCur); + } + } else { + RegeneratePathName(pFile); + } + + WMSG0("Okay!\n"); + +bail: + return dierr; +} + +/* + * Regenerate fPathName for the specified file. + * + * Has no effect on the magic volume dir entry. + * + * This could be implemented more efficiently, but it's only used when + * renaming files, so there's not much point. + */ +DIError +DiskFSProDOS::RegeneratePathName(A2FileProDOS* pFile) +{ + A2FileProDOS* pParent; + char* buf = nil; + int len; + + /* nothing to do here */ + if (pFile->IsVolumeDirectory()) + return kDIErrNone; + + /* compute the length of the path name */ + len = strlen(pFile->GetFileName()); + pParent = (A2FileProDOS*) pFile->GetParent(); + while (!pParent->IsVolumeDirectory()) { + len++; // leave space for the ':' + len += strlen(pParent->GetFileName()); + + pParent = (A2FileProDOS*) pParent->GetParent(); + } + + buf = new char[len+1]; + if (buf == nil) + return kDIErrMalloc; + + /* generate the new path name */ + int partLen; + partLen = strlen(pFile->GetFileName()); + strcpy(buf + len - partLen, pFile->GetFileName()); + len -= partLen; + + pParent = (A2FileProDOS*) pFile->GetParent(); + while (!pParent->IsVolumeDirectory()) { + assert(len > 0); + buf[--len] = kDIFssep; + + partLen = strlen(pParent->GetFileName()); + strncpy(buf + len - partLen, pParent->GetFileName(), partLen); + len -= partLen; + assert(len >= 0); + + pParent = (A2FileProDOS*) pParent->GetParent(); + } + + WMSG2("Replacing '%s' with '%s'\n", pFile->GetPathName(), buf); + pFile->SetPathName("", buf); + delete[] buf; + + return kDIErrNone; +} + + +/* + * Change the attributes of the specified file. + * + * Subdirectories have access bits in the subdir header as well as their + * file entry. The BASIC.SYSTEM "lock" command only changes the access + * bits of the file; the permissions inside the subdir remain 0xe3. (Which + * might explain why you can still add files to a locked subdir.) I'm going + * to mimic this behavior. + * + * This does, of course, mean that there's no meaning in attempts to change + * the file access permissions of the volume directory. + */ +DIError +DiskFSProDOS::SetFileInfo(A2File* pGenericFile, long fileType, long auxType, + long accessFlags) +{ + DIError dierr = kDIErrNone; + A2FileProDOS* pFile = (A2FileProDOS*) pGenericFile; + + if (fpImg->GetReadOnly()) + return kDIErrAccessDenied; + if (pFile == nil) { + assert(false); + return kDIErrInvalidArg; + } + if ((fileType & ~(0xff)) != 0 || + (auxType & ~(0xffff)) != 0 || + (accessFlags & ~(0xff)) != 0) + { + return kDIErrInvalidArg; + } + if (pFile->IsVolumeDirectory()) { + WMSG0(" ProDOS refusing to change file info for volume dir\n"); + return kDIErrAccessDenied; // not quite right + } + + WMSG4("ProDOS changing values for '%s' to 0x%02lx 0x%04lx 0x%02lx\n", + pFile->GetPathName(), fileType, auxType, accessFlags); + + /* load the directory block for this file */ + unsigned char thisDirBuf[kBlkSize]; + dierr = fpImg->ReadBlock(pFile->fParentDirBlock, thisDirBuf); + if (dierr != kDIErrNone) + goto bail; + + /* find the right entry, and set the fields */ + unsigned char* ptr; + assert(pFile->fParentDirIdx >= 0 && + pFile->fParentDirIdx < kEntriesPerBlock); + ptr = thisDirBuf + 4 + pFile->fParentDirIdx * kEntryLength; + if ((*ptr) >> 4 != pFile->fDirEntry.storageType) { + WMSG2("ProDOS GLITCH: mismatched storage types (%d vs %d)\n", + (*ptr) >> 4, pFile->fDirEntry.storageType); + assert(false); + dierr = kDIErrBadDirectory; + goto bail; + } + if ((size_t) (*ptr & 0x0f) != strlen(pFile->fDirEntry.fileName)) { + WMSG2("ProDOS GLITCH: wrong file? (len=%d vs %d)\n", + *ptr & 0x0f, strlen(pFile->fDirEntry.fileName)); + assert(false); + dierr = kDIErrBadDirectory; + goto bail; + } + + ptr[0x10] = (unsigned char) fileType; + ptr[0x1e] = (unsigned char) accessFlags; + PutShortLE(&ptr[0x1f], (unsigned short) auxType); + + dierr = fpImg->WriteBlock(pFile->fParentDirBlock, thisDirBuf); + if (dierr != kDIErrNone) + goto bail; + + /* update our local copy */ + pFile->fDirEntry.fileType = (unsigned char) fileType; + pFile->fDirEntry.auxType = (unsigned short) auxType; + pFile->fDirEntry.access = (unsigned char) accessFlags; + +bail: + return dierr; +} + + +/* + * Change the disk volume name. + * + * This is a lot like renaming a subdirectory, except that there's no parent + * directory to update, and the name of the volume dir doesn't affect the + * pathname of anything else. There's also no risk of a duplicate. + * + * Internally we need to update the "fake" entry and the cached copies in + * fVolumeName and fVolumeID. + */ +DIError +DiskFSProDOS::RenameVolume(const char* newName) +{ + DIError dierr = kDIErrNone; + char upperName[A2FileProDOS::kMaxFileName+1]; + A2FileProDOS* pFile; + + if (!IsValidVolumeName(newName)) + return kDIErrInvalidArg; + if (fpImg->GetReadOnly()) + return kDIErrAccessDenied; + + pFile = (A2FileProDOS*) GetNextFile(nil); + assert(pFile != nil); + assert(strcmp(pFile->GetFileName(), fVolumeName) == 0); + + WMSG2(" ProDOS renaming volume '%s' to '%s'\n", + pFile->GetPathName(), newName); + + /* + * Figure out the lower-case flags. + */ + unsigned short lcFlags; + bool allowLowerCase; + + UpperCaseName(upperName, newName); + allowLowerCase = GetParameter(kParmProDOS_AllowLowerCase) != 0; + if (allowLowerCase) + lcFlags = GenerateLowerCaseBits(upperName, newName, false); + else + lcFlags = 0; + + /* + * Update the volume dir header. + */ + unsigned char thisDirBuf[kBlkSize]; + unsigned char* ptr; + assert(pFile->fDirEntry.keyPointer == kVolHeaderBlock); + + dierr = fpImg->ReadBlock(pFile->fDirEntry.keyPointer, thisDirBuf); + if (dierr != kDIErrNone) + goto bail; + + ptr = thisDirBuf + 4; + if ((*ptr) >> 4 != A2FileProDOS::kStorageVolumeDirHeader) { + WMSG1("ProDOS GLITCH: bad storage type in voldir header (%d)\n", + (*ptr) >> 4); + assert(false); + dierr = kDIErrBadDirectory; + goto bail; + } + ptr[0x00] = (ptr[0x00] & 0xf0) | strlen(upperName); + memcpy(&ptr[0x01], upperName, A2FileProDOS::kMaxFileName); + PutShortLE(&ptr[0x16], lcFlags); // reserved fields + + dierr = fpImg->WriteBlock(pFile->fDirEntry.keyPointer, thisDirBuf); + if (dierr != kDIErrNone) + goto bail; + + /* + * Set the volume name, based on the upper-case name and lower-case flags + * we just wrote. If "allowLowerCase" was set to false, it may not be + * the same as what's in "newName". + */ + char lowerName[A2FileProDOS::kMaxFileName+1]; + memset(lowerName, 0, sizeof(lowerName)); // lowerName won't be term'ed + GenerateLowerCaseName(upperName, lowerName, lcFlags, false); + + strcpy(fVolumeName, lowerName); + SetVolumeID(); + strcpy(pFile->fDirEntry.fileName, lowerName); + + /* update the entry in the linear file list */ + pFile->SetPathName(":", fVolumeName); + +bail: + return dierr; +} + + +/* + * =========================================================================== + * A2FileProDOS + * =========================================================================== + */ + +/* + * Convert from ProDOS compact date format to a time_t. + * + * Byte 0 and 1: yyyyyyymmmmddddd + * Byte 2 and 3: 000hhhhh00mmmmmm + * + * The field is set entirely to zero if no date was assigned (which cannot + * be a valid date since "day" ranges from 1 to 31). If this is found then + * ((time_t) 0) is returned. + */ +/*static*/ time_t +A2FileProDOS::ConvertProDate(ProDate proDate) +{ + unsigned short prodosDate, prodosTime; + int year, month, day, hour, minute, second; + + if (proDate == 0) + return 0; + + prodosDate = (unsigned short) (proDate & 0x0000ffff); + prodosTime = (unsigned short) ((proDate >> 16) & 0x0000ffff); + + second = 0; + minute = prodosTime & 0x3f; + hour = (prodosTime >> 8) & 0x1f; + day = prodosDate & 0x1f; + month = (prodosDate >> 5) & 0x0f; + year = (prodosDate >> 9) & 0x7f; + if (year < 40) + year += 100; /* P8 uses 0-39 for 2000-2039 */ + + struct tm tmbuf; + time_t when; + + tmbuf.tm_sec = second; + tmbuf.tm_min = minute; + tmbuf.tm_hour = hour; + tmbuf.tm_mday = day; + tmbuf.tm_mon = month-1; // ProDOS uses 1-12 + tmbuf.tm_year = year; + tmbuf.tm_wday = 0; + tmbuf.tm_yday = 0; + tmbuf.tm_isdst = -1; // let it figure DST and time zone + when = mktime(&tmbuf); + + if (when == (time_t) -1) + when = 0; + + return when; +} + +/* + * Convert a time_t to a ProDOS-format date. + * + * CiderPress uses kDateInvalid==-1 and kDateNone==-2. + */ +/*static*/ A2FileProDOS::ProDate +A2FileProDOS::ConvertProDate(time_t unixDate) +{ + ProDate proDate; + unsigned long prodosDate, prodosTime; + struct tm* ptm; + int year; + + if (unixDate == 0 || unixDate == -1 || unixDate == -2) + return 0; + + ptm = localtime(&unixDate); + if (ptm == nil) + return 0; // must've been invalid or unspecified + + year = ptm->tm_year; +#ifdef OLD_PRODOS_DATES + /* ProSel-16 volume repair complaints about dates < 1980 and >= Y2K */ + if (year > 100) + year -= 20; +#endif + + if (year >= 100) + year -= 100; + if (year < 0 || year >= 128) { + WMSG2("WHOOPS: got year %d from %d\n", year, ptm->tm_year); + year = 70; + } + + prodosDate = year << 9 | (ptm->tm_mon+1) << 5 | ptm->tm_mday; + prodosTime = ptm->tm_hour << 8 | ptm->tm_min; + + proDate = prodosTime << 16 | prodosDate; + return proDate; +} + + +/* + * Return the file creation time as a time_t. + */ +time_t +A2FileProDOS::GetCreateWhen(void) const +{ + return ConvertProDate(fDirEntry.createWhen); +} + +/* + * Return the file modification time as a time_t. + */ +time_t +A2FileProDOS::GetModWhen(void) const +{ + return ConvertProDate(fDirEntry.modWhen); +} + +/* + * Set the full pathname to a combination of the base path and the + * current file's name. + * + * If we're in the volume directory, pass in "" for the base path (not nil). + */ +void +A2FileProDOS::SetPathName(const char* basePath, const char* fileName) +{ + assert(basePath != nil && fileName != nil); + if (fPathName != nil) + delete[] fPathName; + + int baseLen = strlen(basePath); + fPathName = new char[baseLen + 1 + strlen(fileName)+1]; + strcpy(fPathName, basePath); + if (baseLen != 0 && + !(baseLen == 1 && basePath[0] == ':')) + { + *(fPathName + baseLen) = kFssep; + baseLen++; + } + strcpy(fPathName + baseLen, fileName); +} + + +/* + * Convert a character in a ProDOS name to lower case. + * + * This is special in that '.' is considered upper case, with ' ' as its + * lower-case counterpart. + */ +/*static*/ char +A2FileProDOS::NameToLower(char ch) +{ + if (ch == '.') + return ' '; + else + return tolower(ch); +} + +/* + * Init the fields in the DirEntry struct from the values in the ProDOS + * directory entry pointed to by "entryBuf". + * + * Deals with lower case conversions on the filename. + */ +/*static*/ void +A2FileProDOS::InitDirEntry(A2FileProDOS::DirEntry* pEntry, + const unsigned char* entryBuf) +{ + int nameLen; + + pEntry->storageType = (entryBuf[0x00] & 0xf0) >> 4; + nameLen = entryBuf[0x00] & 0x0f; + memcpy(pEntry->fileName, &entryBuf[0x01], nameLen); + pEntry->fileName[nameLen] = '\0'; + pEntry->fileType = entryBuf[0x10]; + pEntry->keyPointer = GetShortLE(&entryBuf[0x11]); + pEntry->blocksUsed = GetShortLE(&entryBuf[0x13]); + pEntry->eof = GetLongLE(&entryBuf[0x15]); + pEntry->eof &= 0x00ffffff; + pEntry->createWhen = GetLongLE(&entryBuf[0x18]); + pEntry->version = entryBuf[0x1c]; + pEntry->minVersion = entryBuf[0x1d]; + pEntry->access = entryBuf[0x1e]; + pEntry->auxType = GetShortLE(&entryBuf[0x1f]); + pEntry->modWhen = GetLongLE(&entryBuf[0x21]); + pEntry->headerPointer = GetShortLE(&entryBuf[0x25]); + + /* generate the name into the buffer; does not null-terminate */ + if (UsesAppleWorksAuxType(pEntry->fileType)) { + DiskFSProDOS::GenerateLowerCaseName(pEntry->fileName, pEntry->fileName, + pEntry->auxType, true); + } else if (pEntry->minVersion & 0x80) { + DiskFSProDOS::GenerateLowerCaseName(pEntry->fileName, pEntry->fileName, + GetShortLE(&entryBuf[0x1c]), false); + } + pEntry->fileName[sizeof(pEntry->fileName)-1] = '\0'; +} + + +/* + * Open one fork of this file. + * + * I really, really dislike forked files. + */ +DIError +A2FileProDOS::Open(A2FileDescr** ppOpenFile, bool readOnly, + bool rsrcFork /*= false*/) +{ + DIError dierr = kDIErrNone; + A2FDProDOS* pOpenFile = nil; + + WMSG3(" ProDOS Open(ro=%d, rsrc=%d) on '%s'\n", + readOnly, rsrcFork, fPathName); + //Dump(); + + if (!readOnly) { + if (fpDiskFS->GetDiskImg()->GetReadOnly()) + return kDIErrAccessDenied; + if (fpDiskFS->GetFSDamaged()) + return kDIErrBadDiskImage; + } + + if (fpOpenFile != nil) { + dierr = kDIErrAlreadyOpen; + goto bail; + } + if (rsrcFork && fDirEntry.storageType != kStorageExtended) { + dierr = kDIErrForkNotFound; + goto bail; + } + + pOpenFile = new A2FDProDOS(this); + if (pOpenFile == nil) + return kDIErrMalloc; + + pOpenFile->fOpenRsrcFork = false; + + if (fDirEntry.storageType == kStorageExtended) { + if (rsrcFork) { + dierr = LoadBlockList(fExtRsrc.storageType, fExtRsrc.keyBlock, + fExtRsrc.eof, &pOpenFile->fBlockCount, + &pOpenFile->fBlockList); + pOpenFile->fOpenEOF = fExtRsrc.eof; + pOpenFile->fOpenBlocksUsed = fExtRsrc.blocksUsed; + pOpenFile->fOpenStorageType = fExtRsrc.storageType; + pOpenFile->fOpenRsrcFork = true; + } else { + dierr = LoadBlockList(fExtData.storageType, fExtData.keyBlock, + fExtData.eof, &pOpenFile->fBlockCount, + &pOpenFile->fBlockList); + pOpenFile->fOpenEOF = fExtData.eof; + pOpenFile->fOpenBlocksUsed = fExtData.blocksUsed; + pOpenFile->fOpenStorageType = fExtData.storageType; + } + } else if (fDirEntry.storageType == kStorageDirectory || + fDirEntry.storageType == kStorageVolumeDirHeader) + { + dierr = LoadDirectoryBlockList(fDirEntry.keyPointer, + fDirEntry.eof, &pOpenFile->fBlockCount, + &pOpenFile->fBlockList); + pOpenFile->fOpenEOF = fDirEntry.eof; + pOpenFile->fOpenBlocksUsed = fDirEntry.blocksUsed; + pOpenFile->fOpenStorageType = fDirEntry.storageType; + } else if (fDirEntry.storageType == kStorageSeedling || + fDirEntry.storageType == kStorageSapling || + fDirEntry.storageType == kStorageTree) + { + dierr = LoadBlockList(fDirEntry.storageType, fDirEntry.keyPointer, + fDirEntry.eof, &pOpenFile->fBlockCount, + &pOpenFile->fBlockList); + pOpenFile->fOpenEOF = fDirEntry.eof; + pOpenFile->fOpenBlocksUsed = fDirEntry.blocksUsed; + pOpenFile->fOpenStorageType = fDirEntry.storageType; + } else { + WMSG1("PrODOS can't open unknown storage type %d\n", + fDirEntry.storageType); + dierr = kDIErrBadDirectory; + goto bail; + } + if (dierr != kDIErrNone) { + WMSG0(" ProDOS open failed\n"); + goto bail; + } + + pOpenFile->fOffset = 0; + //pOpenFile->DumpBlockList(); + + fpOpenFile = pOpenFile; // add it to our single-member "open file set" + *ppOpenFile = pOpenFile; + pOpenFile = nil; + +bail: + delete pOpenFile; + return dierr; +} + + +/* + * Gather a linear, non-sparse list of file blocks into an array. + * + * Pass in the storage type and top-level key block. Separation of + * extended files should have been handled by the caller. This loads the + * list for only one fork. + * + * There are two kinds of sparse: sparse *inside* data, and sparse + * *past* data. The latter is interesting, because there is no need + * to create space in index blocks to hold it. Thus, a sapling could + * hold a file with an EOF of 16MB. + * + * If "pIndexBlockCount" and "pIndexBlockList" are non-nil, then we + * also accumulate the list of index blocks and return those as well. + * For a Tree-structured file, the first entry in the index list is + * the master index block. + * + * The caller must delete[] "*pBlockList" and "*pIndexBlockList". + */ +DIError +A2FileProDOS::LoadBlockList(int storageType, unsigned short keyBlock, + long eof, long* pBlockCount, unsigned short** pBlockList, + long* pIndexBlockCount, unsigned short** pIndexBlockList) +{ + if (storageType == kStorageDirectory || + storageType == kStorageVolumeDirHeader) + { + assert(pIndexBlockList == nil && pIndexBlockCount == nil); + return LoadDirectoryBlockList(keyBlock, eof, pBlockCount, pBlockList); + } + + assert(keyBlock != 0); + assert(pBlockCount != nil); + assert(pBlockList != nil); + assert(*pBlockList == nil); + if (storageType != kStorageSeedling && + storageType != kStorageSapling && + storageType != kStorageTree) + { + /* + * We can get here if somebody puts a bad storage type inside the + * extended key block of a forked file. Bad storage types on other + * kinds of files are caught earlier. + */ + WMSG2(" ProDOS unexpected storageType %d in '%s'\n", + storageType, GetPathName()); + return kDIErrNotSupported; + } + + DIError dierr = kDIErrNone; + unsigned short* list = nil; + long count; + + assert(eof < 1024*1024*16); + count = (eof + kBlkSize -1) / kBlkSize; + if (count == 0) + count = 1; + list = new unsigned short[count+1]; + if (list == nil) { + dierr = kDIErrMalloc; + goto bail; + } + + if (pIndexBlockList != nil) { + assert(pIndexBlockCount != nil); + assert(*pIndexBlockList == nil); + } + + /* this should take care of trailing sparse entries */ + memset(list, 0, sizeof(unsigned short) * count); + list[count] = kInvalidBlockNum; // overrun check + + if (storageType == kStorageSeedling) { + list[0] = keyBlock; + + if (pIndexBlockList != nil) { + *pIndexBlockCount = 0; + *pIndexBlockList = nil; + } + } else if (storageType == kStorageSapling) { + dierr = LoadIndexBlock(keyBlock, list, count); + if (dierr != kDIErrNone) + goto bail; + + if (pIndexBlockList != nil) { + *pIndexBlockCount = 1; + *pIndexBlockList = new unsigned short[1]; + **pIndexBlockList = keyBlock; + } + } else if (storageType == kStorageTree) { + unsigned char blkBuf[kBlkSize]; + unsigned short* listPtr = list; + unsigned short* outIndexPtr = nil; + long countDown = count; + int idx = 0; + + dierr = fpDiskFS->GetDiskImg()->ReadBlock(keyBlock, blkBuf); + if (dierr != kDIErrNone) + goto bail; + + if (pIndexBlockList != nil) { + int numIndices = (count + kMaxBlocksPerIndex-1) / kMaxBlocksPerIndex; + numIndices++; // add one for the master index block + *pIndexBlockList = new unsigned short[numIndices]; + outIndexPtr = *pIndexBlockList; + *outIndexPtr++ = keyBlock; + *pIndexBlockCount = 1; + } + + while (countDown) { + long blockCount = countDown; + if (blockCount > kMaxBlocksPerIndex) + blockCount = kMaxBlocksPerIndex; + unsigned short idxBlock; + + idxBlock = blkBuf[idx] | (unsigned short) blkBuf[idx+256] << 8; + if (idxBlock == 0) { + /* fully sparse index block */ + //WMSG1(" ProDOS that's seriously sparse (%d)!\n", idx); + memset(listPtr, 0, blockCount * sizeof(unsigned short)); + if (pIndexBlockList != nil) { + *outIndexPtr++ = idxBlock; + (*pIndexBlockCount)++; + } + } else { + dierr = LoadIndexBlock(idxBlock, listPtr, blockCount); + if (dierr != kDIErrNone) + goto bail; + + if (pIndexBlockList != nil) { + *outIndexPtr++ = idxBlock; + (*pIndexBlockCount)++; + } + } + + idx++; + listPtr += blockCount; + countDown -= blockCount; + } + } else { + assert(false); + } + + assert(list[count] == kInvalidBlockNum); + + dierr = ValidateBlockList(list, count); + if (dierr != kDIErrNone) + goto bail; + + *pBlockCount = count; + *pBlockList = list; + +bail: + if (dierr != kDIErrNone) { + delete[] list; + assert(*pBlockList == nil); + + if (pIndexBlockList != nil && *pIndexBlockList != nil) { + delete[] *pIndexBlockList; + *pIndexBlockList = nil; + } + } + return dierr; +} + +/* + * Make sure all values in the block list fall in accepted ranges. + * + * We allow zero (used for sparse blocks), but disallow values in the "system" + * area (block 1 through the end of the usage map). + * + * It's hard to say whether we should compare against the DiskImg block count + * (representing blocks we can physically read but aren't necessarily part + * of the filesystem) or the filesystem "total blocks" value from the volume + * header. Using the one in the volume header is correct, but sometimes the + * value is off on an otherwise reasonable disk. + * + * I'm falling on the side of generosity, allowing files that reference + * potentially bad data to appear okay. My main reason is that, except for + * CFFA volumes that have been tweaked by CiderPress users, very few ProDOS + * disks will have a large disparity between the two numbers unless somebody + * has trashed the volume dir header. + * + * What we really need is three states for each file: good, suspect, damaged. + */ +DIError +A2FileProDOS::ValidateBlockList(const unsigned short* list, long count) +{ + DiskImg* pImg = fpDiskFS->GetDiskImg(); + bool foundBad = false; + + while (count--) { + if (*list > pImg->GetNumBlocks() || + (*list > 0 && *list <= 2)) // not enough, but it'll do + { + WMSG2("Invalid block %d in '%s'\n", *list, fDirEntry.fileName); + SetQuality(kQualityDamaged); + return kDIErrBadFile; + } + if (*list > fpDiskFS->GetFSNumBlocks()) + foundBad = true; + list++; + } + + if (foundBad) { + WMSG1(" --- found out-of-range block in '%s'\n", GetPathName()); + SetQuality(kQualitySuspicious); + } + + return kDIErrNone; +} + +/* + * Copy the entries from the index block in "block" to "list", copying + * at most "maxCount" entries. + */ +DIError +A2FileProDOS::LoadIndexBlock(unsigned short block, unsigned short* list, + int maxCount) +{ + DIError dierr = kDIErrNone; + unsigned char blkBuf[kBlkSize]; + int i; + + if (maxCount > kMaxBlocksPerIndex) + maxCount = kMaxBlocksPerIndex; + + dierr = fpDiskFS->GetDiskImg()->ReadBlock(block, blkBuf); + if (dierr != kDIErrNone) + goto bail; + + //WMSG1("LOADING 0x%04x\n", block); + for (i = 0; i < maxCount; i++) { + *list++ = blkBuf[i] | (unsigned short) blkBuf[i+256] << 8; + } + +bail: + return dierr; +} + +/* + * Load the block list from a directory, which is essentially a linear + * linked list. + */ +DIError +A2FileProDOS::LoadDirectoryBlockList(unsigned short keyBlock, + long eof, long* pBlockCount, unsigned short** pBlockList) +{ + DIError dierr = kDIErrNone; + unsigned char blkBuf[kBlkSize]; + unsigned short* list = nil; + unsigned short* listPtr; + int iterations; + long count; + + assert(eof < 1024*1024*16); + count = (eof + kBlkSize -1) / kBlkSize; + if (count == 0) + count = 1; + list = new unsigned short[count+1]; + if (list == nil) { + dierr = kDIErrMalloc; + goto bail; + } + + /* this should take care of trailing sparse entries */ + memset(list, 0, sizeof(unsigned short) * count); + list[count] = kInvalidBlockNum; // overrun check + + iterations = 0; + listPtr = list; + + while (keyBlock && iterations < kMaxCatalogIterations) { + if (keyBlock < 2 || + keyBlock >= fpDiskFS->GetDiskImg()->GetNumBlocks()) + { + WMSG1(" ProDOS ERROR: directory block %u out of range\n", keyBlock); + dierr = kDIErrInvalidBlock; + goto bail; + } + + *listPtr++ = keyBlock; + + dierr = fpDiskFS->GetDiskImg()->ReadBlock(keyBlock, blkBuf); + if (dierr != kDIErrNone) + goto bail; + + keyBlock = GetShortLE(&blkBuf[0x02]); + iterations++; + } + if (iterations == kMaxCatalogIterations) { + WMSG0(" ProDOS subdir iteration count exceeded\n"); + dierr = kDIErrDirectoryLoop; + goto bail; + } + + assert(list[count] == kInvalidBlockNum); + + *pBlockCount = count; + *pBlockList = list; + +bail: + if (dierr != kDIErrNone) + delete list; + return dierr; +} + +/* + * Dump the contents. + */ +void +A2FileProDOS::Dump(void) const +{ + WMSG2(" ProDOS file '%s' (path='%s')\n", + fDirEntry.fileName, fPathName); + WMSG3(" fileType=0x%02x auxType=0x%04x storage=%d\n", + fDirEntry.fileType, fDirEntry.auxType, fDirEntry.storageType); + WMSG3(" keyPointer=%d blocksUsed=%d eof=%ld\n", + fDirEntry.keyPointer, fDirEntry.blocksUsed, fDirEntry.eof); + WMSG3(" access=0x%02x create=0x%08lx mod=0x%08lx\n", + fDirEntry.access, fDirEntry.createWhen, fDirEntry.modWhen); + WMSG3(" version=%d minVersion=%d headerPtr=%d\n", + fDirEntry.version, fDirEntry.minVersion, fDirEntry.headerPointer); + if (fDirEntry.storageType == kStorageExtended) { + WMSG4(" DATA storage=%d keyBlk=%d blkUsed=%d eof=%ld\n", + fExtData.storageType, fExtData.keyBlock, fExtData.blocksUsed, + fExtData.eof); + WMSG4(" RSRC storage=%d keyBlk=%d blkUsed=%d eof=%ld\n", + fExtRsrc.storageType, fExtRsrc.keyBlock, fExtRsrc.blocksUsed, + fExtRsrc.eof); + } + WMSG2(" * sparseData=%ld sparseRsrc=%ld\n", + (long) fSparseDataEof, (long) fSparseRsrcEof); +} + + +/* + * =========================================================================== + * A2FDProDOS + * =========================================================================== + */ + +/* + * Read a chunk of data from whichever fork is open. + */ +DIError +A2FDProDOS::Read(void* buf, size_t len, size_t* pActual) +{ + WMSG3(" ProDOS reading %d bytes from '%s' (offset=%ld)\n", + len, fpFile->GetPathName(), (long) fOffset); + //if (fBlockList == nil) + // return kDIErrNotReady; + + if (fOffset + (long)len > fOpenEOF) { + if (pActual == nil) + return kDIErrDataUnderrun; + len = (long) (fOpenEOF - fOffset); + } + if (pActual != nil) + *pActual = len; +// + long incrLen = len; + + DIError dierr = kDIErrNone; + unsigned char blkBuf[kBlkSize]; + long blockIndex = (long) (fOffset / kBlkSize); + int bufOffset = (int) (fOffset % kBlkSize); // (& 0x01ff) + size_t thisCount; + long progressCounter = 0; + + if (len == 0) { + ///* one block allocated for empty file */ + //SetLastBlock(fBlockList[0], true); + return kDIErrNone; + } + assert(fOpenEOF != 0); + + assert(blockIndex >= 0 && blockIndex < fBlockCount); + + while (len) { + if (fBlockList[blockIndex] == 0) { + //WMSG1(" ProDOS sparse index %d\n", blockIndex); + memset(blkBuf, 0, sizeof(blkBuf)); + } else { + //WMSG1(" ProDOS non-sparse index %d\n", blockIndex); + dierr = fpFile->GetDiskFS()->GetDiskImg()->ReadBlock(fBlockList[blockIndex], + blkBuf); + if (dierr != kDIErrNone) { + WMSG3(" ProDOS error reading block [%ld]=%d of '%s'\n", + blockIndex, fBlockList[blockIndex], fpFile->GetPathName()); + return dierr; + } + } + thisCount = kBlkSize - bufOffset; + if (thisCount > len) + thisCount = len; + + memcpy(buf, blkBuf + bufOffset, thisCount); + len -= thisCount; + buf = (char*)buf + thisCount; + + bufOffset = 0; + blockIndex++; + + progressCounter++; + if (progressCounter > 100 && len) { + progressCounter = 0; + /* + * Show progress within the current read request. This only + * kicks in for large reads, e.g. reformatting the entire file. + * For smaller reads, used when we're extracting w/o reformatting, + * "progressCounter" never gets large enough. + */ + if (!UpdateProgress(fOffset + incrLen - len)) { + dierr = kDIErrCancelled; + return dierr; + } + //::Sleep(100); // DEBUG DEBUG + } + } + + fOffset += incrLen; + + if (!UpdateProgress(fOffset)) + dierr = kDIErrCancelled; + + return dierr; +} + +/* + * Write data at the current offset. + * + * For simplicity, we assume that there can only be one of two situations: + * (1) We're writing a directory, which might expand by one block; or + * (2) We're writing all of a brand-new file in one shot. + * + * Modifies fOpenEOF, fOpenBlocksUsed, fStorageType, and sets fModified. + * + * HEY: ProSel-16 describes these as fragmented, and it's probably right. + * The correct way to do this is to allocate index blocks before allocating + * the blocks they refer to, so that we don't have to jump all over the disk + * to read the indexes (which, at the moment, appear at the end of the file). + * A bit tricky, but doable. + */ +DIError +A2FDProDOS::Write(const void* buf, size_t len, size_t* pActual) +{ + DIError dierr = kDIErrNone; + A2FileProDOS* pFile = (A2FileProDOS*) fpFile; + DiskFSProDOS* pDiskFS = (DiskFSProDOS*) fpFile->GetDiskFS(); + bool allocSparse = (pDiskFS->GetParameter(DiskFS::kParmProDOS_AllocSparse) != 0); + unsigned char blkBuf[kBlkSize]; + unsigned short keyBlock; + + if (len >= 0x01000000) { // 16MB + assert(false); + return kDIErrInvalidArg; + } + + /* use separate function for directories */ + if (pFile->fDirEntry.storageType == A2FileProDOS::kStorageDirectory || + pFile->fDirEntry.storageType == A2FileProDOS::kStorageVolumeDirHeader) + { + return WriteDirectory(buf, len, pActual); + } + + dierr = pDiskFS->LoadVolBitmap(); + if (dierr != kDIErrNone) + goto bail; + + assert(fOffset == 0); // big simplifying assumption + assert(fOpenEOF == 0); // another one + assert(fOpenBlocksUsed == 1); + assert(buf != nil); + + /* nothing to do for zero-length write; don't even set fModified */ + if (len == 0) + goto bail; + + if (pFile->fDirEntry.storageType != A2FileProDOS::kStorageExtended) + keyBlock = pFile->fDirEntry.keyPointer; + else { + if (fOpenRsrcFork) + keyBlock = pFile->fExtRsrc.keyBlock; + else + keyBlock = pFile->fExtData.keyBlock; + } + + /* + * Special-case seedling files. Just write the data into the key block + * and we're done. + */ + if (len <= (size_t)kBlkSize) { + memset(blkBuf, 0, sizeof(blkBuf)); + memcpy(blkBuf, buf, len); + dierr = pDiskFS->GetDiskImg()->WriteBlock(keyBlock, blkBuf); + if (dierr != kDIErrNone) + goto bail; + + fOpenEOF = len; + fOpenBlocksUsed = 1; + assert(fOpenStorageType == A2FileProDOS::kStorageSeedling); + fOffset += len; + fModified = true; + goto bail; + } + + /* + * Start by allocating space for the block list. The list is always the + * same size, regardless of sparse allocations. + * + * We over-alloc by one so we can have an overrun detection entry. + */ + fBlockCount = (len + kBlkSize-1) / kBlkSize; + assert(fBlockCount > 0); + delete[] fBlockList; + fBlockList = new unsigned short[fBlockCount+1]; + if (fBlockList == nil) { + dierr = kDIErrMalloc; + goto bail; + } + fBlockList[fBlockCount] = A2FileProDOS::kInvalidBlockNum; + + /* + * Write the data blocks to disk, allocating as we go. We have to treat + * the last entry specially because it might not fill an entire block. + */ + const unsigned char* blkPtr; + long blockIdx; + bool allZero; + long progressCounter; + + progressCounter = 0; + allZero = true; + blkPtr = (const unsigned char*) buf; + for (blockIdx = 0; blockIdx < fBlockCount; blockIdx++) { + long newBlock; + + if (blockIdx == fBlockCount-1) { + /* for last block, copy partial and move blkPtr */ + int copyLen = len - (blockIdx * kBlkSize); + assert(copyLen > 0 && copyLen <= kBlkSize); + memset(blkBuf, 0, sizeof(blkBuf)); + memcpy(blkBuf, blkPtr, copyLen); + blkPtr = blkBuf; + } + + if (allocSparse && IsEmptyBlock(blkPtr)) + newBlock = 0; + else { + newBlock = pDiskFS->AllocBlock(); + fOpenBlocksUsed++; + allZero = false; + } + + if (newBlock < 0) { + WMSG0(" ProDOS disk full during write!\n"); + dierr = kDIErrDiskFull; + goto bail; + } + + fBlockList[blockIdx] = (unsigned short) newBlock; + + if (newBlock != 0) { + dierr = pDiskFS->GetDiskImg()->WriteBlock(newBlock, blkPtr); + if (dierr != kDIErrNone) + goto bail; + } + + blkPtr += kBlkSize; + + /* + * Update the progress counter and check to see if the "cancel" button + * has been hit. We don't call UpdateProgress on the last block + * because we could be passing an offset value larger than "len". + * Also, we don't want the progress bar to hit 100% until we've + * actually finished. + * + * We do NOT want to check this after we start writing index blocks. + * If we do, we need to make sure that whatever index blocks the file + * has match up with what we've allocated in the disk block map. + * + * We don't want to save the disk block map if the user cancels here, + * because then the blocks will be marked as "used" even though the + * index blocks for this file haven't been written yet. + * + * It's tricky to get this right, which is why we allocate space + * for the index blocks now -- running out of disk space and + * user cancellation are handled the same way. Once we get to the + * point where we're updating the file structure, we can neither be + * cancelled nor run out of space. (We can still hit a bad block, + * though, which we currently don't handle.) + */ + progressCounter++; // update every N blocks + if (progressCounter > 100 && blockIdx != fBlockCount) { + progressCounter = 0; + if (!UpdateProgress(blockIdx * kBlkSize)) { + dierr = kDIErrCancelled; + goto bail; + } + } + } + + assert(fBlockList[fBlockCount] == A2FileProDOS::kInvalidBlockNum); + + /* + * Now we have a full block map. Allocate any needed index blocks and + * write them. + * + * If our block map is empty, i.e. the entire file is sparse, then + * there's no need to create a sapling. We just leave the file in + * seedling form. This can only happen for a completely empty file. + */ + if (allZero) { + WMSG0("+++ ProDOS storing large but empty file as seedling\n"); + /* make sure key block is empty */ + memset(blkBuf, 0, sizeof(blkBuf)); + dierr = pDiskFS->GetDiskImg()->WriteBlock(keyBlock, blkBuf); + if (dierr != kDIErrNone) + goto bail; + fOpenStorageType = A2FileProDOS::kStorageSeedling; + fBlockList[0] = keyBlock; + } else if (fBlockCount <= 256) { + /* sapling file, write an index block into the key block */ + bool allzero = true; + assert(fBlockCount > 1); + memset(blkBuf, 0, sizeof(blkBuf)); + int i; + for (i = 0; i < fBlockCount; i++) { + if (fBlockList[i] != 0) + allzero = false; + blkBuf[i] = fBlockList[i] & 0xff; + blkBuf[256 + i] = (fBlockList[i] >> 8) & 0xff; + } + + dierr = pDiskFS->GetDiskImg()->WriteBlock(keyBlock, blkBuf); + if (dierr != kDIErrNone) + goto bail; + fOpenStorageType = A2FileProDOS::kStorageSapling; + } else { + /* tree file, write two or more indexes and write master into key */ + unsigned char masterBlk[kBlkSize]; + int idx; + + memset(masterBlk, 0, sizeof(masterBlk)); + + for (idx = 0; idx < fBlockCount; ) { + long newBlock; + int i; + + memset(blkBuf, 0, sizeof(blkBuf)); + for (i = 0; i < 256 && idx < fBlockCount; i++, idx++) { + blkBuf[i] = fBlockList[idx] & 0xff; + blkBuf[256+i] = (fBlockList[idx] >> 8) & 0xff; + } + + /* allocate a new index block, if needed */ + if (allocSparse && IsEmptyBlock(blkBuf)) + newBlock = 0; + else { + newBlock = pDiskFS->AllocBlock(); + fOpenBlocksUsed++; + } + if (newBlock != 0) { + dierr = pDiskFS->GetDiskImg()->WriteBlock(newBlock, blkBuf); + if (dierr != kDIErrNone) + goto bail; + } + + masterBlk[(idx-1) / 256] = (unsigned char) newBlock; + masterBlk[256 + (idx-1)/256] = (unsigned char) (newBlock >> 8); + } + + dierr = pDiskFS->GetDiskImg()->WriteBlock(keyBlock, masterBlk); + if (dierr != kDIErrNone) + goto bail; + fOpenStorageType = A2FileProDOS::kStorageTree; + } + + fOpenEOF = len; + fOffset += len; + fModified = true; + +bail: + if (dierr == kDIErrNone) + dierr = pDiskFS->SaveVolBitmap(); + + /* + * We need to check UpdateProgress *after* the volume bitmap has been + * saved. Otherwise we'll have blocks allocated in the file's structure + * but not marked in-use in the map when the "dierr" check above fails. + */ + if (dierr == kDIErrNone) { + if (!UpdateProgress(fOffset)) + dierr = kDIErrCancelled; + } + + pDiskFS->FreeVolBitmap(); + return dierr; +} + +/* + * Determine whether a block is filled entirely with zeroes. + */ +bool +A2FDProDOS::IsEmptyBlock(const unsigned char* blk) +{ + int i; + + for (i = 0; i < kBlkSize; i++) { + if (*blk++ != 0) + return false; + } + + return true; +} + +/* + * Write a directory, possibly extending it by one block. + * + * If we're growing, the extra block will already have been allocated, and is + * pointed to by the "next" pointer in the next-to-last block. (This + * pre-allocation makes our lives easier, and avoids a situation where we + * would have to update the volume bitmap when another function is already + * making lots of changes to it.) + */ +DIError +A2FDProDOS::WriteDirectory(const void* buf, size_t len, size_t* pActual) +{ + DIError dierr = kDIErrNone; + + WMSG2("ProDOS writing %d bytes to directory '%s'\n", + len, fpFile->GetPathName()); + + assert(len >= (size_t)kBlkSize); + assert((len % kBlkSize) == 0); + assert(len == (size_t)fOpenEOF || len == (size_t)fOpenEOF + kBlkSize); + + if (len > (size_t)fOpenEOF) { + /* + * Extend the block list, remembering that we add an extra item + * on the end to check for overruns. + */ + unsigned short* newBlockList; + + fBlockCount++; + newBlockList = new unsigned short[fBlockCount+1]; + memcpy(newBlockList, fBlockList, + sizeof(unsigned short) * fBlockCount); + newBlockList[fBlockCount] = A2FileProDOS::kInvalidBlockNum; + + unsigned char* blkPtr; + blkPtr = (unsigned char*)buf + fOpenEOF - kBlkSize; + assert(blkPtr >= buf); + assert(GetShortLE(&blkPtr[0x02]) != 0); + newBlockList[fBlockCount-1] = GetShortLE(&blkPtr[0x02]); + + delete[] fBlockList; + fBlockList = newBlockList; + + WMSG0(" ProDOS updated block list for subdir:\n"); + DumpBlockList(); + } + + /* + * Now just run down the block list writing the directory. + */ + assert(len == (size_t)fBlockCount * kBlkSize); + int idx; + for (idx = 0; idx < fBlockCount; idx++) { + assert(fBlockList[idx] >= kVolHeaderBlock); + dierr = fpFile->GetDiskFS()->GetDiskImg()->WriteBlock(fBlockList[idx], + (unsigned char*)buf + idx * kBlkSize); + if (dierr != kDIErrNone) { + WMSG1(" ProDOS failed writing dir, block=%d\n", fBlockList[idx]); + goto bail; + } + } + + fOpenEOF = len; + fOpenBlocksUsed = (unsigned short) fBlockCount; // very simple for subdirs + //fOpenStorageType + fModified = true; + +bail: + return dierr; +} + +/* + * Seek to a new position within the file. + */ +DIError +A2FDProDOS::Seek(di_off_t offset, DIWhence whence) +{ + DIError dierr = kDIErrNone; + switch (whence) { + case kSeekSet: + if (offset < 0 || offset > fOpenEOF) + return kDIErrInvalidArg; + fOffset = offset; + break; + case kSeekEnd: + if (offset > 0 || offset < -fOpenEOF) + return kDIErrInvalidArg; + fOffset = fOpenEOF + offset; + break; + case kSeekCur: + if (offset < -fOffset || + offset >= (fOpenEOF - fOffset)) + { + return kDIErrInvalidArg; + } + fOffset += offset; + break; + default: + assert(false); + return kDIErrInvalidArg; + } + + assert(fOffset >= 0 && fOffset <= fOpenEOF); + + return dierr; +} + +/* + * Return current offset. + */ +di_off_t +A2FDProDOS::Tell(void) +{ + //if (fBlockList == nil) + // return kDIErrNotReady; + + return fOffset; +} + +/* + * Release file state. + * + * Most applications don't check the value of "Close", or call it from a + * destructor, so we call CloseDescr whether we succeed or not. + */ +DIError +A2FDProDOS::Close(void) +{ + DIError dierr = kDIErrNone; + + if (fModified) { + A2FileProDOS* pFile = (A2FileProDOS*) fpFile; + unsigned char blkBuf[kBlkSize]; + unsigned char newStorageType = fOpenStorageType; + unsigned short newBlocksUsed = fOpenBlocksUsed; + unsigned long newEOF = (unsigned long) fOpenEOF; + unsigned short combinedBlocksUsed; + unsigned long combinedEOF; + + /* + * If this is an extended file, fix the entries in the extended + * key block, and adjust the values to be stored in the directory. + */ + if (pFile->fDirEntry.storageType == A2FileProDOS::kStorageExtended) { + /* these two don't change */ + newStorageType = pFile->fDirEntry.storageType; + + dierr = fpFile->GetDiskFS()->GetDiskImg()->ReadBlock( + pFile->fDirEntry.keyPointer, blkBuf); + if (dierr != kDIErrNone) + goto bail; + + int offset = 0; + if (fOpenRsrcFork) + offset = 256; + + blkBuf[0x00 + offset] = fOpenStorageType; + // key block doesn't change + PutShortLE(&blkBuf[0x03 + offset], newBlocksUsed); + blkBuf[0x05 + offset] = (unsigned char) newEOF; + blkBuf[0x06 + offset] = (unsigned char) (newEOF >> 8); + blkBuf[0x07 + offset] = (unsigned char) (newEOF >> 16); + + dierr = fpFile->GetDiskFS()->GetDiskImg()->WriteBlock( + pFile->fDirEntry.keyPointer, blkBuf); + if (dierr != kDIErrNone) + goto bail; + + // file blocks used is sum of data and rsrc block counts +1 for key + combinedBlocksUsed = + GetShortLE(&blkBuf[0x03]) + GetShortLE(&blkBuf[0x103]) +1; + combinedEOF = 512; // for some reason this gets stuffed in + } else { + combinedBlocksUsed = newBlocksUsed; + combinedEOF = newEOF; + } + + /* + * Update fields in the file's directory entry. Unless, of course, + * this is the volume directory itself. + */ + if (pFile->fParentDirBlock != 0) { + dierr = fpFile->GetDiskFS()->GetDiskImg()->ReadBlock( + pFile->fParentDirBlock, blkBuf); + if (dierr != kDIErrNone) + goto bail; + + unsigned char* pParentPtr; + pParentPtr = blkBuf + 0x04 + pFile->fParentDirIdx * kEntryLength; + assert(pParentPtr + kEntryLength < blkBuf + kBlkSize); + if (toupper(pParentPtr[0x01]) != toupper(pFile->fDirEntry.fileName[0])) + { + WMSG0("ProDOS ERROR: parent pointer has wrong entry??\n"); + assert(false); + dierr = kDIErrInternal; + goto bail; + } + + /* update the fields from the open file */ + pParentPtr[0x00] = + (pParentPtr[0x00] & 0x0f) | (newStorageType << 4); + PutShortLE(&pParentPtr[0x13], combinedBlocksUsed); + if (pFile->fDirEntry.storageType != A2FileProDOS::kStorageExtended) + { + PutShortLE(&pParentPtr[0x15], (unsigned short) newEOF); + pParentPtr[0x17] = (unsigned char) (newEOF >> 16); + } + /* don't update the mod date for now */ + //PutLongLE(&pParentPtr[0x21], A2FileProDOS::ConvertProDate(time(nil))); + + dierr = fpFile->GetDiskFS()->GetDiskImg()->WriteBlock( + pFile->fParentDirBlock, blkBuf); + if (dierr != kDIErrNone) + goto bail; + } + + /* + * Find the #of sparse blocks. + */ + int sparseCount = 0; + for (int i = 0; i < fBlockCount; i++) { + if (fBlockList[i] == 0) + sparseCount++; + } + + /* + * Update our internal copies of stuff. The EOFs have changed, and + * in theory we'd want to update the modification date. In practice + * we're usually shuffling data from one archive to another and want + * to preserve the mod date. (Could be a DiskFS global pref?) + */ + pFile->fDirEntry.storageType = newStorageType; + pFile->fDirEntry.blocksUsed = combinedBlocksUsed; + pFile->fDirEntry.eof = combinedEOF; + + if (newStorageType == A2FileProDOS::kStorageExtended) { + if (!fOpenRsrcFork) { + pFile->fExtData.storageType = fOpenStorageType; + pFile->fExtData.blocksUsed = newBlocksUsed; + pFile->fExtData.eof = newEOF; + pFile->fSparseDataEof = (di_off_t) newEOF - (sparseCount * kBlkSize); + if (pFile->fSparseDataEof < 0) + pFile->fSparseDataEof = 0; + } else { + pFile->fExtRsrc.storageType = fOpenStorageType; + pFile->fExtRsrc.blocksUsed = newBlocksUsed; + pFile->fExtRsrc.eof = newEOF; + pFile->fSparseRsrcEof = (di_off_t) newEOF - (sparseCount * kBlkSize); + if (pFile->fSparseRsrcEof < 0) + pFile->fSparseRsrcEof = 0; + } + } else { + pFile->fSparseDataEof = (di_off_t) newEOF - (sparseCount * kBlkSize); + if (pFile->fSparseDataEof < 0) + pFile->fSparseDataEof = 0; + } + // update mod date? + + //WMSG1("File '%s' closed\n", pFile->GetPathName()); + //pFile->Dump(); + } + +bail: + fpFile->CloseDescr(this); + return dierr; +} + + +/* + * Return the #of sectors/blocks in the file. + */ +long +A2FDProDOS::GetSectorCount(void) const +{ + //if (fBlockList == nil) + // return kDIErrNotReady; + return fBlockCount * 2; +} +long +A2FDProDOS::GetBlockCount(void) const +{ + //if (fBlockList == nil) + // return kDIErrNotReady; + return fBlockCount; +} + +/* + * Return the Nth track/sector in this file. + */ +DIError +A2FDProDOS::GetStorage(long sectorIdx, long* pTrack, long* pSector) const +{ + //if (fBlockList == nil) + // return kDIErrNotReady; + long prodosIdx = sectorIdx / 2; + if (prodosIdx < 0 || prodosIdx >= fBlockCount) + return kDIErrInvalidIndex; + long prodosBlock = fBlockList[prodosIdx]; + + if (prodosBlock == 0) + *pTrack = *pSector = 0; // special-case to avoid returning (0,1) + else + BlockToTrackSector(prodosBlock, (sectorIdx & 0x01) != 0, pTrack, pSector); + return kDIErrNone; +} +/* + * Return the Nth 512-byte block in this file. + */ +DIError +A2FDProDOS::GetStorage(long blockIdx, long* pBlock) const +{ + //if (fBlockList == nil) + // return kDIErrNotReady; + if (blockIdx < 0 || blockIdx >= fBlockCount) + return kDIErrInvalidIndex; + long prodosBlock = fBlockList[blockIdx]; + + *pBlock = prodosBlock; + assert(*pBlock < fpFile->GetDiskFS()->GetDiskImg()->GetNumBlocks()); + return kDIErrNone; +} + +/* + * Dump the list of blocks from an open file, skipping over + * "sparsed-out" entries. + */ +void +A2FDProDOS::DumpBlockList(void) const +{ + long ll; + + WMSG1(" ProDOS file block list (count=%ld)\n", fBlockCount); + for (ll = 0; ll <= fBlockCount; ll++) { + if (fBlockList[ll] != 0) { + WMSG2(" %5ld: 0x%04x\n", ll, fBlockList[ll]); + } + } +} diff --git a/diskimg/RDOS.cpp b/diskimg/RDOS.cpp new file mode 100644 index 0000000..b9e8afb --- /dev/null +++ b/diskimg/RDOS.cpp @@ -0,0 +1,739 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Implementation of DiskFSRDOS class. + */ +#include "StdAfx.h" +#include "DiskImgPriv.h" + + +/* + * =========================================================================== + * DiskFSRDOS + * =========================================================================== + */ + +const int kSctSize = 256; +const int kCatTrack = 1; +const int kNumCatSectors = 11; // 0 through 10 +const int kDirectoryEntryLen = 32; +const int kNumDirEntryPerSect = (256 / kDirectoryEntryLen); // 8 + +/* + * See if this looks like a RDOS volume. + * + * There are three variants: + * RDOS32 (e.g. ComputerAmbush.nib): + * 13-sector disk + * sector (1,0) starts with "RDOS 2" + * sector (1,12) has catalog code, CHAIN in (1,11) + * uses "physical" ordering + * NOTE: track 0 may be unreadable with RDOS 3.2 NibbleDescr + * RDOS33 (e.g. disk #199): + * 16-sector disk + * sector (1,0) starts with "RDOS 3" + * sector (1,12) has catalog code + * uses "ProDOS" ordering + * RDOS3 (e.g. disk #108): + * 16-sector disk, but only 13 sectors of each track are used + * sector (1,0) starts with "RDOS 2" + * sector (0,1) has catalog code + * uses "physical" orering + * + * In all cases: + * catalog found on (1,0) through (1,10) + * + * The initial value of "pFormatFound" is ignored, because we can reliably + * detect which variant we're looking at. + */ +static DIError +TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder, + DiskImg::FSFormat* pFormatFound) +{ + DIError dierr = kDIErrNone; + unsigned char sctBuf[kSctSize]; + + + if (pImg->GetNumSectPerTrack() == 13) { + /* must be a nibble image; check it for RDOS 3.2 */ + dierr = pImg->ReadTrackSectorSwapped(kCatTrack, 0, sctBuf, + imageOrder, DiskImg::kSectorOrderPhysical); + if (dierr != kDIErrNone) + goto bail; + } else if (pImg->GetNumSectPerTrack() == 16) { + /* could be RDOS3 or RDOS 3.3 */ + dierr = pImg->ReadTrackSectorSwapped(kCatTrack, 0, sctBuf, + imageOrder, DiskImg::kSectorOrderPhysical); + if (dierr != kDIErrNone) + goto bail; + } else { + WMSG0(" RDOS neither 13 nor 16 sector, bailing\n"); + goto bail; + } + + /* check for RDOS string and correct #of blocks */ + if (!( sctBuf[0] == 'R'+0x80 && + sctBuf[1] == 'D'+0x80 && + sctBuf[2] == 'O'+0x80 && + sctBuf[3] == 'S'+0x80 && + sctBuf[4] == ' '+0x80) || + !(sctBuf[25] == 26 || sctBuf[25] == 32)) + { + WMSG1(" RDOS no signature found on (%d,0)\n", kCatTrack); + dierr = kDIErrGeneric; + goto bail; + } + + /* + * Guess at the format based on the first catalog entry, which usually + * begins "RDOS 2.0", "RDOS 2.1", or "RDOS 3.3". + */ + if (pImg->GetNumSectPerTrack() == 13) { + *pFormatFound = DiskImg::kFormatRDOS32; + } else { + if (sctBuf[5] == '2'+0x80) + *pFormatFound = DiskImg::kFormatRDOS3; + else + *pFormatFound = DiskImg::kFormatRDOS33; + } + + /* + * The above came from sector 0, which doesn't help us figure out the + * sector ordering. Look for the catalog code. + */ + { + int track, sector, offset; + unsigned char orMask; + static const char* kCompare = ""; + DiskImg::SectorOrder order; + + if (*pFormatFound == DiskImg::kFormatRDOS32 || + *pFormatFound == DiskImg::kFormatRDOS3) + { + track = 1; + sector = 12; + offset = 0xa2; + orMask = 0x80; + order = DiskImg::kSectorOrderPhysical; + } else { + track = 0; + sector = 1; + offset = 0x98; + orMask = 0; + order = DiskImg::kSectorOrderProDOS; + } + + dierr = pImg->ReadTrackSectorSwapped(track, sector, sctBuf, + imageOrder, order); + if (dierr != kDIErrNone) + goto bail; + + int i; + for (i = strlen(kCompare)-1; i >= 0; i--) { + if (sctBuf[offset+i] != ((unsigned char)kCompare[i] | orMask)) + break; + } + if (i >= 0) { + dierr = kDIErrGeneric; + goto bail; + } + + WMSG2(" RDOS found '%s' signature (order=%d)\n", kCompare, imageOrder); + } + + dierr = kDIErrNone; + +bail: + return dierr; +} + +/* + * Common RDOS test code. + */ +/*static*/ DIError +DiskFSRDOS::TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency) +{ + if (!pImg->GetHasSectors()) { + WMSG0(" RDOS - image doesn't have sectors, not trying\n"); + return kDIErrFilesystemNotFound; + } + if (pImg->GetNumTracks() != 35) { + WMSG0(" RDOS - not a 35-track disk, not trying\n"); + return kDIErrFilesystemNotFound; + } + DiskImg::FSFormat formatFound; + + DiskImg::SectorOrder ordering[DiskImg::kSectorOrderMax]; + + DiskImg::GetSectorOrderArray(ordering, *pOrder); + + for (int i = 0; i < DiskImg::kSectorOrderMax; i++) { + if (ordering[i] == DiskImg::kSectorOrderUnknown) + continue; + if (TestImage(pImg, ordering[i], &formatFound) == kDIErrNone) { + *pFormat = formatFound; + *pOrder = ordering[i]; + //*pFormat = DiskImg::kFormatXXX; + return kDIErrNone; + } + } + + WMSG0(" RDOS didn't find valid FS\n"); + return kDIErrFilesystemNotFound; +} + +#if 0 +/* + * Test to see if the image is an RDOS 3.3 disk. + */ +/*static*/ DIError +DiskFSRDOS::TestFS33(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + FSLeniency leniency) +{ + DIError dierr; + DiskImg::FSFormat formatFound = DiskImg::kFormatUnknown; + + dierr = TestCommon(pImg, pOrder, leniency, &formatFound); + if (dierr != kDIErrNone) + return dierr; + if (formatFound != DiskImg::kFormatRDOS33) { + WMSG0(" RDOS found RDOS but wrong type\n"); + return kDIErrFilesystemNotFound; + } + + return kDIErrNone; +} + +/* + * Test to see if the image is an RDOS 3.2 disk. + */ +/*static*/ DIError +DiskFSRDOS::TestFS32(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + FSLeniency leniency) +{ + DIError dierr; + DiskImg::FSFormat formatFound = DiskImg::kFormatUnknown; + + dierr = TestCommon(pImg, pOrder, leniency, &formatFound); + if (dierr != kDIErrNone) + return dierr; + if (formatFound != DiskImg::kFormatRDOS32) { + WMSG0(" RDOS found RDOS but wrong type\n"); + return kDIErrFilesystemNotFound; + } + + return kDIErrNone; +} + +/* + * Test to see if the image is an RDOS 3 (cracked 3.2) disk. + */ +/*static*/ DIError +DiskFSRDOS::TestFS3(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + FSLeniency leniency) +{ + DIError dierr; + DiskImg::FSFormat formatFound = DiskImg::kFormatUnknown; + + dierr = TestCommon(pImg, pOrder, leniency, &formatFound); + if (dierr != kDIErrNone) + return dierr; + if (formatFound != DiskImg::kFormatRDOS3) { + WMSG0(" RDOS found RDOS but wrong type\n"); + return kDIErrFilesystemNotFound; + } + + return kDIErrNone; +} +#endif + + +/* + * Get things rolling. + * + * Since we're assured that this is a valid disk, errors encountered from here + * on out must be handled somehow, possibly by claiming that the disk is + * completely full and has no files on it. + */ +DIError +DiskFSRDOS::Initialize(void) +{ + DIError dierr = kDIErrNone; + const char* volStr; + + switch (GetDiskImg()->GetFSFormat()) { + case DiskImg::kFormatRDOS33: + volStr = "RDOS 3.3"; + fOurSectPerTrack = 16; + break; + case DiskImg::kFormatRDOS32: + volStr = "RDOS 3.2"; + fOurSectPerTrack = 13; + break; + case DiskImg::kFormatRDOS3: + volStr = "RDOS 3"; + fOurSectPerTrack = 13; + break; + default: + assert(false); + return kDIErrInternal; + } + assert(strlen(volStr) < sizeof(fVolumeName)); + strcpy(fVolumeName, volStr); + + dierr = ReadCatalog(); + if (dierr != kDIErrNone) + goto bail; + + fVolumeUsage.Create(fpImg->GetNumTracks(), fOurSectPerTrack); + dierr = ScanFileUsage(); + if (dierr != kDIErrNone) { + /* this might not be fatal; just means that *some* files are bad */ + goto bail; + } + fVolumeUsage.Dump(); + + //A2File* pFile; + //pFile = GetNextFile(nil); + //while (pFile != nil) { + // pFile->Dump(); + // pFile = GetNextFile(pFile); + //} + +bail: + return dierr; +} + + +/* + * Read the catalog from the disk. + * + * To make life easy we slurp the whole thing into memory. + */ +DIError +DiskFSRDOS::ReadCatalog(void) +{ + DIError dierr = kDIErrNone; + unsigned char* dir = nil; + unsigned char* dirPtr; + int track, sector; + + dir = new unsigned char[kSctSize * kNumCatSectors]; + if (dir == nil) { + dierr = kDIErrMalloc; + goto bail; + } + + track = kCatTrack; + dirPtr = dir; + for (sector = 0; sector < kNumCatSectors; sector++) { + dierr = fpImg->ReadTrackSector(track, sector, dirPtr); + if (dierr != kDIErrNone) + goto bail; + + dirPtr += kSctSize; + } + + int i; + A2FileRDOS* pFile; + dirPtr = dir; + for (i = 0; i < kNumCatSectors * kNumDirEntryPerSect; + i++, dirPtr += kDirectoryEntryLen) + { + if (dirPtr[0] == 0x80 || dirPtr[24] == 0xa0) // deleted file + continue; + if (dirPtr[24] == 0x00) // unused entry; must be at end of catalog + break; + + pFile = new A2FileRDOS(this); + + memcpy(pFile->fFileName, dirPtr, A2FileRDOS::kMaxFileName); + pFile->fFileName[A2FileRDOS::kMaxFileName] = '\0'; + pFile->FixFilename(); + + switch (dirPtr[24]) { + case 'A'+0x80: pFile->fFileType = A2FileRDOS::kTypeApplesoft; break; + case 'B'+0x80: pFile->fFileType = A2FileRDOS::kTypeBinary; break; + case 'T'+0x80: pFile->fFileType = A2FileRDOS::kTypeText; break; + // 0x00 is end of catalog, ' '+0x80 is deleted file, both handled above + default: pFile->fFileType = A2FileRDOS::kTypeUnknown; break; + } + pFile->fNumSectors = dirPtr[25]; + pFile->fLoadAddr = GetShortLE(&dirPtr[26]); + pFile->fLength = GetShortLE(&dirPtr[28]); + pFile->fStartSector = GetShortLE(&dirPtr[30]); + + if (pFile->fStartSector + pFile->fNumSectors > + fpImg->GetNumTracks() * fOurSectPerTrack) + { + WMSG4(" RDOS invalid start/count (%d + %d) (max %ld) '%s'\n", + pFile->fStartSector, pFile->fNumSectors, fpImg->GetNumBlocks(), + pFile->fFileName); + pFile->fStartSector = pFile->fNumSectors = 0; + pFile->fLength = 0; + pFile->SetQuality(A2File::kQualityDamaged); + } + + AddFileToList(pFile); + } + +bail: + delete[] dir; + return dierr; +} + + +/* + * Create the volume usage map. Since RDOS volumes have neither + * in-use maps nor index blocks, this is pretty straightforward. + */ +DIError +DiskFSRDOS::ScanFileUsage(void) +{ + int track, sector, block, count; + + A2FileRDOS* pFile; + pFile = (A2FileRDOS*) GetNextFile(nil); + while (pFile != nil) { + block = pFile->fStartSector; + count = pFile->fNumSectors; + while (count--) { + track = block / fOurSectPerTrack; + sector = block % fOurSectPerTrack; + + SetSectorUsage(track, sector, VolumeUsage::kChunkPurposeUserData); + + block++; + } + + pFile = (A2FileRDOS*) GetNextFile(pFile); + } + + return kDIErrNone; +} + +/* + * Update an entry in the usage map. + */ +void +DiskFSRDOS::SetSectorUsage(long track, long sector, + VolumeUsage::ChunkPurpose purpose) +{ + VolumeUsage::ChunkState cstate; + + fVolumeUsage.GetChunkState(track, sector, &cstate); + if (cstate.isUsed) { + cstate.purpose = VolumeUsage::kChunkPurposeConflict; + WMSG2(" RDOS conflicting uses for sct=(%ld,%ld)\n", track, sector); + } else { + cstate.isUsed = true; + cstate.isMarkedUsed = true; + cstate.purpose = purpose; + } + fVolumeUsage.SetChunkState(track, sector, &cstate); +} + + +/* + * =========================================================================== + * A2FileRDOS + * =========================================================================== + */ + +/* + * Convert RDOS file type to ProDOS file type. + */ +long +A2FileRDOS::GetFileType(void) const +{ + long retval; + + switch (fFileType) { + case kTypeText: retval = 0x04; break; // TXT + case kTypeApplesoft: retval = 0xfc; break; // BAS + case kTypeBinary: retval = 0x06; break; // BIN + case kTypeUnknown: + default: retval = 0x00; break; // NON + } + + return retval; +} + + +/* + * Dump the contents of the A2File structure. + */ +void +A2FileRDOS::Dump(void) const +{ + WMSG2("A2FileRDOS '%s' (type=%d)\n", fFileName, fFileType); + WMSG4(" start=%d num=%d len=%d addr=0x%04x\n", + fStartSector, fNumSectors, fLength, fLoadAddr); +} + +/* + * "Fix" an RDOS filename. Convert DOS-ASCII to normal ASCII, and strip + * trailing spaces. + * + * It's possible that RDOS 3.3 forces the filename to high-ASCII, because + * one disk (#938A) has a file left by the crackers whose name is in + * low-ASCII. The inverse-mode correction turns it into punctuation, but + * I don't see a good way around it. Or any particular need to fix it. + */ +void +A2FileRDOS::FixFilename(void) +{ + DiskFSDOS33::LowerASCII((unsigned char*)fFileName, kMaxFileName); + TrimTrailingSpaces(fFileName); +} + +/* + * Trim the spaces off the end of a filename. + * + * Assumes the filename has already been converted to low ASCII. + */ +void +A2FileRDOS::TrimTrailingSpaces(char* filename) +{ + char* lastspc = filename + strlen(filename); + + assert(*lastspc == '\0'); + + while (--lastspc) { + if (*lastspc != ' ') + break; + } + + *(lastspc+1) = '\0'; +} + + +/* + * Not a whole lot to do, since there's no fancy index blocks. + */ +DIError +A2FileRDOS::Open(A2FileDescr** ppOpenFile, bool readOnly, + bool rsrcFork /*=false*/) +{ + if (fpOpenFile != nil) + return kDIErrAlreadyOpen; + if (rsrcFork) + return kDIErrForkNotFound; + assert(readOnly == true); + + A2FDRDOS* pOpenFile = new A2FDRDOS(this); + + pOpenFile->fOffset = 0; + //fOpen = true; + + fpOpenFile = pOpenFile; + *ppOpenFile = pOpenFile; + pOpenFile = nil; + + return kDIErrNone; +} + + +/* + * =========================================================================== + * A2FDRDOS + * =========================================================================== + */ + +/* + * Read a chunk of data from the current offset. + */ +DIError +A2FDRDOS::Read(void* buf, size_t len, size_t* pActual) +{ + WMSG3(" RDOS reading %d bytes from '%s' (offset=%ld)\n", + len, fpFile->GetPathName(), (long) fOffset); + //if (!fOpen) + // return kDIErrNotReady; + + A2FileRDOS* pFile = (A2FileRDOS*) fpFile; + + /* don't allow them to read past the end of the file */ + if (fOffset + (long)len > pFile->fLength) { + if (pActual == nil) + return kDIErrDataUnderrun; + len = (size_t) (pFile->fLength - fOffset); + } + if (pActual != nil) + *pActual = len; + long incrLen = len; + + DIError dierr = kDIErrNone; + unsigned char sctBuf[kSctSize]; + long block = pFile->fStartSector + (long) (fOffset / kSctSize); + int bufOffset = (int) (fOffset % kSctSize); // (& 0xff) + int ourSectPerTrack = GetOurSectPerTrack(); + size_t thisCount; + + if (len == 0) { + ///* one block allocated for empty file */ + //SetLastBlock(block, true); + return kDIErrNone; + } + assert(pFile->fLength != 0); + + while (len) { + assert(block >= pFile->fStartSector && + block < pFile->fStartSector + pFile->fNumSectors); + + dierr = pFile->GetDiskFS()->GetDiskImg()->ReadTrackSector(block / ourSectPerTrack, + block % ourSectPerTrack, sctBuf); + if (dierr != kDIErrNone) { + WMSG1(" RDOS error reading file '%s'\n", pFile->fFileName); + return dierr; + } + thisCount = kSctSize - bufOffset; + if (thisCount > len) + thisCount = len; + + memcpy(buf, sctBuf + bufOffset, thisCount); + len -= thisCount; + buf = (char*)buf + thisCount; + + bufOffset = 0; + block++; + } + + fOffset += incrLen; + + return dierr; +} + +/* + * Write data at the current offset. + */ +DIError +A2FDRDOS::Write(const void* buf, size_t len, size_t* pActual) +{ + //if (!fOpen) + // return kDIErrNotReady; + return kDIErrNotSupported; +} + +/* + * Seek to a new offset. + */ +DIError +A2FDRDOS::Seek(di_off_t offset, DIWhence whence) +{ + //if (!fOpen) + // return kDIErrNotReady; + + long fileLen = ((A2FileRDOS*) fpFile)->fLength; + + switch (whence) { + case kSeekSet: + if (offset < 0 || offset > fileLen) + return kDIErrInvalidArg; + fOffset = offset; + break; + case kSeekEnd: + if (offset > 0 || offset < -fileLen) + return kDIErrInvalidArg; + fOffset = fileLen + offset; + break; + case kSeekCur: + if (offset < -fOffset || + offset >= (fileLen - fOffset)) + { + return kDIErrInvalidArg; + } + fOffset += offset; + break; + default: + assert(false); + return kDIErrInvalidArg; + } + + assert(fOffset >= 0 && fOffset <= fileLen); + return kDIErrNone; +} + +/* + * Return current offset. + */ +di_off_t +A2FDRDOS::Tell(void) +{ + //if (!fOpen) + // return kDIErrNotReady; + + return fOffset; +} + +/* + * Release file state, such as it is. + */ +DIError +A2FDRDOS::Close(void) +{ + fpFile->CloseDescr(this); + return kDIErrNone; +} + +/* + * Return the #of sectors/blocks in the file. + */ +long +A2FDRDOS::GetSectorCount(void) const +{ + //if (!fOpen) + // return kDIErrNotReady; + return ((A2FileRDOS*) fpFile)->fNumSectors; +} +long +A2FDRDOS::GetBlockCount(void) const +{ + //if (!fOpen) + // return kDIErrNotReady; + return ((A2FileRDOS*) fpFile)->fNumSectors / 2; +} + +/* + * Return the Nth track/sector in this file. + */ +DIError +A2FDRDOS::GetStorage(long sectorIdx, long* pTrack, long* pSector) const +{ + //if (!fOpen) + // return kDIErrNotReady; + A2FileRDOS* pFile = (A2FileRDOS*) fpFile; + long rdosBlock = pFile->fStartSector + sectorIdx; + int ourSectPerTrack = GetOurSectPerTrack(); + if (rdosBlock >= pFile->fStartSector + pFile->fNumSectors) + return kDIErrInvalidIndex; + + *pTrack = rdosBlock / ourSectPerTrack; + *pSector = rdosBlock % ourSectPerTrack; + + return kDIErrNone; +} +/* + * Return the Nth 512-byte block in this file. + */ +DIError +A2FDRDOS::GetStorage(long blockIdx, long* pBlock) const +{ + //if (!fOpen) + // return kDIErrNotReady; + A2FileRDOS* pFile = (A2FileRDOS*) fpFile; + long rdosBlock = pFile->fStartSector + blockIdx*2; + if (rdosBlock >= pFile->fStartSector + pFile->fNumSectors) + return kDIErrInvalidIndex; + + *pBlock = rdosBlock / 2; + + if (pFile->GetDiskFS()->GetDiskImg()->GetHasBlocks()) { + assert(*pBlock < pFile->GetDiskFS()->GetDiskImg()->GetNumBlocks()); + } + return kDIErrNone; +} diff --git a/diskimg/SCSIDefs.h b/diskimg/SCSIDefs.h new file mode 100644 index 0000000..f1902e9 --- /dev/null +++ b/diskimg/SCSIDefs.h @@ -0,0 +1,308 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Definitions for SCSI (Small Computer System Interface). + * + * These structures and defines are passed to the SCSI driver, so they work + * equally well for ASPI and SPTI + * + * Consult the SCSI-2 and MMC-2 specifications for details. + */ +#ifndef __SCSI_DEFS__ +#define __SCSI_DEFS__ + +/* + * SCSI-2 operation codes. + */ +typedef enum { + kScsiOpTestUnitReady = 0x00, + kScsiOpRezeroUnit = 0x01, + kScsiOpRewind = 0x01, + kScsiOpRequestBlockAddr = 0x02, + kScsiOpRequestSense = 0x03, + kScsiOpFormatUnit = 0x04, + kScsiOpReadBlockLimits = 0x05, + kScsiOpReassignBlocks = 0x07, + kScsiOpRead6 = 0x08, + kScsiOpReceive = 0x08, + kScsiOpWrite6 = 0x0a, + kScsiOpPrint = 0x0a, + kScsiOpSend = 0x0a, + kScsiOpSeek6 = 0x0b, + kScsiOpTrackSelect = 0x0b, + kScsiOpSlewPrint = 0x0b, + kScsiOpSeekBlock = 0x0c, + kScsiOpPartition = 0x0d, + kScsiOpReadReverse = 0x0f, + kScsiOpWriteFilemarks = 0x10, + kScsiOpFlushBuffer = 0x10, + kScsiOpSpace = 0x11, + kScsiOpInquiry = 0x12, + kScsiOpVerify6 = 0x13, + kScsiOpRecoverBufferedData = 0x14, + kScsiOpModeSelect = 0x15, + kScsiOpReserveUnit = 0x16, + kScsiOpReleaseUnit = 0x17, + kScsiOpCopy = 0x18, + kScsiOpErase = 0x19, + kScsiOpModeSense = 0x1a, + kScsiOpStartStopUnit = 0x1b, + kScsiOpStopPrint = 0x1b, + kScsiOpLoadUnload = 0x1b, + kScsiOpReceiveDiagnosticResults = 0x1c, + kScsiOpSendDiagnostic = 0x1d, + kScsiOpMediumRemoval = 0x1e, + kScsiOpReadFormattedCapacity = 0x23, + kScsiOpReadCapacity = 0x25, + kScsiOpRead = 0x28, // READ(10) + kScsiOpWrite = 0x2a, // WRITE(10) + kScsiOpSeek = 0x2b, + kScsiOpLocate = 0x2b, + kScsiOpPositionToElement = 0x2b, + kScsiOpWriteVerify = 0x2e, + kScsiOpVerify = 0x2f, // VERIFY(10) + kScsiOpSearchDataHigh = 0x30, + kScsiOpSearchDataEqual = 0x31, + kScsiOpSearchDataLow = 0x32, + kScsiOpSetLimits = 0x33, + kScsiOpReadPosition = 0x34, + kScsiOpSynchronizeCache = 0x35, + kScsiOpCompare = 0x39, + kScsiOpCopyAndVerify = 0x3a, + kScsiOpWriteBuffer = 0x3b, + kScsiOpReadBuffer = 0x3c, + kScsiOpChangeDefinition = 0x40, + kScsiOpReadSubChannel = 0x42, + kScsiOpReadTOC = 0x43, // READ TOC/PMA/ATIP + kScsiOpReadHeader = 0x44, + kScsiOpPlayAudio = 0x45, + kScsiOpPlayAudioMSF = 0x47, + kScsiOpPlayTrackIndex = 0x48, + kScsiOpPlayTrackRelative = 0x49, + kScsiOpPauseResume = 0x4b, + kScsiOpLogSelect = 0x4c, + kScsiOpLogSense = 0x4c, + kScsiOpStopPlayScan = 0x4e, + kScsiOpReadDiscInformation = 0x51, + kScsiOpReadTrackInformation = 0x52, + kScsiOpSendOPCInformation = 0x54, + kScsiOpModeSelect10 = 0x55, + kScsiOpRepairTrack = 0x58, + kScsiOpModeSense10 = 0x5a, + kScsiOpReportLuns = 0xa0, + kScsiOpVerify12 = 0xa2, + kScsiOpSendKey = 0xa3, + kScsiOpReportKey = 0xa4, + kScsiOpMoveMedium = 0xa5, + kScsiOpLoadUnloadSlot = 0xa6, + kScsiOpExchangeMedium = 0xa6, + kScsiOpSetReadAhead = 0xa7, + kScsiOpReadDVDStructure = 0xad, + kScsiOpWriteAndVerify = 0xae, + kScsiOpRequestVolElement = 0xb5, + kScsiOpSendVolumeTag = 0xb6, + kScsiOpReadElementStatus = 0xb8, + kScsiOpReadCDMSF = 0xb9, + kScsiOpScanCD = 0xba, + kScsiOpSetCDSpeed = 0xbb, + kScsiOpPlayCD = 0xbc, + kScsiOpMechanismStatus = 0xbd, + kScsiOpReadCD = 0xbe, + kScsiOpInitElementRange = 0xe7, +} SCSIOperationCode; + + +/* + * SCSI status codes. + */ +typedef enum { + kScsiStatGood = 0x00, + kScsiStatCheckCondition = 0x02, + kScsiStatConditionMet = 0x04, + kScsiStatBusy = 0x08, + kScsiStatIntermediate = 0x10, + kScsiStatIntermediateCondMet = 0x14, + kScsiStatReservationConflict = 0x18, + kScsiStatCommandTerminated = 0x22, + kScsiStatQueueFull = 0x28, +} SCSIStatus; + +/* + * SCSI sense codes. + */ +typedef enum { + kScsiSenseNoSense = 0x00, + kScsiSenseRecoveredError = 0x01, + kScsiSenseNotReady = 0x02, + kScsiSenseMediumError = 0x03, + kScsiSenseHardwareError = 0x04, + kScsiSenseIllegalRequest = 0x05, + kScsiSenseUnitAttention = 0x06, + kScsiSenseDataProtect = 0x07, + kScsiSenseBlankCheck = 0x08, + kScsiSenseUnqiue = 0x09, + kScsiSenseCopyAborted = 0x0a, + kScsiSenseAbortedCommand = 0x0b, + kScsiSenseEqual = 0x0c, + kScsiSenseVolOverflow = 0x0d, + kScsiSenseMiscompare = 0x0e, + kScsiSenseReserved = 0x0f, +} SCSISenseCode; + + +/* + * SCSI additional sense codes. + */ +typedef enum { + kScsiAdSenseNoSense = 0x00, + kScsiAdSenseInvalidMedia = 0x30, + kScsiAdSenseNoMediaInDevice = 0x3a, +} SCSIAdSenseCode; + +/* + * SCSI device types. + */ +typedef enum { + kScsiDevTypeDASD = 0x00, // Disk Device + kScsiDevTypeSEQD = 0x01, // Tape Device + kScsiDevTypePRNT = 0x02, // Printer + kScsiDevTypePROC = 0x03, // Processor + kScsiDevTypeWORM = 0x04, // Write-once read-multiple + kScsiDevTypeCDROM = 0x05, // CD-ROM device + kScsiDevTypeSCAN = 0x06, // Scanner device + kScsiDevTypeOPTI = 0x07, // Optical memory device + kScsiDevTypeJUKE = 0x08, // Medium Changer device + kScsiDevTypeCOMM = 0x09, // Communications device + kScsiDevTypeRESL = 0x0a, // Reserved (low) + kScsiDevTypeRESH = 0x1e, // Reserved (high) + kScsiDevTypeUNKNOWN = 0x1f, // Unknown or no device type +} SCSIDeviceType; + +/* + * Generic 6-byte request block. + */ +typedef struct CDB6 { + unsigned char operationCode; + unsigned char immediate : 1; + unsigned char commandUniqueBits : 4; + unsigned char logicalUnitNumber : 3; + unsigned char commandUniqueBytes[3]; + unsigned char link : 1; + unsigned char flag : 1; + unsigned char reserved : 4; + unsigned char vendorUnique : 2; +} CDB6; + +/* + * Generic 10-byte request block. + * + * Use for READ(10), READ CAPACITY. + */ +typedef struct CDB10 { + unsigned char operationCode; + unsigned char relativeAddress : 1; + unsigned char reserved1 : 2; + unsigned char forceUnitAccess : 1; + unsigned char disablePageOut : 1; + unsigned char logicalUnitNumber : 3; + unsigned char logicalBlockAddr0; // MSB + unsigned char logicalBlockAddr1; + unsigned char logicalBlockAddr2; + unsigned char logicalBlockAddr3; // LSB + unsigned char reserved2; + unsigned char transferLength0; // MSB + unsigned char transferLength1; // LSB + unsigned char control; +} CDB10; + +/* + * INQUIRY request block. + */ +typedef struct CDB6Inquiry { + unsigned char operationCode; + unsigned char EVPD : 1; + unsigned char reserved1 : 4; + unsigned char logicalUnitNumber : 3; + unsigned char pageCode; + unsigned char reserved2; + unsigned char allocationLength; + unsigned char control; +} CDB6Inquiry; + +/* + * Sense data (ASPI SenseArea). + */ +typedef struct CDB_SenseData { + unsigned char errorCode:7; + unsigned char valid:1; + unsigned char segmentNumber; + unsigned char senseKey:4; + unsigned char reserved:1; + unsigned char incorrectLength:1; + unsigned char endOfMedia:1; + unsigned char fileMark:1; + unsigned char information[4]; + unsigned char additionalSenseLength; + unsigned char commandSpecificInformation[4]; + unsigned char additionalSenseCode; // ASC + unsigned char additionalSenseCodeQualifier; // ASCQ + unsigned char fieldReplaceableUnitCode; + unsigned char senseKeySpecific[3]; +} CDB_SenseData; + +/* + * Default sense buffer size. + */ +#define kSenseBufferSize 18 + + +//#define INQUIRYDATABUFFERSIZE 36 + +/* + * Result from INQUIRY. + */ +typedef struct CDB_InquiryData { + unsigned char deviceType : 5; + unsigned char deviceTypeQualifier : 3; + unsigned char deviceTypeModifier : 7; + unsigned char removableMedia : 1; + unsigned char versions; + unsigned char responseDataFormat : 4; + unsigned char reserved1 : 2; + unsigned char trmIOP : 1; + unsigned char AENC : 1; + unsigned char additionalLength; + unsigned char reserved2[2]; + unsigned char softReset : 1; + unsigned char commandQueue : 1; + unsigned char reserved3 : 1; + unsigned char linkedCommands : 1; + unsigned char synchronous : 1; + unsigned char wide16Bit : 1; + unsigned char wide32Bit : 1; + unsigned char relativeAddressing : 1; + unsigned char vendorId[8]; + unsigned char productId[16]; + unsigned char productRevisionLevel[4]; + unsigned char vendorSpecific[20]; + unsigned char reserved4[40]; +} CDB_InquiryData; + +/* + * Result from READ CAPACITY. + */ +typedef struct CDB_ReadCapacityData { + unsigned char logicalBlockAddr0; // MSB + unsigned char logicalBlockAddr1; + unsigned char logicalBlockAddr2; + unsigned char logicalBlockAddr3; // LSB + unsigned char bytesPerBlock0; // MSB + unsigned char bytesPerBlock1; + unsigned char bytesPerBlock2; + unsigned char bytesPerBlock3; // LSB +} CDB_ReadCapacityData; + +#endif /*__SCSI_DEFS__*/ diff --git a/diskimg/SPTI.cpp b/diskimg/SPTI.cpp new file mode 100644 index 0000000..3a4ee7f --- /dev/null +++ b/diskimg/SPTI.cpp @@ -0,0 +1,139 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Implementation of some SPTI functions. + */ +#include "StdAfx.h" +#ifdef _WIN32 + +#include "DiskImgPriv.h" +#include "SCSIDefs.h" +#include "CP_ntddscsi.h" +#include "SPTI.h" + + +/* + * Get the capacity of the device. + * + * Returns the LBA of the last valid block and the device's block size. + */ +/*static*/ DIError +SPTI::GetDeviceCapacity(HANDLE handle, unsigned long* pLastBlock, + unsigned long* pBlockSize) +{ + SCSI_PASS_THROUGH_DIRECT sptd; + unsigned long lba, blockLen; + CDB_ReadCapacityData dataBuf; + DWORD cb; + BOOL status; + + assert(sizeof(dataBuf) == 8); // READ CAPACITY returns two longs + + memset(&sptd, 0, sizeof(sptd)); + sptd.Length = sizeof(sptd); + sptd.PathId = 0; // SCSI card ID filled in by ioctl + sptd.TargetId = 0; // SCSI target ID filled in by ioctl + sptd.Lun = 0; // SCSI lun ID filled in by ioctl + sptd.CdbLength = 10; // CDB size is 10 for READ CAPACITY + sptd.SenseInfoLength = 0; // don't return any sense data + sptd.DataIn = SCSI_IOCTL_DATA_IN; // will be data from drive + sptd.DataTransferLength = sizeof(dataBuf); + sptd.TimeOutValue = 10; // SCSI timeout value, in seconds + sptd.DataBuffer = (PVOID) &dataBuf; + sptd.SenseInfoOffset = 0; // offset to request-sense buffer + + CDB10* pCdb = (CDB10*) &sptd.Cdb; + pCdb->operationCode = kScsiOpReadCapacity; + // rest of CDB is zero + + status = ::DeviceIoControl(handle, IOCTL_SCSI_PASS_THROUGH_DIRECT, + &sptd, sizeof(sptd), NULL, 0, &cb, NULL); + + if (!status) { + DWORD lastError = ::GetLastError(); + WMSG1("DeviceIoControl(SCSI READ CAPACITY) failed, err=%ld\n", + ::GetLastError()); + if (lastError == ERROR_IO_DEVICE) // no disc in drive + return kDIErrDeviceNotReady; + else + return kDIErrSPTIFailure; + } + + lba = (unsigned long) dataBuf.logicalBlockAddr0 << 24 | + (unsigned long) dataBuf.logicalBlockAddr1 << 16 | + (unsigned long) dataBuf.logicalBlockAddr2 << 8 | + (unsigned long) dataBuf.logicalBlockAddr3; + blockLen = (unsigned long) dataBuf.bytesPerBlock0 << 24 | + (unsigned long) dataBuf.bytesPerBlock1 << 16 | + (unsigned long) dataBuf.bytesPerBlock2 << 8 | + (unsigned long) dataBuf.bytesPerBlock3; + + *pLastBlock = lba; + *pBlockSize = blockLen; + + return kDIErrNone; +} + + +/* + * Read one or more blocks from the specified SCSI device. + * + * "buf" must be able to hold (numBlocks * blockSize) bytes. + */ +/*static*/ DIError +SPTI::ReadBlocks(HANDLE handle, long startBlock, short numBlocks, + long blockSize, void* buf) +{ + SCSI_PASS_THROUGH_DIRECT sptd; + DWORD cb; + BOOL status; + + assert(startBlock >= 0); + assert(numBlocks > 0); + assert(buf != nil); + + //WMSG2(" SPTI phys read block (%ld) %d\n", startBlock, numBlocks); + + memset(&sptd, 0, sizeof(sptd)); + sptd.Length = sizeof(sptd); // size of struct (+ request-sense buffer) + sptd.ScsiStatus = 0; + sptd.PathId = 0; // SCSI card ID filled in by ioctl + sptd.TargetId = 0; // SCSI target ID filled in by ioctl + sptd.Lun = 0; // SCSI lun ID filled in by ioctl + sptd.CdbLength = 10; // CDB size is 10 for READ CAPACITY + sptd.SenseInfoLength = 0; // don't return any sense data + sptd.DataIn = SCSI_IOCTL_DATA_IN; // will be data from drive + sptd.DataTransferLength = blockSize * numBlocks; + sptd.TimeOutValue = 10; // SCSI timeout value (in seconds) + sptd.DataBuffer = (PVOID) buf; + sptd.SenseInfoOffset = 0; // offset from start of struct to request-sense + + CDB10* pCdb = (CDB10*) &sptd.Cdb; + pCdb->operationCode = kScsiOpRead; + pCdb->logicalBlockAddr0 = (unsigned char) (startBlock >> 24); // MSB + pCdb->logicalBlockAddr1 = (unsigned char) (startBlock >> 16); + pCdb->logicalBlockAddr2 = (unsigned char) (startBlock >> 8); + pCdb->logicalBlockAddr3 = (unsigned char) startBlock; // LSB + pCdb->transferLength0 = (unsigned char) (numBlocks >> 8); // MSB + pCdb->transferLength1 = (unsigned char) numBlocks; // LSB + + status = ::DeviceIoControl(handle, IOCTL_SCSI_PASS_THROUGH_DIRECT, + &sptd, sizeof(sptd), NULL, 0, &cb, NULL); + + if (!status) { + WMSG1("DeviceIoControl(SCSI READ(10)) failed, err=%ld\n", + ::GetLastError()); + return kDIErrReadFailed; // close enough + } + if (sptd.ScsiStatus != 0) { + WMSG1("SCSI READ(10) failed, status=%d\n", sptd.ScsiStatus); + return kDIErrReadFailed; + } + + return kDIErrNone; +} + +#endif /*_WIN32*/ diff --git a/diskimg/SPTI.h b/diskimg/SPTI.h new file mode 100644 index 0000000..c8b8242 --- /dev/null +++ b/diskimg/SPTI.h @@ -0,0 +1,40 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Declarations for the Win32 SCSI Pass-Through Interface. + */ +#ifndef __SPTI__ +#define __SPTI__ + +#ifdef _WIN32 + +namespace DiskImgLib { + +/* + * This is currently implemented as a set of static functions. Do not + * instantiate the class. + */ +class DISKIMG_API SPTI { +public: + // Read blocks from the device. + static DIError ReadBlocks(HANDLE handle, long startBlock, short numBlocks, + long blockSize, void* buf); + + // Get the capacity, expressed as the highest-available LBA and the device + // block size. + static DIError GetDeviceCapacity(HANDLE handle, unsigned long* pLastBlock, + unsigned long* pBlockSize); + +private: + SPTI(void) {} + ~SPTI(void) {} +}; + +}; // namespace DiskImgLib + +#endif /*_WIN32*/ + +#endif /*__SPTI__*/ diff --git a/diskimg/StdAfx.cpp b/diskimg/StdAfx.cpp new file mode 100644 index 0000000..f859ec7 --- /dev/null +++ b/diskimg/StdAfx.cpp @@ -0,0 +1,13 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +// stdafx.cpp : source file that includes just the standard includes +// diskimg.pch will be the pre-compiled header +// stdafx.obj will contain the pre-compiled type information + +#include "StdAfx.h" + +// TODO: reference any additional headers you need in STDAFX.H +// and not in this file diff --git a/diskimg/StdAfx.h b/diskimg/StdAfx.h new file mode 100644 index 0000000..37e38b2 --- /dev/null +++ b/diskimg/StdAfx.h @@ -0,0 +1,69 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +// stdafx.h : include file for standard system include files, +// or project specific include files that are used frequently, but +// are changed infrequently +// + +#ifndef _WIN32 + +/* UNIX includes */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define O_BINARY 0 + +#define HAVE_VSNPRINTF +#define HAVE_FSEEKO +#define HAVE_FTRUNCATE + +#else /*_WIN32*/ + +#if !defined(AFX_STDAFX_H__1CB7B33E_42BF_4A98_B814_4198EA8ACC58__INCLUDED_) +#define AFX_STDAFX_H__1CB7B33E_42BF_4A98_B814_4198EA8ACC58__INCLUDED_ + +#if _MSC_VER > 1000 +#pragma once +#endif // _MSC_VER > 1000 + +#define HAVE_WINDOWS_CDROM // enable CD-ROM access under Windows +#define HAVE_CHSIZE + +// Insert your headers here +# define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers + +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_WINDOWS_CDROM +# include +#endif + +#ifndef _SSIZE_T_DEFINED +typedef unsigned int ssize_t; +#define _SSIZE_T_DEFINED +#endif + +#define HAVE__VSNPRINTF +#define strcasecmp stricmp + + +//{{AFX_INSERT_LOCATION}} +// Microsoft Visual C++ will insert additional declarations immediately before the previous line. + +#endif // !defined(AFX_STDAFX_H__1CB7B33E_42BF_4A98_B814_4198EA8ACC58__INCLUDED_) +#endif /*_WIN32*/ diff --git a/diskimg/TwoImg.cpp b/diskimg/TwoImg.cpp new file mode 100644 index 0000000..80551f7 --- /dev/null +++ b/diskimg/TwoImg.cpp @@ -0,0 +1,576 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Support for 2MG/2IMG wrapper files. + * + * This needs to be directly accessible from external applications, so try not + * to hook into private DiskImg state. + */ +#include "StdAfx.h" +#include "TwoImg.h" +#include "DiskImgPriv.h" + +///*static*/ const char* TwoImgHeader::kMagic = "2IMG"; // file magic # +///*static*/ const char* TwoImgHeader::kCreator = "CdrP"; // our "creator" ID + + +/* + * Initialize a header to default values, using the supplied image length + * where appropriate. + * + * Sets up a header for a 2MG file with the disk data and nothing else. + * + * Returns 0 on success, -1 if one of the arguments was bad. + */ +int +TwoImgHeader::InitHeader(int imageFormat, long imageSize, long imageBlockCount) +{ + if (imageSize <= 0) + return -1; + if (imageFormat < kImageFormatDOS || imageFormat > kImageFormatNibble) + return -1; + + if (imageFormat != kImageFormatNibble && + imageSize != imageBlockCount * 512) + { + WMSG3("2MG InitHeader: bad sizes %d %ld %ld\n", imageFormat, + imageSize, imageBlockCount); + return -1; + } + + assert(fComment == nil); + + //memcpy(fMagic, kMagic, 4); + //memcpy(fCreator, kCreator, 4); + fMagic = kMagic; + fCreator = kCreatorCiderPress; + fHeaderLen = kOurHeaderLen; + fVersion = kOurVersion; + fImageFormat = imageFormat; + fFlags = 0; + fNumBlocks = imageBlockCount; + fDataOffset = kOurHeaderLen; + fDataLen = imageSize; + fCmtOffset = 0; + fCmtLen = 0; + fCreatorOffset = 0; + fCreatorLen = 0; + fSpare[0] = fSpare[1] = fSpare[2] = fSpare[3] = 0; + + return 0; +} + +/* + * Get the DOS volume number. + * + * If not set, we currently return the initial value (-1), rather than the + * default volume number. For the way we currently make use of this, this + * makes the most sense. + */ +short +TwoImgHeader::GetDOSVolumeNum(void) const +{ + assert(fFlags & kDOSVolumeSet); + return fDOSVolumeNum; +} + +/* + * Set the DOS volume number. + */ +void +TwoImgHeader::SetDOSVolumeNum(short dosVolumeNum) +{ + assert(dosVolumeNum >= 0 && dosVolumeNum < 256); + fFlags |= dosVolumeNum; + fFlags |= kDOSVolumeSet; +} + +/* + * Set the comment. + */ +void +TwoImgHeader::SetComment(const char* comment) +{ + delete[] fComment; + if (comment == nil) { + fComment = nil; + } else { + fComment = new char[strlen(comment)+1]; + if (fComment != nil) + strcpy(fComment, comment); + // else throw alloc failure + } + + if (fComment == nil) { + fCmtLen = 0; + fCmtOffset = 0; + if (fCreatorOffset > 0) + fCreatorOffset = fDataOffset + fDataLen; + } else { + fCmtLen = strlen(fComment); + fCmtOffset = fDataOffset + fDataLen; + if (fCreatorOffset > 0) + fCreatorOffset = fCmtOffset + fCmtLen; + } +} + +/* + * Set the creator chunk. + */ +void +TwoImgHeader::SetCreatorChunk(const void* chunk, long len) +{ + assert(len >= 0); + + delete[] fCreatorChunk; + if (chunk == nil || len == 0) { + fCreatorChunk = nil; + } else { + fCreatorChunk = new char[len]; + if (fCreatorChunk != nil) + memcpy(fCreatorChunk, chunk, len); + // else throw alloc failure + } + + if (fCreatorChunk == nil) { + fCreatorLen = 0; + fCreatorOffset = 0; + } else { + fCreatorLen = len; + if (fCmtOffset > 0) + fCreatorOffset = fCmtOffset + fCmtLen; + else + fCreatorOffset = fDataOffset + fDataLen; + } +} + +/* + * Read the header from a 2IMG file. Pass in "totalLength" as a sanity check. + * + * THOUGHT: provide a simple GenericFD conversion for FILE*, and then just + * call the GenericFD version of ReadHeader. + * + * Returns 0 on success, nonzero on error or invalid header. + */ +int +TwoImgHeader::ReadHeader(FILE* fp, long totalLength) +{ + unsigned char buf[kOurHeaderLen]; + + fread(buf, kOurHeaderLen, 1, fp); + if (ferror(fp)) + return errno ? errno : -1; + + if (UnpackHeader(buf, totalLength) != 0) + return -1; + + /* + * Extract the comment, if any. + */ + if (fCmtOffset > 0 && fCmtLen > 0) { + if (GetChunk(fp, fCmtOffset - kOurHeaderLen, fCmtLen, + (void**) &fComment) != 0) + { + WMSG0("Throwing comment away\n"); + fCmtLen = 0; + fCmtOffset = 0; + } else { + WMSG1("Got comment: '%s'\n", fComment); + } + } + + /* + * Extract the creator chunk, if any. + */ + if (fCreatorOffset > 0 && fCreatorLen > 0) { + if (GetChunk(fp, fCreatorOffset - kOurHeaderLen, fCreatorLen, + (void**) &fCreatorChunk) != 0) + { + WMSG0("Throwing creator chunk away\n"); + fCreatorLen = 0; + fCreatorOffset = 0; + } else { + //WMSG1("Got creator chunk: '%s'\n", fCreatorChunk); + } + } + + return 0; +} + +/* + * Read the header from a 2IMG file. Pass in "totalLength" as a sanity check. + * + * Returns 0 on success, nonzero on error or invalid header. + */ +int +TwoImgHeader::ReadHeader(GenericFD* pGFD, long totalLength) +{ + DIError dierr; + unsigned char buf[kOurHeaderLen]; + + dierr = pGFD->Read(buf, kOurHeaderLen); + if (dierr != kDIErrNone) + return -1; + + if (UnpackHeader(buf, totalLength) != 0) + return -1; + + /* + * Extract the comment, if any. + */ + if (fCmtOffset > 0 && fCmtLen > 0) { + if (GetChunk(pGFD, fCmtOffset - kOurHeaderLen, fCmtLen, + (void**) &fComment) != 0) + { + WMSG0("Throwing comment away\n"); + fCmtLen = 0; + fCmtOffset = 0; + } else { + WMSG1("Got comment: '%s'\n", fComment); + } + } + + /* + * Extract the creator chunk, if any. + */ + if (fCreatorOffset > 0 && fCreatorLen > 0) { + if (GetChunk(pGFD, fCreatorOffset - kOurHeaderLen, fCreatorLen, + (void**) &fCreatorChunk) != 0) + { + WMSG0("Throwing creator chunk away\n"); + fCreatorLen = 0; + fCreatorOffset = 0; + } else { + //WMSG1("Got creator chunk: '%s'\n", fCreatorChunk); + } + } + + return 0; +} + +/* + * Grab a chunk of data from a relative offset. + */ +int +TwoImgHeader::GetChunk(GenericFD* pGFD, di_off_t relOffset, long len, + void** pBuf) +{ + DIError dierr; + di_off_t curPos; + + /* remember current offset */ + curPos = pGFD->Tell(); + + /* seek out to chunk and grab it */ + dierr = pGFD->Seek(relOffset, kSeekCur); + if (dierr != kDIErrNone) { + WMSG0("2MG seek to chunk failed\n"); + return -1; + } + + assert(*pBuf == nil); + *pBuf = new char[len+1]; // one extra, for null termination + + dierr = pGFD->Read(*pBuf, len); + if (dierr != kDIErrNone) { + WMSG0("2MG chunk read failed\n"); + delete[] (char*) (*pBuf); + *pBuf = nil; + (void) pGFD->Seek(curPos, kSeekSet); + return -1; + } + + /* null-terminate, in case this was a string */ + ((char*) *pBuf)[len] = '\0'; + + /* seek back to where we were */ + (void) pGFD->Seek(curPos, kSeekSet); + + return 0; +} + +/* + * Grab a chunk of data from a relative offset. + */ +int +TwoImgHeader::GetChunk(FILE* fp, di_off_t relOffset, long len, + void** pBuf) +{ + long curPos; + int count; + + /* remember current offset */ + curPos = ftell(fp); + WMSG1("Current offset=%ld\n", curPos); + + /* seek out to chunk and grab it */ + if (fseek(fp, (long) relOffset, SEEK_CUR) == -1) { + WMSG0("2MG seek to chunk failed\n"); + return errno ? errno : -1;; + } + + assert(*pBuf == nil); + *pBuf = new char[len+1]; // one extra, for null termination + + count = fread(*pBuf, len, 1, fp); + if (!count || ferror(fp) || feof(fp)) { + WMSG0("2MG chunk read failed\n"); + delete[] (char*) (*pBuf); + *pBuf = nil; + (void) fseek(fp, curPos, SEEK_SET); + clearerr(fp); + return errno ? errno : -1;; + } + + /* null-terminate, in case this was a string */ + ((char*) *pBuf)[len] = '\0'; + + /* seek back to where we were */ + (void) fseek(fp, curPos, SEEK_SET); + + return 0; +} + + +/* + * Unpack the 64-byte 2MG header. + * + * Performs some sanity checks. Returns 0 on success, -1 on failure. + */ +int +TwoImgHeader::UnpackHeader(const unsigned char* buf, long totalLength) +{ + fMagic = GetLongBE(&buf[0x00]); + fCreator = GetLongBE(&buf[0x04]); + fHeaderLen = GetShortLE(&buf[0x08]); + fVersion = GetShortLE(&buf[0x0a]); + fImageFormat = GetLongLE(&buf[0x0c]); + fFlags = GetLongLE(&buf[0x10]); + fNumBlocks = GetLongLE(&buf[0x14]); + fDataOffset = GetLongLE(&buf[0x18]); + fDataLen = GetLongLE(&buf[0x1c]); + fCmtOffset = GetLongLE(&buf[0x20]); + fCmtLen = GetLongLE(&buf[0x24]); + fCreatorOffset = GetLongLE(&buf[0x28]); + fCreatorLen = GetLongLE(&buf[0x2c]); + fSpare[0] = GetLongLE(&buf[0x30]); + fSpare[1] = GetLongLE(&buf[0x34]); + fSpare[2] = GetLongLE(&buf[0x38]); + fSpare[3] = GetLongLE(&buf[0x3c]); + + fMagicStr[0] = (char) (fMagic >> 24); + fMagicStr[1] = (char) (fMagic >> 16); + fMagicStr[2] = (char) (fMagic >> 8); + fMagicStr[3] = (char) fMagic; + fMagicStr[4] = '\0'; + fCreatorStr[0] = (char) (fCreator >> 24); + fCreatorStr[1] = (char) (fCreator >> 16); + fCreatorStr[2] = (char) (fCreator >> 8); + fCreatorStr[3] = (char) fCreator; + fCreatorStr[4] = '\0'; + + if (fMagic != kMagic) { + WMSG0("Magic number does not match 2IMG\n"); + return -1; + } + + if (fVersion > 1) { + WMSG1("ERROR: unsupported version=%d\n", fVersion); + return -1; // bad header until I hear otherwise + } + + if (fFlags & kDOSVolumeSet) + fDOSVolumeNum = fFlags & kDOSVolumeMask; + + DumpHeader(); + + /* fix broken 'WOOF' images from Sweet-16 */ + if (fCreator == kCreatorSweet16 && fDataLen == 0 && + fImageFormat != kImageFormatNibble) + { + fDataLen = fNumBlocks * kBlockSize; + WMSG1("NOTE: fixing zero dataLen in 'WOOF' image (set to %ld)\n", + fDataLen); + } + + /* + * Perform some sanity checks. + */ + if (fImageFormat != kImageFormatNibble && + fNumBlocks * kBlockSize != fDataLen) + { + WMSG2("numBlocks/dataLen mismatch (%ld vs %ld)\n", + fNumBlocks * kBlockSize, fDataLen); + return -1; + } + if (fDataLen + fDataOffset > totalLength) { + WMSG3("Invalid dataLen/offset/fileLength (dl=%ld, off=%ld, tlen=%ld)\n", + fDataLen, fDataOffset, totalLength); + return -1; + } + if (fImageFormat < kImageFormatDOS || fImageFormat > kImageFormatNibble) { + WMSG1("Invalid image format %ld\n", fImageFormat); + return -1; + } + + if (fCmtOffset > 0 && fCmtOffset < fDataOffset + fDataLen) { + WMSG2("2MG comment is inside the data section (off=%ld, data end=%ld)\n", + fCmtOffset, fDataOffset+fDataLen); + DebugBreak(); + // ignore the comment + fCmtOffset = 0; + fCmtLen = 0; + } + if (fCreatorOffset > 0 && fCreatorLen > 0) { + long prevEnd = fDataOffset + fDataLen + fCmtLen; + + if (fCreatorOffset < prevEnd) { + WMSG2("2MG creator chunk is inside prev data (off=%ld, data end=%ld)\n", + fCreatorOffset, prevEnd); + DebugBreak(); + // ignore the creator chunk + fCreatorOffset = 0; + fCreatorLen = 0; + } + } + + return 0; +} + +/* + * Write the header to a 2IMG file. + * + * Returns 0 on success, or an errno value on failure. + */ +int +TwoImgHeader::WriteHeader(FILE* fp) const +{ + unsigned char buf[kOurHeaderLen]; + + PackHeader(buf); + if (fwrite(buf, kOurHeaderLen, 1, fp) != 1) + return errno ? errno : -1; + return 0; +} + +/* + * Write the header to a 2IMG file. + * + * Returns 0 on success, or an errno value on failure. + */ +int +TwoImgHeader::WriteHeader(GenericFD* pGFD) const +{ + unsigned char buf[kOurHeaderLen]; + + PackHeader(buf); + + if (pGFD->Write(buf, kOurHeaderLen) != kDIErrNone) + return -1; + + return 0; +} + +/* + * Write the footer. File must be seeked to end of data chunk. + */ +int +TwoImgHeader::WriteFooter(FILE* fp) const +{ + WMSG1("Writing footer at offset=%ld\n", (long) ftell(fp)); + + if (fCmtLen) { + fwrite(fComment, fCmtLen, 1, fp); + } + if (fCreatorLen) { + fwrite(fCreatorChunk, fCreatorLen, 1, fp); + } + if (ferror(fp)) + return errno ? errno : -1; + + return 0; +} + + +/* + * Write the footer. File must be seeked to end of data chunk. + */ +int +TwoImgHeader::WriteFooter(GenericFD* pGFD) const +{ + WMSG1("Writing footer at offset=%ld\n", (long) pGFD->Tell()); + + if (fCmtLen) { + if (pGFD->Write(fComment, fCmtLen) != kDIErrNone) + return -1; + } + if (fCreatorLen) { + if (pGFD->Write(fCreatorChunk, fCreatorLen) != kDIErrNone) + return -1; + } + + return 0; +} + +/* + * Pack the header values into a 64-byte buffer. + */ +void +TwoImgHeader::PackHeader(unsigned char* buf) const +{ + if (fCmtLen > 0 && fCmtOffset == 0) { + assert(false); + } + if (fCreatorLen > 0 && fCreatorOffset == 0) { + assert(false); + } + + PutLongBE(&buf[0x00], fMagic); + PutLongBE(&buf[0x04], fCreator); + PutShortLE(&buf[0x08], fHeaderLen); + PutShortLE(&buf[0x0a], fVersion); + PutLongLE(&buf[0x0c], fImageFormat); + PutLongLE(&buf[0x10], fFlags); + PutLongLE(&buf[0x14], fNumBlocks); + PutLongLE(&buf[0x18], fDataOffset); + PutLongLE(&buf[0x1c], fDataLen); + PutLongLE(&buf[0x20], fCmtOffset); + PutLongLE(&buf[0x24], fCmtLen); + PutLongLE(&buf[0x28], fCreatorOffset); + PutLongLE(&buf[0x2c], fCreatorLen); + PutLongLE(&buf[0x30], fSpare[0]); + PutLongLE(&buf[0x34], fSpare[1]); + PutLongLE(&buf[0x38], fSpare[2]); + PutLongLE(&buf[0x3c], fSpare[3]); +} + +/* + * Dump the contents of an ImgHeader. + */ +void +TwoImgHeader::DumpHeader(void) const +{ + WMSG0("--- header contents:\n"); + WMSG2("\tmagic = '%s' (0x%08lx)\n", fMagicStr, fMagic); + WMSG2("\tcreator = '%s' (0x%08lx)\n", fCreatorStr, fCreator); + WMSG1("\theaderLen = %d\n", fHeaderLen); + WMSG1("\tversion = %d\n", fVersion); + WMSG1("\timageFormat = %ld\n", fImageFormat); + WMSG1("\tflags = 0x%08lx\n", fFlags); + WMSG1("\t locked = %s\n", + (fFlags & kFlagLocked) ? "true" : "false"); + WMSG2("\t DOS volume = %s (%ld)\n", + (fFlags & kDOSVolumeSet) ? "true" : "false", + fFlags & kDOSVolumeMask); + WMSG1("\tnumBlocks = %ld\n", fNumBlocks); + WMSG1("\tdataOffset = %ld\n", fDataOffset); + WMSG1("\tdataLen = %ld\n", fDataLen); + WMSG1("\tcmtOffset = %ld\n", fCmtOffset); + WMSG1("\tcmtLen = %ld\n", fCmtLen); + WMSG1("\tcreatorOffset = %ld\n", fCreatorOffset); + WMSG1("\tcreatorLen = %ld\n", fCreatorLen); + WMSG0("\n"); +} diff --git a/diskimg/TwoImg.h b/diskimg/TwoImg.h new file mode 100644 index 0000000..24fe562 --- /dev/null +++ b/diskimg/TwoImg.h @@ -0,0 +1,136 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Support for the "2MG"/"2IMG" disk image format. + * + * This gets its own header because CiderPress uses these definitions and + * functions directly. + */ +#ifndef __TWOIMG__ +#define __TWOIMG__ + +#include "DiskImg.h" + +namespace DiskImgLib { + +/* + * 2IMG header definition (was on http://www.magnet.ch/emutech/Tech/, + * now on http://www.a2central.com/programming/filetypes/ftne00130.html + * as filetype $e0/$0130). + * + * Meaning of "flags": + * bit 31 : disk is "locked"; used by emulators as write-protect sticker. + * bit 8 : if set, bits 0-7 specify DOS 3.3 volume number + * bit 0-7: if bit 8 is set, use this as DOS volume; else use 254 + * + * All values are stored little-endian. + */ +class DISKIMG_API TwoImgHeader { +public: + TwoImgHeader(void) : fDOSVolumeNum(-1), fComment(NULL), fCreatorChunk(NULL) + {} + virtual ~TwoImgHeader(void) { + delete[] fComment; + delete[] fCreatorChunk; + } + + /* + * Header fields. + */ + //char fMagic[4]; + //char fCreator[4]; + unsigned long fMagic; + unsigned long fCreator; + short fHeaderLen; + short fVersion; + long fImageFormat; + unsigned long fFlags; // may include DOS volume num + long fNumBlocks; // 512-byte blocks + long fDataOffset; + long fDataLen; + long fCmtOffset; + long fCmtLen; + long fCreatorOffset; + long fCreatorLen; + long fSpare[4]; + + /* + * Related constants. + */ + enum { + // imageFormat + kImageFormatDOS = 0, + kImageFormatProDOS = 1, + kImageFormatNibble = 2, + // flags + kFlagLocked = (1L<<31), + kDOSVolumeSet = (1L<<8), + kDOSVolumeMask = (0xff), + kDefaultVolumeNum = 254, + + // constants used when creating a new header + kOurHeaderLen = 64, + kOurVersion = 1, + + kBlockSize = 512, + kMagic = 0x32494d47, // 2IMG + kCreatorCiderPress = 0x43647250, // CdrP + kCreatorSweet16 = 0x574f4f46, // WOOF + }; + + /* + * Basic functions. + * + * The read header function will read the comment, but the write + * header function will not. This is because the GenericFD functions + * don't allow seeking past the current EOF. + * + * ReadHeader/WriteHeader expect the file to be seeked to the initial + * offset. WriteFooter expects the file to be seeked just past the + * end of the data section. This is done in case the file has some + * sort of wrapper outside the 2MG header. + */ + int InitHeader(int imageFormat, long imageSize, long imageBlockCount); + int ReadHeader(FILE* fp, long totalLength); + int ReadHeader(GenericFD* pGFD, long totalLength); + int WriteHeader(FILE* fp) const; + int WriteHeader(GenericFD* pGFD) const; + int WriteFooter(FILE* fp) const; + int WriteFooter(GenericFD* pGFD) const; + void DumpHeader(void) const; // for debugging + + /* + * Getters & setters. + */ + const char* GetMagicStr(void) const { return fMagicStr; } + const char* GetCreatorStr(void) const { return fCreatorStr; } + + short GetDOSVolumeNum(void) const; + void SetDOSVolumeNum(short dosVolumeNum); + const char* GetComment(void) const { return fComment; } + void SetComment(const char* comment); + const void* GetCreatorChunk(void) const { return fCreatorChunk; } + void SetCreatorChunk(const void* creatorBlock, long len); + +private: + int UnpackHeader(const unsigned char* buf, long totalLength); + void PackHeader(unsigned char* buf) const; + int GetChunk(GenericFD* pGFD, di_off_t relOffset, long len, + void** pBuf); + int GetChunk(FILE* fp, di_off_t relOffset, long len, + void** pBuf); + + int fDOSVolumeNum; + char fMagicStr[5]; + char fCreatorStr[5]; + + char* fComment; + char* fCreatorChunk; +}; + +}; // namespace DiskImgLib + +#endif /*TWOIMG*/ diff --git a/diskimg/UNIDOS.cpp b/diskimg/UNIDOS.cpp new file mode 100644 index 0000000..850bc39 --- /dev/null +++ b/diskimg/UNIDOS.cpp @@ -0,0 +1,365 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Implementation of DiskFSUNIDOS class. + * + * The "UNIDOS" filesystem doesn't actually hold files. Instead, it holds + * two 400K DOS 3.3 volumes on an 800K disk. + * + * We do have a test here for "wide" DOS 3.3, which is largely a clone of + * the standard DOS 3.3 test. The trick is that we have to adjust our + * detection to account for 32-sector tracks, and do so while the object + * is still in a state where it believes it has 16 sectors per track. + */ +#include "StdAfx.h" +#include "DiskImgPriv.h" + + +/* + * =========================================================================== + * DiskFSUNIDOS + * =========================================================================== + */ + +const int kExpectedNumBlocks = 1600; +const int kExpectedTracks = 50; +const int kExpectedSectors = 32; +const int kVTOCTrack = 17; +const int kVTOCSector = 0; +const int kSctSize = 256; + +const int kCatalogEntrySize = 0x23; // length in bytes of catalog entries +const int kCatalogEntriesPerSect = 7; // #of entries per catalog sector +const int kMaxTSPairs = 0x7a; // 122 entries for 256-byte sectors +const int kTSOffset = 0x0c; // first T/S entry in a T/S list + +const int kMaxTSIterations = 32; +const int kMaxCatalogIterations = 64; + + +/* + * Read a track/sector, adjusting for 32-sector disks being treated as + * if they were 16-sector. + */ +static DIError +ReadTrackSectorAdjusted(DiskImg* pImg, int track, int sector, + int trackOffset, unsigned char* buf, DiskImg::SectorOrder imageOrder) +{ + track += trackOffset; + track *= 2; + if (sector >= 16) { + track++; + sector -= 16; + } + return pImg->ReadTrackSectorSwapped(track, sector, buf, imageOrder, + DiskImg::kSectorOrderDOS); +} + +/* + * Test for presence of 400K DOS 3.3 volumes. + */ +static DIError +TestImageHalf(DiskImg* pImg, int trackOffset, DiskImg::SectorOrder imageOrder, + int* pGoodCount) +{ + DIError dierr = kDIErrNone; + unsigned char sctBuf[kSctSize]; + int numTracks, numSectors; + int catTrack, catSect; + int foundGood = 0; + int iterations = 0; + + *pGoodCount = 0; + + dierr = ReadTrackSectorAdjusted(pImg, kVTOCTrack, kVTOCSector, + trackOffset, sctBuf, imageOrder); + if (dierr != kDIErrNone) + goto bail; + + catTrack = sctBuf[0x01]; + catSect = sctBuf[0x02]; + numTracks = sctBuf[0x34]; + numSectors = sctBuf[0x35]; + + if (!(sctBuf[0x27] == kMaxTSPairs) || + /*!(sctBuf[0x36] == 0 && sctBuf[0x37] == 1) ||*/ // bytes per sect + !(numTracks == kExpectedTracks) || + !(numSectors == 32) || + !(catTrack < numTracks && catSect < numSectors) || + 0) + { + WMSG0(" UNI/Wide DOS header test failed\n"); + dierr = kDIErrFilesystemNotFound; + goto bail; + } + + foundGood++; // score one for a valid-looking VTOC + + /* + * Walk through the catalog track to try to figure out ordering. + */ + while (catTrack != 0 && catSect != 0 && + iterations < DiskFSDOS33::kMaxCatalogSectors) + { + dierr = ReadTrackSectorAdjusted(pImg, catTrack, catSect, + trackOffset, sctBuf, imageOrder); + if (dierr != kDIErrNone) { + dierr = kDIErrNone; + break; /* allow it if earlier stuff was okay */ + } + + if (catTrack == sctBuf[1] && catSect == sctBuf[2] +1) + foundGood++; + else if (catTrack == sctBuf[1] && catSect == sctBuf[2]) { + WMSG2(" WideDOS detected self-reference on cat (%d,%d)\n", + catTrack, catSect); + break; + } + + catTrack = sctBuf[1]; + catSect = sctBuf[2]; + iterations++; // watch for infinite loops + } + if (iterations >= DiskFSDOS33::kMaxCatalogSectors) { + dierr = kDIErrDirectoryLoop; + WMSG0(" WideDOS directory links cause a loop\n"); + goto bail; + } + + WMSG3(" WideDOS foundGood=%d off=%d swap=%d\n", foundGood, + trackOffset, imageOrder); + *pGoodCount = foundGood; + +bail: + return dierr; +} + +/* + * Test both of the DOS partitions. + */ +static DIError +TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder, int* pGoodCount) +{ + DIError dierr; + int goodCount1, goodCount2; + + *pGoodCount = 0; + + WMSG1(" UNIDOS checking first half (imageOrder=%d)\n", imageOrder); + dierr = TestImageHalf(pImg, 0, imageOrder, &goodCount1); + if (dierr != kDIErrNone) + return dierr; + + WMSG1(" UNIDOS checking second half (imageOrder=%d)\n", imageOrder); + dierr = TestImageHalf(pImg, kExpectedTracks, imageOrder, &goodCount2); + if (dierr != kDIErrNone) + return dierr; + + if (goodCount1 > goodCount2) + *pGoodCount = goodCount1; + else + *pGoodCount = goodCount2; + + return kDIErrNone; +} + +/* + * Test to see if the image is a UNIDOS volume. + */ +/*static*/ DIError +DiskFSUNIDOS::TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency) +{ + /* only on 800K disks (at the least, insist on numTracks being even) */ + if (pImg->GetNumBlocks() != kExpectedNumBlocks) + return kDIErrFilesystemNotFound; + + DiskImg::SectorOrder ordering[DiskImg::kSectorOrderMax]; + + DiskImg::GetSectorOrderArray(ordering, *pOrder); + + DiskImg::SectorOrder bestOrder = DiskImg::kSectorOrderUnknown; + int bestCount = 0; + + for (int i = 0; i < DiskImg::kSectorOrderMax; i++) { + int goodCount = 0; + + if (ordering[i] == DiskImg::kSectorOrderUnknown) + continue; + if (TestImage(pImg, ordering[i], &goodCount) == kDIErrNone) { + if (goodCount > bestCount) { + bestCount = goodCount; + bestOrder = ordering[i]; + } + } + } + + if (bestCount >= 4 || + (leniency == kLeniencyVery && bestCount >= 2)) + { + WMSG2(" WideDOS test: bestCount=%d for order=%d\n", bestCount, bestOrder); + assert(bestOrder != DiskImg::kSectorOrderUnknown); + *pOrder = bestOrder; + *pFormat = DiskImg::kFormatUNIDOS; + return kDIErrNone; + } + + WMSG0(" UNIDOS didn't find valid FS\n"); + return kDIErrFilesystemNotFound; +} + +/* + * Test to see if the image is a 'wide' (32-sector) DOS3.3 volume, i.e. + * half of a UNIDOS volume (usually found embedded in ProDOS). + * + * Trying all possible formats is important here, because the wrong value for + * swap can return a "good" value of 7 (much less than the expected 30, but + * above a threshold of reasonableness). + */ +/*static*/ DIError +DiskFSUNIDOS::TestWideFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, + DiskImg::FSFormat* pFormat, FSLeniency leniency) +{ + /* only on 400K "disks" */ + if (pImg->GetNumBlocks() != kExpectedNumBlocks/2) { + WMSG1(" WideDOS ignoring volume (numBlocks=%ld)\n", + pImg->GetNumBlocks()); + return kDIErrFilesystemNotFound; + } + + DiskImg::SectorOrder ordering[DiskImg::kSectorOrderMax]; + + DiskImg::GetSectorOrderArray(ordering, *pOrder); + + DiskImg::SectorOrder bestOrder = DiskImg::kSectorOrderUnknown; + int bestCount = 0; + + for (int i = 0; i < DiskImg::kSectorOrderMax; i++) { + int goodCount = 0; + + if (ordering[i] == DiskImg::kSectorOrderUnknown) + continue; + if (TestImageHalf(pImg, 0, ordering[i], &goodCount) == kDIErrNone) { + if (goodCount > bestCount) { + bestCount = goodCount; + bestOrder = ordering[i]; + } + } + } + + if (bestCount >= 4 || + (leniency == kLeniencyVery && bestCount >= 2)) + { + WMSG2(" UNI/Wide test: bestCount=%d for order=%d\n", bestCount, bestOrder); + assert(bestOrder != DiskImg::kSectorOrderUnknown); + *pOrder = bestOrder; + *pFormat = DiskImg::kFormatDOS33; + // up to the caller to adjust numTracks/numSectPerTrack + return kDIErrNone; + } + + WMSG0(" UNI/Wide didn't find valid FS\n"); + return kDIErrFilesystemNotFound; +} + + +/* + * Set up our sub-volumes. + */ +DIError +DiskFSUNIDOS::Initialize(void) +{ + DIError dierr = kDIErrNone; + + if (fScanForSubVolumes != kScanSubDisabled) { + dierr = OpenSubVolume(0); + if (dierr != kDIErrNone) + return dierr; + + dierr = OpenSubVolume(1); + if (dierr != kDIErrNone) + return dierr; + } else { + WMSG0(" UNIDOS not scanning for sub-volumes\n"); + } + + SetVolumeUsageMap(); + + return kDIErrNone; +} + +/* + * Open up one of the DOS 3.3 sub-volumes. + */ +DIError +DiskFSUNIDOS::OpenSubVolume(int idx) +{ + DIError dierr = kDIErrNone; + DiskFS* pNewFS = nil; + DiskImg* pNewImg = nil; + + pNewImg = new DiskImg; + if (pNewImg == nil) { + dierr = kDIErrMalloc; + goto bail; + } + + dierr = pNewImg->OpenImage(fpImg, kExpectedTracks * idx, 0, + kExpectedTracks * kExpectedSectors); + if (dierr != kDIErrNone) { + WMSG3(" UNISub: OpenImage(%d,0,%d) failed (err=%d)\n", + kExpectedTracks * idx, kExpectedTracks * kExpectedSectors, dierr); + goto bail; + } + + dierr = pNewImg->AnalyzeImage(); + if (dierr != kDIErrNone) { + WMSG1(" UNISub: analysis failed (err=%d)\n", dierr); + goto bail; + } + + if (pNewImg->GetFSFormat() == DiskImg::kFormatUnknown || + pNewImg->GetSectorOrder() == DiskImg::kSectorOrderUnknown) + { + WMSG0(" UNISub: unable to identify filesystem\n"); + dierr = kDIErrUnsupportedFSFmt; + goto bail; + } + + /* open a DiskFS for the sub-image */ + WMSG1(" UNISub %d succeeded!\n", idx); + pNewFS = pNewImg->OpenAppropriateDiskFS(); + if (pNewFS == nil) { + WMSG0(" UNISub: OpenAppropriateDiskFS failed\n"); + dierr = kDIErrUnsupportedFSFmt; + goto bail; + } + + /* load the files from the sub-image */ + dierr = pNewFS->Initialize(pNewImg, kInitFull); + if (dierr != kDIErrNone) { + WMSG1(" UNISub: error %d reading list of files from disk", dierr); + goto bail; + } + + /* if this really is DOS 3.3, override the "volume name" */ + if (pNewImg->GetFSFormat() == DiskImg::kFormatDOS33) { + DiskFSDOS33* pDOS = (DiskFSDOS33*) pNewFS; /* eek, a downcast */ + pDOS->SetDiskVolumeNum(idx+1); + } + + /* + * Success, add it to the sub-volume list. + */ + AddSubVolumeToList(pNewImg, pNewFS); + +bail: + if (dierr != kDIErrNone) { + delete pNewFS; + delete pNewImg; + } + return dierr; +} diff --git a/diskimg/VolumeUsage.cpp b/diskimg/VolumeUsage.cpp new file mode 100644 index 0000000..7c49bc0 --- /dev/null +++ b/diskimg/VolumeUsage.cpp @@ -0,0 +1,278 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Support for the VolumeUsage sub-class in DiskFS. + */ +#include "StdAfx.h" +#include "DiskImgPriv.h" + + +/* + * Initialize structures for a block-structured disk. + */ +DIError +DiskFS::VolumeUsage::Create(long numBlocks) +{ + if (numBlocks <= 0 || numBlocks > 32*1024*1024) // 16GB + return kDIErrInvalidArg; + + fByBlocks = true; + fNumSectors = -1; + fTotalChunks = numBlocks; + fListSize = numBlocks; + fList = new unsigned char[fListSize]; + if (fList == nil) + return kDIErrMalloc; + + memset(fList, 0, fListSize); + + return kDIErrNone; +} + +/* + * Initialize structures for a track/sector-structured disk. + */ +DIError +DiskFS::VolumeUsage::Create(long numTracks, long numSectors) +{ + long count = numTracks * numSectors; + if (numTracks <= 0 || count <= 0 || count > 32*1024*1024) + return kDIErrInvalidArg; + + fByBlocks = false; + fNumSectors = numSectors; + fTotalChunks = count; + fListSize = count; + fList = new unsigned char[fListSize]; + if (fList == nil) + return kDIErrMalloc; + + memset(fList, 0, fListSize); + + return kDIErrNone; +} + +/* + * Return the state of a particular chunk. + */ +DIError +DiskFS::VolumeUsage::GetChunkState(long block, ChunkState* pState) const +{ + if (!fByBlocks) + return kDIErrInvalidArg; + return GetChunkStateIdx(block, pState); +} +DIError +DiskFS::VolumeUsage::GetChunkState(long track, long sector, + ChunkState* pState) const +{ + if (fByBlocks) + return kDIErrInvalidArg; + if (track < 0 || sector < 0 || sector >= fNumSectors) + return kDIErrInvalidArg; + return GetChunkStateIdx(track * fNumSectors + sector, pState); +} +DIError +DiskFS::VolumeUsage::GetChunkStateIdx(int idx, ChunkState* pState) const +{ + if (fList == nil || idx < 0 || idx >= fListSize) { + assert(false); + return kDIErrInvalidArg; + } + + unsigned char val = fList[idx]; + pState->isUsed = (val & kChunkUsedFlag) != 0; + pState->isMarkedUsed = (val & kChunkMarkedUsedFlag) != 0; + pState->purpose = (ChunkPurpose)(val & kChunkPurposeMask); + + return kDIErrNone; +} + +/* + * Set the state of a particular chunk. + */ +DIError +DiskFS::VolumeUsage::SetChunkState(long block, const ChunkState* pState) +{ + if (!fByBlocks) + return kDIErrInvalidArg; + return SetChunkStateIdx(block, pState); +} +DIError +DiskFS::VolumeUsage::SetChunkState(long track, long sector, + const ChunkState* pState) +{ + if (fByBlocks) + return kDIErrInvalidArg; + if (track < 0 || sector < 0 || sector >= fNumSectors) + return kDIErrInvalidArg; + return SetChunkStateIdx(track * fNumSectors + sector, pState); +} +DIError +DiskFS::VolumeUsage::SetChunkStateIdx(int idx, const ChunkState* pState) +{ + if (fList == nil || idx < 0 || idx >= fListSize) { + assert(false); + return kDIErrInvalidArg; + } + + unsigned char val = 0; + if (pState->isUsed) { + if ((pState->purpose & ~kChunkPurposeMask) != 0) { + assert(false); + return kDIErrInvalidArg; + } + val |= kChunkUsedFlag; + val |= (int)pState->purpose; + } + if (pState->isMarkedUsed) + val |= kChunkMarkedUsedFlag; + + fList[idx] = val; + + return kDIErrNone; +} + + +/* + * Count up the #of free chunks. + */ +long +DiskFS::VolumeUsage::GetActualFreeChunks(void) const +{ + ChunkState cstate; // could probably do this bitwise... + int freeCount = 0; + int funkyCount = 0; + + for (int i = 0; i < fTotalChunks; i++) { + if (GetChunkStateIdx(i, &cstate) != kDIErrNone) { + assert(false); + return -1; + } + + if (!cstate.isUsed && !cstate.isMarkedUsed) + freeCount++; + + if ((!cstate.isUsed && cstate.isMarkedUsed) || + (cstate.isUsed && !cstate.isMarkedUsed) || + (cstate.isUsed && cstate.purpose == kChunkPurposeConflict)) + { + funkyCount++; + } + } + + WMSG3(" VU total=%ld free=%d funky=%d\n", + fTotalChunks, freeCount, funkyCount); + + return freeCount; +} + + +/* + * Convert a ChunkState into a single, hopefully meaningful, character. + * + * Possible states: + * '.' - !inuse, !marked (free space) + * 'X' - !inuse, marked (could be embedded volume) + * '!' - inuse, !marked (danger!) + * '#' - inuse, marked, used by more than one thing + * 'S' - inuse, marked, used by system (directories, volume bit map) + * 'I' - inuse, marked, used by file structure (index block) + * 'F' - inuse, marked, used by file + */ +inline char +DiskFS::VolumeUsage::StateToChar(ChunkState* pState) const +{ + if (!pState->isUsed && !pState->isMarkedUsed) + return '.'; + if (!pState->isUsed && pState->isMarkedUsed) + return 'X'; + if (pState->isUsed && !pState->isMarkedUsed) + return '!'; + assert(pState->isUsed && pState->isMarkedUsed); + if (pState->purpose == kChunkPurposeUnknown) + return '?'; + if (pState->purpose == kChunkPurposeConflict) + return '#'; + if (pState->purpose == kChunkPurposeSystem) + return 'S'; + if (pState->purpose == kChunkPurposeVolumeDir) + return 'V'; + if (pState->purpose == kChunkPurposeSubdir) + return 'D'; + if (pState->purpose == kChunkPurposeUserData) + return 'F'; + if (pState->purpose == kChunkPurposeFileStruct) + return 'I'; + if (pState->purpose == kChunkPurposeEmbedded) + return 'E'; + + assert(false); + return '?'; +} + +/* + * Dump the list. + */ +void +DiskFS::VolumeUsage::Dump(void) const +{ +#define kMapInit "--------------------------------" + if (fList == nil) { + WMSG0(" VU asked to dump empty list?\n"); + return; + } + + WMSG1(" VU VolumeUsage dump (%ld free chunks):\n", + GetActualFreeChunks()); + + if (fByBlocks) { + ChunkState cstate; + char freemap[32+1] = kMapInit; + int block; + const int kEntriesPerLine = 32; // use 20 to match Copy][+ + + for (block = 0; block < fTotalChunks; block++) { + if (GetChunkState(block, &cstate) != kDIErrNone) { + assert(false); + return; + } + + freemap[block % kEntriesPerLine] = StateToChar(&cstate); + if ((block % kEntriesPerLine) == kEntriesPerLine-1) { + WMSG2(" 0x%04x: %s\n", block-(kEntriesPerLine-1), freemap); + } + } + if ((block % kEntriesPerLine) != 0) { + memset(freemap + (block % kEntriesPerLine), '-', + kEntriesPerLine - (block % kEntriesPerLine)); + WMSG2(" 0x%04x: %s\n", block-(kEntriesPerLine-1), freemap); + } + } else { + ChunkState cstate; + char freemap[32+1] = kMapInit; + long numTracks = fTotalChunks / fNumSectors; + int track, sector; + + if (fNumSectors > 32) { + WMSG1(" VU too many sectors (%ld)\n", fNumSectors); + return; + } + + WMSG0(" map 0123456789abcdef\n"); + + for (track = 0; track < numTracks; track++) { + for (sector = 0; sector < fNumSectors; sector++) { + if (GetChunkState(track, sector, &cstate) != kDIErrNone) { + assert(false); + return; + } + freemap[sector] = StateToChar(&cstate); + } + WMSG2(" %2d: %s\n", track, freemap); + } + } +} diff --git a/diskimg/Win32BlockIO.cpp b/diskimg/Win32BlockIO.cpp new file mode 100644 index 0000000..4093792 --- /dev/null +++ b/diskimg/Win32BlockIO.cpp @@ -0,0 +1,2164 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Win32 block I/O routines. + * + * See the header file for an explanation of why. + */ +#include "StdAfx.h" +#ifdef _WIN32 +#include "DiskImgPriv.h" +#include "SCSIDefs.h" +#include "ASPI.h" +#include "SPTI.h" + + +/* + * This is an ugly hack until I can figure out the right way to do this. To + * help prevent people from trashing their Windows volumes, we refuse to + * open "C:\" or physical0 for writing. On most systems, this is fine. + * + * The problem is that, on some systems with a mix of SATA and IDE devices, + * the boot disk is not physical disk 0. There should be a way to determine + * which physical disk is used for booting, or which physical disk + * corresponds to "C:", but I haven't been able to find it. + * + * So, for now, allow a global setting that disables the protection. I'm + * doing it this way rather than passing a parameter through because it + * requires adding an argument to several layers of "open", and I'm hoping + * to make this go away. + */ +//extern bool DiskImgLib::gAllowWritePhys0; + + +/* + * =========================================================================== + * Win32VolumeAccess + * =========================================================================== + */ + +/* + * Open a logical volume. + */ +DIError +Win32VolumeAccess::Open(const char* deviceName, bool readOnly) +{ + DIError dierr = kDIErrNone; + + assert(deviceName != nil); + + if (fpBlockAccess != nil) { + assert(false); + return kDIErrAlreadyOpen; + } + + if (strncmp(deviceName, kASPIDev, strlen(kASPIDev)) == 0) { + fpBlockAccess = new ASPIBlockAccess; + if (fpBlockAccess == nil) { + dierr = kDIErrMalloc; + goto bail; + } + dierr = fpBlockAccess->Open(deviceName, readOnly); + if (dierr != kDIErrNone) + goto bail; + } else if (deviceName[0] >= 'A' && deviceName[0] <= 'Z') { + fpBlockAccess = new LogicalBlockAccess; + if (fpBlockAccess == nil) { + dierr = kDIErrMalloc; + goto bail; + } + dierr = fpBlockAccess->Open(deviceName, readOnly); + if (dierr != kDIErrNone) + goto bail; + } else if (deviceName[0] >= '0' && deviceName[0] <= '9') { + fpBlockAccess = new PhysicalBlockAccess; + if (fpBlockAccess == nil) { + dierr = kDIErrMalloc; + goto bail; + } + dierr = fpBlockAccess->Open(deviceName, readOnly); + if (dierr != kDIErrNone) + goto bail; + } else { + WMSG1(" Win32VA: '%s' isn't the name of a device\n", deviceName); + return kDIErrInternal; + } + + // Need to do this now so we can use floppy geometry. + dierr = fpBlockAccess->DetectCapacity(&fTotalBlocks); + if (dierr != kDIErrNone) + goto bail; + assert(fTotalBlocks >= 0); + +bail: + if (dierr != kDIErrNone) { + delete fpBlockAccess; + fpBlockAccess = nil; + } + return dierr; +} + +/* + * Close the device. + */ +void +Win32VolumeAccess::Close(void) +{ + if (fpBlockAccess != nil) { + DIError dierr; + WMSG0(" Win32VolumeAccess closing\n"); + + dierr = FlushCache(true); + if (dierr != kDIErrNone) { + WMSG1("WARNING: Win32VA Close: FlushCache failed (err=%ld)\n", + dierr); + // not much we can do + } + dierr = fpBlockAccess->Close(); + if (dierr != kDIErrNone) { + WMSG1("WARNING: Win32VolumeAccess BlockAccess Close failed (err=%ld)\n", + dierr); + } + delete fpBlockAccess; + fpBlockAccess = nil; + } +} + + +/* + * Read a range of blocks from the device. + * + * Because some things like to read partial blocks, we cache the last block + * we read whenever the caller asks for a single block. This results in + * increased data copying, but since we know we're reading from a logical + * volume it's safe to assume that memory is *many* times faster than + * reading from this handle. + * + * Returns with an error if any of the blocks could not be read. + */ +DIError +Win32VolumeAccess::ReadBlocks(long startBlock, short blockCount, + void* buf) +{ + DIError dierr = kDIErrNone; + + assert(startBlock >= 0); + assert(blockCount > 0); + assert(buf != nil); + assert(fpBlockAccess != nil); + + if (blockCount == 1) { + if (fBlockCache.IsBlockInCache(startBlock)) { + fBlockCache.GetFromCache(startBlock, buf); + return kDIErrNone; + } + } else { + // If they're reading in large stretches, we don't need to use + // the cache. There is some chance that what we're about to + // read might include dirty blocks currently in the cache, so + // we flush what we have before the read. + dierr = FlushCache(true); + if (dierr != kDIErrNone) + return dierr; + } + + /* go read from the volume */ + dierr = fpBlockAccess->ReadBlocks(startBlock, blockCount, buf); + if (dierr != kDIErrNone) + goto bail; + + + /* if we're doing single-block reads, put it in the cache */ + if (blockCount == 1) { + if (!fBlockCache.IsRoomInCache(startBlock)) { + dierr = FlushCache(true); // make room + if (dierr != kDIErrNone) + goto bail; + } + + // after flushing, this should never fail + dierr = fBlockCache.PutInCache(startBlock, buf, false); + if (dierr != kDIErrNone) + goto bail; + } + +bail: + return dierr; +} + + +/* + * Write a range of blocks to the device. For the most part this just + * writes to the cache. + * + * Returns with an error if any of the blocks could not be read. + */ +DIError +Win32VolumeAccess::WriteBlocks(long startBlock, short blockCount, + const void* buf) +{ + DIError dierr = kDIErrNone; + + assert(startBlock >= 0); + assert(blockCount > 0); + assert(buf != nil); + + if (blockCount == 1) { + /* is this block already in the cache? */ + if (!fBlockCache.IsBlockInCache(startBlock)) { + /* not present, make sure it fits */ + if (!fBlockCache.IsRoomInCache(startBlock)) { + dierr = FlushCache(true); // make room + if (dierr != kDIErrNone) + goto bail; + } + } + + // after flushing, this should never fail + dierr = fBlockCache.PutInCache(startBlock, buf, true); + if (dierr != kDIErrNone) + goto bail; + } else { + // If they're writing in large stretches, we don't need to use + // the cache. We need to flush the cache in case what we're about + // to write would overwrite something in the cache -- if we don't, + // what's in the cache will effectively revert what we're about to + // write. + dierr = FlushCache(true); + if (dierr != kDIErrNone) + goto bail; + dierr = DoWriteBlocks(startBlock, blockCount, buf); + if (dierr != kDIErrNone) + goto bail; + } + +bail: + return dierr; +} + + +/* + * Write all blocks in the cache to disk if any of them are dirty. In some + * ways this is wasteful -- we could be writing stuff that isn't dirty, + * which isn't a great idea on (say) a CF volume -- but in practice we + * don't mix a lot of adjacent reads and writes, so this is pretty + * harmless. + * + * The goal was to write whole tracks on floppies. It's easy enough to + * disable the cache for CF devices if need be. + * + * If "purge" is set, we discard the blocks after writing them. + */ +DIError +Win32VolumeAccess::FlushCache(bool purge) +{ + DIError dierr = kDIErrNone; + + //WMSG1(" Win32VA: FlushCache (%d)\n", purge); + + if (fBlockCache.IsDirty()) { + long firstBlock; + int numBlocks; + void* buf; + + fBlockCache.GetCachePointer(&firstBlock, &numBlocks, &buf); + + WMSG3("FlushCache writing first=%d num=%d (purge=%d)\n", + firstBlock, numBlocks, purge); + dierr = DoWriteBlocks(firstBlock, numBlocks, buf); + if (dierr != kDIErrNone) { + WMSG1(" Win32VA: FlushCache write blocks failed (err=%d)\n", + dierr); + goto bail; + } + + // all written, clear the dirty flags + fBlockCache.Scrub(); + } + + if (purge) + fBlockCache.Purge(); + +bail: + return dierr; +} + + +/* + * =========================================================================== + * BlockAccess + * =========================================================================== + */ + +/* + * Detect the capacity of a drive using the SCSI READ CAPACITY command. Only + * works on CD-ROM drives and SCSI devices. + * + * Unfortunately, if you're accessing a hard drive through the BIOS, SPTI + * doesn't work. There must be a better way. + * + * On success, "*pNumBlocks" gets the number of 512-byte blocks. + */ +DIError +Win32VolumeAccess::BlockAccess::DetectCapacitySPTI(HANDLE handle, + bool isCDROM, long* pNumBlocks) +{ +#ifndef HAVE_WINDOWS_CDROM + if (isCDROM) + return kDIErrCDROMNotSupported; +#endif + + DIError dierr = kDIErrNone; + unsigned long lba, blockLen; + + dierr = SPTI::GetDeviceCapacity(handle, &lba, &blockLen); + if (dierr != kDIErrNone) + goto bail; + + WMSG3("READ CAPACITY reports lba=%lu blockLen=%lu (total=%lu)\n", + lba, blockLen, lba*blockLen); + + if (isCDROM && blockLen != kCDROMSectorSize) { + WMSG1("Unacceptable CD-ROM blockLen=%ld, bailing\n", blockLen); + dierr = kDIErrReadFailed; + goto bail; + } + + // The LBA is the last valid block on the disk. To get the disk size, + // we need to add one. + + *pNumBlocks = (blockLen/512) * (lba+1); + WMSG1(" SPTI returning 512-byte block count %ld\n", *pNumBlocks); + +bail: + return dierr; +} + +/* + * Figure out how large this disk volume is by probing for readable blocks. + * We take some guesses for common sizes and then binary-search if necessary. + * + * CF cards typically don't have as much space as they're rated for, possibly + * because of bad-block mapping (either bad blocks or space reserved for when + * blocks do go bad). + * + * This sets "*pNumBlocks" on success. The largest size this will detect + * is currently 8GB. + */ +/*static*/ DIError +Win32VolumeAccess::BlockAccess::ScanCapacity(BlockAccess* pThis, long* pNumBlocks) +{ + DIError dierr = kDIErrNone; + // max out at 8GB (-1 block) + const long kLargest = DiskImgLib::kVolumeMaxBlocks; + const long kTypicalSizes[] = { + // these must be in ascending order + 720*1024 / BlockAccess::kBlockSize, // 720K floppy + 1440*1024 / BlockAccess::kBlockSize, // 1.44MB floppy + 32*1024*1024 / BlockAccess::kBlockSize, // 32MB flash card + 64*1024*1024 / BlockAccess::kBlockSize, // 64MB flash card + 128*1024*1024 / BlockAccess::kBlockSize, // 128MB flash card + 256*1024*1024 / BlockAccess::kBlockSize, // 256MB flash card + //512*1024*1024 / BlockAccess::kBlockSize, // 512MB flash card + 2*1024*1024*(1024/BlockAccess::kBlockSize), // 2GB mark + kLargest + }; + long totalBlocks = 0; + int i; + + + // Trivial check to make sure *anything* works, and to establish block 0 + // as valid in case we have to bin-search. + if (!CanReadBlock(pThis, 0)) { + WMSG0(" Win32VolumeAccess: can't read block 0\n"); + dierr = kDIErrReadFailed; + goto bail; + } + + for (i = 0; i < NELEM(kTypicalSizes); i++) { + if (!CanReadBlock(pThis, kTypicalSizes[i])) { + /* failed reading, see if N-1 is readable */ + if (CanReadBlock(pThis, kTypicalSizes[i] - 1)) { + /* found it */ + totalBlocks = kTypicalSizes[i]; + break; + } else { + /* we overshot, binary-search backwards */ + WMSG1("OVERSHOT at %ld\n", kTypicalSizes[i]); + long good, bad; + if (i == 0) + good = 0; + else + good = kTypicalSizes[i-1]; // know this one is good + bad = kTypicalSizes[i]-1; // know this one is bad + + while (good != bad-1) { + long check = (good + bad) / 2; + assert(check > good); + assert(check < bad); + + if (CanReadBlock(pThis, check)) + good = check; + else + bad = check; + } + totalBlocks = bad; + break; + } + } + } + if (totalBlocks == 0) { + if (i == NELEM(kTypicalSizes)) { + /* huge volume, we never found a bad block */ + totalBlocks = kLargest; + } else { + /* we never found a good block */ + WMSG0(" Win32VolumeAccess unable to determine size\n"); + dierr = kDIErrReadFailed; + goto bail; + } + } + + if (totalBlocks > (3 * 1024 * 1024) / BlockAccess::kBlockSize) { + WMSG2(" GFDWinVolume: size is %ld (%.2fMB)\n", + totalBlocks, (float) totalBlocks / (2048.0)); + } else { + WMSG2(" GFDWinVolume: size is %ld (%.2fKB)\n", + totalBlocks, (float) totalBlocks / 2.0); + } + *pNumBlocks = totalBlocks; + +bail: + return dierr; +} + +/* + * Figure out if the block at "blockNum" exists. + */ +/*static*/ bool +Win32VolumeAccess::BlockAccess::CanReadBlock(BlockAccess* pThis, long blockNum) +{ + DIError dierr; + unsigned char buf[BlockAccess::kBlockSize]; + + dierr = pThis->ReadBlocks(blockNum, 1, buf); + if (dierr == kDIErrNone) { + WMSG1(" +++ Checked %ld (good)\n", blockNum); + return true; + } else { + WMSG1(" +++ Checked %ld (bad)\n", blockNum); + return false; + } +} + + +#ifndef INVALID_SET_FILE_POINTER +# define INVALID_SET_FILE_POINTER 0xFFFFFFFF +#endif + + +/* + * Most of these definitions come from MSFT knowledge base article #174569, + * "BUG: Int 21 Read/Write Track on Logical Drive Fails on OSR2 and Later". + */ +#define VWIN32_DIOC_DOS_IOCTL 1 // Int 21h 4400h through 4411h +#define VWIN32_DIOC_DOS_INT25 2 +#define VWIN32_DIOC_DOS_INT26 3 +#define VWIN32_DIOC_DOS_INT13 4 +#define VWIN32_DIOC_DOS_DRIVEINFO 6 // Int 21h 730x commands + +typedef struct _DIOC_REGISTERS { + DWORD reg_EBX; + DWORD reg_EDX; + DWORD reg_ECX; + DWORD reg_EAX; + DWORD reg_EDI; + DWORD reg_ESI; + DWORD reg_Flags; +} DIOC_REGISTERS, *PDIOC_REGISTERS; + +#define CARRY_FLAG 1 + +#pragma pack(1) +typedef struct _DISKIO { + DWORD dwStartSector; // starting logical sector number + WORD wSectors; // number of sectors + DWORD dwBuffer; // address of read/write buffer +} DISKIO, *PDISKIO; +typedef struct _DRIVEMAPINFO { + BYTE dmiAllocationLength; + BYTE dmiInfoLength; + BYTE dmiFlags; + BYTE dmiInt13Unit; + DWORD dmiAssociatedDriveMap; + DWORD dmiPartitionStartRBA_lo; + DWORD dmiPartitionStartRBA_hi; +} DRIVEMAPINFO, *PDRIVEMAPINFO; +#pragma pack() + +#define kInt13StatusMissingAddrMark 2 // sector number above media format +#define kInt13StatusWriteProtected 3 // disk is write protected +#define kInt13StatusSectorNotFound 4 // sector number above drive cap +// == 10 for bad blocks?? +#define kInt13StatusTimeout 128 // drive not responding + +#if 0 +/* + * Determine the mapping between a logical device number (A=1, B=2, etc) + * and the Int13h unit number (floppy=00h, hard drive=80h, etc). + * + * Pass in the vwin32 handle. + */ +/*static*/ DIError +Win32VolumeAccess::BlockAccess::GetInt13Unit(HANDLE handle, int driveNum, int* pInt13Unit) +{ + DIError dierr = kDIErrNone; + BOOL result; + DWORD cb; + DRIVEMAPINFO driveMapInfo = {0}; + DIOC_REGISTERS reg = {0}; + const int kGetDriveMapInfo = 0x6f; + const int kDeviceCategory1 = 0x08; // for older stuff + const int kDeviceCategory2 = 0x48; // for FAT32 + + assert(handle != nil); + assert(driveNum > 0 && driveNum <= kNumLogicalVolumes); + assert(pInt13Unit != nil); + + *pInt13Unit = -1; + + driveMapInfo.dmiAllocationLength = sizeof(DRIVEMAPINFO); // should be 16 + + reg.reg_EAX = 0x440d; // generic IOCTL + reg.reg_EBX = driveNum; + reg.reg_ECX = MAKEWORD(kGetDriveMapInfo, kDeviceCategory1); + reg.reg_EDX = (DWORD) &driveMapInfo; + + result = ::DeviceIoControl(handle, VWIN32_DIOC_DOS_IOCTL, + ®, sizeof(reg), + ®, sizeof(reg), &cb, 0); + if (result == 0) { + WMSG1(" DeviceIoControl(Int21h, 6fh) FAILED (err=%ld)\n", + GetLastError()); + dierr = LastErrorToDIError(); + goto bail; + } + + if (reg.reg_Flags & CARRY_FLAG) { + WMSG1(" --- retrying GDMI with alternate device category (ax=%ld)\n", + reg.reg_EAX); + reg.reg_EAX = 0x440d; // generic IOCTL + reg.reg_EBX = driveNum; + reg.reg_ECX = MAKEWORD(kGetDriveMapInfo, kDeviceCategory2); + reg.reg_EDX = (DWORD) &driveMapInfo; + result = ::DeviceIoControl(handle, VWIN32_DIOC_DOS_IOCTL, + ®, sizeof(reg), + ®, sizeof(reg), &cb, 0); + } + if (result == 0) { + WMSG1(" DeviceIoControl(Int21h, 6fh)(retry) FAILED (err=%ld)\n", + GetLastError()); + dierr = LastErrorToDIError(); + goto bail; + } + if (reg.reg_Flags & CARRY_FLAG) { + WMSG1(" --- GetDriveMapInfo call (retry) failed (ax=%ld)\n", + reg.reg_EAX); + dierr = kDIErrReadFailed; // close enough + goto bail; + } + + WMSG3(" +++ DriveMapInfo len=%d flags=%d unit=%d\n", + driveMapInfo.dmiInfoLength, driveMapInfo.dmiFlags, + driveMapInfo.dmiInt13Unit); + WMSG3(" +++ driveMap=0x%08lx RBA=0x%08lx 0x%08lx\n", + driveMapInfo.dmiAssociatedDriveMap, + driveMapInfo.dmiPartitionStartRBA_hi, + driveMapInfo.dmiPartitionStartRBA_lo); + + if (driveMapInfo.dmiInfoLength < 4) { + /* results not covered in reply? */ + dierr = kDIErrReadFailed; // not close, but it'll do + goto bail; + } + + *pInt13Unit = driveMapInfo.dmiInt13Unit; + +bail: + return dierr; +} +#endif + +#if 0 +/* + * Look up the geometry for a floppy disk whose total size is "totalBlocks". + * There is no BIOS function to detect the media size and geometry, so + * we have to do it this way. PC "FAT" disks have a size indication + * in the boot block, but we can't rely on that. + * + * Returns "true" if the geometry is known, "false" otherwise. When "true" + * is returned, "*pNumTracks", "*pNumHeads", and "*pNumSectors" will receive + * values if the pointers are non-nil. + */ +/*static*/ bool +Win32VolumeAccess::BlockAccess::LookupFloppyGeometry(long totalBlocks, + DiskGeometry* pGeometry) +{ + static const struct { + FloppyKind kind; + long blockCount; // total #of blocks on the disk + int numCyls; // #of cylinders + int numHeads; // #of heads per cylinder + int numSectors; // #of sectors/track + } floppyGeometry[] = { + { kFloppyUnknown, -1, -1, -1, -1 }, + { kFloppy525_360, 360*2, 40, 2, 9 }, + { kFloppy525_1200, 1200*2, 80, 2, 15 }, + { kFloppy35_720, 720*2, 80, 2, 9 }, + { kFloppy35_1440, 1440*2, 80, 2, 18 }, + { kFloppy35_2880, 2880*2, 80, 2, 36 } + }; + + /* verify that we can directly index the table with the enum */ + for (int chk = 0; chk < NELEM(floppyGeometry); chk++) { + assert(floppyGeometry[chk].kind == chk); + } + assert(chk == kFloppyMax); + + if (totalBlocks <= 0) { + // still auto-detecting volume size? + return false; + } + + int i; + for (i = 0; i < NELEM(floppyGeometry); i++) + if (floppyGeometry[i].blockCount == totalBlocks) + break; + + if (i == NELEM(floppyGeometry)) { + WMSG1( "GetFloppyGeometry: no match for blocks=%ld\n", totalBlocks); + return false; + } + + pGeometry->numCyls = floppyGeometry[i].numCyls; + pGeometry->numHeads = floppyGeometry[i].numHeads; + pGeometry->numSectors = floppyGeometry[i].numSectors; + + return true; +} +#endif + +/* + * Convert a block number to a cylinder/head/sector offset. Also figures + * out what the last block on the current track is. Sectors are returned + * in 1-based form. + * + * Returns "true" on success, "false" on failure. + */ +/*static*/ bool +Win32VolumeAccess::BlockAccess::BlockToCylinderHeadSector(long blockNum, + const DiskGeometry* pGeometry, int* pCylinder, int* pHead, + int* pSector, long* pLastBlockOnTrack) +{ + int cylinder, head, sector; + long lastBlockOnTrack; + int leftOver; + + cylinder = blockNum / (pGeometry->numSectors * pGeometry->numHeads); + leftOver = blockNum - cylinder * (pGeometry->numSectors * pGeometry->numHeads); + head = leftOver / pGeometry->numSectors; + sector = leftOver - (head * pGeometry->numSectors); + + assert(cylinder >= 0 && cylinder < pGeometry->numCyls); + assert(head >= 0 && head < pGeometry->numHeads); + assert(sector >= 0 && sector < pGeometry->numSectors); + + lastBlockOnTrack = blockNum + (pGeometry->numSectors - sector -1); + + if (pCylinder != nil) + *pCylinder = cylinder; + if (pHead != nil) + *pHead = head; + if (pSector != nil) + *pSector = sector+1; + if (pLastBlockOnTrack != nil) + *pLastBlockOnTrack = lastBlockOnTrack; + + return true; +} + +/* + * Get the floppy drive kind (*not* the media kind) using Int13h func 8. + */ +/*static*/ DIError +Win32VolumeAccess::BlockAccess::GetFloppyDriveKind(HANDLE handle, int unitNum, + FloppyKind* pKind) +{ + DIOC_REGISTERS reg = {0}; + DWORD cb; + BOOL result; + + reg.reg_EAX = MAKEWORD(0, 0x08); // Read Diskette Drive Parameters + reg.reg_EDX = MAKEWORD(unitNum, 0); + + result = DeviceIoControl(handle, VWIN32_DIOC_DOS_INT13, ®, + sizeof(reg), ®, sizeof(reg), &cb, 0); + + if (result == 0 || (reg.reg_Flags & CARRY_FLAG)) { + WMSG3(" GetFloppyKind failed: result=%d flags=0x%04x AH=%d\n", + result, reg.reg_Flags, HIBYTE(reg.reg_EAX)); + return kDIErrGeneric; + } + + int bl = LOBYTE(reg.reg_EBX); + if (bl > 0 && bl < 6) + *pKind = (FloppyKind) bl; + else { + WMSG1(" GetFloppyKind: unrecognized kind %d\n", bl); + return kDIErrGeneric; + } + + return kDIErrNone; +} + +/* + * Read one or more blocks using Int13h services. This only works on + * floppy drives due to Win9x limitations. + * + * The average BIOS will only read one or two tracks reliably, and may + * not work well when straddling tracks. It's up to the caller to + * ensure that the parameters are set properly. + * + * "cylinder" and "head" are 0-based, "sector" is 1-based. + * + * Returns 0 on success, the status code from AH on failure. If the call + * fails but AH is zero, -1 is returned. + */ +/*static*/ int +Win32VolumeAccess::BlockAccess::ReadBlocksInt13h(HANDLE handle, int unitNum, + int cylinder, int head, int sector, short blockCount, void* buf) +{ + DIOC_REGISTERS reg = {0}; + DWORD cb; + BOOL result; + + if (unitNum < 0 || unitNum >= 4) { + assert(false); + return kDIErrInternal; + } + + for (int retry = 0; retry < kMaxFloppyRetries; retry++) { + reg.reg_EAX = MAKEWORD(blockCount, 0x02); // read N sectors + reg.reg_EBX = (DWORD) buf; + reg.reg_ECX = MAKEWORD(sector, cylinder); + reg.reg_EDX = MAKEWORD(unitNum, head); + + //WMSG4(" DIOC Int13h read c=%d h=%d s=%d rc=%d\n", + // cylinder, head, sector, blockCount); + result = DeviceIoControl(handle, VWIN32_DIOC_DOS_INT13, ®, + sizeof(reg), ®, sizeof(reg), &cb, 0); + + if (result != 0 && !(reg.reg_Flags & CARRY_FLAG)) + break; // success! + + /* if it's an invalid sector request, bail out immediately */ + if (HIBYTE(reg.reg_EAX) == kInt13StatusSectorNotFound || + HIBYTE(reg.reg_EAX) == kInt13StatusMissingAddrMark) + { + break; + } + WMSG1(" DIOC soft read failure, ax=0x%08lx\n", reg.reg_EAX); + } + if (!result || (reg.reg_Flags & CARRY_FLAG)) { + int ah = HIBYTE(reg.reg_EAX); + WMSG2(" DIOC read failed, result=%d ah=%ld\n", result, ah); + if (ah != 0) + return ah; + else + return -1; + } + + return 0; +} + + +/* + * Read one or more blocks using Int13h services. This only works on + * floppy drives due to Win9x limitations. + * + * It's important to be able to read multiple blocks for performance + * reasons. Because this is fairly "raw", we have to retry it 3x before + * giving up. + */ +/*static*/ DIError +Win32VolumeAccess::BlockAccess::ReadBlocksInt13h(HANDLE handle, int unitNum, + const DiskGeometry* pGeometry, long startBlock, short blockCount, + void* buf) +{ + int cylinder, head, sector; + int status, runCount; + long lastBlockOnTrack; + + if (unitNum < 0 || unitNum >= 4) { + assert(false); + return kDIErrInternal; + } + if (startBlock < 0 || blockCount <= 0) + return kDIErrInvalidArg; + if (startBlock + blockCount > pGeometry->blockCount) { + WMSG2(" ReadInt13h: invalid request for start=%ld count=%d\n", + startBlock, blockCount); + return kDIErrReadFailed; + } + + while (blockCount) { + int result; + result = BlockToCylinderHeadSector(startBlock, pGeometry, + &cylinder, &head, §or, &lastBlockOnTrack); + assert(result); + + /* + * According to "The Undocumented PC", the average BIOS will read + * at most one or two tracks reliably. It's really geared toward + * writing a single track or cylinder with one call. We want to + * be sure that our read doesn't straddle tracks, so we break it + * down as needed. + */ + runCount = lastBlockOnTrack - startBlock +1; + if (runCount > blockCount) + runCount = blockCount; + //WMSG3("R runCount=%d lastBlkOnT=%d start=%d\n", + // runCount, lastBlockOnTrack, startBlock); + assert(runCount > 0); + + status = ReadBlocksInt13h(handle, unitNum, cylinder, head, sector, + runCount, buf); + if (status != 0) { + WMSG1(" DIOC read failed (status=%d)\n", status); + return kDIErrReadFailed; + } + + startBlock += runCount; + blockCount -= runCount; + } + + return kDIErrNone; +} + +/* + * Write one or more blocks using Int13h services. This only works on + * floppy drives due to Win9x limitations. + * + * "cylinder" and "head" are 0-based, "sector" is 1-based. + * + * It's important to be able to write multiple blocks for performance + * reasons. Because this is fairly "raw", we have to retry it 3x before + * giving up. + */ +/*static*/ int +Win32VolumeAccess::BlockAccess::WriteBlocksInt13h(HANDLE handle, int unitNum, + int cylinder, int head, int sector, short blockCount, + const void* buf) +{ + DIOC_REGISTERS reg = {0}; + DWORD cb; + BOOL result; + + for (int retry = 0; retry < kMaxFloppyRetries; retry++) { + reg.reg_EAX = MAKEWORD(blockCount, 0x03); // write N sectors + reg.reg_EBX = (DWORD) buf; + reg.reg_ECX = MAKEWORD(sector, cylinder); + reg.reg_EDX = MAKEWORD(unitNum, head); + + WMSG4(" DIOC Int13h write c=%d h=%d s=%d rc=%d\n", + cylinder, head, sector, blockCount); + result = DeviceIoControl(handle, VWIN32_DIOC_DOS_INT13, ®, + sizeof(reg), ®, sizeof(reg), &cb, 0); + + if (result != 0 && !(reg.reg_Flags & CARRY_FLAG)) + break; // success! + + if (HIBYTE(reg.reg_EAX) == kInt13StatusWriteProtected) + break; // no point retrying this + WMSG1(" DIOC soft write failure, ax=0x%08lx\n", reg.reg_EAX); + } + if (!result || (reg.reg_Flags & CARRY_FLAG)) { + int ah = HIBYTE(reg.reg_EAX); + WMSG2(" DIOC write failed, result=%d ah=%ld\n", result, ah); + if (ah != 0) + return ah; + else + return -1; + } + + return 0; +} + +/* + * Write one or more blocks using Int13h services. This only works on + * floppy drives. + * + * It's important to be able to write multiple blocks for performance + * reasons. Because this is fairly "raw", we have to retry it 3x before + * giving up. + * + * Returns "true" on success, "false" on failure. + */ +/*static*/ DIError +Win32VolumeAccess::BlockAccess::WriteBlocksInt13h(HANDLE handle, int unitNum, + const DiskGeometry* pGeometry, long startBlock, short blockCount, + const void* buf) +{ + int cylinder, head, sector; + int status, runCount; + long lastBlockOnTrack; + + // make sure this is a floppy drive and we know which unit it is + if (unitNum < 0 || unitNum >= 4) { + assert(false); + return kDIErrInternal; + } + if (startBlock < 0 || blockCount <= 0) + return kDIErrInvalidArg; + if (startBlock + blockCount > pGeometry->blockCount) { + WMSG2(" WriteInt13h: invalid request for start=%ld count=%d\n", + startBlock, blockCount); + return kDIErrWriteFailed; + } + + while (blockCount) { + int result; + result = BlockToCylinderHeadSector(startBlock, pGeometry, + &cylinder, &head, §or, &lastBlockOnTrack); + assert(result); + + runCount = lastBlockOnTrack - startBlock +1; + if (runCount > blockCount) + runCount = blockCount; + //WMSG3("W runCount=%d lastBlkOnT=%d start=%d\n", + // runCount, lastBlockOnTrack, startBlock); + assert(runCount > 0); + + status = WriteBlocksInt13h(handle, unitNum, cylinder, head, sector, + runCount, buf); + if (status != 0) { + WMSG1(" DIOC write failed (status=%d)\n", status); + if (status == kInt13StatusWriteProtected) + return kDIErrWriteProtected; + else + return kDIErrWriteFailed; + } + + startBlock += runCount; + blockCount -= runCount; + } + + return kDIErrNone; +} + + +/* + * Read blocks from a Win9x logical volume, using Int21h func 7305h. Pass in + * a handle to vwin32 and the logical drive number (A=1, B=2, etc). + * + * Works on Win95 OSR2 and later. Earlier versions require Int25 or Int13. + */ +/*static*/ DIError +Win32VolumeAccess::BlockAccess::ReadBlocksInt21h(HANDLE handle, int driveNum, + long startBlock, short blockCount, void* buf) +{ +#if 0 + assert(false); // discouraged + BOOL result; + DWORD cb; + DIOC_REGISTERS reg = {0}; + DISKIO dio = {0}; + + dio.dwStartSector = startBlock; + dio.wSectors = (WORD) blockCount; + dio.dwBuffer = (DWORD) buf; + + reg.reg_EAX = fDriveNum - 1; // Int 25h drive numbers are 0-based. + reg.reg_EBX = (DWORD)&dio; + reg.reg_ECX = 0xFFFF; // use DISKIO struct + + WMSG3(" Int25 read start=%d count=%d\n", + startBlock, blockCount); + result = ::DeviceIoControl(handle, VWIN32_DIOC_DOS_INT25, + ®, sizeof(reg), + ®, sizeof(reg), &cb, 0); + + // Determine if the DeviceIoControl call and the read succeeded. + result = result && !(reg.reg_Flags & CARRY_FLAG); + + WMSG2(" +++ read from block %ld (result=%d)\n", startBlock, result); + if (!result) + return kDIErrReadFailed; +#else + BOOL result; + DWORD cb; + DIOC_REGISTERS reg = {0}; + DISKIO dio = {0}; + + dio.dwStartSector = startBlock; + dio.wSectors = (WORD) blockCount; + dio.dwBuffer = (DWORD) buf; + + reg.reg_EAX = 0x7305; // Ext_ABSDiskReadWrite + reg.reg_EBX = (DWORD)&dio; + reg.reg_ECX = -1; + reg.reg_EDX = driveNum; // Int 21h, fn 7305h drive numbers are 1-based + assert(reg.reg_ESI == 0); // read/write flag + + WMSG2(" Int21/7305h read start=%d count=%d\n", + startBlock, blockCount); + result = ::DeviceIoControl(handle, VWIN32_DIOC_DOS_DRIVEINFO, + ®, sizeof(reg), + ®, sizeof(reg), &cb, 0); + + // Determine if the DeviceIoControl call and the read succeeded. + result = result && !(reg.reg_Flags & CARRY_FLAG); + + WMSG4(" +++ RB21h %ld %d (result=%d lastError=%ld)\n", + startBlock, blockCount, result, GetLastError()); + if (!result) + return kDIErrReadFailed; +#endif + + return kDIErrNone; +} + +/* + * Write blocks to a Win9x logical volume. Pass in a handle to vwin32 and + * the logical drive number (A=1, B=2, etc). + * + * Works on Win95 OSR2 and later. Earlier versions require Int26 or Int13. + */ +/*static*/ DIError +Win32VolumeAccess::BlockAccess::WriteBlocksInt21h(HANDLE handle, int driveNum, + long startBlock, short blockCount, const void* buf) +{ + BOOL result; + DWORD cb; + DIOC_REGISTERS reg = {0}; + DISKIO dio = {0}; + + dio.dwStartSector = startBlock; + dio.wSectors = (WORD) blockCount; + dio.dwBuffer = (DWORD) buf; + + reg.reg_EAX = 0x7305; // Ext_ABSDiskReadWrite + reg.reg_EBX = (DWORD)&dio; + reg.reg_ECX = -1; + reg.reg_EDX = driveNum; // Int 21h, fn 7305h drive numbers are 1-based + reg.reg_ESI = 0x6001; // write normal data (bit flags) + + //WMSG2(" Int21/7305h write start=%d count=%d\n", + // startBlock, blockCount); + result = ::DeviceIoControl(handle, VWIN32_DIOC_DOS_DRIVEINFO, + ®, sizeof(reg), + ®, sizeof(reg), &cb, 0); + + // Determine if the DeviceIoControl call and the read succeeded. + result = result && !(reg.reg_Flags & CARRY_FLAG); + + WMSG4(" +++ WB21h %ld %d (result=%d lastError=%ld)\n", + startBlock, blockCount, result, GetLastError()); + if (!result) + return kDIErrWriteFailed; + + return kDIErrNone; +} + + +/* + * Read blocks from a Win2K logical or physical volume. + */ +/*static*/ DIError +Win32VolumeAccess::BlockAccess::ReadBlocksWin2K(HANDLE handle, + long startBlock, short blockCount, void* buf) +{ + /* + * Try to read the blocks. Under Win2K the seek and read calls appear + * to succeed, but the value in "actual" is set to zero to indicate + * that we're trying to read past EOF. + */ + DWORD posn, actual; + + /* + * Win2K: the 3rd argument holds the high 32 bits of the distance to + * move. This isn't supported in Win9x, which means Win9x is limited + * to 2GB files. + */ + LARGE_INTEGER li; + li.QuadPart = (LONGLONG) startBlock * (LONGLONG) kBlockSize; + posn = ::SetFilePointer(handle, li.LowPart, &li.HighPart, + FILE_BEGIN); + if (posn == INVALID_SET_FILE_POINTER) { + DWORD lerr = GetLastError(); + WMSG1(" GFDWinVolume ReadBlock: SFP failed (err=%ld)\n", lerr); + return LastErrorToDIError(); + } + + //WMSG2(" ReadFile block start=%d count=%d\n", startBlock, blockCount); + + BOOL result; + result = ::ReadFile(handle, buf, blockCount * kBlockSize, &actual, nil); + if (!result) { + DWORD lerr = GetLastError(); + WMSG3(" ReadBlocksWin2K: ReadFile failed (start=%ld count=%d err=%ld)\n", + startBlock, blockCount, lerr); + return LastErrorToDIError(); + } + if ((long) actual != blockCount * kBlockSize) + return kDIErrEOF; + + return kDIErrNone; +} + + +/* + * Write blocks to a Win2K logical or physical volume. + */ +/*static*/ DIError +Win32VolumeAccess::BlockAccess::WriteBlocksWin2K(HANDLE handle, + long startBlock, short blockCount, const void* buf) +{ + DWORD posn, actual; + + posn = ::SetFilePointer(handle, startBlock * kBlockSize, nil, + FILE_BEGIN); + if (posn == INVALID_SET_FILE_POINTER) { + DWORD lerr = GetLastError(); + WMSG2(" GFDWinVolume ReadBlocks: SFP %ld failed (err=%ld)\n", + startBlock * kBlockSize, lerr); + return LastErrorToDIError(); + } + + BOOL result; + result = ::WriteFile(handle, buf, blockCount * kBlockSize, &actual, nil); + if (!result) { + DWORD lerr = GetLastError(); + WMSG1(" GFDWinVolume WriteBlocks: WriteFile failed (err=%ld)\n", + lerr); + return LastErrorToDIError(); + } + if ((long) actual != blockCount * kBlockSize) + return kDIErrEOF; // unexpected on a write call? + + return kDIErrNone; +} + + +/* + * =========================================================================== + * LogicalBlockAccess + * =========================================================================== + */ + +/* + * Open a logical device. The device name should be of the form "A:\". + */ +DIError +Win32VolumeAccess::LogicalBlockAccess::Open(const char* deviceName, bool readOnly) +{ + DIError dierr = kDIErrNone; + const bool kPreferASPI = true; + + assert(fHandle == nil); + fIsCDROM = false; + fDriveNum = -1; + + if (deviceName[0] < 'A' || deviceName[0] > 'Z' || + deviceName[1] != ':' || deviceName[2] != '\\' || + deviceName[3] != '\0') + { + WMSG1(" LogicalBlockAccess: invalid device name '%s'\n", deviceName); + assert(false); + dierr = kDIErrInvalidArg; + goto bail; + } + if (deviceName[0] == 'C') { + if (readOnly == false) { + WMSG0(" REFUSING WRITE ACCESS TO C:\\ \n"); + return kDIErrVWAccessForbidden; + } + } + + DWORD access; + if (readOnly) + access = GENERIC_READ; + else + access = GENERIC_READ | GENERIC_WRITE; + + UINT driveType; + driveType = GetDriveType(deviceName); + if (driveType == DRIVE_CDROM) { + if (!Global::GetHasSPTI() && !Global::GetHasASPI()) + return kDIErrCDROMNotSupported; + + fIsCDROM = true; + // SPTI needs this -- maybe to enforce exclusive access? + access |= GENERIC_WRITE; + } + + if (fIsWin9x) { + if (fIsCDROM) + return kDIErrCDROMNotSupported; + + fHandle = CreateFile("\\\\.\\vwin32", 0, 0, NULL, + OPEN_EXISTING, FILE_FLAG_DELETE_ON_CLOSE, NULL); + if (fHandle == INVALID_HANDLE_VALUE) { + DWORD lastError = GetLastError(); + WMSG1(" Win32LVOpen: CreateFile(vwin32) failed (err=%ld)\n", + lastError); + dierr = LastErrorToDIError(); + goto bail; + } + fDriveNum = deviceName[0] - 'A' +1; + assert(fDriveNum > 0 && fDriveNum <= kNumLogicalVolumes); + +#if 0 + int int13Unit; + dierr = GetInt13Unit(fHandle, fDriveNum, &int13Unit); + if (dierr != kDIErrNone) + goto bail; + if (int13Unit < 4) { + fFloppyUnit = int13Unit; + WMSG2(" Logical volume #%d looks like floppy unit %d\n", + fDriveNum, fFloppyUnit); + } +#endif + } else { + char device[7] = "\\\\.\\_:"; + device[4] = deviceName[0]; + WMSG1("Opening '%s'\n", device); + + // If we're reading, allow others to write. If we're writing, insist + // upon exclusive access to the volume. + DWORD shareMode = FILE_SHARE_READ; + if (access == GENERIC_READ) + shareMode |= FILE_SHARE_WRITE; + + fHandle = CreateFile(device, access, shareMode, + NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (fHandle == INVALID_HANDLE_VALUE) { + DWORD lastError = GetLastError(); + dierr = LastErrorToDIError(); + if (lastError == ERROR_INVALID_PARAMETER && shareMode == FILE_SHARE_READ) + { + // Win2K spits this back if the volume is open and we're + // not specifying FILE_SHARE_WRITE. Give it a try, just to + // see if it works, so we can tell the difference between + // an internal library error and an active filesystem. + HANDLE tmpHandle; + tmpHandle = CreateFile(device, access, FILE_SHARE_READ|FILE_SHARE_WRITE, + NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (tmpHandle != INVALID_HANDLE_VALUE) { + dierr = kDIErrNoExclusiveAccess; + ::CloseHandle(tmpHandle); + } + } + WMSG2(" LBAccess Open: CreateFile failed (err=%ld dierr=%d)\n", + lastError, dierr); + goto bail; + } + } + + assert(fHandle != nil && fHandle != INVALID_HANDLE_VALUE); + +#if 0 + if (fIsCDROM) { + assert(Global::GetHasSPTI() || Global::GetHasASPI()); + if (Global::GetHasASPI() && (!Global::GetHasSPTI() || kPreferASPI)) { + WMSG0(" LBAccess using ASPI\n"); + fCDBaggage.fUseASPI = true; + } else { + WMSG0(" LBAccess using SPTI\n"); + fCDBaggage.fUseASPI = false; + } + + if (fCDBaggage.fUseASPI) { + dierr = FindASPIDriveMapping(deviceName[0]); + if (dierr != kDIErrNone) { + WMSG0("ERROR: couldn't find ASPI drive mapping\n"); + dierr = kDIErrNoASPIMapping; + goto bail; + } + } + } +#endif + +bail: + if (dierr != kDIErrNone) { + if (fHandle != nil && fHandle != INVALID_HANDLE_VALUE) + ::CloseHandle(fHandle); + fHandle = nil; + } + return dierr; +} + +/* + * Close the device handle. + */ +DIError +Win32VolumeAccess::LogicalBlockAccess::Close(void) +{ + if (fHandle != nil) { + ::CloseHandle(fHandle); + fHandle = nil; + } + return kDIErrNone; +} + + +/* + * Read 512-byte blocks from CD-ROM media using SPTI calls. + */ +DIError +Win32VolumeAccess::LogicalBlockAccess::ReadBlocksCDROM(HANDLE handle, + long startBlock, short blockCount, void* buf) +{ +#ifdef HAVE_WINDOWS_CDROM + DIError dierr; + + assert(handle != nil); + assert(startBlock >= 0); + assert(blockCount > 0); + assert(buf != nil); + + //WMSG2(" CDROM read block %ld (%ld)\n", startBlock, block); + + /* alloc sector buffer on first use */ + if (fLastSectorCache == nil) { + fLastSectorCache = new unsigned char[kCDROMSectorSize]; + if (fLastSectorCache == nil) + return kDIErrMalloc; + assert(fLastSectorNum == -1); + } + + /* + * Map a range of 512-byte blocks to a range of 2048-byte blocks. + */ + assert(kCDROMSectorSize % kBlockSize == 0); + const int kFactor = kCDROMSectorSize / kBlockSize; + long sectorIndex = startBlock / kFactor; + int sectorOffset = (int) (startBlock % kFactor); // 0-3 + + /* + * When possible, do multi-block reads directly into "buf". The first + * and last block may require special handling. + */ + while (blockCount) { + assert(blockCount > 0); + + if (sectorOffset != 0 || blockCount < kFactor) { + assert(sectorOffset >= 0 && sectorOffset < kFactor); + + /* get from single-block cache or from disc */ + if (sectorIndex != fLastSectorNum) { + fLastSectorNum = -1; // invalidate, in case of error + + dierr = SPTI::ReadBlocks(handle, sectorIndex, 1, + kCDROMSectorSize, fLastSectorCache); + if (dierr != kDIErrNone) + return dierr; + + fLastSectorNum = sectorIndex; + } + + int thisNumBlocks; + thisNumBlocks = kFactor - sectorOffset; + if (thisNumBlocks > blockCount) + thisNumBlocks = blockCount; + + //WMSG3(" Small copy (sectIdx=%ld off=%d*512 size=%d*512)\n", + // sectorIndex, sectorOffset, thisNumBlocks); + memcpy(buf, fLastSectorCache + sectorOffset * kBlockSize, + thisNumBlocks * kBlockSize); + + blockCount -= thisNumBlocks; + buf = (unsigned char*) buf + (thisNumBlocks * kBlockSize); + + sectorOffset = 0; + sectorIndex++; + } else { + fLastSectorNum = -1; // invalidate single-block cache + + int numSectors; + numSectors = blockCount / kFactor; // rounds down + + //WMSG2(" Big read (sectIdx=%ld numSect=%d)\n", + // sectorIndex, numSectors); + dierr = SPTI::ReadBlocks(handle, sectorIndex, numSectors, + kCDROMSectorSize, buf); + if (dierr != kDIErrNone) + return dierr; + + blockCount -= numSectors * kFactor; + buf = (unsigned char*) buf + (numSectors * kCDROMSectorSize); + + sectorIndex += numSectors; + } + } + + return kDIErrNone; + +#else + return kDIErrCDROMNotSupported; +#endif +} + + +/* + * =========================================================================== + * PhysicalBlockAccess + * =========================================================================== + */ + +/* + * Open a physical device. The device name should be of the form "80:\". + */ +DIError +Win32VolumeAccess::PhysicalBlockAccess::Open(const char* deviceName, bool readOnly) +{ + DIError dierr = kDIErrNone; + + // initialize all local state + assert(fHandle == nil); + fInt13Unit = -1; + fFloppyKind = kFloppyUnknown; + memset(&fGeometry, 0, sizeof(fGeometry)); + + // sanity-check name; not this only works for first 10 devices + if (deviceName[0] < '0' || deviceName[0] > '9' || + deviceName[1] < '0' || deviceName[1] > '9' || + deviceName[2] != ':' || deviceName[3] != '\\' || + deviceName[4] != '\0') + { + WMSG1(" PhysicalBlockAccess: invalid device name '%s'\n", deviceName); + assert(false); + dierr = kDIErrInvalidArg; + goto bail; + } + + if (deviceName[0] == '8' && deviceName[1] == '0') { + if (!gAllowWritePhys0 && readOnly == false) { + WMSG0(" REFUSING WRITE ACCESS TO 80:\\ \n"); + return kDIErrVWAccessForbidden; + } + } + + fInt13Unit = (deviceName[0] - '0') * 16 + deviceName[1] - '0'; + if (!fIsWin9x && fInt13Unit < 0x80) { + WMSG0("GLITCH: can't open floppy as physical unit in Win2K\n"); + dierr = kDIErrInvalidArg; + goto bail; + } + if (fIsWin9x && fInt13Unit >= 0x80) { + WMSG0("GLITCH: can't access physical HD in Win9x\n"); + dierr = kDIErrInvalidArg; + goto bail; + } + if ((fInt13Unit >= 0x00 && fInt13Unit < 0x04) || + (fInt13Unit >= 0x80 && fInt13Unit < 0x88)) + { + WMSG1(" Win32VA/P: opening unit %02xh\n", fInt13Unit); + } else { + WMSG2("GLITCH: converted '%s' to %02xh\n", deviceName, fInt13Unit); + dierr = kDIErrInternal; + goto bail; + } + + DWORD access; + if (readOnly) + access = GENERIC_READ; + else + access = GENERIC_READ | GENERIC_WRITE; + + if (fIsWin9x) { + fHandle = CreateFile("\\\\.\\vwin32", 0, 0, NULL, + OPEN_EXISTING, FILE_FLAG_DELETE_ON_CLOSE, NULL); + if (fHandle == INVALID_HANDLE_VALUE) { + DWORD lastError = GetLastError(); + WMSG1(" Win32VA/PBOpen: CreateFile(vwin32) failed (err=%ld)\n", + lastError); + dierr = LastErrorToDIError(); + goto bail; + } + + /* figure out the geometry */ + dierr = DetectFloppyGeometry(); + if (dierr != kDIErrNone) + goto bail; + } else { + char device[19] = "\\\\.\\PhysicalDrive_"; + assert(fInt13Unit >= 0x80 && fInt13Unit <= 0x89); + device[17] = fInt13Unit - 0x80 + '0'; + WMSG2("Opening '%s' (access=0x%02x)\n", device, access); + + // If we're reading, allow others to write. If we're writing, insist + // upon exclusive access to the volume. + DWORD shareMode = FILE_SHARE_READ; + if (access == GENERIC_READ) + shareMode |= FILE_SHARE_WRITE; + + fHandle = CreateFile(device, access, shareMode, + NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (fHandle == INVALID_HANDLE_VALUE) { + DWORD lastError = GetLastError(); + dierr = LastErrorToDIError(); + WMSG2(" PBAccess Open: CreateFile failed (err=%ld dierr=%d)\n", + lastError, dierr); + goto bail; + } + } + + assert(fHandle != nil && fHandle != INVALID_HANDLE_VALUE); + +bail: + if (dierr != kDIErrNone) { + if (fHandle != nil && fHandle != INVALID_HANDLE_VALUE) + ::CloseHandle(fHandle); + fHandle = nil; + } + + return dierr; +} + +/* + * Auto-detect the geometry of a floppy drive. + * + * Sets "fFloppyKind" and "fGeometry". + */ +DIError +Win32VolumeAccess::PhysicalBlockAccess::DetectFloppyGeometry(void) +{ + DIError dierr = kDIErrNone; + static const struct { + FloppyKind kind; + DiskGeometry geom; + } floppyGeometry[] = { + { kFloppyUnknown, { -1, -1, -1, -1 } }, + { kFloppy525_360, { 40, 2, 9, 360*2 } }, + { kFloppy525_1200, { 80, 2, 15, 1200*2 } }, + { kFloppy35_720, { 80, 2, 9, 720*2 } }, + { kFloppy35_1440, { 80, 2, 18, 1440*2 } }, + { kFloppy35_2880, { 80, 2, 36, 2880*2 } } + }; + unsigned char buf[kBlockSize]; + FloppyKind driveKind; + int status; + + /* verify that we can directly index the table with the enum */ + for (int chk = 0; chk < NELEM(floppyGeometry); chk++) { + assert(floppyGeometry[chk].kind == chk); + } + assert(chk == kFloppyMax); + + + /* + * Issue a BIOS call to determine the kind of drive we're looking at. + */ + dierr = GetFloppyDriveKind(fHandle, fInt13Unit, &driveKind); + if (dierr != kDIErrNone) + goto bail; + + switch (driveKind) { + case kFloppy35_2880: + status = ReadBlocksInt13h(fHandle, fInt13Unit, 0, 0, 36, 1, buf); + if (status == 0) { + fFloppyKind = kFloppy35_2880; + break; + } + // else, fall through + case kFloppy35_1440: + status = ReadBlocksInt13h(fHandle, fInt13Unit, 0, 0, 18, 1, buf); + if (status == 0) { + fFloppyKind = kFloppy35_1440; + break; + } + // else, fall through + case kFloppy35_720: + status = ReadBlocksInt13h(fHandle, fInt13Unit, 0, 0, 9, 1, buf); + if (status == 0) { + fFloppyKind = kFloppy35_720; + break; + } + // else, fail + dierr = kDIErrReadFailed; + goto bail; + + case kFloppy525_1200: + status = ReadBlocksInt13h(fHandle, fInt13Unit, 0, 0, 15, 1, buf); + if (status == 0) { + fFloppyKind = kFloppy525_1200; + break; + } + // else, fall through + case kFloppy525_360: + status = ReadBlocksInt13h(fHandle, fInt13Unit, 0, 0, 9, 1, buf); + if (status == 0) { + fFloppyKind = kFloppy525_360; + break; + } + // else, fail + dierr = kDIErrReadFailed; + goto bail; + + default: + WMSG1(" Unknown driveKind %d\n", driveKind); + assert(false); + dierr = kDIErrInternal; + goto bail; + } + + WMSG1("PBA: floppy disk appears to be type=%d\n", fFloppyKind); + fGeometry = floppyGeometry[fFloppyKind].geom; + +bail: + return dierr; +} + +#if 0 +/* + * Flush the system disk cache. + */ +DIError +Win32VolumeAccess::PhysicalBlockAccess::FlushBlockDevice(void) +{ + DIError dierr = kDIErrNone; + + if (::FlushFileBuffers(fHandle) == FALSE) { + DWORD lastError = GetLastError(); + WMSG1(" Win32VA/PBAFlush: FlushFileBuffers failed (err=%ld)\n", + lastError); + dierr = LastErrorToDIError(); + } + return dierr; +} +#endif + +/* + * Close the device handle. + */ +DIError +Win32VolumeAccess::PhysicalBlockAccess::Close(void) +{ + if (fHandle != nil) { + ::CloseHandle(fHandle); + fHandle = nil; + } + return kDIErrNone; +} + + +/* + * =========================================================================== + * ASPIBlockAccess + * =========================================================================== + */ + +/* + * Unpack device name and verify that the device is a CD-ROM drive or + * direct-access storage device. + */ +DIError +Win32VolumeAccess::ASPIBlockAccess::Open(const char* deviceName, bool readOnly) +{ + DIError dierr = kDIErrNone; + + if (fpASPI != nil) + return kDIErrAlreadyOpen; + + fpASPI = Global::GetASPI(); + if (fpASPI == nil) + return kDIErrASPIFailure; + + if (strncmp(deviceName, kASPIDev, strlen(kASPIDev)) != 0) { + assert(false); + dierr = kDIErrInternal; + goto bail; + } + + const char* cp; + int adapter, target, lun; + int result; + + cp = deviceName + strlen(kASPIDev); + result = 0; + result |= ExtractInt(&cp, &adapter); + result |= ExtractInt(&cp, &target); + result |= ExtractInt(&cp, &lun); + if (result != 0) { + WMSG1(" Win32VA couldn't parse '%s'\n", deviceName); + dierr = kDIErrInternal; + goto bail; + } + + fAdapter = adapter; + fTarget = target; + fLun = lun; + + unsigned char deviceType; + dierr = fpASPI->GetDeviceType(fAdapter, fTarget, fLun, &deviceType); + if (dierr != kDIErrNone || + (deviceType != kScsiDevTypeCDROM && deviceType != kScsiDevTypeDASD)) + { + WMSG2(" Win32VA bad GetDeviceType err=%d type=%d\n", + dierr, deviceType); + dierr = kDIErrInternal; // should not be here at all + goto bail; + } + if (deviceType == kScsiDevTypeCDROM) + fReadOnly = true; + else + fReadOnly = readOnly; + + WMSG4(" Win32VA successful 'open' of '%s' on %d:%d:%d\n", + deviceName, fAdapter, fTarget, fLun); + +bail: + if (dierr != kDIErrNone) + fpASPI = nil; + return dierr; +} + +/* + * Extract an integer from a string, advancing to the next integer after + * doing so. + * + * Returns 0 on success, -1 on failure. + */ +int +Win32VolumeAccess::ASPIBlockAccess::ExtractInt(const char** pStr, int* pResult) +{ + char* end = nil; + + if (*pStr == nil) { + assert(false); + return -1; + } + + *pResult = (int) strtol(*pStr, &end, 10); + + if (end == nil) + *pStr = nil; + else + *pStr = end+1; + + return 0; +} + +/* + * Return the device capacity in 512-byte blocks. + * + * Sets fChunkSize as a side-effect. + */ +DIError +Win32VolumeAccess::ASPIBlockAccess::DetectCapacity(long* pNumBlocks) +{ + DIError dierr = kDIErrNone; + unsigned long lba, blockLen; + + dierr = fpASPI->GetDeviceCapacity(fAdapter, fTarget, fLun, &lba, + &blockLen); + if (dierr != kDIErrNone) + goto bail; + + WMSG3("READ CAPACITY reports lba=%lu blockLen=%lu (total=%lu)\n", + lba, blockLen, lba*blockLen); + + fChunkSize = blockLen; + + if ((blockLen % 512) != 0) { + WMSG1("Unacceptable CD-ROM blockLen=%ld, bailing\n", blockLen); + dierr = kDIErrReadFailed; + goto bail; + } + + // The LBA is the last valid block on the disk. To get the disk size, + // we need to add one. + + *pNumBlocks = (blockLen/512) * (lba+1); + WMSG1(" ASPI returning 512-byte block count %ld\n", *pNumBlocks); + +bail: + return dierr; +} + +/* + * Read one or more 512-byte blocks from the device. + * + * SCSI doesn't promise it'll be in a chunk size we like, but it's pretty + * safe to assume that it'll be at least 512 bytes, and divisible by 512. + */ +DIError +Win32VolumeAccess::ASPIBlockAccess::ReadBlocks(long startBlock, short blockCount, + void* buf) +{ + DIError dierr; + + // we're expecting fBlockSize to be 512 or 2048 + assert(fChunkSize >= kBlockSize && fChunkSize <= 65536); + assert((fChunkSize % kBlockSize) == 0); + + /* alloc chunk buffer on first use */ + if (fLastChunkCache == nil) { + fLastChunkCache = new unsigned char[fChunkSize]; + if (fLastChunkCache == nil) + return kDIErrMalloc; + assert(fLastChunkNum == -1); + } + + /* + * Map a range of N-byte blocks to a range of 2048-byte blocks. + */ + const int kFactor = fChunkSize / kBlockSize; + long chunkIndex = startBlock / kFactor; + int chunkOffset = (int) (startBlock % kFactor); // small integer + + /* + * When possible, do multi-block reads directly into "buf". The first + * and last block may require special handling. + */ + while (blockCount) { + assert(blockCount > 0); + + if (chunkOffset != 0 || blockCount < kFactor) { + assert(chunkOffset >= 0 && chunkOffset < kFactor); + + /* get from single-block cache or from disc */ + if (chunkIndex != fLastChunkNum) { + fLastChunkNum = -1; // invalidate, in case of error + + dierr = fpASPI->ReadBlocks(fAdapter, fTarget, fLun, chunkIndex, + 1, fChunkSize, fLastChunkCache); + if (dierr != kDIErrNone) + return dierr; + + fLastChunkNum = chunkIndex; + } + + int thisNumBlocks; + thisNumBlocks = kFactor - chunkOffset; + if (thisNumBlocks > blockCount) + thisNumBlocks = blockCount; + + //WMSG3(" Small copy (chIdx=%ld off=%d*512 size=%d*512)\n", + // chunkIndex, chunkOffset, thisNumBlocks); + memcpy(buf, fLastChunkCache + chunkOffset * kBlockSize, + thisNumBlocks * kBlockSize); + + blockCount -= thisNumBlocks; + buf = (unsigned char*) buf + (thisNumBlocks * kBlockSize); + + chunkOffset = 0; + chunkIndex++; + } else { + fLastChunkNum = -1; // invalidate single-block cache + + int numChunks; + numChunks = blockCount / kFactor; // rounds down + + //WMSG2(" Big read (chIdx=%ld numCh=%d)\n", + // chunkIndex, numChunks); + dierr = fpASPI->ReadBlocks(fAdapter, fTarget, fLun, chunkIndex, + numChunks, fChunkSize, buf); + if (dierr != kDIErrNone) + return dierr; + + blockCount -= numChunks * kFactor; + buf = (unsigned char*) buf + (numChunks * fChunkSize); + + chunkIndex += numChunks; + } + } + + return kDIErrNone; +} + +/* + * Write one or more 512-byte blocks to the device. + * + * SCSI doesn't promise it'll be in a chunk size we like, but it's pretty + * safe to assume that it'll be at least 512 bytes, and divisible by 512. + */ +DIError +Win32VolumeAccess::ASPIBlockAccess::WriteBlocks(long startBlock, short blockCount, + const void* buf) +{ + DIError dierr; + + if (fReadOnly) + return kDIErrAccessDenied; + + // we're expecting fBlockSize to be 512 or 2048 + assert(fChunkSize >= kBlockSize && fChunkSize <= 65536); + assert((fChunkSize % kBlockSize) == 0); + + /* throw out the cache */ + fLastChunkNum = -1; + + /* + * Map a range of N-byte blocks to a range of 2048-byte blocks. + */ + const int kFactor = fChunkSize / kBlockSize; + long chunkIndex = startBlock / kFactor; + int chunkOffset = (int) (startBlock % kFactor); // small integer + + /* + * When possible, do multi-block writes directly from "buf". The first + * and last block may require special handling. + */ + while (blockCount) { + assert(blockCount > 0); + + if (chunkOffset != 0 || blockCount < kFactor) { + assert(chunkOffset >= 0 && chunkOffset < kFactor); + + /* read the chunk we're writing a part of */ + dierr = fpASPI->ReadBlocks(fAdapter, fTarget, fLun, chunkIndex, + 1, fChunkSize, fLastChunkCache); + if (dierr != kDIErrNone) + return dierr; + + int thisNumBlocks; + thisNumBlocks = kFactor - chunkOffset; + if (thisNumBlocks > blockCount) + thisNumBlocks = blockCount; + + WMSG3(" Small copy out (chIdx=%ld off=%d*512 size=%d*512)\n", + chunkIndex, chunkOffset, thisNumBlocks); + memcpy(fLastChunkCache + chunkOffset * kBlockSize, buf, + thisNumBlocks * kBlockSize); + + blockCount -= thisNumBlocks; + buf = (const unsigned char*) buf + (thisNumBlocks * kBlockSize); + + chunkOffset = 0; + chunkIndex++; + } else { + int numChunks; + numChunks = blockCount / kFactor; // rounds down + + WMSG2(" Big write (chIdx=%ld numCh=%d)\n", + chunkIndex, numChunks); + dierr = fpASPI->WriteBlocks(fAdapter, fTarget, fLun, chunkIndex, + numChunks, fChunkSize, buf); + if (dierr != kDIErrNone) + return dierr; + + blockCount -= numChunks * kFactor; + buf = (const unsigned char*) buf + (numChunks * fChunkSize); + + chunkIndex += numChunks; + } + } + + return kDIErrNone; +} + +/* + * Not much to do, really, since we're not holding onto any OS structures. + */ +DIError +Win32VolumeAccess::ASPIBlockAccess::Close(void) +{ + fpASPI = nil; + return kDIErrNone; +} + + +/* + * =========================================================================== + * CBCache + * =========================================================================== + */ + +/* + * Determine whether we're holding a block in the cache. + */ +bool +CBCache::IsBlockInCache(long blockNum) const +{ + if (fFirstBlock == kEmpty) + return false; + assert(fNumBlocks > 0); + + if (blockNum >= fFirstBlock && blockNum < fFirstBlock + fNumBlocks) + return true; + else + return false; +} + +/* + * Retrieve a single block from the cache. + */ +DIError +CBCache::GetFromCache(long blockNum, void* buf) +{ + if (!IsBlockInCache(blockNum)) { + assert(false); + return kDIErrInternal; + } + + //WMSG1(" CBCache: getting block %d from cache\n", blockNum); + int offset = (blockNum - fFirstBlock) * kBlockSize; + assert(offset >= 0); + + memcpy(buf, fCache + offset, kBlockSize); + return kDIErrNone; +} + +/* + * Determine whether a block will "fit" in the cache. There are two + * criteria: (1) there must actually be room at the end, and (2) the + * block in question must be the next consecutive block. + */ +bool +CBCache::IsRoomInCache(long blockNum) const +{ + if (fFirstBlock == kEmpty) + return true; + + // already in cache? + if (blockNum >= fFirstBlock && blockNum < fFirstBlock + fNumBlocks) + return true; + + // running off the end? + if (fNumBlocks == kMaxCachedBlocks) + return false; + + // is it the exact next one? + if (fFirstBlock + fNumBlocks != blockNum) + return false; + + return true; +} + +/* + * Add a block to the cache. + * + * We might be adding it after a read or a write. The "isDirty" flag + * tells us what the deal is. If somebody tries to overwrite a dirty + * block with a new one and doesn't have "isDirty" set, it probably means + * they're trying to overwrite dirty cached data with the result of a new + * read, which is a bug. Trap it here. + */ +DIError +CBCache::PutInCache(long blockNum, const void* buf, bool isDirty) +{ + int blockOffset = -1; + if (!IsRoomInCache(blockNum)) { + assert(false); + return kDIErrInternal; + } + + if (fFirstBlock == kEmpty) { + //WMSG1(" CBCache: starting anew with block %ld\n", blockNum); + fFirstBlock = blockNum; + fNumBlocks = 1; + blockOffset = 0; + } else if (blockNum == fFirstBlock + fNumBlocks) { + //WMSG1(" CBCache: appending block %ld\n", blockNum); + blockOffset = fNumBlocks; + fNumBlocks++; + } else if (blockNum >= fFirstBlock && blockNum < fFirstBlock + fNumBlocks) { + blockOffset = blockNum - fFirstBlock; + } else { + assert(false); + return kDIErrInternal; + } + assert(blockOffset != -1); + assert(blockOffset < kMaxCachedBlocks); + + if (fDirty[blockOffset] && !isDirty) { + WMSG1("BUG: CBCache trying to clear dirty flag for block %ld\n", + blockNum); + assert(false); + return kDIErrInternal; + } + fDirty[blockOffset] = isDirty; + + //WMSG2(" CBCache: adding block %d to cache at %d\n", blockNum, blockOffset); + int offset = blockOffset * kBlockSize; + assert(offset >= 0); + + memcpy(fCache + offset, buf, kBlockSize); + return kDIErrNone; +} + +/* + * Determine whether there are any dirty blocks in the cache. + */ +bool +CBCache::IsDirty(void) const +{ + if (fFirstBlock == kEmpty) + return false; + + assert(fNumBlocks > 0); + for (int i = 0; i < fNumBlocks; i++) { + if (fDirty[i]) { + //WMSG0(" CBCache: dirty blocks found\n"); + return true; + } + } + + //WMSG0(" CBCache: no dirty blocks found\n"); + return false; +} + +/* + * Return a pointer to the cache goodies, so that the object sitting + * on the disk hardware can write our stuff. + */ +void +CBCache::GetCachePointer(long* pFirstBlock, int* pNumBlocks, void** pBuf) const +{ + assert(fFirstBlock != kEmpty); // not essential, but why call here if not? + + *pFirstBlock = fFirstBlock; + *pNumBlocks = fNumBlocks; + *pBuf = (void*) fCache; +} + +/* + * Clear all the dirty flags. + */ +void +CBCache::Scrub(void) +{ + if (fFirstBlock == kEmpty) + return; + + for (int i = 0; i < fNumBlocks; i++) + fDirty[i] = false; +} + +/* + * Trash all of our entries. If any are dirty, scream bloody murder. + */ +void +CBCache::Purge(void) +{ + if (fFirstBlock == kEmpty) + return; + + if (IsDirty()) { + // Should only happen after a write failure causes us to clean up. + WMSG0("HEY: CBCache purging dirty blocks!\n"); + //assert(false); + } + Scrub(); + + fFirstBlock = kEmpty; + fNumBlocks = 0; +} + + +#endif /*_WIN32*/ diff --git a/diskimg/Win32BlockIO.h b/diskimg/Win32BlockIO.h new file mode 100644 index 0000000..0a16516 --- /dev/null +++ b/diskimg/Win32BlockIO.h @@ -0,0 +1,405 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +#ifdef _WIN32 +/* + * Structures and functions for performing block-level I/O on Win32 logical + * and physical volumes. + * + * Under Win2K/XP this is pretty straightforward: open the volume and + * issue seek and read calls. It's not quite that simple -- reads need to + * be in 512-byte sectors for floppy and hard drives, seeks need to be on + * sector boundaries, and you can't seek from the end, which makes it hard + * to figure out how big the volume is -- but it's palatable. + * + * Under Win95/Win98/WinME, life is more difficult. You need to use the + * Int21h/7305h services to access logical volumes. Of course, those weren't + * available until Win95 OSR2, before which you used Int25h/6000h, but those + * don't work with FAT32 volumes. Access to physical devices requires Int13h, + * which is fine for floppies but requires 16-bit flat thunks for hard drives + * (see Q137176: "DeviceIoControl Int 13h Does Not Support Hard Disks"). + * + * If Win98 can't recognize the volume on a floppy, it tries to reacquire + * the volume information every time you ask it to read a sector. This makes + * things *VERY* slow. The solution is to use the physical drive Int13h + * services. These come in two variants, one of which will work on just + * about any machine but only works with floppies. The other will work on + * anything built since about 1996. + * + * Figuring out whether something is or isn't a floppy requires yet + * another call. All things considered it's quite an ordeal. The block I/O + * functions are wrapped up in classes so nobody else has to worry about all + * this mess. + * + * Implementation note: this class is broken down by how the devices are + * opened, e.g. logical, physical, or ASPI address. Breaking it down by device + * type seems more appropriate, but Win98 vs Win2K can require completely + * different approaches (e.g. physical vs. logical for floppy disk, logical + * vs. ASPI for CD-ROM). There is no perfect decomposition. + * + * Summary: + * Win9x/ME physical drive: Int13h (doesn't work for hard drives) + * Win9x/ME logical drive: Int21h/7305h + * Win9x/ME SCSI drive or CD-ROM drive: ASPI + * Win2K/XP physical drive: CreateFile("\\.\PhysicalDriveN") + * Win2K/XP logical drive: CreateFile("\\.\X") + * Win2K/XP SCSI drive or CD-ROM drive: SPTI + */ +#ifndef __WIN32BLOCKIO__ +#define __WIN32BLOCKIO__ + + +namespace DiskImgLib { + +extern bool IsWin9x(void); + + +/* + * Cache a contiguous set of blocks. This was originally motivated by poor + * write performance, but that problem was largely solved in other ways. + * It's still handy to write an entire track at once under Win98 though. + * + * Only storing continuous runs of blocks makes the cache less useful, but + * much easier to write, and hence less likely to break in unpleasant ways. + * + * This class just manages the blocks. The FlushCache() function in + * Win32LogicalVolume is responsible for actually pushing the writes through. + * + * (I'm not entirely happy with this, especially since it doesn't take into + * account the underlying device block size. This could've been a good place + * to handle the 2048-byte CD-ROM block size, rather than caching it again in + * the CD-ROM handler.) + */ +class CBCache { +public: + CBCache(void) : fFirstBlock(kEmpty), fNumBlocks(0) + { + for (int i = 0; i < kMaxCachedBlocks; i++) + fDirty[i] = false; + } + virtual ~CBCache(void) { Purge(); } + + enum { kEmpty = -1 }; + + // is the block we want in the cache? + bool IsBlockInCache(long blockNum) const; + // read block out of cache (after verifying that it's present) + DIError GetFromCache(long blockNum, void* buf); + // can the cache store this block? + bool IsRoomInCache(long blockNum) const; + // write block to cache (after verifying that it will fit) + DIError PutInCache(long blockNum, const void* buf, bool isDirty); + + // are there any dirty blocks in the cache? + bool IsDirty(void) const; + // get start, count, and buffer so we can write the cached data + void GetCachePointer(long* pFirstBlock, int* pNumBlocks, void** pBuf) const; + // clear all the dirty flags + void Scrub(void); + // purge all cache entries (ideally after writing w/help from GetCachePtr) + void Purge(void); + +private: + enum { + kMaxCachedBlocks = 18, // one track on 1.4MB floppy + kBlockSize = 512, // must match with Win32LogicalVolume:: + }; + + long fFirstBlock; // set to kEmpty when cache is empty + int fNumBlocks; + bool fDirty[kMaxCachedBlocks]; + unsigned char fCache[kMaxCachedBlocks * kBlockSize]; +}; + + +/* + * This class encapsulates block access to a logical or physical volume. + */ +class Win32VolumeAccess { +public: + Win32VolumeAccess(void) : fpBlockAccess(NULL) + {} + virtual ~Win32VolumeAccess(void) { + if (fpBlockAccess != NULL) { + FlushCache(true); + fpBlockAccess->Close(); + } + } + + // "deviceName" has the form "X:\" (logical), "81:\" (physical), or + // "ASPI:x:y:z\" (ASPI) + DIError Open(const char* deviceName, bool readOnly); + // close the device + void Close(void); + // is the device open and working? + bool Ready(void) const { return fpBlockAccess != NULL; } + + // return the volume's EOF + long GetTotalBlocks(void) const { return fTotalBlocks; } + // return the block size for this volume (always a power of 2) + int GetBlockSize(void) const { return BlockAccess::kBlockSize; } + + // read one or more consecutive blocks + DIError ReadBlocks(long startBlock, short blockCount, void* buf); + // write one or more consecutive blocks + DIError WriteBlocks(long startBlock, short blockCount, const void* buf); + // flush our internal cache + DIError FlushCache(bool purge); + +private: + /* + * Abstract base class with some handy functions. + */ + class BlockAccess { + public: + BlockAccess(void) { fIsWin9x = DiskImgLib::IsWin9x(); } + virtual ~BlockAccess(void) {} + + typedef struct { + int numCyls; + int numHeads; + int numSectors; + long blockCount; // total #of blocks on this kind of disk + } DiskGeometry; + + // generic interfaces + virtual DIError Open(const char* deviceName, bool readOnly) = 0; + virtual DIError DetectCapacity(long* pNumBlocks) = 0; + virtual DIError ReadBlocks(long startBlock, short blockCount, + void* buf) = 0; + virtual DIError WriteBlocks(long startBlock, short blockCount, + const void* buf) = 0; + virtual DIError Close(void) = 0; + + static bool BlockToCylinderHeadSector(long blockNum, + const DiskGeometry* pGeometry, int* pCylinder, int* pHead, + int* pSector, long* pLastBlockOnTrack); + + enum { + kNumLogicalVolumes = 26, // A-Z + kBlockSize = 512, + kCDROMSectorSize = 2048, + kMaxFloppyRetries = 3, // retry floppy reads/writes + }; + + // BIOS floppy disk drive type; doubles here as media type + typedef enum { + kFloppyUnknown = 0, + kFloppy525_360 = 1, + kFloppy525_1200 = 2, + kFloppy35_720 = 3, + kFloppy35_1440 = 4, + kFloppy35_2880 = 5, + + kFloppyMax + } FloppyKind; + + protected: + static DIError GetFloppyDriveKind(HANDLE handle, int unitNum, + FloppyKind* pKind); + // detect the #of blocks on the volume + static DIError ScanCapacity(BlockAccess* pThis, long* pNumBlocks); + // determine whether a block is readable + static bool CanReadBlock(BlockAccess* pThis, long blockNum); + // try to detect device capacity using SPTI + DIError DetectCapacitySPTI(HANDLE handle, + bool isCDROM, long* pNumBlocks); + + static int ReadBlocksInt13h(HANDLE handle, int unitNum, + int cylinder, int head, int sector, short blockCount, void* buf); + static DIError ReadBlocksInt13h(HANDLE handle, int unitNum, + const DiskGeometry* pGeometry, long startBlock, short blockCount, + void* buf); + static int WriteBlocksInt13h(HANDLE handle, int unitNum, + int cylinder, int head, int sector, short blockCount, + const void* buf); + static DIError WriteBlocksInt13h(HANDLE handle, int unitNum, + const DiskGeometry* pGeometry, long startBlock, short blockCount, + const void* buf); + + static DIError ReadBlocksInt21h(HANDLE handle, int driveNum, + long startBlock, short blockCount, void* buf); + static DIError WriteBlocksInt21h(HANDLE handle, int driveNum, + long startBlock, short blockCount, const void* buf); + + static DIError ReadBlocksWin2K(HANDLE handle, + long startBlock, short blockCount, void* buf); + static DIError WriteBlocksWin2K(HANDLE handle, + long startBlock, short blockCount, const void* buf); + + bool fIsWin9x; // Win9x/ME=true, Win2K/XP=false + }; + + /* + * Access to a logical volume (e.g. "C:\") under Win9x and Win2K/XP. + */ + class LogicalBlockAccess : public BlockAccess { + public: + LogicalBlockAccess(void) : fHandle(NULL), fIsCDROM(false), + fDriveNum(-1), fLastSectorCache(nil), fLastSectorNum(-1) + {} + virtual ~LogicalBlockAccess(void) { + if (fHandle != NULL) { + //WMSG0("HEY: LogicalBlockAccess: forcing close\n"); + Close(); + } + delete[] fLastSectorCache; + } + + virtual DIError Open(const char* deviceName, bool readOnly); + virtual DIError DetectCapacity(long* pNumBlocks) { + /* use SCSI length value if at all possible */ + DIError dierr; + dierr = DetectCapacitySPTI(fHandle, fIsCDROM, pNumBlocks); + if (fIsCDROM) + return dierr; // SPTI should always work for CD-ROM + if (dierr != kDIErrNone) + return ScanCapacity(this, pNumBlocks); // fall back on scan + else + return dierr; + } + virtual DIError ReadBlocks(long startBlock, short blockCount, + void* buf) + { + if (fIsCDROM) + return ReadBlocksCDROM(fHandle, startBlock, blockCount, buf); + if (fIsWin9x) + return ReadBlocksInt21h(fHandle, fDriveNum, startBlock, + blockCount, buf); + else + return ReadBlocksWin2K(fHandle, startBlock, blockCount, buf); + } + virtual DIError WriteBlocks(long startBlock, short blockCount, + const void* buf) + { + if (fIsCDROM) + return kDIErrWriteProtected; + if (fIsWin9x) + return WriteBlocksInt21h(fHandle, fDriveNum, startBlock, + blockCount, buf); + else + return WriteBlocksWin2K(fHandle, startBlock, blockCount, buf); + } + virtual DIError Close(void); + + private: + //DIError DetectCapacitySPTI(long* pNumBlocks); + DIError ReadBlocksCDROM(HANDLE handle, + long startBlock, short numBlocks, void* buf); + + // Win2K/XP and Win9x/ME + HANDLE fHandle; + bool fIsCDROM; // set for CD-ROM devices + // Win9x/ME + int fDriveNum; // 1=A, 3=C, etc + // CD-ROM goodies + unsigned char* fLastSectorCache; + long fLastSectorNum; + }; + + /* + * Access to a physical volume (e.g. 00h or 80h) under Win9x and + * Win2K/XP. + */ + class PhysicalBlockAccess : public BlockAccess { + public: + PhysicalBlockAccess(void) : fHandle(NULL), fInt13Unit(-1) {} + virtual ~PhysicalBlockAccess(void) {} + + virtual DIError Open(const char* deviceName, bool readOnly); + virtual DIError DetectCapacity(long* pNumBlocks) { + /* try SPTI in case it happens to work */ + DIError dierr; + dierr = DetectCapacitySPTI(fHandle, false, pNumBlocks); + if (dierr != kDIErrNone) + return ScanCapacity(this, pNumBlocks); + else + return dierr; + } + virtual DIError ReadBlocks(long startBlock, short blockCount, + void* buf) + { + if (fIsWin9x) + return ReadBlocksInt13h(fHandle, fInt13Unit, + &fGeometry, startBlock, blockCount, buf); + else + return ReadBlocksWin2K(fHandle, + startBlock, blockCount, buf); + } + virtual DIError WriteBlocks(long startBlock, short blockCount, + const void* buf) + { + if (fIsWin9x) + return WriteBlocksInt13h(fHandle, fInt13Unit, + &fGeometry, startBlock, blockCount, buf); + else + return WriteBlocksWin2K(fHandle, + startBlock, blockCount, buf); + } + virtual DIError Close(void); + + private: + DIError DetectFloppyGeometry(void); + + // Win2K/XP + HANDLE fHandle; + // Win9x/ME + int fInt13Unit; // 00h=floppy #1, 80h=HD#1 + FloppyKind fFloppyKind; + DiskGeometry fGeometry; + }; + + /* + * Access to a SCSI volume via the ASPI interface. + */ + class ASPIBlockAccess : public BlockAccess { + public: + ASPIBlockAccess(void) : fpASPI(nil), + fAdapter(0xff), fTarget(0xff), fLun(0xff), fReadOnly(false), + fLastChunkCache(nil), fLastChunkNum(-1), fChunkSize(-1) + {} + virtual ~ASPIBlockAccess(void) { delete[] fLastChunkCache; } + + virtual DIError Open(const char* deviceName, bool readOnly); + virtual DIError DetectCapacity(long* pNumBlocks); + virtual DIError ReadBlocks(long startBlock, short blockCount, + void* buf); + virtual DIError WriteBlocks(long startBlock, short blockCount, + const void* buf); + virtual DIError Close(void); + + private: + int ExtractInt(const char** pStr, int* pResult); + + ASPI* fpASPI; + unsigned char fAdapter; + unsigned char fTarget; + unsigned char fLun; + + bool fReadOnly; + + // block cache + unsigned char* fLastChunkCache; + long fLastChunkNum; + long fChunkSize; // set by DetectCapacity + }; + + + // write a series of blocks to the volume + DIError DoWriteBlocks(long startBlock, short blockCount, const void* buf) + { + return fpBlockAccess->WriteBlocks(startBlock, blockCount, buf); + } + + long fTotalBlocks; + BlockAccess* fpBlockAccess; // really LogicalBA or PhysicalBA + CBCache fBlockCache; +}; + +}; // namespace DiskImgLib + +#endif /*WIN32BLOCKIO*/ + +#endif /*_WIN32*/ diff --git a/diskimg/Win32Extra.h b/diskimg/Win32Extra.h new file mode 100644 index 0000000..8f22291 --- /dev/null +++ b/diskimg/Win32Extra.h @@ -0,0 +1,60 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Visual C++ 6.0 doesn't have this definition, because it wasn't added until + * WinXP. + * + * (Do we want IOCTL_DISK_GET_DRIVE_LAYOUT_EX too?) + */ +#ifndef __WIN32EXTRA__ +#define __WIN32EXTRA__ + +#include // base definitions + +#ifndef IOCTL_DISK_GET_DRIVE_GEOMETRY_EX + +/* +BOOL DeviceIoControl( + (HANDLE) hDevice, // handle to volume + IOCTL_DISK_GET_DRIVE_GEOMETRY_EX, // dwIoControlCode + NULL, // lpInBuffer + 0, // nInBufferSize + (LPVOID) lpOutBuffer, // output buffer + (DWORD) nOutBufferSize, // size of output buffer + (LPDWORD) lpBytesReturned, // number of bytes returned + (LPOVERLAPPED) lpOverlapped // OVERLAPPED structure +); +*/ + +#define IOCTL_DISK_GET_DRIVE_GEOMETRY_EX \ + CTL_CODE(IOCTL_DISK_BASE, 0x0028, METHOD_BUFFERED, FILE_ANY_ACCESS) + +typedef struct _DISK_GEOMETRY_EX { + DISK_GEOMETRY Geometry; + LARGE_INTEGER DiskSize; + BYTE Data[1]; +} DISK_GEOMETRY_EX, *PDISK_GEOMETRY_EX; + +#if 0 +typedef struct _DISK_DETECTION_INFO { + DWORD SizeOfDetectInfo; + DETECTION_TYPE DetectionType; + union { + struct { + DISK_INT13_INFO Int13; + DISK_EX_INT13_INFO ExInt13; + }; + }; +} DISK_DETECTION_INFO, *PDISK_DETECTION_INFO; + +PDISK_DETECTION_INFO DiskGeometryGetDetect(PDISK_GEOMETRY_EX Geometry); +PDISK_PARTITION_INFO DiskGeometryGetPartition(PDISK_GEOMETRY_EX Geometry); +#endif + + +#endif /*IOCTL_DISK_GET_DRIVE_GEOMETRY_EX*/ + +#endif /*__WIN32EXTRA__*/ diff --git a/diskimg/diskimg.dsp b/diskimg/diskimg.dsp new file mode 100644 index 0000000..834b899 --- /dev/null +++ b/diskimg/diskimg.dsp @@ -0,0 +1,294 @@ +# Microsoft Developer Studio Project File - Name="diskimg" - Package Owner=<4> +# Microsoft Developer Studio Generated Build File, Format Version 6.00 +# ** DO NOT EDIT ** + +# TARGTYPE "Win32 (x86) Dynamic-Link Library" 0x0102 + +CFG=diskimg - Win32 Debug +!MESSAGE This is not a valid makefile. To build this project using NMAKE, +!MESSAGE use the Export Makefile command and run +!MESSAGE +!MESSAGE NMAKE /f "diskimg.mak". +!MESSAGE +!MESSAGE You can specify a configuration when running NMAKE +!MESSAGE by defining the macro CFG on the command line. For example: +!MESSAGE +!MESSAGE NMAKE /f "diskimg.mak" CFG="diskimg - Win32 Debug" +!MESSAGE +!MESSAGE Possible choices for configuration are: +!MESSAGE +!MESSAGE "diskimg - Win32 Release" (based on "Win32 (x86) Dynamic-Link Library") +!MESSAGE "diskimg - Win32 Debug" (based on "Win32 (x86) Dynamic-Link Library") +!MESSAGE + +# Begin Project +# PROP AllowPerConfigDependencies 0 +# PROP Scc_ProjName "" +# PROP Scc_LocalPath "" +CPP=cl.exe +MTL=midl.exe +RSC=rc.exe + +!IF "$(CFG)" == "diskimg - Win32 Release" + +# PROP BASE Use_MFC 0 +# PROP BASE Use_Debug_Libraries 0 +# PROP BASE Output_Dir "Release" +# PROP BASE Intermediate_Dir "Release" +# PROP BASE Target_Dir "" +# PROP Use_MFC 0 +# PROP Use_Debug_Libraries 0 +# PROP Output_Dir "Release" +# PROP Intermediate_Dir "Release" +# PROP Ignore_Export_Lib 0 +# PROP Target_Dir "" +# ADD BASE CPP /nologo /MT /W3 /GX /O2 /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "DISKIMG_EXPORTS" /Yu"stdafx.h" /FD /c +# ADD CPP /nologo /MD /W3 /GX /O2 /D "WIN32" /D "NDEBUGX" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "DISKIMG_EXPORTS" /Yu"stdafx.h" /FD /c +# ADD BASE MTL /nologo /D "NDEBUG" /mktyplib203 /win32 +# ADD MTL /nologo /D "NDEBUG" /mktyplib203 /win32 +# ADD BASE RSC /l 0x409 /d "NDEBUG" +# ADD RSC /l 0x409 /d "NDEBUG" +BSC32=bscmake.exe +# ADD BASE BSC32 /nologo +# ADD BSC32 /nologo +LINK32=link.exe +# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /dll /machine:I386 +# ADD LINK32 ..\prebuilt\nufxlib2.lib ..\prebuilt\zdll.lib kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /dll /machine:I386 /out:"Release/diskimg4.dll" +# Begin Special Build Tool +SOURCE="$(InputPath)" +PostBuild_Desc=Copying DLL to app directory +PostBuild_Cmds=copy Release\diskimg4.dll ..\app copy Release\diskimg4.dll ..\mdc +# End Special Build Tool + +!ELSEIF "$(CFG)" == "diskimg - Win32 Debug" + +# PROP BASE Use_MFC 0 +# PROP BASE Use_Debug_Libraries 1 +# PROP BASE Output_Dir "Debug" +# PROP BASE Intermediate_Dir "Debug" +# PROP BASE Target_Dir "" +# PROP Use_MFC 0 +# PROP Use_Debug_Libraries 1 +# PROP Output_Dir "Debug" +# PROP Intermediate_Dir "Debug" +# PROP Ignore_Export_Lib 0 +# PROP Target_Dir "" +# ADD BASE CPP /nologo /MTd /W3 /Gm /GX /ZI /Od /D "WIN32" /D "_DEBUG" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "DISKIMG_EXPORTS" /Yu"stdafx.h" /FD /GZ /c +# ADD CPP /nologo /MDd /W3 /Gm /GX /ZI /Od /D "WIN32" /D "_DEBUG" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "DISKIMG_EXPORTS" /Yu"stdafx.h" /FD /GZ /c +# ADD BASE MTL /nologo /D "_DEBUG" /mktyplib203 /win32 +# ADD MTL /nologo /D "_DEBUG" /mktyplib203 /win32 +# ADD BASE RSC /l 0x409 /d "_DEBUG" +# ADD RSC /l 0x409 /d "_DEBUG" +BSC32=bscmake.exe +# ADD BASE BSC32 /nologo +# ADD BSC32 /nologo +LINK32=link.exe +# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /dll /debug /machine:I386 /pdbtype:sept +# ADD LINK32 ..\prebuilt\nufxlib2D.lib ..\prebuilt\zdll.lib kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /dll /debug /machine:I386 /out:"Debug/diskimg4.dll" /pdbtype:sept +# Begin Special Build Tool +SOURCE="$(InputPath)" +PostBuild_Desc=Copying debug DLL to app directory +PostBuild_Cmds=copy Debug\diskimg4.dll ..\app copy Debug\diskimg4.dll ..\mdc +# End Special Build Tool + +!ENDIF + +# Begin Target + +# Name "diskimg - Win32 Release" +# Name "diskimg - Win32 Debug" +# Begin Group "Source Files" + +# PROP Default_Filter "cpp;c;cxx;rc;def;r;odl;idl;hpj;bat" +# Begin Source File + +SOURCE=.\ASPI.cpp +# End Source File +# Begin Source File + +SOURCE=.\CFFA.cpp +# End Source File +# Begin Source File + +SOURCE=.\Container.cpp +# End Source File +# Begin Source File + +SOURCE=.\CPM.cpp +# End Source File +# Begin Source File + +SOURCE=.\DDD.cpp +# End Source File +# Begin Source File + +SOURCE=.\DiskFS.cpp +# End Source File +# Begin Source File + +SOURCE=.\DiskImg.cpp +# End Source File +# Begin Source File + +SOURCE=.\DIUtil.cpp +# End Source File +# Begin Source File + +SOURCE=.\DOS33.cpp +# End Source File +# Begin Source File + +SOURCE=.\DOSImage.cpp +# End Source File +# Begin Source File + +SOURCE=.\FAT.CPP +# End Source File +# Begin Source File + +SOURCE=.\FDI.cpp +# End Source File +# Begin Source File + +SOURCE=.\FocusDrive.cpp +# End Source File +# Begin Source File + +SOURCE=.\GenericFD.cpp +# End Source File +# Begin Source File + +SOURCE=.\Global.cpp +# End Source File +# Begin Source File + +SOURCE=.\HFS.cpp +# End Source File +# Begin Source File + +SOURCE=.\ImageWrapper.cpp +# End Source File +# Begin Source File + +SOURCE=.\MacPart.cpp +# End Source File +# Begin Source File + +SOURCE=.\MicroDrive.cpp +# End Source File +# Begin Source File + +SOURCE=.\Nibble.cpp +# End Source File +# Begin Source File + +SOURCE=.\Nibble35.cpp +# End Source File +# Begin Source File + +SOURCE=.\OuterWrapper.cpp +# End Source File +# Begin Source File + +SOURCE=.\OzDOS.cpp +# End Source File +# Begin Source File + +SOURCE=.\Pascal.cpp +# End Source File +# Begin Source File + +SOURCE=.\ProDOS.cpp +# End Source File +# Begin Source File + +SOURCE=.\RDOS.cpp +# End Source File +# Begin Source File + +SOURCE=.\SPTI.cpp +# End Source File +# Begin Source File + +SOURCE=.\StdAfx.cpp +# ADD CPP /Yc"stdafx.h" +# End Source File +# Begin Source File + +SOURCE=.\TwoImg.cpp +# End Source File +# Begin Source File + +SOURCE=.\UNIDOS.cpp +# End Source File +# Begin Source File + +SOURCE=.\VolumeUsage.cpp +# End Source File +# Begin Source File + +SOURCE=.\Win32BlockIO.cpp +# End Source File +# End Group +# Begin Group "Header Files" + +# PROP Default_Filter "h;hpp;hxx;hm;inl" +# Begin Source File + +SOURCE=.\ASPI.h +# End Source File +# Begin Source File + +SOURCE=.\CP_ntddscsi.h +# End Source File +# Begin Source File + +SOURCE=.\CP_WNASPI32.H +# End Source File +# Begin Source File + +SOURCE=.\DiskImg.h +# End Source File +# Begin Source File + +SOURCE=.\DiskImgDetail.h +# End Source File +# Begin Source File + +SOURCE=.\DiskImgPriv.h +# End Source File +# Begin Source File + +SOURCE=.\GenericFD.h +# End Source File +# Begin Source File + +SOURCE=.\SCSIDefs.h +# End Source File +# Begin Source File + +SOURCE=.\SPTI.h +# End Source File +# Begin Source File + +SOURCE=.\StdAfx.h +# End Source File +# Begin Source File + +SOURCE=.\TwoImg.h +# End Source File +# Begin Source File + +SOURCE=.\Win32BlockIO.h +# End Source File +# Begin Source File + +SOURCE=.\Win32Extra.h +# End Source File +# End Group +# Begin Group "Resource Files" + +# PROP Default_Filter "ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe" +# End Group +# End Target +# End Project diff --git a/diskimg/diskimg.vcproj b/diskimg/diskimg.vcproj new file mode 100644 index 0000000..261f53b --- /dev/null +++ b/diskimg/diskimg.vcproj @@ -0,0 +1,560 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/diskimg/libhfs/COPYRIGHT b/diskimg/libhfs/COPYRIGHT new file mode 100644 index 0000000..0960cef --- /dev/null +++ b/diskimg/libhfs/COPYRIGHT @@ -0,0 +1,21 @@ + + hfsutils - tools for reading and writing Macintosh HFS volumes + Copyright (C) 1996-1998 Robert Leslie + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + + If you would like to negotiate alternate licensing terms, you may do + so by contacting the author: Robert Leslie + diff --git a/diskimg/libhfs/Makefile b/diskimg/libhfs/Makefile new file mode 100644 index 0000000..1d2af04 --- /dev/null +++ b/diskimg/libhfs/Makefile @@ -0,0 +1,149 @@ +# +# DiskImg libhfs makefile for Linux. +# +SHELL = /bin/sh +CC = gcc +AR = ar +OPT = -g -DHAVE_CONFIG_H +#OPT = -g -O2 -DHAVE_CONFIG_H +GCC_FLAGS = -Wall -Wwrite-strings -Wpointer-arith -Wshadow -Wstrict-prototypes +CFLAGS = $(OPT) $(GCC_FLAGS) -D_FILE_OFFSET_BITS=64 + +SRCS = os.c data.c block.c low.c medium.c file.c btree.c node.c \ + record.c volume.c hfs.c version.c +OBJS = os.o data.o block.o low.o medium.o file.o btree.o node.o \ + record.o volume.o hfs.o version.o + +STATIC_PRODUCT = libhfs.a +PRODUCT = $(STATIC_PRODUCT) + +all: $(PRODUCT) + @true + +$(STATIC_PRODUCT): $(OBJS) + -rm -f $(STATIC_PRODUCT) + $(AR) rcv $@ $(OBJS) + +clean: + -rm -f *.o core + -rm -f $(STATIC_PRODUCT) + -rm -f Makefile.bak + +tags:: + @ctags -R --totals * + +depend: + makedepend -- $(CFLAGS) -- $(SRCS) + +# DO NOT DELETE THIS LINE -- make depend depends on it. + +os.o: config.h /usr/include/fcntl.h /usr/include/features.h +os.o: /usr/include/sys/cdefs.h /usr/include/gnu/stubs.h +os.o: /usr/include/bits/wordsize.h /usr/include/gnu/stubs-32.h +os.o: /usr/include/bits/fcntl.h /usr/include/sys/types.h +os.o: /usr/include/bits/types.h +os.o: /usr/lib/gcc/i386-redhat-linux/4.0.0/include/stddef.h +os.o: /usr/include/bits/typesizes.h /usr/include/time.h /usr/include/unistd.h +os.o: /usr/include/bits/posix_opt.h /usr/include/bits/confname.h +os.o: /usr/include/errno.h /usr/include/bits/errno.h +os.o: /usr/include/linux/errno.h /usr/include/asm/errno.h +os.o: /usr/include/sys/stat.h /usr/include/bits/stat.h /usr/include/stdlib.h +os.o: /usr/include/stdio.h /usr/include/libio.h /usr/include/_G_config.h +os.o: /usr/include/wchar.h /usr/include/bits/wchar.h /usr/include/gconv.h +os.o: /usr/lib/gcc/i386-redhat-linux/4.0.0/include/stdarg.h +os.o: /usr/include/bits/stdio_lim.h /usr/include/bits/sys_errlist.h libhfs.h +os.o: hfs.h apple.h os.h +data.o: config.h /usr/include/string.h /usr/include/features.h +data.o: /usr/include/sys/cdefs.h /usr/include/gnu/stubs.h +data.o: /usr/include/bits/wordsize.h /usr/include/gnu/stubs-32.h +data.o: /usr/lib/gcc/i386-redhat-linux/4.0.0/include/stddef.h +data.o: /usr/include/time.h /usr/include/bits/types.h +data.o: /usr/include/bits/typesizes.h data.h +block.o: config.h /usr/include/stdlib.h /usr/include/features.h +block.o: /usr/include/sys/cdefs.h /usr/include/gnu/stubs.h +block.o: /usr/include/bits/wordsize.h /usr/include/gnu/stubs-32.h +block.o: /usr/lib/gcc/i386-redhat-linux/4.0.0/include/stddef.h +block.o: /usr/include/string.h /usr/include/errno.h /usr/include/bits/errno.h +block.o: /usr/include/linux/errno.h /usr/include/asm/errno.h libhfs.h hfs.h +block.o: /usr/include/time.h /usr/include/bits/types.h +block.o: /usr/include/bits/typesizes.h apple.h volume.h block.h os.h +low.o: config.h /usr/include/stdlib.h /usr/include/features.h +low.o: /usr/include/sys/cdefs.h /usr/include/gnu/stubs.h +low.o: /usr/include/bits/wordsize.h /usr/include/gnu/stubs-32.h +low.o: /usr/lib/gcc/i386-redhat-linux/4.0.0/include/stddef.h +low.o: /usr/include/string.h /usr/include/errno.h /usr/include/bits/errno.h +low.o: /usr/include/linux/errno.h /usr/include/asm/errno.h libhfs.h hfs.h +low.o: /usr/include/time.h /usr/include/bits/types.h +low.o: /usr/include/bits/typesizes.h apple.h low.h data.h block.h file.h +medium.o: config.h /usr/include/stdlib.h /usr/include/features.h +medium.o: /usr/include/sys/cdefs.h /usr/include/gnu/stubs.h +medium.o: /usr/include/bits/wordsize.h /usr/include/gnu/stubs-32.h +medium.o: /usr/lib/gcc/i386-redhat-linux/4.0.0/include/stddef.h +medium.o: /usr/include/string.h /usr/include/errno.h +medium.o: /usr/include/bits/errno.h /usr/include/linux/errno.h +medium.o: /usr/include/asm/errno.h libhfs.h hfs.h /usr/include/time.h +medium.o: /usr/include/bits/types.h /usr/include/bits/typesizes.h apple.h +medium.o: block.h low.h medium.h +file.o: config.h /usr/include/string.h /usr/include/features.h +file.o: /usr/include/sys/cdefs.h /usr/include/gnu/stubs.h +file.o: /usr/include/bits/wordsize.h /usr/include/gnu/stubs-32.h +file.o: /usr/lib/gcc/i386-redhat-linux/4.0.0/include/stddef.h +file.o: /usr/include/errno.h /usr/include/bits/errno.h +file.o: /usr/include/linux/errno.h /usr/include/asm/errno.h libhfs.h hfs.h +file.o: /usr/include/time.h /usr/include/bits/types.h +file.o: /usr/include/bits/typesizes.h apple.h file.h btree.h record.h +file.o: volume.h +btree.o: config.h /usr/include/stdlib.h /usr/include/features.h +btree.o: /usr/include/sys/cdefs.h /usr/include/gnu/stubs.h +btree.o: /usr/include/bits/wordsize.h /usr/include/gnu/stubs-32.h +btree.o: /usr/lib/gcc/i386-redhat-linux/4.0.0/include/stddef.h +btree.o: /usr/include/string.h /usr/include/errno.h /usr/include/bits/errno.h +btree.o: /usr/include/linux/errno.h /usr/include/asm/errno.h libhfs.h hfs.h +btree.o: /usr/include/time.h /usr/include/bits/types.h +btree.o: /usr/include/bits/typesizes.h apple.h btree.h data.h file.h block.h +btree.o: node.h +node.o: config.h /usr/include/stdlib.h /usr/include/features.h +node.o: /usr/include/sys/cdefs.h /usr/include/gnu/stubs.h +node.o: /usr/include/bits/wordsize.h /usr/include/gnu/stubs-32.h +node.o: /usr/lib/gcc/i386-redhat-linux/4.0.0/include/stddef.h +node.o: /usr/include/string.h /usr/include/errno.h /usr/include/bits/errno.h +node.o: /usr/include/linux/errno.h /usr/include/asm/errno.h libhfs.h hfs.h +node.o: /usr/include/time.h /usr/include/bits/types.h +node.o: /usr/include/bits/typesizes.h apple.h node.h data.h btree.h +record.o: config.h /usr/include/string.h /usr/include/features.h +record.o: /usr/include/sys/cdefs.h /usr/include/gnu/stubs.h +record.o: /usr/include/bits/wordsize.h /usr/include/gnu/stubs-32.h +record.o: /usr/lib/gcc/i386-redhat-linux/4.0.0/include/stddef.h libhfs.h +record.o: hfs.h /usr/include/time.h /usr/include/bits/types.h +record.o: /usr/include/bits/typesizes.h apple.h /usr/include/errno.h +record.o: /usr/include/bits/errno.h /usr/include/linux/errno.h +record.o: /usr/include/asm/errno.h record.h data.h +volume.o: config.h /usr/include/stdlib.h /usr/include/features.h +volume.o: /usr/include/sys/cdefs.h /usr/include/gnu/stubs.h +volume.o: /usr/include/bits/wordsize.h /usr/include/gnu/stubs-32.h +volume.o: /usr/lib/gcc/i386-redhat-linux/4.0.0/include/stddef.h +volume.o: /usr/include/string.h /usr/include/time.h /usr/include/bits/types.h +volume.o: /usr/include/bits/typesizes.h /usr/include/errno.h +volume.o: /usr/include/bits/errno.h /usr/include/linux/errno.h +volume.o: /usr/include/asm/errno.h /usr/include/assert.h /usr/include/stdio.h +volume.o: /usr/include/libio.h /usr/include/_G_config.h /usr/include/wchar.h +volume.o: /usr/include/bits/wchar.h /usr/include/gconv.h +volume.o: /usr/lib/gcc/i386-redhat-linux/4.0.0/include/stdarg.h +volume.o: /usr/include/bits/stdio_lim.h /usr/include/bits/sys_errlist.h +volume.o: libhfs.h hfs.h apple.h volume.h data.h block.h low.h medium.h +volume.o: file.h btree.h record.h os.h +hfs.o: config.h /usr/include/stdlib.h /usr/include/features.h +hfs.o: /usr/include/sys/cdefs.h /usr/include/gnu/stubs.h +hfs.o: /usr/include/bits/wordsize.h /usr/include/gnu/stubs-32.h +hfs.o: /usr/lib/gcc/i386-redhat-linux/4.0.0/include/stddef.h +hfs.o: /usr/include/string.h /usr/include/time.h /usr/include/bits/types.h +hfs.o: /usr/include/bits/typesizes.h /usr/include/errno.h +hfs.o: /usr/include/bits/errno.h /usr/include/linux/errno.h +hfs.o: /usr/include/asm/errno.h /usr/include/assert.h /usr/include/stdio.h +hfs.o: /usr/include/libio.h /usr/include/_G_config.h /usr/include/wchar.h +hfs.o: /usr/include/bits/wchar.h /usr/include/gconv.h +hfs.o: /usr/lib/gcc/i386-redhat-linux/4.0.0/include/stdarg.h +hfs.o: /usr/include/bits/stdio_lim.h /usr/include/bits/sys_errlist.h libhfs.h +hfs.o: hfs.h apple.h data.h block.h medium.h file.h btree.h node.h record.h +hfs.o: volume.h +version.o: version.h diff --git a/diskimg/libhfs/README b/diskimg/libhfs/README new file mode 100644 index 0000000..5f7dec5 --- /dev/null +++ b/diskimg/libhfs/README @@ -0,0 +1,5 @@ +HFS utility library, part of hfsutils v3.2.6 by Robert Leslie. + +Adapted for use with CiderPress by Andy McFadden. The os_* functions +have been replaced with a callback mechanism, and code that uses global +variables has been (mostly) removed. diff --git a/diskimg/libhfs/apple.h b/diskimg/libhfs/apple.h new file mode 100644 index 0000000..d860874 --- /dev/null +++ b/diskimg/libhfs/apple.h @@ -0,0 +1,272 @@ +/* + * libhfs - library for reading and writing Macintosh HFS volumes + * Copyright (C) 1996-1998 Robert Leslie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + * + * $Id$ + */ + +typedef signed char Char; +typedef unsigned char UChar; +typedef signed char SignedByte; +typedef signed short Integer; +typedef unsigned short UInteger; +typedef signed long LongInt; +typedef unsigned long ULongInt; +typedef char Str15[16]; +typedef char Str31[32]; +typedef long OSType; + +typedef struct { + Integer sbSig; /* device signature (should be 0x4552) */ + Integer sbBlkSize; /* block size of the device (in bytes) */ + LongInt sbBlkCount; /* number of blocks on the device */ + Integer sbDevType; /* reserved */ + Integer sbDevId; /* reserved */ + LongInt sbData; /* reserved */ + Integer sbDrvrCount; /* number of driver descriptor entries */ + LongInt ddBlock; /* first driver's starting block */ + Integer ddSize; /* size of the driver, in 512-byte blocks */ + Integer ddType; /* driver operating system type (MacOS = 1) */ + Integer ddPad[243]; /* additional drivers, if any */ +} Block0; + +typedef struct { + Integer pmSig; /* partition signature (0x504d or 0x5453) */ + Integer pmSigPad; /* reserved */ + LongInt pmMapBlkCnt; /* number of blocks in partition map */ + LongInt pmPyPartStart; /* first physical block of partition */ + LongInt pmPartBlkCnt; /* number of blocks in partition */ + Char pmPartName[33]; /* partition name */ + Char pmParType[33]; /* partition type */ + LongInt pmLgDataStart; /* first logical block of data area */ + LongInt pmDataCnt; /* number of blocks in data area */ + LongInt pmPartStatus; /* partition status information */ + LongInt pmLgBootStart; /* first logical block of boot code */ + LongInt pmBootSize; /* size of boot code, in bytes */ + LongInt pmBootAddr; /* boot code load address */ + LongInt pmBootAddr2; /* reserved */ + LongInt pmBootEntry; /* boot code entry point */ + LongInt pmBootEntry2; /* reserved */ + LongInt pmBootCksum; /* boot code checksum */ + Char pmProcessor[17];/* processor type */ + Integer pmPad[188]; /* reserved */ +} Partition; + +typedef struct { + Integer bbID; /* boot blocks signature */ + LongInt bbEntry; /* entry point to boot code */ + Integer bbVersion; /* boot blocks version number */ + Integer bbPageFlags; /* used internally */ + Str15 bbSysName; /* System filename */ + Str15 bbShellName; /* Finder filename */ + Str15 bbDbg1Name; /* debugger filename */ + Str15 bbDbg2Name; /* debugger filename */ + Str15 bbScreenName; /* name of startup screen */ + Str15 bbHelloName; /* name of startup program */ + Str15 bbScrapName; /* name of system scrap file */ + Integer bbCntFCBs; /* number of FCBs to allocate */ + Integer bbCntEvts; /* number of event queue elements */ + LongInt bb128KSHeap; /* system heap size on 128K Mac */ + LongInt bb256KSHeap; /* used internally */ + LongInt bbSysHeapSize; /* system heap size on all machines */ + Integer filler; /* reserved */ + LongInt bbSysHeapExtra; /* additional system heap space */ + LongInt bbSysHeapFract; /* fraction of RAM for system heap */ +} BootBlkHdr; + +typedef struct { + UInteger xdrStABN; /* first allocation block */ + UInteger xdrNumABlks; /* number of allocation blocks */ +} ExtDescriptor; + +typedef ExtDescriptor ExtDataRec[3]; + +typedef struct { + SignedByte xkrKeyLen; /* key length */ + SignedByte xkrFkType; /* fork type (0x00/0xff == data/resource */ + ULongInt xkrFNum; /* file number */ + UInteger xkrFABN; /* starting file allocation block */ +} ExtKeyRec; + +typedef struct { + SignedByte ckrKeyLen; /* key length */ + SignedByte ckrResrv1; /* reserved */ + ULongInt ckrParID; /* parent directory ID */ + Str31 ckrCName; /* catalog node name */ +} CatKeyRec; + +typedef struct { + Integer v; /* vertical coordinate */ + Integer h; /* horizontal coordinate */ +} Point; + +typedef struct { + Integer top; /* top edge of rectangle */ + Integer left; /* left edge */ + Integer bottom; /* bottom edge */ + Integer right; /* right edge */ +} Rect; + +typedef struct { + Rect frRect; /* folder's rectangle */ + Integer frFlags; /* flags */ + Point frLocation; /* folder's location */ + Integer frView; /* folder's view */ +} DInfo; + +typedef struct { + Point frScroll; /* scroll position */ + LongInt frOpenChain; /* directory ID chain of open folders */ + Integer frUnused; /* reserved */ + Integer frComment; /* comment ID */ + LongInt frPutAway; /* directory ID */ +} DXInfo; + +typedef struct { + OSType fdType; /* file type */ + OSType fdCreator; /* file's creator */ + Integer fdFlags; /* flags */ + Point fdLocation; /* file's location */ + Integer fdFldr; /* file's window */ +} FInfo; + +typedef struct { + Integer fdIconID; /* icon ID */ + Integer fdUnused[4]; /* reserved */ + Integer fdComment; /* comment ID */ + LongInt fdPutAway; /* home directory ID */ +} FXInfo; + +typedef struct { + Integer drSigWord; /* volume signature (0x4244 for HFS) */ + LongInt drCrDate; /* date and time of volume creation */ + LongInt drLsMod; /* date and time of last modification */ + Integer drAtrb; /* volume attributes */ + UInteger drNmFls; /* number of files in root directory */ + UInteger drVBMSt; /* first block of volume bit map (always 3) */ + UInteger drAllocPtr; /* start of next allocation search */ + UInteger drNmAlBlks; /* number of allocation blocks in volume */ + ULongInt drAlBlkSiz; /* size (in bytes) of allocation blocks */ + ULongInt drClpSiz; /* default clump size */ + UInteger drAlBlSt; /* first allocation block in volume */ + LongInt drNxtCNID; /* next unused catalog node ID (dir/file ID) */ + UInteger drFreeBks; /* number of unused allocation blocks */ + char drVN[28]; /* volume name (1-27 chars) */ + LongInt drVolBkUp; /* date and time of last backup */ + Integer drVSeqNum; /* volume backup sequence number */ + ULongInt drWrCnt; /* volume write count */ + ULongInt drXTClpSiz; /* clump size for extents overflow file */ + ULongInt drCTClpSiz; /* clump size for catalog file */ + UInteger drNmRtDirs; /* number of directories in root directory */ + ULongInt drFilCnt; /* number of files in volume */ + ULongInt drDirCnt; /* number of directories in volume */ + LongInt drFndrInfo[8]; /* information used by the Finder */ + UInteger drEmbedSigWord; /* type of embedded volume */ + ExtDescriptor drEmbedExtent; /* location of embedded volume */ + ULongInt drXTFlSize; /* size (in bytes) of extents overflow file */ + ExtDataRec drXTExtRec; /* first extent record for extents file */ + ULongInt drCTFlSize; /* size (in bytes) of catalog file */ + ExtDataRec drCTExtRec; /* first extent record for catalog file */ +} MDB; + +typedef enum { + cdrDirRec = 1, + cdrFilRec = 2, + cdrThdRec = 3, + cdrFThdRec = 4 +} CatDataType; + +typedef struct { + SignedByte cdrType; /* record type */ + SignedByte cdrResrv2; /* reserved */ + union { + struct { /* cdrDirRec */ + Integer dirFlags; /* directory flags */ + UInteger dirVal; /* directory valence */ + ULongInt dirDirID; /* directory ID */ + LongInt dirCrDat; /* date and time of creation */ + LongInt dirMdDat; /* date and time of last modification */ + LongInt dirBkDat; /* date and time of last backup */ + DInfo dirUsrInfo; /* Finder information */ + DXInfo dirFndrInfo; /* additional Finder information */ + LongInt dirResrv[4]; /* reserved */ + } dir; + struct { /* cdrFilRec */ + SignedByte + filFlags; /* file flags */ + SignedByte + filTyp; /* file type */ + FInfo filUsrWds; /* Finder information */ + ULongInt filFlNum; /* file ID */ + UInteger filStBlk; /* first alloc block of data fork */ + ULongInt filLgLen; /* logical EOF of data fork */ + ULongInt filPyLen; /* physical EOF of data fork */ + UInteger filRStBlk; /* first alloc block of resource fork */ + ULongInt filRLgLen; /* logical EOF of resource fork */ + ULongInt filRPyLen; /* physical EOF of resource fork */ + LongInt filCrDat; /* date and time of creation */ + LongInt filMdDat; /* date and time of last modification */ + LongInt filBkDat; /* date and time of last backup */ + FXInfo filFndrInfo; /* additional Finder information */ + UInteger filClpSize; /* file clump size */ + ExtDataRec + filExtRec; /* first data fork extent record */ + ExtDataRec + filRExtRec; /* first resource fork extent record */ + LongInt filResrv; /* reserved */ + } fil; + struct { /* cdrThdRec */ + LongInt thdResrv[2]; /* reserved */ + ULongInt thdParID; /* parent ID for this directory */ + Str31 thdCName; /* name of this directory */ + } dthd; + struct { /* cdrFThdRec */ + LongInt fthdResrv[2]; /* reserved */ + ULongInt fthdParID; /* parent ID for this file */ + Str31 fthdCName; /* name of this file */ + } fthd; + } u; +} CatDataRec; + +typedef struct { + ULongInt ndFLink; /* forward link */ + ULongInt ndBLink; /* backward link */ + SignedByte ndType; /* node type */ + SignedByte ndNHeight; /* node level */ + UInteger ndNRecs; /* number of records in node */ + Integer ndResv2; /* reserved */ +} NodeDescriptor; + +enum { + ndIndxNode = (SignedByte) 0x00, + ndHdrNode = (SignedByte) 0x01, + ndMapNode = (SignedByte) 0x02, + ndLeafNode = (SignedByte) 0xff +}; + +typedef struct { + UInteger bthDepth; /* current depth of tree */ + ULongInt bthRoot; /* number of root node */ + ULongInt bthNRecs; /* number of leaf records in tree */ + ULongInt bthFNode; /* number of first leaf node */ + ULongInt bthLNode; /* number of last leaf node */ + UInteger bthNodeSize; /* size of a node */ + UInteger bthKeyLen; /* maximum length of a key */ + ULongInt bthNNodes; /* total number of nodes in tree */ + ULongInt bthFree; /* number of free nodes */ + SignedByte bthResv[76]; /* reserved */ +} BTHdrRec; diff --git a/diskimg/libhfs/block.c b/diskimg/libhfs/block.c new file mode 100644 index 0000000..de6317c --- /dev/null +++ b/diskimg/libhfs/block.c @@ -0,0 +1,807 @@ +/* + * libhfs - library for reading and writing Macintosh HFS volumes + * Copyright (C) 1996-1998 Robert Leslie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + * + * $Id$ + */ + +# ifdef HAVE_CONFIG_H +# include "config.h" +# endif + +# include +# include +# include + +# include "libhfs.h" +# include "volume.h" +# include "block.h" +# include "os.h" + +# define INUSE(b) ((b)->flags & HFS_BUCKET_INUSE) +# define DIRTY(b) ((b)->flags & HFS_BUCKET_DIRTY) + +/* + * NAME: block->init() + * DESCRIPTION: initialize a volume's block cache + */ +int b_init(hfsvol *vol) +{ + bcache *cache; + int i; + + ASSERT(vol->cache == 0); + + cache = ALLOC(bcache, 1); + if (cache == 0) + ERROR(ENOMEM, 0); + + vol->cache = cache; + + cache->vol = vol; + cache->tail = &cache->chain[HFS_CACHESZ - 1]; + + cache->hits = 0; + cache->misses = 0; + + for (i = 0; i < HFS_CACHESZ; ++i) + { + bucket *b = &cache->chain[i]; + + b->flags = 0; + b->count = 0; + + b->bnum = 0; + b->data = &cache->pool[i]; + + b->cnext = b + 1; + b->cprev = b - 1; + + b->hnext = 0; + b->hprev = 0; + } + + cache->chain[0].cprev = cache->tail; + cache->tail->cnext = &cache->chain[0]; + + for (i = 0; i < HFS_HASHSZ; ++i) + cache->hash[i] = 0; + + return 0; + +fail: + return -1; +} + +# ifdef DEBUG +/* + * NAME: block->showstats() + * DESCRIPTION: output cache hit/miss ratio + */ +void b_showstats(const bcache *cache) +{ + fprintf(stderr, "BLOCK: CACHE vol 0x%lx \"%s\" hit/miss ratio = %.3f\n", + (unsigned long) cache->vol, cache->vol->mdb.drVN, + (float) cache->hits / (float) cache->misses); +} + +/* + * NAME: block->dumpcache() + * DESCRIPTION: dump the cache tables for a volume + */ +void b_dumpcache(const bcache *cache) +{ + const bucket *b; + int i; + + fprintf(stderr, "BLOCK CACHE DUMP:\n"); + + for (i = 0, b = cache->tail->cnext; i < HFS_CACHESZ; ++i, b = b->cnext) + { + if (INUSE(b)) + { + fprintf(stderr, "\t %lu", b->bnum); + if (DIRTY(b)) + fprintf(stderr, "*"); + + fprintf(stderr, ":%u", b->count); + } + } + + fprintf(stderr, "\n"); + + fprintf(stderr, "BLOCK HASH DUMP:\n"); + + for (i = 0; i < HFS_HASHSZ; ++i) + { + int seen = 0; + + for (b = cache->hash[i]; b; b = b->hnext) + { + if (! seen) + fprintf(stderr, " %d:", i); + + if (INUSE(b)) + { + fprintf(stderr, " %lu", b->bnum); + if (DIRTY(b)) + fprintf(stderr, "*"); + + fprintf(stderr, ":%u", b->count); + } + + seen = 1; + } + + if (seen) + fprintf(stderr, "\n"); + } +} +# endif + +/* + * NAME: fillchain() + * DESCRIPTION: fill a chain of bucket buffers with a single read + */ +static +int fillchain(hfsvol *vol, bucket **bptr, unsigned int *count) +{ + bucket *blist[HFS_BLOCKBUFSZ], **start = bptr; + unsigned long bnum; + unsigned int len, i; + + for (len = 0; len < HFS_BLOCKBUFSZ && + (unsigned int) (bptr - start) < *count; ++bptr) + { + if (INUSE(*bptr)) + continue; + + if (len > 0 && (*bptr)->bnum != bnum) + break; + + blist[len++] = *bptr; + bnum = (*bptr)->bnum + 1; + } + + *count = bptr - start; + + if (len == 0) + goto done; + else if (len == 1) + { + if (b_readpb(vol, vol->vstart + blist[0]->bnum, + blist[0]->data, 1) == -1) + goto fail; + } + else + { + block buffer[HFS_BLOCKBUFSZ]; + + if (b_readpb(vol, vol->vstart + blist[0]->bnum, buffer, len) == -1) + goto fail; + + for (i = 0; i < len; ++i) + memcpy(blist[i]->data, buffer[i], HFS_BLOCKSZ); + } + + for (i = 0; i < len; ++i) + { + blist[i]->flags |= HFS_BUCKET_INUSE; + blist[i]->flags &= ~HFS_BUCKET_DIRTY; + } + +done: + return 0; + +fail: + return -1; +} + +/* + * NAME: flushchain() + * DESCRIPTION: store a chain of bucket buffers with a single write + */ +static +int flushchain(hfsvol *vol, bucket **bptr, unsigned int *count) +{ + bucket *blist[HFS_BLOCKBUFSZ], **start = bptr; + unsigned long bnum; + unsigned int len, i; + + for (len = 0; len < HFS_BLOCKBUFSZ && + (unsigned int) (bptr - start) < *count; ++bptr) + { + if (! INUSE(*bptr) || ! DIRTY(*bptr)) + continue; + + if (len > 0 && (*bptr)->bnum != bnum) + break; + + blist[len++] = *bptr; + bnum = (*bptr)->bnum + 1; + } + + *count = bptr - start; + + if (len == 0) + goto done; + else if (len == 1) + { + if (b_writepb(vol, vol->vstart + blist[0]->bnum, + blist[0]->data, 1) == -1) + goto fail; + } + else + { + block buffer[HFS_BLOCKBUFSZ]; + + for (i = 0; i < len; ++i) + memcpy(buffer[i], blist[i]->data, HFS_BLOCKSZ); + + if (b_writepb(vol, vol->vstart + blist[0]->bnum, buffer, len) == -1) + goto fail; + } + + for (i = 0; i < len; ++i) + blist[i]->flags &= ~HFS_BUCKET_DIRTY; + +done: + return 0; + +fail: + return -1; +} + +/* + * NAME: compare() + * DESCRIPTION: comparison function for qsort of cache bucket pointers + */ +static +int compare(const bucket **b1, const bucket **b2) +{ + long diff; + + diff = (*b1)->bnum - (*b2)->bnum; + + if (diff < 0) + return -1; + else if (diff > 0) + return 1; + else + return 0; +} + +/* + * NAME: dobuckets() + * DESCRIPTION: fill or flush an array of cache buckets to a volume + */ +static +int dobuckets(hfsvol *vol, bucket **chain, unsigned int len, + int (*func)(hfsvol *, bucket **, unsigned int *)) +{ + unsigned int count, i; + int result = 0; + + qsort(chain, len, sizeof(*chain), + (int (*)(const void *, const void *)) compare); + + for (i = 0; i < len; i += count) + { + count = len - i; + if (func(vol, chain + i, &count) == -1) + result = -1; + } + + return result; +} + +# define fillbuckets(vol, chain, len) dobuckets(vol, chain, len, fillchain) +# define flushbuckets(vol, chain, len) dobuckets(vol, chain, len, flushchain) + +/* + * NAME: block->flush() + * DESCRIPTION: commit dirty cache blocks to a volume + */ +int b_flush(hfsvol *vol) +{ + bcache *cache = vol->cache; + bucket *chain[HFS_CACHESZ]; + int i; + + if (cache == 0 || (vol->flags & HFS_VOL_READONLY)) + goto done; + + for (i = 0; i < HFS_CACHESZ; ++i) + chain[i] = &cache->chain[i]; + + if (flushbuckets(vol, chain, HFS_CACHESZ) == -1) + goto fail; + +done: +# ifdef DEBUG + if (cache) + b_showstats(cache); +# endif + + return 0; + +fail: + return -1; +} + +/* + * NAME: block->finish() + * DESCRIPTION: commit and free a volume's block cache + */ +int b_finish(hfsvol *vol) +{ + int result = 0; + + if (vol->cache == 0) + goto done; + +# ifdef DEBUG + b_dumpcache(vol->cache); +# endif + + result = b_flush(vol); + + FREE(vol->cache); + vol->cache = 0; + +done: + return result; +} + +/* + * NAME: findbucket() + * DESCRIPTION: locate a bucket in the cache, and/or its hash slot + */ +static +bucket *findbucket(bcache *cache, unsigned long bnum, bucket ***hslot) +{ + bucket *b; + + *hslot = &cache->hash[bnum & (HFS_HASHSZ - 1)]; + + for (b = **hslot; b; b = b->hnext) + { + if (INUSE(b) && b->bnum == bnum) + break; + } + + return b; +} + +/* + * NAME: reuse() + * DESCRIPTION: free a bucket for reuse, flushing if necessary + */ +static +int reuse(bcache *cache, bucket *b, unsigned long bnum) +{ + bucket *chain[HFS_BLOCKBUFSZ], *bptr; + int i; + +# ifdef DEBUG + if (INUSE(b)) + fprintf(stderr, "BLOCK: CACHE reusing bucket containing " + "vol 0x%lx block %lu:%u\n", + (unsigned long) cache->vol, b->bnum, b->count); +# endif + + if (INUSE(b) && DIRTY(b)) + { + /* flush most recently unused buckets */ + + for (bptr = b, i = 0; i < HFS_BLOCKBUFSZ; ++i) + { + chain[i] = bptr; + bptr = bptr->cprev; + } + + if (flushbuckets(cache->vol, chain, HFS_BLOCKBUFSZ) == -1) + goto fail; + } + + b->flags &= ~HFS_BUCKET_INUSE; + b->count = 1; + b->bnum = bnum; + + return 0; + +fail: + return -1; +} + +/* + * NAME: cplace() + * DESCRIPTION: move a bucket to an appropriate place near head of the chain + */ +static +void cplace(bcache *cache, bucket *b) +{ + bucket *p; + + for (p = cache->tail->cnext; p->count > 1; p = p->cnext) + --p->count; + + b->cnext->cprev = b->cprev; + b->cprev->cnext = b->cnext; + + if (cache->tail == b) + cache->tail = b->cprev; + + b->cprev = p->cprev; + b->cnext = p; + + p->cprev->cnext = b; + p->cprev = b; +} + +/* + * NAME: hplace() + * DESCRIPTION: move a bucket to the head of its hash slot + */ +static +void hplace(bucket **hslot, bucket *b) +{ + if (*hslot != b) + { + if (b->hprev) + *b->hprev = b->hnext; + if (b->hnext) + b->hnext->hprev = b->hprev; + + b->hprev = hslot; + b->hnext = *hslot; + + if (*hslot) + (*hslot)->hprev = &b->hnext; + + *hslot = b; + } +} + +/* + * NAME: getbucket() + * DESCRIPTION: fetch a bucket from the cache, or an empty one to be filled + */ +static +bucket *getbucket(bcache *cache, unsigned long bnum, int fill) +{ + bucket **hslot, *b, *p, *bptr, + *chain[HFS_BLOCKBUFSZ], **slots[HFS_BLOCKBUFSZ]; + + b = findbucket(cache, bnum, &hslot); + + if (b) + { + /* cache hit; move towards head of cache chain */ + + ++cache->hits; + + if (++b->count > b->cprev->count && + b != cache->tail->cnext) + { + p = b->cprev; + + p->cprev->cnext = b; + b->cnext->cprev = p; + + p->cnext = b->cnext; + b->cprev = p->cprev; + + p->cprev = b; + b->cnext = p; + + if (cache->tail == b) + cache->tail = p; + } + } + else + { + /* cache miss; reuse least-used cache bucket */ + + ++cache->misses; + + b = cache->tail; + + if (reuse(cache, b, bnum) == -1) + goto fail; + + if (fill) + { + unsigned int len = 0; + + chain[len] = b; + slots[len++] = hslot; + + for (bptr = b->cprev; + len < (HFS_BLOCKBUFSZ >> 1) && ++bnum < cache->vol->vlen; + bptr = bptr->cprev) + { + if (findbucket(cache, bnum, &hslot)) + break; + + if (reuse(cache, bptr, bnum) == -1) + goto fail; + + chain[len] = bptr; + slots[len++] = hslot; + } + + if (fillbuckets(cache->vol, chain, len) == -1) + goto fail; + + while (--len) + { + cplace(cache, chain[len]); + hplace(slots[len], chain[len]); + } + + hslot = slots[0]; + } + + /* move bucket to appropriate place in chain */ + + cplace(cache, b); + } + + /* insert at front of hash chain */ + + hplace(hslot, b); + + return b; + +fail: + return 0; +} + +/* + * NAME: block->readpb() + * DESCRIPTION: read blocks from the physical medium (bypassing cache) + */ +int b_readpb(hfsvol *vol, unsigned long bnum, block *bp, unsigned int blen) +{ + unsigned long nblocks; + +# ifdef DEBUG + fprintf(stderr, "BLOCK: READ vol 0x%lx block %lu", + (unsigned long) vol, bnum); + if (blen > 1) + fprintf(stderr, "+%u[..%lu]\n", blen - 1, bnum + blen - 1); + else + fprintf(stderr, "\n"); +# endif + + nblocks = os_seek(&vol->priv, bnum); + if (nblocks == (unsigned long) -1) + goto fail; + + if (nblocks != bnum) + ERROR(EIO, "block seek failed for read"); + + nblocks = os_read(&vol->priv, bp, blen); + if (nblocks == (unsigned long) -1) + goto fail; + + if (nblocks != blen) + ERROR(EIO, "incomplete block read"); + + return 0; + +fail: + return -1; +} + +/* + * NAME: block->writepb() + * DESCRIPTION: write blocks to the physical medium (bypassing cache) + */ +int b_writepb(hfsvol *vol, unsigned long bnum, const block *bp, + unsigned int blen) +{ + unsigned long nblocks; + +# ifdef DEBUG + fprintf(stderr, "BLOCK: WRITE vol 0x%lx block %lu", + (unsigned long) vol, bnum); + if (blen > 1) + fprintf(stderr, "+%u[..%lu]\n", blen - 1, bnum + blen - 1); + else + fprintf(stderr, "\n"); +# endif + + nblocks = os_seek(&vol->priv, bnum); + if (nblocks == (unsigned long) -1) + goto fail; + + if (nblocks != bnum) + ERROR(EIO, "block seek failed for write"); + + nblocks = os_write(&vol->priv, bp, blen); + if (nblocks == (unsigned long) -1) + goto fail; + + if (nblocks != blen) + ERROR(EIO, "incomplete block write"); + + return 0; + +fail: + return -1; +} + +/* + * NAME: block->readlb() + * DESCRIPTION: read a logical block from a volume (or from the cache) + */ +int b_readlb(hfsvol *vol, unsigned long bnum, block *bp) +{ + if (vol->vlen > 0 && bnum >= vol->vlen) + ERROR(EIO, "read nonexistent logical block"); + + if (vol->cache) + { + bucket *b; + + b = getbucket(vol->cache, bnum, 1); + if (b == 0) + goto fail; + + memcpy(bp, b->data, HFS_BLOCKSZ); + } + else + { + if (b_readpb(vol, vol->vstart + bnum, bp, 1) == -1) + goto fail; + } + + return 0; + +fail: + return -1; +} + +/* + * NAME: block->writelb() + * DESCRIPTION: write a logical block to a volume (or to the cache) + */ +int b_writelb(hfsvol *vol, unsigned long bnum, const block *bp) +{ + if (vol->vlen > 0 && bnum >= vol->vlen) + ERROR(EIO, "write nonexistent logical block"); + + if (vol->cache) + { + bucket *b; + + b = getbucket(vol->cache, bnum, 0); + if (b == 0) + goto fail; + + if (! INUSE(b) || + memcmp(b->data, bp, HFS_BLOCKSZ) != 0) + { + memcpy(b->data, bp, HFS_BLOCKSZ); + b->flags |= HFS_BUCKET_INUSE | HFS_BUCKET_DIRTY; + } + } + else + { + if (b_writepb(vol, vol->vstart + bnum, bp, 1) == -1) + goto fail; + } + + return 0; + +fail: + return -1; +} + +/* + * NAME: block->readab() + * DESCRIPTION: read a block from an allocation block from a volume + */ +int b_readab(hfsvol *vol, unsigned int anum, unsigned int idx, block *bp) +{ + /* verify the allocation block exists and is marked as in-use */ + + if (anum >= vol->mdb.drNmAlBlks) + ERROR(EIO, "read nonexistent allocation block"); + else if (vol->vbm && ! BMTST(vol->vbm, anum)) + ERROR(EIO, "read unallocated block"); + + return b_readlb(vol, vol->mdb.drAlBlSt + anum * vol->lpa + idx, bp); + +fail: + return -1; +} + +/* + * NAME: block->writeab() + * DESCRIPTION: write a block to an allocation block to a volume + */ +int b_writeab(hfsvol *vol, + unsigned int anum, unsigned int idx, const block *bp) +{ + /* verify the allocation block exists and is marked as in-use */ + + if (anum >= vol->mdb.drNmAlBlks) + ERROR(EIO, "write nonexistent allocation block"); + else if (vol->vbm && ! BMTST(vol->vbm, anum)) + ERROR(EIO, "write unallocated block"); + + if (v_dirty(vol) == -1) + goto fail; + + return b_writelb(vol, vol->mdb.drAlBlSt + anum * vol->lpa + idx, bp); + +fail: + return -1; +} + +/* + * NAME: block->size() + * DESCRIPTION: return the number of physical blocks on a volume's medium + */ +unsigned long b_size(hfsvol *vol) +{ + unsigned long low, high, mid; + block b; + + high = os_seek(&vol->priv, -1); + + if (high != (unsigned long) -1 && high > 0) + return high; + + /* manual size detection: first check there is at least 1 block in medium */ + + if (b_readpb(vol, 0, &b, 1) == -1) + ERROR(EIO, "size of medium indeterminable or empty"); + + for (low = 0, high = 2880; + high > 0 && b_readpb(vol, high - 1, &b, 1) != -1; + high <<= 1) + low = high - 1; + + if (high == 0) + ERROR(EIO, "size of medium indeterminable or too large"); + + /* common case: 1440K floppy */ + + if (low == 2879 && b_readpb(vol, 2880, &b, 1) == -1) + return 2880; + + /* binary search for other sizes */ + + while (low < high - 1) + { + mid = (low + high) >> 1; + + if (b_readpb(vol, mid, &b, 1) == -1) + high = mid; + else + low = mid; + } + + return low + 1; + +fail: + return 0; +} diff --git a/diskimg/libhfs/block.h b/diskimg/libhfs/block.h new file mode 100644 index 0000000..a18f007 --- /dev/null +++ b/diskimg/libhfs/block.h @@ -0,0 +1,40 @@ +/* + * libhfs - library for reading and writing Macintosh HFS volumes + * Copyright (C) 1996-1998 Robert Leslie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + * + * $Id$ + */ + +int b_init(hfsvol *); +int b_flush(hfsvol *); +int b_finish(hfsvol *); + +int b_readpb(hfsvol *, unsigned long, block *, unsigned int); +int b_writepb(hfsvol *, unsigned long, const block *, unsigned int); + +int b_readlb(hfsvol *, unsigned long, block *); +int b_writelb(hfsvol *, unsigned long, const block *); + +int b_readab(hfsvol *, unsigned int, unsigned int, block *); +int b_writeab(hfsvol *, unsigned int, unsigned int, const block *); + +unsigned long b_size(hfsvol *); + +# ifdef DEBUG +void b_showstats(const bcache *); +void b_dumpcache(const bcache *); +# endif diff --git a/diskimg/libhfs/btree.c b/diskimg/libhfs/btree.c new file mode 100644 index 0000000..9d4331f --- /dev/null +++ b/diskimg/libhfs/btree.c @@ -0,0 +1,700 @@ +/* + * libhfs - library for reading and writing Macintosh HFS volumes + * Copyright (C) 1996-1998 Robert Leslie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + * + * $Id$ + */ + +# ifdef HAVE_CONFIG_H +# include "config.h" +# endif + +# include +# include +# include + +# include "libhfs.h" +# include "btree.h" +# include "data.h" +# include "file.h" +# include "block.h" +# include "node.h" + +/* + * NAME: btree->getnode() + * DESCRIPTION: retrieve a numbered node from a B*-tree file + */ +int bt_getnode(node *np, btree *bt, unsigned long nnum) +{ + block *bp = &np->data; + const byte *ptr; + int i; + + np->bt = bt; + np->nnum = nnum; + +# if 0 + fprintf(stderr, "BTREE: GET vol \"%s\" btree \"%s\" node %lu\n", + bt->f.vol->mdb.drVN, bt->f.name, np->nnum); +# endif + + /* verify the node exists and is marked as in-use */ + + if (nnum > 0 && nnum >= bt->hdr.bthNNodes) + ERROR(EIO, "read nonexistent b*-tree node"); + else if (bt->map && ! BMTST(bt->map, nnum)) + ERROR(EIO, "read unallocated b*-tree node"); + + if (f_getblock(&bt->f, nnum, bp) == -1) + goto fail; + + ptr = *bp; + + d_fetchul(&ptr, &np->nd.ndFLink); + d_fetchul(&ptr, &np->nd.ndBLink); + d_fetchsb(&ptr, &np->nd.ndType); + d_fetchsb(&ptr, &np->nd.ndNHeight); + d_fetchuw(&ptr, &np->nd.ndNRecs); + d_fetchsw(&ptr, &np->nd.ndResv2); + + if (np->nd.ndNRecs > HFS_MAX_NRECS) + ERROR(EIO, "too many b*-tree node records"); + + i = np->nd.ndNRecs + 1; + + ptr = *bp + HFS_BLOCKSZ - (2 * i); + + while (i--) + d_fetchuw(&ptr, &np->roff[i]); + + return 0; + +fail: + return -1; +} + +/* + * NAME: btree->putnode() + * DESCRIPTION: store a numbered node into a B*-tree file + */ +int bt_putnode(node *np) +{ + btree *bt = np->bt; + block *bp = &np->data; + byte *ptr; + int i; + +# if 0 + fprintf(stderr, "BTREE: PUT vol \"%s\" btree \"%s\" node %lu\n", + bt->f.vol->mdb.drVN, bt->f.name, np->nnum); +# endif + + /* verify the node exists and is marked as in-use */ + + if (np->nnum > 0 && np->nnum >= bt->hdr.bthNNodes) + ERROR(EIO, "write nonexistent b*-tree node"); + else if (bt->map && ! BMTST(bt->map, np->nnum)) + ERROR(EIO, "write unallocated b*-tree node"); + + ptr = *bp; + + d_storeul(&ptr, np->nd.ndFLink); + d_storeul(&ptr, np->nd.ndBLink); + d_storesb(&ptr, np->nd.ndType); + d_storesb(&ptr, np->nd.ndNHeight); + d_storeuw(&ptr, np->nd.ndNRecs); + d_storesw(&ptr, np->nd.ndResv2); + + if (np->nd.ndNRecs > HFS_MAX_NRECS) + ERROR(EIO, "too many b*-tree node records"); + + i = np->nd.ndNRecs + 1; + + ptr = *bp + HFS_BLOCKSZ - (2 * i); + + while (i--) + d_storeuw(&ptr, np->roff[i]); + + return f_putblock(&bt->f, np->nnum, bp); + +fail: + return -1; +} + +/* + * NAME: btree->readhdr() + * DESCRIPTION: read the header node of a B*-tree + */ +int bt_readhdr(btree *bt) +{ + const byte *ptr; + byte *map = 0; + int i; + unsigned long nnum; + + if (bt_getnode(&bt->hdrnd, bt, 0) == -1) + goto fail; + + if (bt->hdrnd.nd.ndType != ndHdrNode || + bt->hdrnd.nd.ndNRecs != 3 || + bt->hdrnd.roff[0] != 0x00e || + bt->hdrnd.roff[1] != 0x078 || + bt->hdrnd.roff[2] != 0x0f8 || + bt->hdrnd.roff[3] != 0x1f8) + ERROR(EIO, "malformed b*-tree header node"); + + /* read header record */ + + ptr = HFS_NODEREC(bt->hdrnd, 0); + + d_fetchuw(&ptr, &bt->hdr.bthDepth); + d_fetchul(&ptr, &bt->hdr.bthRoot); + d_fetchul(&ptr, &bt->hdr.bthNRecs); + d_fetchul(&ptr, &bt->hdr.bthFNode); + d_fetchul(&ptr, &bt->hdr.bthLNode); + d_fetchuw(&ptr, &bt->hdr.bthNodeSize); + d_fetchuw(&ptr, &bt->hdr.bthKeyLen); + d_fetchul(&ptr, &bt->hdr.bthNNodes); + d_fetchul(&ptr, &bt->hdr.bthFree); + + for (i = 0; i < 76; ++i) + d_fetchsb(&ptr, &bt->hdr.bthResv[i]); + + if (bt->hdr.bthNodeSize != HFS_BLOCKSZ) + ERROR(EINVAL, "unsupported b*-tree node size"); + + /* read map record; construct btree bitmap */ + /* don't set bt->map until we're done, since getnode() checks it */ + + map = ALLOC(byte, HFS_MAP1SZ); + if (map == 0) + ERROR(ENOMEM, 0); + + memcpy(map, HFS_NODEREC(bt->hdrnd, 2), HFS_MAP1SZ); + bt->mapsz = HFS_MAP1SZ; + + /* read continuation map records, if any */ + + nnum = bt->hdrnd.nd.ndFLink; + + while (nnum) + { + node n; + byte *newmap; + + if (bt_getnode(&n, bt, nnum) == -1) + goto fail; + + if (n.nd.ndType != ndMapNode || + n.nd.ndNRecs != 1 || + n.roff[0] != 0x00e || + n.roff[1] != 0x1fa) + ERROR(EIO, "malformed b*-tree map node"); + + newmap = REALLOC(map, byte, bt->mapsz + HFS_MAPXSZ); + if (newmap == 0) + ERROR(ENOMEM, 0); + + map = newmap; + + memcpy(map + bt->mapsz, HFS_NODEREC(n, 0), HFS_MAPXSZ); + bt->mapsz += HFS_MAPXSZ; + + nnum = n.nd.ndFLink; + } + + bt->map = map; + + return 0; + +fail: + FREE(map); + return -1; +} + +/* + * NAME: btree->writehdr() + * DESCRIPTION: write the header node of a B*-tree + */ +int bt_writehdr(btree *bt) +{ + byte *ptr, *map; + unsigned long mapsz, nnum; + int i; + + ASSERT(bt->hdrnd.bt == bt && + bt->hdrnd.nnum == 0 && + bt->hdrnd.nd.ndType == ndHdrNode && + bt->hdrnd.nd.ndNRecs == 3); + + ptr = HFS_NODEREC(bt->hdrnd, 0); + + d_storeuw(&ptr, bt->hdr.bthDepth); + d_storeul(&ptr, bt->hdr.bthRoot); + d_storeul(&ptr, bt->hdr.bthNRecs); + d_storeul(&ptr, bt->hdr.bthFNode); + d_storeul(&ptr, bt->hdr.bthLNode); + d_storeuw(&ptr, bt->hdr.bthNodeSize); + d_storeuw(&ptr, bt->hdr.bthKeyLen); + d_storeul(&ptr, bt->hdr.bthNNodes); + d_storeul(&ptr, bt->hdr.bthFree); + + for (i = 0; i < 76; ++i) + d_storesb(&ptr, bt->hdr.bthResv[i]); + + memcpy(HFS_NODEREC(bt->hdrnd, 2), bt->map, HFS_MAP1SZ); + + if (bt_putnode(&bt->hdrnd) == -1) + goto fail; + + map = bt->map + HFS_MAP1SZ; + mapsz = bt->mapsz - HFS_MAP1SZ; + + nnum = bt->hdrnd.nd.ndFLink; + + while (mapsz) + { + node n; + + if (nnum == 0) + ERROR(EIO, "truncated b*-tree map"); + + if (bt_getnode(&n, bt, nnum) == -1) + goto fail; + + if (n.nd.ndType != ndMapNode || + n.nd.ndNRecs != 1 || + n.roff[0] != 0x00e || + n.roff[1] != 0x1fa) + ERROR(EIO, "malformed b*-tree map node"); + + memcpy(HFS_NODEREC(n, 0), map, HFS_MAPXSZ); + + if (bt_putnode(&n) == -1) + goto fail; + + map += HFS_MAPXSZ; + mapsz -= HFS_MAPXSZ; + + nnum = n.nd.ndFLink; + } + + bt->flags &= ~HFS_BT_UPDATE_HDR; + + return 0; + +fail: + return -1; +} + +/* High-Level B*-Tree Routines ============================================= */ + +/* + * NAME: btree->space() + * DESCRIPTION: assert space for new records, or extend the file + */ +int bt_space(btree *bt, unsigned int nrecs) +{ + unsigned int nnodes; + long space; + + nnodes = nrecs * (bt->hdr.bthDepth + 1); + + if (nnodes <= bt->hdr.bthFree) + goto done; + + /* make sure the extents tree has room too */ + + if (bt != &bt->f.vol->ext) + { + if (bt_space(&bt->f.vol->ext, 1) == -1) + goto fail; + } + + space = f_alloc(&bt->f); + if (space == -1) + goto fail; + + nnodes = space * (bt->f.vol->mdb.drAlBlkSiz / bt->hdr.bthNodeSize); + + bt->hdr.bthNNodes += nnodes; + bt->hdr.bthFree += nnodes; + + bt->flags |= HFS_BT_UPDATE_HDR; + + bt->f.vol->flags |= HFS_VOL_UPDATE_ALTMDB; + + while (bt->hdr.bthNNodes > bt->mapsz * 8) + { + byte *newmap; + node mapnd; + + /* extend tree map */ + + newmap = REALLOC(bt->map, byte, bt->mapsz + HFS_MAPXSZ); + if (newmap == 0) + ERROR(ENOMEM, 0); + + memset(newmap + bt->mapsz, 0, HFS_MAPXSZ); + + bt->map = newmap; + bt->mapsz += HFS_MAPXSZ; + + n_init(&mapnd, bt, ndMapNode, 0); + if (n_new(&mapnd) == -1) + goto fail; + + mapnd.nd.ndNRecs = 1; + mapnd.roff[1] = 0x1fa; + + /* link the new map node */ + + if (bt->hdrnd.nd.ndFLink == 0) + { + bt->hdrnd.nd.ndFLink = mapnd.nnum; + mapnd.nd.ndBLink = 0; + } + else + { + node n; + unsigned long nnum; + + nnum = bt->hdrnd.nd.ndFLink; + + while (1) + { + if (bt_getnode(&n, bt, nnum) == -1) + goto fail; + + if (n.nd.ndFLink == 0) + break; + + nnum = n.nd.ndFLink; + } + + n.nd.ndFLink = mapnd.nnum; + mapnd.nd.ndBLink = n.nnum; + + if (bt_putnode(&n) == -1) + goto fail; + } + + if (bt_putnode(&mapnd) == -1) + goto fail; + } + +done: + return 0; + +fail: + return -1; +} + +/* + * NAME: insertx() + * DESCRIPTION: recursively locate a node and insert a record + */ +static +int insertx(node *np, byte *record, int *reclen) +{ + node child; + byte *rec; + int result = 0; + + if (n_search(np, record)) + ERROR(EIO, "b*-tree record already exists"); + + switch (np->nd.ndType) + { + case ndIndxNode: + if (np->rnum == -1) + rec = HFS_NODEREC(*np, 0); + else + rec = HFS_NODEREC(*np, np->rnum); + + if (bt_getnode(&child, np->bt, d_getul(HFS_RECDATA(rec))) == -1 || + insertx(&child, record, reclen) == -1) + goto fail; + + if (np->rnum == -1) + { + n_index(&child, rec, 0); + if (*reclen == 0) + { + result = bt_putnode(np); + goto done; + } + } + + if (*reclen) + result = n_insert(np, record, reclen); + + break; + + case ndLeafNode: + result = n_insert(np, record, reclen); + break; + + default: + ERROR(EIO, "unexpected b*-tree node"); + } + +done: + return result; + +fail: + return -1; +} + +/* + * NAME: btree->insert() + * DESCRIPTION: insert a new node record into a tree + */ +int bt_insert(btree *bt, const byte *record, unsigned int reclen) +{ + node root; + byte newrec[HFS_MAX_RECLEN]; + + if (bt->hdr.bthRoot == 0) + { + /* create root node */ + + n_init(&root, bt, ndLeafNode, 1); + if (n_new(&root) == -1 || + bt_putnode(&root) == -1) + goto fail; + + bt->hdr.bthDepth = 1; + bt->hdr.bthRoot = root.nnum; + bt->hdr.bthFNode = root.nnum; + bt->hdr.bthLNode = root.nnum; + + bt->flags |= HFS_BT_UPDATE_HDR; + } + else if (bt_getnode(&root, bt, bt->hdr.bthRoot) == -1) + goto fail; + + memcpy(newrec, record, reclen); + + if (insertx(&root, newrec, &reclen) == -1) + goto fail; + + if (reclen) + { + byte oroot[HFS_MAX_RECLEN]; + unsigned int orootlen; + + /* root node was split; create a new root */ + + n_index(&root, oroot, &orootlen); + + n_init(&root, bt, ndIndxNode, root.nd.ndNHeight + 1); + if (n_new(&root) == -1) + goto fail; + + ++bt->hdr.bthDepth; + bt->hdr.bthRoot = root.nnum; + + bt->flags |= HFS_BT_UPDATE_HDR; + + /* insert index records for new root */ + + n_search(&root, oroot); + n_insertx(&root, oroot, orootlen); + + n_search(&root, newrec); + n_insertx(&root, newrec, reclen); + + if (bt_putnode(&root) == -1) + goto fail; + } + + ++bt->hdr.bthNRecs; + bt->flags |= HFS_BT_UPDATE_HDR; + + return 0; + +fail: + return -1; +} + +/* + * NAME: deletex() + * DESCRIPTION: recursively locate a node and delete a record + */ +static +int deletex(node *np, const byte *key, byte *record, int *flag) +{ + node child; + byte *rec; + int found, result = 0; + + found = n_search(np, key); + + switch (np->nd.ndType) + { + case ndIndxNode: + if (np->rnum == -1) + ERROR(EIO, "b*-tree record not found"); + + rec = HFS_NODEREC(*np, np->rnum); + + if (bt_getnode(&child, np->bt, d_getul(HFS_RECDATA(rec))) == -1 || + deletex(&child, key, rec, flag) == -1) + goto fail; + + if (*flag) + { + *flag = 0; + + if (HFS_RECKEYLEN(rec) == 0) + { + result = n_delete(np, record, flag); + break; + } + + if (np->rnum == 0) + { + /* propagate index record change into parent */ + + n_index(np, record, 0); + *flag = 1; + } + + result = bt_putnode(np); + } + + break; + + case ndLeafNode: + if (found == 0) + ERROR(EIO, "b*-tree record not found"); + + result = n_delete(np, record, flag); + break; + + default: + ERROR(EIO, "unexpected b*-tree node"); + } + + return result; + +fail: + return -1; +} + +/* + * NAME: btree->delete() + * DESCRIPTION: remove a node record from a tree + */ +int bt_delete(btree *bt, const byte *key) +{ + node root; + byte record[HFS_MAX_RECLEN]; + int flag = 0; + + if (bt->hdr.bthRoot == 0) + ERROR(EIO, "empty b*-tree"); + + if (bt_getnode(&root, bt, bt->hdr.bthRoot) == -1 || + deletex(&root, key, record, &flag) == -1) + goto fail; + + if (bt->hdr.bthDepth > 1 && root.nd.ndNRecs == 1) + { + const byte *rec; + + /* root only has one record; eliminate it and decrease the tree depth */ + + rec = HFS_NODEREC(root, 0); + + --bt->hdr.bthDepth; + bt->hdr.bthRoot = d_getul(HFS_RECDATA(rec)); + + if (n_free(&root) == -1) + goto fail; + } + else if (bt->hdr.bthDepth == 1 && root.nd.ndNRecs == 0) + { + /* root node was deleted */ + + bt->hdr.bthDepth = 0; + bt->hdr.bthRoot = 0; + } + + --bt->hdr.bthNRecs; + bt->flags |= HFS_BT_UPDATE_HDR; + + return 0; + +fail: + return -1; +} + +/* + * NAME: btree->search() + * DESCRIPTION: locate a data record given a search key + */ +int bt_search(btree *bt, const byte *key, node *np) +{ + int found = 0; + unsigned long nnum; + + nnum = bt->hdr.bthRoot; + + if (nnum == 0) + ERROR(ENOENT, 0); + + while (1) + { + const byte *rec; + + if (bt_getnode(np, bt, nnum) == -1) + { + found = -1; + goto fail; + } + + found = n_search(np, key); + + switch (np->nd.ndType) + { + case ndIndxNode: + if (np->rnum == -1) + ERROR(ENOENT, 0); + + rec = HFS_NODEREC(*np, np->rnum); + nnum = d_getul(HFS_RECDATA(rec)); + + break; + + case ndLeafNode: + if (! found) + ERROR(ENOENT, 0); + + goto done; + + default: + found = -1; + ERROR(EIO, "unexpected b*-tree node"); + } + } + +done: +fail: + return found; +} diff --git a/diskimg/libhfs/btree.h b/diskimg/libhfs/btree.h new file mode 100644 index 0000000..cdf9427 --- /dev/null +++ b/diskimg/libhfs/btree.h @@ -0,0 +1,33 @@ +/* + * libhfs - library for reading and writing Macintosh HFS volumes + * Copyright (C) 1996-1998 Robert Leslie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + * + * $Id$ + */ + +int bt_getnode(node *, btree *, unsigned long); +int bt_putnode(node *); + +int bt_readhdr(btree *); +int bt_writehdr(btree *); + +int bt_space(btree *, unsigned int); + +int bt_insert(btree *, const byte *, unsigned int); +int bt_delete(btree *, const byte *); + +int bt_search(btree *, const byte *, node *); diff --git a/diskimg/libhfs/config.h b/diskimg/libhfs/config.h new file mode 100644 index 0000000..fd76a54 --- /dev/null +++ b/diskimg/libhfs/config.h @@ -0,0 +1,60 @@ +/* config.h. Generated automatically by configure. */ +/* config.h.in. Generated automatically from configure.in by autoheader. */ +/* + * libhfs - library for reading and writing Macintosh HFS volumes + * Copyright (C) 1996-1998 Robert Leslie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + * + * $Id$ + */ + +/***************************************************************************** + * Definitions selected automatically by `configure' * + *****************************************************************************/ + +/* Define to empty if the keyword does not work. */ +/* #undef const */ + +/* Define to `unsigned' if doesn't define. */ +/* #undef size_t */ + +/* Define if you have the ANSI C header files. */ +#define STDC_HEADERS 1 + +/* Define if your declares struct tm. */ +/* #undef TM_IN_SYS_TIME */ + +/* Define if you want to enable diagnostic debugging support. */ +/* #undef DEBUG */ + +/* Define if you have the mktime function. */ +#define HAVE_MKTIME 1 + +/* Define if you have the header file. */ +#define HAVE_FCNTL_H 1 + +/* Define if you have the header file. */ +#ifndef _WIN32 +# define HAVE_UNISTD_H 1 +#endif + +/***************************************************************************** + * End of automatically configured definitions * + *****************************************************************************/ + +# ifdef DEBUG +# include +# endif diff --git a/diskimg/libhfs/data.c b/diskimg/libhfs/data.c new file mode 100644 index 0000000..5cf763c --- /dev/null +++ b/diskimg/libhfs/data.c @@ -0,0 +1,485 @@ +/* + * libhfs - library for reading and writing Macintosh HFS volumes + * Copyright (C) 1996-1998 Robert Leslie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + * + * $Id$ + */ + +# ifdef HAVE_CONFIG_H +# include "config.h" +# endif + +# include +# include + +# ifdef TM_IN_SYS_TIME +# include +# endif + +# include "data.h" + +# define TIMEDIFF 2082844800UL + +static +time_t tzdiff = -1; + +const +unsigned char hfs_charorder[256] = { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + + 0x20, 0x22, 0x23, 0x28, 0x29, 0x2a, 0x2b, 0x2c, + 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, + 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, + 0x3f, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, + + 0x47, 0x48, 0x58, 0x5a, 0x5e, 0x60, 0x67, 0x69, + 0x6b, 0x6d, 0x73, 0x75, 0x77, 0x79, 0x7b, 0x7f, + 0x8d, 0x8f, 0x91, 0x93, 0x96, 0x98, 0x9f, 0xa1, + 0xa3, 0xa5, 0xa8, 0xaa, 0xab, 0xac, 0xad, 0xae, + + 0x54, 0x48, 0x58, 0x5a, 0x5e, 0x60, 0x67, 0x69, + 0x6b, 0x6d, 0x73, 0x75, 0x77, 0x79, 0x7b, 0x7f, + 0x8d, 0x8f, 0x91, 0x93, 0x96, 0x98, 0x9f, 0xa1, + 0xa3, 0xa5, 0xa8, 0xaf, 0xb0, 0xb1, 0xb2, 0xb3, + + 0x4c, 0x50, 0x5c, 0x62, 0x7d, 0x81, 0x9a, 0x55, + 0x4a, 0x56, 0x4c, 0x4e, 0x50, 0x5c, 0x62, 0x64, + 0x65, 0x66, 0x6f, 0x70, 0x71, 0x72, 0x7d, 0x89, + 0x8a, 0x8b, 0x81, 0x83, 0x9c, 0x9d, 0x9e, 0x9a, + + 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0x95, + 0xbb, 0xbc, 0xbd, 0xbe, 0xbf, 0xc0, 0x52, 0x85, + 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, + 0xc9, 0xca, 0xcb, 0x57, 0x8c, 0xcc, 0x52, 0x85, + + 0xcd, 0xce, 0xcf, 0xd0, 0xd1, 0xd2, 0xd3, 0x26, + 0x27, 0xd4, 0x20, 0x4a, 0x4e, 0x83, 0x87, 0x87, + 0xd5, 0xd6, 0x24, 0x25, 0x2d, 0x2e, 0xd7, 0xd8, + 0xa7, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde, 0xdf, + + 0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, + 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef, + 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, + 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff +}; + +/* + * NAME: data->getsb() + * DESCRIPTION: marshal 1 signed byte into local host format + */ +signed char d_getsb(register const unsigned char *ptr) +{ + return ptr[0]; +} + +/* + * NAME: data->getub() + * DESCRIPTION: marshal 1 unsigned byte into local host format + */ +unsigned char d_getub(register const unsigned char *ptr) +{ + return ptr[0]; +} + +/* + * NAME: data->getsw() + * DESCRIPTION: marshal 2 signed bytes into local host format + */ +signed short d_getsw(register const unsigned char *ptr) +{ + return + ((( signed short) ptr[0] << 8) | + ((unsigned short) ptr[1] << 0)); +} + +/* + * NAME: data->getuw() + * DESCRIPTION: marshal 2 unsigned bytes into local host format + */ +unsigned short d_getuw(register const unsigned char *ptr) +{ + return + (((unsigned short) ptr[0] << 8) | + ((unsigned short) ptr[1] << 0)); +} + +/* + * NAME: data->getsl() + * DESCRIPTION: marshal 4 signed bytes into local host format + */ +signed long d_getsl(register const unsigned char *ptr) +{ + return + ((( signed long) ptr[0] << 24) | + ((unsigned long) ptr[1] << 16) | + ((unsigned long) ptr[2] << 8) | + ((unsigned long) ptr[3] << 0)); +} + +/* + * NAME: data->getul() + * DESCRIPTION: marshal 4 unsigned bytes into local host format + */ +unsigned long d_getul(register const unsigned char *ptr) +{ + return + (((unsigned long) ptr[0] << 24) | + ((unsigned long) ptr[1] << 16) | + ((unsigned long) ptr[2] << 8) | + ((unsigned long) ptr[3] << 0)); +} + +/* + * NAME: data->putsb() + * DESCRIPTION: marshal 1 signed byte out in big-endian format + */ +void d_putsb(register unsigned char *ptr, + register signed char data) +{ + *ptr = data; +} + +/* + * NAME: data->putub() + * DESCRIPTION: marshal 1 unsigned byte out in big-endian format + */ +void d_putub(register unsigned char *ptr, + register unsigned char data) +{ + *ptr = data; +} + +/* + * NAME: data->putsw() + * DESCRIPTION: marshal 2 signed bytes out in big-endian format + */ +void d_putsw(register unsigned char *ptr, + register signed short data) +{ + *ptr++ = ((unsigned short) data & 0xff00) >> 8; + *ptr = ((unsigned short) data & 0x00ff) >> 0; +} + +/* + * NAME: data->putuw() + * DESCRIPTION: marshal 2 unsigned bytes out in big-endian format + */ +void d_putuw(register unsigned char *ptr, + register unsigned short data) +{ + *ptr++ = (data & 0xff00) >> 8; + *ptr = (data & 0x00ff) >> 0; +} + +/* + * NAME: data->putsl() + * DESCRIPTION: marshal 4 signed bytes out in big-endian format + */ +void d_putsl(register unsigned char *ptr, + register signed long data) +{ + *ptr++ = ((unsigned long) data & 0xff000000UL) >> 24; + *ptr++ = ((unsigned long) data & 0x00ff0000UL) >> 16; + *ptr++ = ((unsigned long) data & 0x0000ff00UL) >> 8; + *ptr = ((unsigned long) data & 0x000000ffUL) >> 0; +} + +/* + * NAME: data->putul() + * DESCRIPTION: marshal 4 unsigned bytes out in big-endian format + */ +void d_putul(register unsigned char *ptr, + register unsigned long data) +{ + *ptr++ = (data & 0xff000000UL) >> 24; + *ptr++ = (data & 0x00ff0000UL) >> 16; + *ptr++ = (data & 0x0000ff00UL) >> 8; + *ptr = (data & 0x000000ffUL) >> 0; +} + +/* + * NAME: data->fetchsb() + * DESCRIPTION: incrementally retrieve a signed byte of data + */ +void d_fetchsb(register const unsigned char **ptr, + register signed char *dest) +{ + *dest = *(*ptr)++; +} + +/* + * NAME: data->fetchub() + * DESCRIPTION: incrementally retrieve an unsigned byte of data + */ +void d_fetchub(register const unsigned char **ptr, + register unsigned char *dest) +{ + *dest = *(*ptr)++; +} + +/* + * NAME: data->fetchsw() + * DESCRIPTION: incrementally retrieve a signed word of data + */ +void d_fetchsw(register const unsigned char **ptr, + register signed short *dest) +{ + *dest = + ((( signed short) (*ptr)[0] << 8) | + ((unsigned short) (*ptr)[1] << 0)); + *ptr += 2; +} + +/* + * NAME: data->fetchuw() + * DESCRIPTION: incrementally retrieve an unsigned word of data + */ +void d_fetchuw(register const unsigned char **ptr, + register unsigned short *dest) +{ + *dest = + (((unsigned short) (*ptr)[0] << 8) | + ((unsigned short) (*ptr)[1] << 0)); + *ptr += 2; +} + +/* + * NAME: data->fetchsl() + * DESCRIPTION: incrementally retrieve a signed long word of data + */ +void d_fetchsl(register const unsigned char **ptr, + register signed long *dest) +{ + *dest = + ((( signed long) (*ptr)[0] << 24) | + ((unsigned long) (*ptr)[1] << 16) | + ((unsigned long) (*ptr)[2] << 8) | + ((unsigned long) (*ptr)[3] << 0)); + *ptr += 4; +} + +/* + * NAME: data->fetchul() + * DESCRIPTION: incrementally retrieve an unsigned long word of data + */ +void d_fetchul(register const unsigned char **ptr, + register unsigned long *dest) +{ + *dest = + (((unsigned long) (*ptr)[0] << 24) | + ((unsigned long) (*ptr)[1] << 16) | + ((unsigned long) (*ptr)[2] << 8) | + ((unsigned long) (*ptr)[3] << 0)); + *ptr += 4; +} + +/* + * NAME: data->storesb() + * DESCRIPTION: incrementally store a signed byte of data + */ +void d_storesb(register unsigned char **ptr, + register signed char data) +{ + *(*ptr)++ = data; +} + +/* + * NAME: data->storeub() + * DESCRIPTION: incrementally store an unsigned byte of data + */ +void d_storeub(register unsigned char **ptr, + register unsigned char data) +{ + *(*ptr)++ = data; +} + +/* + * NAME: data->storesw() + * DESCRIPTION: incrementally store a signed word of data + */ +void d_storesw(register unsigned char **ptr, + register signed short data) +{ + *(*ptr)++ = ((unsigned short) data & 0xff00) >> 8; + *(*ptr)++ = ((unsigned short) data & 0x00ff) >> 0; +} + +/* + * NAME: data->storeuw() + * DESCRIPTION: incrementally store an unsigned word of data + */ +void d_storeuw(register unsigned char **ptr, + register unsigned short data) +{ + *(*ptr)++ = (data & 0xff00) >> 8; + *(*ptr)++ = (data & 0x00ff) >> 0; +} + +/* + * NAME: data->storesl() + * DESCRIPTION: incrementally store a signed long word of data + */ +void d_storesl(register unsigned char **ptr, + register signed long data) +{ + *(*ptr)++ = ((unsigned long) data & 0xff000000UL) >> 24; + *(*ptr)++ = ((unsigned long) data & 0x00ff0000UL) >> 16; + *(*ptr)++ = ((unsigned long) data & 0x0000ff00UL) >> 8; + *(*ptr)++ = ((unsigned long) data & 0x000000ffUL) >> 0; +} + +/* + * NAME: data->storeul() + * DESCRIPTION: incrementally store an unsigned long word of data + */ +void d_storeul(register unsigned char **ptr, + register unsigned long data) +{ + *(*ptr)++ = (data & 0xff000000UL) >> 24; + *(*ptr)++ = (data & 0x00ff0000UL) >> 16; + *(*ptr)++ = (data & 0x0000ff00UL) >> 8; + *(*ptr)++ = (data & 0x000000ffUL) >> 0; +} + +/* + * NAME: data->fetchstr() + * DESCRIPTION: incrementally retrieve a string + */ +void d_fetchstr(const unsigned char **ptr, char *dest, unsigned size) +{ + unsigned len; + + len = d_getub(*ptr); + + if (len > 0 && len < size) + memcpy(dest, *ptr + 1, len); + else + len = 0; + + dest[len] = 0; + + *ptr += size; +} + +/* + * NAME: data->storestr() + * DESCRIPTION: incrementally store a string + */ +void d_storestr(unsigned char **ptr, const char *src, unsigned size) +{ + unsigned len; + + len = strlen(src); + if (len > --size) + len = 0; + + d_storeub(ptr, (unsigned char) len); + + memcpy(*ptr, src, len); + memset(*ptr + len, 0, size - len); + + *ptr += size; +} + +/* + * NAME: data->relstring() + * DESCRIPTION: compare two strings as per MacOS for HFS + */ +int d_relstring(const char *str1, const char *str2) +{ + register int diff; + + while (*str1 && *str2) + { + diff = hfs_charorder[(unsigned char) *str1] - + hfs_charorder[(unsigned char) *str2]; + + if (diff) + return diff; + + ++str1, ++str2; + } + + if (! *str1 && *str2) + return -1; + else if (*str1 && ! *str2) + return 1; + + return 0; +} + +/* + * NAME: calctzdiff() + * DESCRIPTION: calculate the timezone difference between local time and UTC + */ +static +void calctzdiff(void) +{ +# ifdef HAVE_MKTIME + + time_t t; + int isdst; + struct tm tm; + const struct tm *tmp; + + time(&t); + isdst = localtime(&t)->tm_isdst; + + tmp = gmtime(&t); + if (tmp) + { + tm = *tmp; + tm.tm_isdst = isdst; + + tzdiff = t - mktime(&tm); + } + else + tzdiff = 0; + +# else + + tzdiff = 0; + +# endif +} + +/* + * NAME: data->ltime() + * DESCRIPTION: convert MacOS time to local time + */ +time_t d_ltime(unsigned long mtime) +{ + if (tzdiff == -1) + calctzdiff(); + + return (time_t) (mtime - TIMEDIFF) - tzdiff; +} + +/* + * NAME: data->mtime() + * DESCRIPTION: convert local time to MacOS time + */ +unsigned long d_mtime(time_t ltime) +{ + if (tzdiff == -1) + calctzdiff(); + + return (unsigned long) (ltime + tzdiff) + TIMEDIFF; +} diff --git a/diskimg/libhfs/data.h b/diskimg/libhfs/data.h new file mode 100644 index 0000000..8cddf00 --- /dev/null +++ b/diskimg/libhfs/data.h @@ -0,0 +1,58 @@ +/* + * libhfs - library for reading and writing Macintosh HFS volumes + * Copyright (C) 1996-1998 Robert Leslie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + * + * $Id$ + */ + +extern const unsigned char hfs_charorder[]; + + signed char d_getsb(register const unsigned char *); +unsigned char d_getub(register const unsigned char *); + signed short d_getsw(register const unsigned char *); +unsigned short d_getuw(register const unsigned char *); + signed long d_getsl(register const unsigned char *); +unsigned long d_getul(register const unsigned char *); + +void d_putsb(register unsigned char *, register signed char); +void d_putub(register unsigned char *, register unsigned char); +void d_putsw(register unsigned char *, register signed short); +void d_putuw(register unsigned char *, register unsigned short); +void d_putsl(register unsigned char *, register signed long); +void d_putul(register unsigned char *, register unsigned long); + +void d_fetchsb(register const unsigned char **, register signed char *); +void d_fetchub(register const unsigned char **, register unsigned char *); +void d_fetchsw(register const unsigned char **, register signed short *); +void d_fetchuw(register const unsigned char **, register unsigned short *); +void d_fetchsl(register const unsigned char **, register signed long *); +void d_fetchul(register const unsigned char **, register unsigned long *); + +void d_storesb(register unsigned char **, register signed char); +void d_storeub(register unsigned char **, register unsigned char); +void d_storesw(register unsigned char **, register signed short); +void d_storeuw(register unsigned char **, register unsigned short); +void d_storesl(register unsigned char **, register signed long); +void d_storeul(register unsigned char **, register unsigned long); + +void d_fetchstr(const unsigned char **, char *, unsigned); +void d_storestr(unsigned char **, const char *, unsigned); + +int d_relstring(const char *, const char *); + +time_t d_ltime(unsigned long); +unsigned long d_mtime(time_t); diff --git a/diskimg/libhfs/file.c b/diskimg/libhfs/file.c new file mode 100644 index 0000000..551a399 --- /dev/null +++ b/diskimg/libhfs/file.c @@ -0,0 +1,520 @@ +/* + * libhfs - library for reading and writing Macintosh HFS volumes + * Copyright (C) 1996-1998 Robert Leslie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + * + * $Id$ + */ + +# ifdef HAVE_CONFIG_H +# include "config.h" +# endif + +# include +# include + +# include "libhfs.h" +# include "file.h" +# include "btree.h" +# include "record.h" +# include "volume.h" + +/* + * NAME: file->init() + * DESCRIPTION: initialize file structure + */ +void f_init(hfsfile *file, hfsvol *vol, long cnid, const char *name) +{ + int i; + + file->vol = vol; + file->parid = 0; + + strcpy(file->name, name); + + file->cat.cdrType = cdrFilRec; + file->cat.cdrResrv2 = 0; + + file->cat.u.fil.filFlags = 0; + file->cat.u.fil.filTyp = 0; + + file->cat.u.fil.filUsrWds.fdType = 0; + file->cat.u.fil.filUsrWds.fdCreator = 0; + file->cat.u.fil.filUsrWds.fdFlags = 0; + file->cat.u.fil.filUsrWds.fdLocation.v = 0; + file->cat.u.fil.filUsrWds.fdLocation.h = 0; + file->cat.u.fil.filUsrWds.fdFldr = 0; + + file->cat.u.fil.filFlNum = cnid; + file->cat.u.fil.filStBlk = 0; + file->cat.u.fil.filLgLen = 0; + file->cat.u.fil.filPyLen = 0; + file->cat.u.fil.filRStBlk = 0; + file->cat.u.fil.filRLgLen = 0; + file->cat.u.fil.filRPyLen = 0; + file->cat.u.fil.filCrDat = 0; + file->cat.u.fil.filMdDat = 0; + file->cat.u.fil.filBkDat = 0; + + file->cat.u.fil.filFndrInfo.fdIconID = 0; + for (i = 0; i < 4; ++i) + file->cat.u.fil.filFndrInfo.fdUnused[i] = 0; + file->cat.u.fil.filFndrInfo.fdComment = 0; + file->cat.u.fil.filFndrInfo.fdPutAway = 0; + + file->cat.u.fil.filClpSize = 0; + + for (i = 0; i < 3; ++i) + { + file->cat.u.fil.filExtRec[i].xdrStABN = 0; + file->cat.u.fil.filExtRec[i].xdrNumABlks = 0; + + file->cat.u.fil.filRExtRec[i].xdrStABN = 0; + file->cat.u.fil.filRExtRec[i].xdrNumABlks = 0; + } + + file->cat.u.fil.filResrv = 0; + + f_selectfork(file, fkData); + + file->flags = 0; + + file->prev = 0; + file->next = 0; +} + +/* + * NAME: file->selectfork() + * DESCRIPTION: choose a fork for file operations + */ +void f_selectfork(hfsfile *file, int fork) +{ + file->fork = fork; + + memcpy(&file->ext, fork == fkData ? + &file->cat.u.fil.filExtRec : &file->cat.u.fil.filRExtRec, + sizeof(ExtDataRec)); + + file->fabn = 0; + file->pos = 0; +} + +/* + * NAME: file->getptrs() + * DESCRIPTION: make pointers to the current fork's lengths and extents + */ +void f_getptrs(hfsfile *file, ExtDataRec **extrec, + unsigned long **lglen, unsigned long **pylen) +{ + if (file->fork == fkData) + { + if (extrec) + *extrec = &file->cat.u.fil.filExtRec; + if (lglen) + *lglen = &file->cat.u.fil.filLgLen; + if (pylen) + *pylen = &file->cat.u.fil.filPyLen; + } + else + { + if (extrec) + *extrec = &file->cat.u.fil.filRExtRec; + if (lglen) + *lglen = &file->cat.u.fil.filRLgLen; + if (pylen) + *pylen = &file->cat.u.fil.filRPyLen; + } +} + +/* + * NAME: file->doblock() + * DESCRIPTION: read or write a numbered block from a file + */ +int f_doblock(hfsfile *file, unsigned long num, block *bp, + int (*func)(hfsvol *, unsigned int, unsigned int, block *)) +{ + unsigned int abnum; + unsigned int blnum; + unsigned int fabn; + int i; + + abnum = num / file->vol->lpa; + blnum = num % file->vol->lpa; + + /* locate the appropriate extent record */ + + fabn = file->fabn; + + if (abnum < fabn) + { + ExtDataRec *extrec; + + f_getptrs(file, &extrec, 0, 0); + + fabn = file->fabn = 0; + memcpy(&file->ext, extrec, sizeof(ExtDataRec)); + } + else + abnum -= fabn; + + while (1) + { + unsigned int n; + + for (i = 0; i < 3; ++i) + { + n = file->ext[i].xdrNumABlks; + + if (abnum < n) + return func(file->vol, file->ext[i].xdrStABN + abnum, blnum, bp); + + fabn += n; + abnum -= n; + } + + if (v_extsearch(file, fabn, &file->ext, 0) <= 0) + goto fail; + + file->fabn = fabn; + } + +fail: + return -1; +} + +/* + * NAME: file->addextent() + * DESCRIPTION: add an extent to a file + */ +int f_addextent(hfsfile *file, ExtDescriptor *blocks) +{ + hfsvol *vol = file->vol; + ExtDataRec *extrec; + unsigned long *pylen; + unsigned int start, end; + node n; + int i; + + f_getptrs(file, &extrec, 0, &pylen); + + start = file->fabn; + end = *pylen / vol->mdb.drAlBlkSiz; + + n.nnum = 0; + i = -1; + + while (start < end) + { + for (i = 0; i < 3; ++i) + { + unsigned int num; + + num = file->ext[i].xdrNumABlks; + start += num; + + if (start == end) + break; + else if (start > end) + ERROR(EIO, "file extents exceed file physical length"); + else if (num == 0) + ERROR(EIO, "empty file extent"); + } + + if (start == end) + break; + + if (v_extsearch(file, start, &file->ext, &n) <= 0) + goto fail; + + file->fabn = start; + } + + if (i >= 0 && + file->ext[i].xdrStABN + file->ext[i].xdrNumABlks == blocks->xdrStABN) + file->ext[i].xdrNumABlks += blocks->xdrNumABlks; + else + { + /* create a new extent descriptor */ + + if (++i < 3) + file->ext[i] = *blocks; + else + { + ExtKeyRec key; + byte record[HFS_MAX_EXTRECLEN]; + unsigned int reclen; + + /* record is full; create a new one */ + + file->ext[0] = *blocks; + + for (i = 1; i < 3; ++i) + { + file->ext[i].xdrStABN = 0; + file->ext[i].xdrNumABlks = 0; + } + + file->fabn = start; + + r_makeextkey(&key, file->fork, file->cat.u.fil.filFlNum, end); + r_packextrec(&key, &file->ext, record, &reclen); + + if (bt_insert(&vol->ext, record, reclen) == -1) + goto fail; + + i = -1; + } + } + + if (i >= 0) + { + /* store the modified extent record */ + + if (file->fabn) + { + if ((n.nnum == 0 && + v_extsearch(file, file->fabn, 0, &n) <= 0) || + v_putextrec(&file->ext, &n) == -1) + goto fail; + } + else + memcpy(extrec, &file->ext, sizeof(ExtDataRec)); + } + + *pylen += blocks->xdrNumABlks * vol->mdb.drAlBlkSiz; + + file->flags |= HFS_FILE_UPDATE_CATREC; + + return 0; + +fail: + return -1; +} + +/* + * NAME: file->alloc() + * DESCRIPTION: reserve allocation blocks for a file + */ +long f_alloc(hfsfile *file) +{ + hfsvol *vol = file->vol; + unsigned long clumpsz; + ExtDescriptor blocks; + + clumpsz = file->cat.u.fil.filClpSize; + if (clumpsz == 0) + { + if (file == &vol->ext.f) + clumpsz = vol->mdb.drXTClpSiz; + else if (file == &vol->cat.f) + clumpsz = vol->mdb.drCTClpSiz; + else + clumpsz = vol->mdb.drClpSiz; + } + + blocks.xdrNumABlks = clumpsz / vol->mdb.drAlBlkSiz; + + if (v_allocblocks(vol, &blocks) == -1) + goto fail; + + if (f_addextent(file, &blocks) == -1) + { + v_freeblocks(vol, &blocks); + goto fail; + } + + return blocks.xdrNumABlks; + +fail: + return -1; +} + +/* + * NAME: file->trunc() + * DESCRIPTION: release allocation blocks unneeded by a file + */ +int f_trunc(hfsfile *file) +{ + hfsvol *vol = file->vol; + ExtDataRec *extrec; + unsigned long *lglen, *pylen, alblksz, newpylen; + unsigned int dlen, start, end; + node n; + int i; + + if (vol->flags & HFS_VOL_READONLY) + goto done; + + f_getptrs(file, &extrec, &lglen, &pylen); + + alblksz = vol->mdb.drAlBlkSiz; + newpylen = (*lglen / alblksz + (*lglen % alblksz != 0)) * alblksz; + + if (newpylen > *pylen) + ERROR(EIO, "file size exceeds physical length"); + else if (newpylen == *pylen) + goto done; + + dlen = (*pylen - newpylen) / alblksz; + + start = file->fabn; + end = newpylen / alblksz; + + if (start >= end) + { + start = file->fabn = 0; + memcpy(&file->ext, extrec, sizeof(ExtDataRec)); + } + + n.nnum = 0; + i = -1; + + while (start < end) + { + for (i = 0; i < 3; ++i) + { + unsigned int num; + + num = file->ext[i].xdrNumABlks; + start += num; + + if (start >= end) + break; + else if (num == 0) + ERROR(EIO, "empty file extent"); + } + + if (start >= end) + break; + + if (v_extsearch(file, start, &file->ext, &n) <= 0) + goto fail; + + file->fabn = start; + } + + if (start > end) + { + ExtDescriptor blocks; + + file->ext[i].xdrNumABlks -= start - end; + dlen -= start - end; + + blocks.xdrStABN = file->ext[i].xdrStABN + file->ext[i].xdrNumABlks; + blocks.xdrNumABlks = start - end; + + if (v_freeblocks(vol, &blocks) == -1) + goto fail; + } + + *pylen = newpylen; + + file->flags |= HFS_FILE_UPDATE_CATREC; + + do + { + while (dlen && ++i < 3) + { + unsigned int num; + + num = file->ext[i].xdrNumABlks; + start += num; + + if (num == 0) + ERROR(EIO, "empty file extent"); + else if (num > dlen) + ERROR(EIO, "file extents exceed physical size"); + + dlen -= num; + + if (v_freeblocks(vol, &file->ext[i]) == -1) + goto fail; + + file->ext[i].xdrStABN = 0; + file->ext[i].xdrNumABlks = 0; + } + + if (file->fabn) + { + if (n.nnum == 0 && + v_extsearch(file, file->fabn, 0, &n) <= 0) + goto fail; + + if (file->ext[0].xdrNumABlks) + { + if (v_putextrec(&file->ext, &n) == -1) + goto fail; + } + else + { + if (bt_delete(&vol->ext, HFS_NODEREC(n, n.rnum)) == -1) + goto fail; + + n.nnum = 0; + } + } + else + memcpy(extrec, &file->ext, sizeof(ExtDataRec)); + + if (dlen) + { + if (v_extsearch(file, start, &file->ext, &n) <= 0) + goto fail; + + file->fabn = start; + i = -1; + } + } + while (dlen); + +done: + return 0; + +fail: + return -1; +} + +/* + * NAME: file->flush() + * DESCRIPTION: flush all pending changes to an open file + */ +int f_flush(hfsfile *file) +{ + hfsvol *vol = file->vol; + + if (vol->flags & HFS_VOL_READONLY) + goto done; + + if (file->flags & HFS_FILE_UPDATE_CATREC) + { + node n; + + file->cat.u.fil.filStBlk = file->cat.u.fil.filExtRec[0].xdrStABN; + file->cat.u.fil.filRStBlk = file->cat.u.fil.filRExtRec[0].xdrStABN; + + if (v_catsearch(vol, file->parid, file->name, 0, 0, &n) <= 0 || + v_putcatrec(&file->cat, &n) == -1) + goto fail; + + file->flags &= ~HFS_FILE_UPDATE_CATREC; + } + +done: + return 0; + +fail: + return -1; +} diff --git a/diskimg/libhfs/file.h b/diskimg/libhfs/file.h new file mode 100644 index 0000000..b4a9501 --- /dev/null +++ b/diskimg/libhfs/file.h @@ -0,0 +1,45 @@ +/* + * libhfs - library for reading and writing Macintosh HFS volumes + * Copyright (C) 1996-1998 Robert Leslie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + * + * $Id$ + */ + +enum { + fkData = 0x00, + fkRsrc = 0xff +}; + +void f_init(hfsfile *, hfsvol *, long, const char *); +void f_selectfork(hfsfile *, int); +void f_getptrs(hfsfile *, ExtDataRec **, unsigned long **, unsigned long **); + +int f_doblock(hfsfile *, unsigned long, block *, + int (*)(hfsvol *, unsigned int, unsigned int, block *)); + +# define f_getblock(file, num, bp) \ + f_doblock((file), (num), (bp), b_readab) +# define f_putblock(file, num, bp) \ + f_doblock((file), (num), (bp), \ + (int (*)(hfsvol *, unsigned int, unsigned int, block *)) \ + b_writeab) + +int f_addextent(hfsfile *, ExtDescriptor *); +long f_alloc(hfsfile *); + +int f_trunc(hfsfile *); +int f_flush(hfsfile *); diff --git a/diskimg/libhfs/hfs.c b/diskimg/libhfs/hfs.c new file mode 100644 index 0000000..ebe119d --- /dev/null +++ b/diskimg/libhfs/hfs.c @@ -0,0 +1,1991 @@ +/* + * libhfs - library for reading and writing Macintosh HFS volumes + * Copyright (C) 1996-1998 Robert Leslie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + * + * $Id$ + */ + +# ifdef HAVE_CONFIG_H +# include "config.h" +# endif + +# include +# include +# include +# include +# include +# include /* debug */ + +# include "libhfs.h" +# include "data.h" +# include "block.h" +# include "medium.h" +# include "file.h" +# include "btree.h" +# include "node.h" +# include "record.h" +# include "volume.h" + +/* should be CP_NO_STATIC, but it's harmless (albeit useless with MT) */ +const char *hfs_error = "no error"; /* static error string */ + +#ifdef CP_NO_STATIC +hfsvol *hfs_mounts; /* linked list of mounted volumes */ + +static +hfsvol *curvol; /* current volume */ +#endif + +/* + * NAME: validvname() + * DESCRIPTION: return true if parameter is a valid volume name + */ +static +int validvname(const char *name) +{ + int len; + + len = strlen(name); + if (len < 1) + ERROR(EINVAL, "volume name cannot be empty"); + else if (len > HFS_MAX_VLEN) + ERROR(ENAMETOOLONG, + "volume name can be at most " STR(HFS_MAX_VLEN) " chars"); + + if (strchr(name, ':')) + ERROR(EINVAL, "volume name may not contain colons"); + + return 1; + +fail: + return 0; +} + +/* + * NAME: getvol() + * DESCRIPTION: validate a volume reference + */ +static +int getvol(hfsvol **vol) +{ +#ifdef CP_NO_STATIC + if (*vol == 0) + { + if (curvol == 0) + ERROR(EINVAL, "no volume is current"); + + *vol = curvol; + } +#else + if (*vol == 0) + ERROR(EINVAL, "no volume is current"); +#endif + + return 0; + +fail: + return -1; +} + +/* High-Level Volume Routines ============================================== */ + +#ifdef CP_NO_STATIC +/* + * NAME: hfs->mount() + * DESCRIPTION: open an HFS volume; return volume descriptor or 0 (error) + */ +hfsvol *hfs_mount(const char *path, int pnum, int mode) +{ + hfsvol *vol, *check; + + /* see if the volume is already mounted */ + + for (check = hfs_mounts; check; check = check->next) + { + if (check->pnum == pnum && v_same(check, path) == 1) + { + /* verify compatible read/write mode */ + + if (((check->flags & HFS_VOL_READONLY) && + ! (mode & HFS_MODE_RDWR)) || + (! (check->flags & HFS_VOL_READONLY) && + (mode & (HFS_MODE_RDWR | HFS_MODE_ANY)))) + { + vol = check; + goto done; + } + } + } + + vol = ALLOC(hfsvol, 1); + if (vol == 0) + ERROR(ENOMEM, 0); + + v_init(vol, mode); + + /* open the medium */ + + switch (mode & HFS_MODE_MASK) + { + case HFS_MODE_RDWR: + case HFS_MODE_ANY: + if (v_open(vol, path, HFS_MODE_RDWR) != -1) + break; + + if ((mode & HFS_MODE_MASK) == HFS_MODE_RDWR) + goto fail; + + case HFS_MODE_RDONLY: + default: + vol->flags |= HFS_VOL_READONLY; + + if (v_open(vol, path, HFS_MODE_RDONLY) == -1) + goto fail; + } + + /* mount the volume */ + + if (v_geometry(vol, pnum) == -1 || + v_mount(vol) == -1) + goto fail; + + /* add to linked list of volumes */ + + vol->prev = 0; + vol->next = hfs_mounts; + + if (hfs_mounts) + hfs_mounts->prev = vol; + + hfs_mounts = vol; + +done: + ++vol->refs; + curvol = vol; + + return vol; + +fail: + if (vol) + { + v_close(vol); + FREE(vol); + } + + return 0; +} +#endif + +/* + * NAME: hfs_callback_open() + * DESCRIPTION: open an HFS volume; return volume descriptor or 0 (error) + */ +hfsvol* hfs_callback_open(oscallback func, void* cookie, int mode) +{ + hfsvol *vol; + + vol = ALLOC(hfsvol, 1); + if (vol == 0) + ERROR(ENOMEM, 0); + + v_init(vol, mode); + + /* open the medium */ + + switch (mode & HFS_MODE_MASK) + { + case HFS_MODE_RDWR: + case HFS_MODE_ANY: + break; + + case HFS_MODE_RDONLY: + default: + vol->flags |= HFS_VOL_READONLY; + } + + /* set up vol->priv */ + v_callback_open(vol, func, cookie); + + + /* mount the volume */ + + if (v_geometry(vol, 0 /*we don't see partition map*/) == -1 || + v_mount(vol) == -1) + goto fail; + + assert(func != 0); + assert(cookie != 0); + +/*done*/ + ++vol->refs; + + return vol; + +fail: + if (vol) + { + v_close(vol); + FREE(vol); + } + + return 0; +} + +/* + * NAME: hfs->callback_close() + * DESCRIPTION: close an HFS volume + */ +int hfs_callback_close(hfsvol *vol) +{ + int result = 0; + + if (getvol(&vol) == -1) + goto fail; + + if (--vol->refs) + { + result = v_flush(vol); + goto done; + } + + /* close all open files and directories */ + + while (vol->files) + { + if (hfs_close(vol->files) == -1) + result = -1; + } + + while (vol->dirs) + { + if (hfs_closedir(vol->dirs) == -1) + result = -1; + } + + /* close medium */ + + if (v_close(vol) == -1) + result = -1; + + FREE(vol); + +done: + return result; + +fail: + return -1; +} + + +/* + * NAME: hfs->flush() + * DESCRIPTION: flush all pending changes to an HFS volume + */ +int hfs_flush(hfsvol *vol) +{ + hfsfile *file; + + if (getvol(&vol) == -1) + goto fail; + + for (file = vol->files; file; file = file->next) + { + if (f_flush(file) == -1) + goto fail; + } + + if (v_flush(vol) == -1) + goto fail; + + return 0; + +fail: + return -1; +} + +#ifdef CP_NO_STATIC +/* + * NAME: hfs->flushall() + * DESCRIPTION: flush all pending changes to all mounted HFS volumes + */ +void hfs_flushall(void) +{ + hfsvol *vol; + + for (vol = hfs_mounts; vol; vol = vol->next) + hfs_flush(vol); +} + +/* + * NAME: hfs->umount() + * DESCRIPTION: close an HFS volume + */ +int hfs_umount(hfsvol *vol) +{ + int result = 0; + + if (getvol(&vol) == -1) + goto fail; + + if (--vol->refs) + { + result = v_flush(vol); + goto done; + } + + /* close all open files and directories */ + + while (vol->files) + { + if (hfs_close(vol->files) == -1) + result = -1; + } + + while (vol->dirs) + { + if (hfs_closedir(vol->dirs) == -1) + result = -1; + } + + /* close medium */ + + if (v_close(vol) == -1) + result = -1; + + /* remove from linked list of volumes */ + + if (vol->prev) + vol->prev->next = vol->next; + if (vol->next) + vol->next->prev = vol->prev; + + if (vol == hfs_mounts) + hfs_mounts = vol->next; + if (vol == curvol) + curvol = 0; + + FREE(vol); + +done: + return result; + +fail: + return -1; +} + +/* + * NAME: hfs->umountall() + * DESCRIPTION: unmount all mounted volumes + */ +void hfs_umountall(void) +{ + while (hfs_mounts) + hfs_umount(hfs_mounts); +} + +/* + * NAME: hfs->getvol() + * DESCRIPTION: return a pointer to a mounted volume + */ +hfsvol *hfs_getvol(const char *name) +{ + hfsvol *vol; + + if (name == 0) + return curvol; + + for (vol = hfs_mounts; vol; vol = vol->next) + { + if (d_relstring(name, vol->mdb.drVN) == 0) + return vol; + } + + return 0; +} + +/* + * NAME: hfs->setvol() + * DESCRIPTION: change the current volume + */ +void hfs_setvol(hfsvol *vol) +{ + curvol = vol; +} +#endif + +/* + * NAME: hfs->vstat() + * DESCRIPTION: return volume statistics + */ +int hfs_vstat(hfsvol *vol, hfsvolent *ent) +{ + if (getvol(&vol) == -1) + goto fail; + + strcpy(ent->name, vol->mdb.drVN); + + ent->flags = (vol->flags & HFS_VOL_READONLY) ? HFS_ISLOCKED : 0; + + ent->totbytes = vol->mdb.drNmAlBlks * vol->mdb.drAlBlkSiz; + ent->freebytes = vol->mdb.drFreeBks * vol->mdb.drAlBlkSiz; + + ent->alblocksz = vol->mdb.drAlBlkSiz; + ent->clumpsz = vol->mdb.drClpSiz; + + ent->numfiles = vol->mdb.drFilCnt; + ent->numdirs = vol->mdb.drDirCnt; + + ent->crdate = d_ltime(vol->mdb.drCrDate); + ent->mddate = d_ltime(vol->mdb.drLsMod); + ent->bkdate = d_ltime(vol->mdb.drVolBkUp); + + ent->blessed = vol->mdb.drFndrInfo[0]; + + return 0; + +fail: + return -1; +} + +/* + * NAME: hfs->vsetattr() + * DESCRIPTION: change volume attributes + */ +int hfs_vsetattr(hfsvol *vol, hfsvolent *ent) +{ + if (getvol(&vol) == -1) + goto fail; + + if (ent->clumpsz % vol->mdb.drAlBlkSiz != 0) + ERROR(EINVAL, "illegal clump size"); + + /* make sure "blessed" folder exists */ + + if (ent->blessed && + v_getdthread(vol, ent->blessed, 0, 0) <= 0) + ERROR(EINVAL, "illegal blessed folder"); + + if (vol->flags & HFS_VOL_READONLY) + ERROR(EROFS, 0); + + vol->mdb.drClpSiz = ent->clumpsz; + + vol->mdb.drCrDate = d_mtime(ent->crdate); + vol->mdb.drLsMod = d_mtime(ent->mddate); + vol->mdb.drVolBkUp = d_mtime(ent->bkdate); + + vol->mdb.drFndrInfo[0] = ent->blessed; + + vol->flags |= HFS_VOL_UPDATE_MDB; + + return 0; + +fail: + return -1; +} + +/* High-Level Directory Routines =========================================== */ + +/* + * NAME: hfs->chdir() + * DESCRIPTION: change current HFS directory + */ +int hfs_chdir(hfsvol *vol, const char *path) +{ + CatDataRec data; + + if (getvol(&vol) == -1 || + v_resolve(&vol, path, &data, 0, 0, 0) <= 0) + goto fail; + + if (data.cdrType != cdrDirRec) + ERROR(ENOTDIR, 0); + + vol->cwd = data.u.dir.dirDirID; + + return 0; + +fail: + return -1; +} + +/* + * NAME: hfs->getcwd() + * DESCRIPTION: return the current working directory ID + */ +unsigned long hfs_getcwd(hfsvol *vol) +{ + if (getvol(&vol) == -1) + return 0; + + return vol->cwd; +} + +/* + * NAME: hfs->setcwd() + * DESCRIPTION: set the current working directory ID + */ +int hfs_setcwd(hfsvol *vol, unsigned long id) +{ + if (getvol(&vol) == -1) + goto fail; + + if (id == vol->cwd) + goto done; + + /* make sure the directory exists */ + + if (v_getdthread(vol, id, 0, 0) <= 0) + goto fail; + + vol->cwd = id; + +done: + return 0; + +fail: + return -1; +} + +/* + * NAME: hfs->dirinfo() + * DESCRIPTION: given a directory ID, return its (name and) parent ID + */ +int hfs_dirinfo(hfsvol *vol, unsigned long *id, char *name) +{ + CatDataRec thread; + + if (getvol(&vol) == -1 || + v_getdthread(vol, *id, &thread, 0) <= 0) + goto fail; + + *id = thread.u.dthd.thdParID; + + if (name) + strcpy(name, thread.u.dthd.thdCName); + + return 0; + +fail: + return -1; +} + +/* + * NAME: hfs->opendir() + * DESCRIPTION: prepare to read the contents of a directory + */ +hfsdir *hfs_opendir(hfsvol *vol, const char *path) +{ + hfsdir *dir = 0; + CatKeyRec key; + CatDataRec data; + byte pkey[HFS_CATKEYLEN]; + + if (getvol(&vol) == -1) + goto fail; + + dir = ALLOC(hfsdir, 1); + if (dir == 0) + ERROR(ENOMEM, 0); + + dir->vol = vol; + + if (*path == 0) + { +#ifdef CP_NO_STATIC + /* meta-directory containing root dirs from all mounted volumes */ + + dir->dirid = 0; + dir->vptr = hfs_mounts; +#else + assert(0); +#endif + } + else + { + if (v_resolve(&vol, path, &data, 0, 0, 0) <= 0) + goto fail; + + if (data.cdrType != cdrDirRec) + ERROR(ENOTDIR, 0); + + dir->dirid = data.u.dir.dirDirID; + dir->vptr = 0; + + r_makecatkey(&key, dir->dirid, ""); + r_packcatkey(&key, pkey, 0); + + if (bt_search(&vol->cat, pkey, &dir->n) <= 0) + goto fail; + } + + dir->prev = 0; + dir->next = vol->dirs; + + if (vol->dirs) + vol->dirs->prev = dir; + + vol->dirs = dir; + + return dir; + +fail: + FREE(dir); + return 0; +} + +/* + * NAME: hfs->readdir() + * DESCRIPTION: return the next entry in the directory + */ +int hfs_readdir(hfsdir *dir, hfsdirent *ent) +{ + CatKeyRec key; + CatDataRec data; + const byte *ptr; + + if (dir->dirid == 0) + { +#ifdef CP_NO_STATIC + hfsvol *vol; + char cname[HFS_MAX_FLEN + 1]; + + for (vol = hfs_mounts; vol; vol = vol->next) + { + if (vol == dir->vptr) + break; + } + + if (vol == 0) + ERROR(ENOENT, "no more entries"); + + if (v_getdthread(vol, HFS_CNID_ROOTDIR, &data, 0) <= 0 || + v_catsearch(vol, HFS_CNID_ROOTPAR, data.u.dthd.thdCName, + &data, cname, 0) <= 0) + goto fail; + + r_unpackdirent(HFS_CNID_ROOTPAR, cname, &data, ent); + + dir->vptr = vol->next; + + goto done; +#else + assert(0); +#endif + } + + if (dir->n.rnum == -1) + ERROR(ENOENT, "no more entries"); + + while (1) + { + ++dir->n.rnum; + + while (dir->n.rnum >= dir->n.nd.ndNRecs) + { + if (dir->n.nd.ndFLink == 0) + { + dir->n.rnum = -1; + ERROR(ENOENT, "no more entries"); + } + + if (bt_getnode(&dir->n, dir->n.bt, dir->n.nd.ndFLink) == -1) + { + dir->n.rnum = -1; + goto fail; + } + + dir->n.rnum = 0; + } + + ptr = HFS_NODEREC(dir->n, dir->n.rnum); + + r_unpackcatkey(ptr, &key); + + if (key.ckrParID != dir->dirid) + { + dir->n.rnum = -1; + ERROR(ENOENT, "no more entries"); + } + + r_unpackcatdata(HFS_RECDATA(ptr), &data); + + switch (data.cdrType) + { + case cdrDirRec: + case cdrFilRec: + r_unpackdirent(key.ckrParID, key.ckrCName, &data, ent); + goto done; + + case cdrThdRec: + case cdrFThdRec: + break; + + default: + dir->n.rnum = -1; + ERROR(EIO, "unexpected directory entry found"); + } + } + +done: + return 0; + +fail: + return -1; +} + +/* + * NAME: hfs->closedir() + * DESCRIPTION: stop reading a directory + */ +int hfs_closedir(hfsdir *dir) +{ + hfsvol *vol = dir->vol; + + if (dir->prev) + dir->prev->next = dir->next; + if (dir->next) + dir->next->prev = dir->prev; + if (dir == vol->dirs) + vol->dirs = dir->next; + + FREE(dir); + + return 0; +} + +/* High-Level File Routines ================================================ */ + +/* + * NAME: hfs->create() + * DESCRIPTION: create and open a new file + */ +hfsfile *hfs_create(hfsvol *vol, const char *path, + const char *type, const char *creator) +{ + hfsfile *file = 0; + unsigned long parid; + char name[HFS_MAX_FLEN + 1]; + CatKeyRec key; + byte record[HFS_MAX_CATRECLEN]; + unsigned reclen; + int found; + + if (getvol(&vol) == -1) + goto fail; + + file = ALLOC(hfsfile, 1); + if (file == 0) + ERROR(ENOMEM, 0); + + found = v_resolve(&vol, path, &file->cat, &parid, name, 0); + if (found == -1 || parid == 0) + goto fail; + + if (found) + ERROR(EEXIST, 0); + + if (parid == HFS_CNID_ROOTPAR) + ERROR(EINVAL, 0); + + if (vol->flags & HFS_VOL_READONLY) + ERROR(EROFS, 0); + + /* create file `name' in parent `parid' */ + + if (bt_space(&vol->cat, 1) == -1) + goto fail; + + f_init(file, vol, vol->mdb.drNxtCNID++, name); + vol->flags |= HFS_VOL_UPDATE_MDB; + + file->parid = parid; + + /* create catalog record */ + + file->cat.u.fil.filUsrWds.fdType = + d_getsl((const unsigned char *) type); + file->cat.u.fil.filUsrWds.fdCreator = + d_getsl((const unsigned char *) creator); + + file->cat.u.fil.filCrDat = d_mtime(time(0)); + file->cat.u.fil.filMdDat = file->cat.u.fil.filCrDat; + + r_makecatkey(&key, file->parid, file->name); + r_packcatrec(&key, &file->cat, record, &reclen); + + if (bt_insert(&vol->cat, record, reclen) == -1 || + v_adjvalence(vol, file->parid, 0, 1) == -1) + goto fail; + + /* package file handle for user */ + + file->next = vol->files; + + if (vol->files) + vol->files->prev = file; + + vol->files = file; + + return file; + +fail: + FREE(file); + return 0; +} + +/* + * NAME: hfs->open() + * DESCRIPTION: prepare a file for I/O + */ +hfsfile *hfs_open(hfsvol *vol, const char *path) +{ + hfsfile *file = 0; + + if (getvol(&vol) == -1) + goto fail; + + file = ALLOC(hfsfile, 1); + if (file == 0) + ERROR(ENOMEM, 0); + + if (v_resolve(&vol, path, &file->cat, &file->parid, file->name, 0) <= 0) + goto fail; + + if (file->cat.cdrType != cdrFilRec) + ERROR(EISDIR, 0); + + /* package file handle for user */ + + file->vol = vol; + file->flags = 0; + + f_selectfork(file, fkData); + + file->prev = 0; + file->next = vol->files; + + if (vol->files) + vol->files->prev = file; + + vol->files = file; + + return file; + +fail: + FREE(file); + return 0; +} + +/* + * NAME: hfs->setfork() + * DESCRIPTION: select file fork for I/O operations + */ +int hfs_setfork(hfsfile *file, int fork) +{ + int result = 0; + + if (f_trunc(file) == -1) + result = -1; + + f_selectfork(file, fork ? fkRsrc : fkData); + + return result; +} + +/* + * NAME: hfs->getfork() + * DESCRIPTION: return the current fork for I/O operations + */ +int hfs_getfork(hfsfile *file) +{ + return file->fork != fkData; +} + +/* + * NAME: hfs->read() + * DESCRIPTION: read from an open file + */ +unsigned long hfs_read(hfsfile *file, void *buf, unsigned long len) +{ + unsigned long *lglen, count; + byte *ptr = buf; + + f_getptrs(file, 0, &lglen, 0); + + if (file->pos + len > *lglen) + len = *lglen - file->pos; + + count = len; + while (count) + { + unsigned long bnum, offs, chunk; + + bnum = file->pos >> HFS_BLOCKSZ_BITS; + offs = file->pos & (HFS_BLOCKSZ - 1); + + chunk = HFS_BLOCKSZ - offs; + if (chunk > count) + chunk = count; + + if (offs == 0 && chunk == HFS_BLOCKSZ) + { + if (f_getblock(file, bnum, (block *) ptr) == -1) + goto fail; + } + else + { + block b; + + if (f_getblock(file, bnum, &b) == -1) + goto fail; + + memcpy(ptr, b + offs, chunk); + } + + ptr += chunk; + + file->pos += chunk; + count -= chunk; + } + + return len; + +fail: + return -1; +} + +/* + * NAME: hfs->write() + * DESCRIPTION: write to an open file + */ +unsigned long hfs_write(hfsfile *file, const void *buf, unsigned long len) +{ + unsigned long *lglen, *pylen, count; + const byte *ptr = buf; + + if (file->vol->flags & HFS_VOL_READONLY) + ERROR(EROFS, 0); + + f_getptrs(file, 0, &lglen, &pylen); + + count = len; + + /* set flag to update (at least) the modification time */ + + if (count) + { + file->cat.u.fil.filMdDat = d_mtime(time(0)); + file->flags |= HFS_FILE_UPDATE_CATREC; + } + + while (count) + { + unsigned long bnum, offs, chunk; + + bnum = file->pos >> HFS_BLOCKSZ_BITS; + offs = file->pos & (HFS_BLOCKSZ - 1); + + chunk = HFS_BLOCKSZ - offs; + if (chunk > count) + chunk = count; + + if (file->pos + chunk > *pylen) + { + if (bt_space(&file->vol->ext, 1) == -1 || + f_alloc(file) == -1) + goto fail; + } + + if (offs == 0 && chunk == HFS_BLOCKSZ) + { + if (f_putblock(file, bnum, (block *) ptr) == -1) + goto fail; + } + else + { + block b; + + if (f_getblock(file, bnum, &b) == -1) + goto fail; + + memcpy(b + offs, ptr, chunk); + + if (f_putblock(file, bnum, &b) == -1) + goto fail; + } + + ptr += chunk; + + file->pos += chunk; + count -= chunk; + + if (file->pos > *lglen) + *lglen = file->pos; + } + + return len; + +fail: + return -1; +} + +/* + * NAME: hfs->truncate() + * DESCRIPTION: truncate an open file + */ +int hfs_truncate(hfsfile *file, unsigned long len) +{ + unsigned long *lglen; + + f_getptrs(file, 0, &lglen, 0); + + if (*lglen > len) + { + if (file->vol->flags & HFS_VOL_READONLY) + ERROR(EROFS, 0); + + *lglen = len; + + file->cat.u.fil.filMdDat = d_mtime(time(0)); + file->flags |= HFS_FILE_UPDATE_CATREC; + + if (file->pos > len) + file->pos = len; + } + + return 0; + +fail: + return -1; +} + +/* + * NAME: hfs->seek() + * DESCRIPTION: change file seek pointer + */ +unsigned long hfs_seek(hfsfile *file, long offset, int from) +{ + unsigned long *lglen, newpos; + + f_getptrs(file, 0, &lglen, 0); + + switch (from) + { + case HFS_SEEK_SET: + newpos = (offset < 0) ? 0 : offset; + break; + + case HFS_SEEK_CUR: + if (offset < 0 && (unsigned long) -offset > file->pos) + newpos = 0; + else + newpos = file->pos + offset; + break; + + case HFS_SEEK_END: + if (offset < 0 && (unsigned long) -offset > *lglen) + newpos = 0; + else + newpos = *lglen + offset; + break; + + default: + ERROR(EINVAL, 0); + } + + if (newpos > *lglen) + newpos = *lglen; + + file->pos = newpos; + + return newpos; + +fail: + return -1; +} + +/* + * NAME: hfs->close() + * DESCRIPTION: close a file + */ +int hfs_close(hfsfile *file) +{ + hfsvol *vol = file->vol; + int result = 0; + + if (f_trunc(file) == -1 || + f_flush(file) == -1) + result = -1; + + if (file->prev) + file->prev->next = file->next; + if (file->next) + file->next->prev = file->prev; + if (file == vol->files) + vol->files = file->next; + + FREE(file); + + return result; +} + +/* High-Level Catalog Routines ============================================= */ + +/* + * NAME: hfs->stat() + * DESCRIPTION: return catalog information for an arbitrary path + */ +int hfs_stat(hfsvol *vol, const char *path, hfsdirent *ent) +{ + CatDataRec data; + unsigned long parid; + char name[HFS_MAX_FLEN + 1]; + + if (getvol(&vol) == -1 || + v_resolve(&vol, path, &data, &parid, name, 0) <= 0) + goto fail; + + r_unpackdirent(parid, name, &data, ent); + + return 0; + +fail: + return -1; +} + +/* + * NAME: hfs->fstat() + * DESCRIPTION: return catalog information for an open file + */ +int hfs_fstat(hfsfile *file, hfsdirent *ent) +{ + r_unpackdirent(file->parid, file->name, &file->cat, ent); + + return 0; +} + +/* + * NAME: hfs->setattr() + * DESCRIPTION: change a file's attributes + */ +int hfs_setattr(hfsvol *vol, const char *path, const hfsdirent *ent) +{ + CatDataRec data; + node n; + + if (getvol(&vol) == -1 || + v_resolve(&vol, path, &data, 0, 0, &n) <= 0) + goto fail; + + if (vol->flags & HFS_VOL_READONLY) + ERROR(EROFS, 0); + + r_packdirent(&data, ent); + + return v_putcatrec(&data, &n); + +fail: + return -1; +} + +/* + * NAME: hfs->fsetattr() + * DESCRIPTION: change an open file's attributes + */ +int hfs_fsetattr(hfsfile *file, const hfsdirent *ent) +{ + if (file->vol->flags & HFS_VOL_READONLY) + ERROR(EROFS, 0); + + r_packdirent(&file->cat, ent); + + file->flags |= HFS_FILE_UPDATE_CATREC; + + return 0; + +fail: + return -1; +} + +/* + * NAME: hfs->mkdir() + * DESCRIPTION: create a new directory + */ +int hfs_mkdir(hfsvol *vol, const char *path) +{ + CatDataRec data; + unsigned long parid; + char name[HFS_MAX_FLEN + 1]; + int found; + + if (getvol(&vol) == -1) + goto fail; + + found = v_resolve(&vol, path, &data, &parid, name, 0); + if (found == -1 || parid == 0) + goto fail; + + if (found) + ERROR(EEXIST, 0); + + if (parid == HFS_CNID_ROOTPAR) + ERROR(EINVAL, 0); + + if (vol->flags & HFS_VOL_READONLY) + ERROR(EROFS, 0); + + return v_mkdir(vol, parid, name); + +fail: + return -1; +} + +/* + * NAME: hfs->rmdir() + * DESCRIPTION: delete an empty directory + */ +int hfs_rmdir(hfsvol *vol, const char *path) +{ + CatKeyRec key; + CatDataRec data; + unsigned long parid; + char name[HFS_MAX_FLEN + 1]; + byte pkey[HFS_CATKEYLEN]; + + if (getvol(&vol) == -1 || + v_resolve(&vol, path, &data, &parid, name, 0) <= 0) + goto fail; + + if (data.cdrType != cdrDirRec) + ERROR(ENOTDIR, 0); + + if (data.u.dir.dirVal != 0) + ERROR(ENOTEMPTY, 0); + + if (parid == HFS_CNID_ROOTPAR) + ERROR(EINVAL, 0); + + if (vol->flags & HFS_VOL_READONLY) + ERROR(EROFS, 0); + + /* delete directory record */ + + r_makecatkey(&key, parid, name); + r_packcatkey(&key, pkey, 0); + + if (bt_delete(&vol->cat, pkey) == -1) + goto fail; + + /* delete thread record */ + + r_makecatkey(&key, data.u.dir.dirDirID, ""); + r_packcatkey(&key, pkey, 0); + + if (bt_delete(&vol->cat, pkey) == -1 || + v_adjvalence(vol, parid, 1, -1) == -1) + goto fail; + + return 0; + +fail: + return -1; +} + +/* + * NAME: hfs->delete() + * DESCRIPTION: remove both forks of a file + */ +int hfs_delete(hfsvol *vol, const char *path) +{ + hfsfile file; + CatKeyRec key; + byte pkey[HFS_CATKEYLEN]; + int found; + + if (getvol(&vol) == -1 || + v_resolve(&vol, path, &file.cat, &file.parid, file.name, 0) <= 0) + goto fail; + + if (file.cat.cdrType != cdrFilRec) + ERROR(EISDIR, 0); + + if (file.parid == HFS_CNID_ROOTPAR) + ERROR(EINVAL, 0); + + if (vol->flags & HFS_VOL_READONLY) + ERROR(EROFS, 0); + + /* free allocation blocks */ + + file.vol = vol; + file.flags = 0; + + file.cat.u.fil.filLgLen = 0; + file.cat.u.fil.filRLgLen = 0; + + f_selectfork(&file, fkData); + if (f_trunc(&file) == -1) + goto fail; + + f_selectfork(&file, fkRsrc); + if (f_trunc(&file) == -1) + goto fail; + + /* delete file record */ + + r_makecatkey(&key, file.parid, file.name); + r_packcatkey(&key, pkey, 0); + + if (bt_delete(&vol->cat, pkey) == -1 || + v_adjvalence(vol, file.parid, 0, -1) == -1) + goto fail; + + /* delete file thread, if any */ + + found = v_getfthread(vol, file.cat.u.fil.filFlNum, 0, 0); + if (found == -1) + goto fail; + + if (found) + { + r_makecatkey(&key, file.cat.u.fil.filFlNum, ""); + r_packcatkey(&key, pkey, 0); + + if (bt_delete(&vol->cat, pkey) == -1) + goto fail; + } + + return 0; + +fail: + return -1; +} + +/* + * NAME: hfs->rename() + * DESCRIPTION: change the name of and/or move a file or directory + */ +int hfs_rename(hfsvol *vol, const char *srcpath, const char *dstpath) +{ + hfsvol *srcvol; + CatDataRec src, dst; + unsigned long srcid, dstid; + CatKeyRec key; + char srcname[HFS_MAX_FLEN + 1], dstname[HFS_MAX_FLEN + 1]; + byte record[HFS_MAX_CATRECLEN]; + unsigned int reclen; + int found, isdir, moving; + node n; + + if (getvol(&vol) == -1 || + v_resolve(&vol, srcpath, &src, &srcid, srcname, 0) <= 0) + goto fail; + + isdir = (src.cdrType == cdrDirRec); + srcvol = vol; + + found = v_resolve(&vol, dstpath, &dst, &dstid, dstname, 0); + if (found == -1) + goto fail; + + if (vol != srcvol) + ERROR(EINVAL, "can't move across volumes"); + + if (dstid == 0) + ERROR(ENOENT, "bad destination path"); + + if (found && + dst.cdrType == cdrDirRec && + dst.u.dir.dirDirID != src.u.dir.dirDirID) + { + dstid = dst.u.dir.dirDirID; + strcpy(dstname, srcname); + + found = v_catsearch(vol, dstid, dstname, 0, 0, 0); + if (found == -1) + goto fail; + } + + moving = (srcid != dstid); + + if (found) + { + const char *ptr; + + ptr = strrchr(dstpath, ':'); + if (ptr == 0) + ptr = dstpath; + else + ++ptr; + + if (*ptr) + strcpy(dstname, ptr); + + if (! moving && strcmp(srcname, dstname) == 0) + goto done; /* source and destination are identical */ + + if (moving || d_relstring(srcname, dstname)) + ERROR(EEXIST, "can't use destination name"); + } + + /* can't move anything into the root directory's parent */ + + if (moving && dstid == HFS_CNID_ROOTPAR) + ERROR(EINVAL, "can't move above root directory"); + + if (moving && isdir) + { + unsigned long id; + + /* can't move root directory anywhere */ + + if (src.u.dir.dirDirID == HFS_CNID_ROOTDIR) + ERROR(EINVAL, "can't move root directory"); + + /* make sure we aren't trying to move a directory inside itself */ + + for (id = dstid; id != HFS_CNID_ROOTDIR; id = dst.u.dthd.thdParID) + { + if (id == src.u.dir.dirDirID) + ERROR(EINVAL, "can't move directory inside itself"); + + if (v_getdthread(vol, id, &dst, 0) <= 0) + goto fail; + } + } + + if (vol->flags & HFS_VOL_READONLY) + ERROR(EROFS, 0); + + /* change volume name */ + + if (dstid == HFS_CNID_ROOTPAR) + { + if (! validvname(dstname)) + goto fail; + + strcpy(vol->mdb.drVN, dstname); + vol->flags |= HFS_VOL_UPDATE_MDB; + } + + /* remove source record */ + + r_makecatkey(&key, srcid, srcname); + r_packcatkey(&key, record, 0); + + if (bt_delete(&vol->cat, record) == -1) + goto fail; + + /* insert destination record */ + + r_makecatkey(&key, dstid, dstname); + r_packcatrec(&key, &src, record, &reclen); + + if (bt_insert(&vol->cat, record, reclen) == -1) + goto fail; + + /* update thread record */ + + if (isdir) + { + if (v_getdthread(vol, src.u.dir.dirDirID, &dst, &n) <= 0) + goto fail; + + dst.u.dthd.thdParID = dstid; + strcpy(dst.u.dthd.thdCName, dstname); + + if (v_putcatrec(&dst, &n) == -1) + goto fail; + } + else + { + found = v_getfthread(vol, src.u.fil.filFlNum, &dst, &n); + if (found == -1) + goto fail; + + if (found) + { + dst.u.fthd.fthdParID = dstid; + strcpy(dst.u.fthd.fthdCName, dstname); + + if (v_putcatrec(&dst, &n) == -1) + goto fail; + } + } + + /* update directory valences */ + + if (moving) + { + if (v_adjvalence(vol, srcid, isdir, -1) == -1 || + v_adjvalence(vol, dstid, isdir, 1) == -1) + goto fail; + } + +done: + return 0; + +fail: + return -1; +} + +/* High-Level Media Routines =============================================== */ + +#ifdef CP_NOT_USED +/* + * NAME: hfs->zero() + * DESCRIPTION: initialize medium with new/empty DDR and partition map + */ +int hfs_zero(const char *path, unsigned int maxparts, unsigned long *blocks) +{ + hfsvol vol; + + v_init(&vol, HFS_OPT_NOCACHE); + + if (maxparts < 1) + ERROR(EINVAL, "must allow at least 1 partition"); + + if (v_open(&vol, path, HFS_MODE_RDWR) == -1 || + v_geometry(&vol, 0) == -1) + goto fail; + + if (m_zeroddr(&vol) == -1 || + m_zeropm(&vol, 1 + maxparts) == -1) + goto fail; + + if (blocks) + { + Partition map; + int found; + + found = m_findpmentry(&vol, "Apple_Free", &map, 0); + if (found == -1) + goto fail; + + if (! found) + ERROR(EIO, "unable to determine free partition space"); + + *blocks = map.pmPartBlkCnt; + } + + if (v_close(&vol) == -1) + goto fail; + + return 0; + +fail: + v_close(&vol); + return -1; +} +#endif + +#ifdef CP_NOT_USED +/* + * NAME: hfs->mkpart() + * DESCRIPTION: create a new HFS partition + */ +int hfs_mkpart(const char *path, unsigned long len) +{ + hfsvol vol; + + v_init(&vol, HFS_OPT_NOCACHE); + + if (v_open(&vol, path, HFS_MODE_RDWR) == -1) + goto fail; + + if (m_mkpart(&vol, "MacOS", "Apple_HFS", len) == -1) + goto fail; + + if (v_close(&vol) == -1) + goto fail; + + return 0; + +fail: + v_close(&vol); + return -1; +} + +/* + * NAME: hfs->nparts() + * DESCRIPTION: return the number of HFS partitions in the medium + */ +int hfs_nparts(const char *path) +{ + hfsvol vol; + int nparts, found; + Partition map; + unsigned long bnum = 0; + + v_init(&vol, HFS_OPT_NOCACHE); + + if (v_open(&vol, path, HFS_MODE_RDONLY) == -1) + goto fail; + + nparts = 0; + while (1) + { + found = m_findpmentry(&vol, "Apple_HFS", &map, &bnum); + if (found == -1) + goto fail; + + if (! found) + break; + + ++nparts; + } + + if (v_close(&vol) == -1) + goto fail; + + return nparts; + +fail: + v_close(&vol); + return -1; +} +#endif + +/* + * NAME: compare() + * DESCRIPTION: comparison function for qsort of blocks to be spared + */ +static +int compare(const unsigned int *n1, const unsigned int *n2) +{ + return *n1 - *n2; +} + +/* + * NAME: hfs->format() + * DESCRIPTION: write a new filesystem + */ +#ifdef CP_NOT_USED +int hfs_format(const char *path, int pnum, int mode, const char *vname, + unsigned int nbadblocks, const unsigned long badblocks[]) +#else +int hfs_callback_format(oscallback func, void* cookie, int mode, + const char* vname) +#endif +{ + hfsvol vol; + btree *ext = &vol.ext; + btree *cat = &vol.cat; + unsigned int i, *badalloc = 0; + + v_init(&vol, mode); + + if (! validvname(vname)) + goto fail; + +#ifdef CP_NOT_USED + if (v_open(&vol, path, HFS_MODE_RDWR) == -1 || + v_geometry(&vol, pnum) == -1) + goto fail; +#else + if (v_callback_open(&vol, func, cookie) != 0 || + v_geometry(&vol, 0) != 0) + goto fail; +#endif + + /* initialize volume geometry */ + + vol.lpa = 1 + ((vol.vlen - 6) >> 16); + + if (vol.flags & HFS_OPT_2048) + vol.lpa = (vol.lpa + 3) & ~3; + + vol.vbmsz = (vol.vlen / vol.lpa + 0x0fff) >> 12; + + vol.mdb.drSigWord = HFS_SIGWORD; + vol.mdb.drCrDate = d_mtime(time(0)); + vol.mdb.drLsMod = vol.mdb.drCrDate; + vol.mdb.drAtrb = 0; + vol.mdb.drNmFls = 0; + vol.mdb.drVBMSt = 3; + vol.mdb.drAllocPtr = 0; + + vol.mdb.drAlBlkSiz = vol.lpa << HFS_BLOCKSZ_BITS; + vol.mdb.drClpSiz = vol.mdb.drAlBlkSiz << 2; + vol.mdb.drAlBlSt = vol.mdb.drVBMSt + vol.vbmsz; + + if (vol.flags & HFS_OPT_2048) + vol.mdb.drAlBlSt = ((vol.vstart & 3) + vol.mdb.drAlBlSt + 3) & ~3; + + vol.mdb.drNmAlBlks = (vol.vlen - 2 - vol.mdb.drAlBlSt) / vol.lpa; + + vol.mdb.drNxtCNID = HFS_CNID_ROOTDIR; /* modified later */ + vol.mdb.drFreeBks = vol.mdb.drNmAlBlks; + + strcpy(vol.mdb.drVN, vname); + + vol.mdb.drVolBkUp = 0; + vol.mdb.drVSeqNum = 0; + vol.mdb.drWrCnt = 0; + + vol.mdb.drXTClpSiz = vol.mdb.drNmAlBlks / 128 * vol.mdb.drAlBlkSiz; + vol.mdb.drCTClpSiz = vol.mdb.drXTClpSiz; + + vol.mdb.drNmRtDirs = 0; + vol.mdb.drFilCnt = 0; + vol.mdb.drDirCnt = -1; /* incremented when root directory is created */ + + for (i = 0; i < 8; ++i) + vol.mdb.drFndrInfo[i] = 0; + + vol.mdb.drEmbedSigWord = 0x0000; + vol.mdb.drEmbedExtent.xdrStABN = 0; + vol.mdb.drEmbedExtent.xdrNumABlks = 0; + + /* vol.mdb.drXTFlSize */ + /* vol.mdb.drCTFlSize */ + + /* vol.mdb.drXTExtRec[0..2] */ + /* vol.mdb.drCTExtRec[0..2] */ + + vol.flags |= HFS_VOL_UPDATE_MDB | HFS_VOL_UPDATE_ALTMDB; + + /* initialize volume bitmap */ + + vol.vbm = ALLOC(block, vol.vbmsz); + if (vol.vbm == 0) + ERROR(ENOMEM, 0); + + memset(vol.vbm, 0, vol.vbmsz << HFS_BLOCKSZ_BITS); + + vol.flags |= HFS_VOL_UPDATE_VBM; + + /* perform initial bad block sparing */ + +#ifdef CP_NOT_USED + if (nbadblocks > 0) + { + if (nbadblocks * 4 > vol.vlen) + ERROR(EINVAL, "volume contains too many bad blocks"); + + badalloc = ALLOC(unsigned int, nbadblocks); + if (badalloc == 0) + ERROR(ENOMEM, 0); + + if (vol.mdb.drNmAlBlks == 1594) + vol.mdb.drFreeBks = --vol.mdb.drNmAlBlks; + + for (i = 0; i < nbadblocks; ++i) + { + unsigned long bnum; + unsigned int anum; + + bnum = badblocks[i]; + + if (bnum < vol.mdb.drAlBlSt || bnum == vol.vlen - 2) + ERROR(EINVAL, "can't spare critical bad block"); + else if (bnum >= vol.vlen) + ERROR(EINVAL, "bad block not in volume"); + + anum = (bnum - vol.mdb.drAlBlSt) / vol.lpa; + + if (anum < vol.mdb.drNmAlBlks) + BMSET(vol.vbm, anum); + + badalloc[i] = anum; + } + + vol.mdb.drAtrb |= HFS_ATRB_BBSPARED; + } +#endif + + /* create extents overflow file */ + + n_init(&ext->hdrnd, ext, ndHdrNode, 0); + + ext->hdrnd.nnum = 0; + ext->hdrnd.nd.ndNRecs = 3; + ext->hdrnd.roff[1] = 0x078; + ext->hdrnd.roff[2] = 0x0f8; + ext->hdrnd.roff[3] = 0x1f8; + + memset(HFS_NODEREC(ext->hdrnd, 1), 0, 128); + + ext->hdr.bthDepth = 0; + ext->hdr.bthRoot = 0; + ext->hdr.bthNRecs = 0; + ext->hdr.bthFNode = 0; + ext->hdr.bthLNode = 0; + ext->hdr.bthNodeSize = HFS_BLOCKSZ; + ext->hdr.bthKeyLen = 0x07; + ext->hdr.bthNNodes = 0; + ext->hdr.bthFree = 0; + for (i = 0; i < 76; ++i) + ext->hdr.bthResv[i] = 0; + + ext->map = ALLOC(byte, HFS_MAP1SZ); + if (ext->map == 0) + ERROR(ENOMEM, 0); + + memset(ext->map, 0, HFS_MAP1SZ); + BMSET(ext->map, 0); + + ext->mapsz = HFS_MAP1SZ; + ext->flags = HFS_BT_UPDATE_HDR; + + /* create catalog file */ + + n_init(&cat->hdrnd, cat, ndHdrNode, 0); + + cat->hdrnd.nnum = 0; + cat->hdrnd.nd.ndNRecs = 3; + cat->hdrnd.roff[1] = 0x078; + cat->hdrnd.roff[2] = 0x0f8; + cat->hdrnd.roff[3] = 0x1f8; + + memset(HFS_NODEREC(cat->hdrnd, 1), 0, 128); + + cat->hdr.bthDepth = 0; + cat->hdr.bthRoot = 0; + cat->hdr.bthNRecs = 0; + cat->hdr.bthFNode = 0; + cat->hdr.bthLNode = 0; + cat->hdr.bthNodeSize = HFS_BLOCKSZ; + cat->hdr.bthKeyLen = 0x25; + cat->hdr.bthNNodes = 0; + cat->hdr.bthFree = 0; + for (i = 0; i < 76; ++i) + cat->hdr.bthResv[i] = 0; + + cat->map = ALLOC(byte, HFS_MAP1SZ); + if (cat->map == 0) + ERROR(ENOMEM, 0); + + memset(cat->map, 0, HFS_MAP1SZ); + BMSET(cat->map, 0); + + cat->mapsz = HFS_MAP1SZ; + cat->flags = HFS_BT_UPDATE_HDR; + + /* allocate space for header nodes (and initial extents) */ + + if (bt_space(ext, 1) == -1 || + bt_space(cat, 1) == -1) + goto fail; + + --ext->hdr.bthFree; + --cat->hdr.bthFree; + + /* create extent records for bad blocks */ + +#ifdef CP_NOT_USED + if (nbadblocks > 0) + { + hfsfile bbfile; + ExtDescriptor extent; + ExtDataRec *extrec; + ExtKeyRec key; + byte record[HFS_MAX_EXTRECLEN]; + unsigned int reclen; + + f_init(&bbfile, &vol, HFS_CNID_BADALLOC, "bad blocks"); + + qsort(badalloc, nbadblocks, sizeof(*badalloc), + (int (*)(const void *, const void *)) compare); + + for (i = 0; i < nbadblocks; ++i) + { + if (i == 0 || badalloc[i] != extent.xdrStABN) + { + extent.xdrStABN = badalloc[i]; + extent.xdrNumABlks = 1; + + if (extent.xdrStABN < vol.mdb.drNmAlBlks && + f_addextent(&bbfile, &extent) == -1) + goto fail; + } + } + + /* flush local extents into extents overflow file */ + + f_getptrs(&bbfile, &extrec, 0, 0); + + r_makeextkey(&key, bbfile.fork, bbfile.cat.u.fil.filFlNum, 0); + r_packextrec(&key, extrec, record, &reclen); + + if (bt_insert(&vol.ext, record, reclen) == -1) + goto fail; + } +#endif + + vol.flags |= HFS_VOL_MOUNTED; + + /* create root directory */ + + if (v_mkdir(&vol, HFS_CNID_ROOTPAR, vname) == -1) + goto fail; + + vol.mdb.drNxtCNID = 16; /* first CNID not reserved by Apple */ + + /* write boot blocks */ + + if (m_zerobb(&vol) == -1) + goto fail; + + /* zero other unused space, if requested */ + + if (vol.flags & HFS_OPT_ZERO) + { + block b; + unsigned long bnum; + + memset(&b, 0, sizeof(b)); + + /* between MDB and VBM (never) */ + + for (bnum = 3; bnum < vol.mdb.drVBMSt; ++bnum) + b_writelb(&vol, bnum, &b); + + /* between VBM and first allocation block (sometimes if HFS_OPT_2048) */ + + for (bnum = vol.mdb.drVBMSt + vol.vbmsz; bnum < vol.mdb.drAlBlSt; ++bnum) + b_writelb(&vol, bnum, &b); + + /* between last allocation block and alternate MDB (sometimes) */ + + for (bnum = vol.mdb.drAlBlSt + vol.mdb.drNmAlBlks * vol.lpa; + bnum < vol.vlen - 2; ++bnum) + b_writelb(&vol, bnum, &b); + + /* final block (always) */ + + b_writelb(&vol, vol.vlen - 1, &b); + } + + /* flush remaining state and close volume */ + + if (v_close(&vol) == -1) + goto fail; + + FREE(badalloc); + + return 0; + +fail: + v_close(&vol); + + FREE(badalloc); + + return -1; +} + diff --git a/diskimg/libhfs/hfs.h b/diskimg/libhfs/hfs.h new file mode 100644 index 0000000..e0b9499 --- /dev/null +++ b/diskimg/libhfs/hfs.h @@ -0,0 +1,201 @@ +/* + * libhfs - library for reading and writing Macintosh HFS volumes + * Copyright (C) 1996-1998 Robert Leslie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + * + * $Id$ + */ + +#ifdef __cplusplus +extern "C" { +#endif + +# include + +# define HFS_BLOCKSZ 512 +# define HFS_BLOCKSZ_BITS 9 + +# define HFS_MAX_FLEN 31 +# define HFS_MAX_VLEN 27 + +typedef struct _hfsvol_ hfsvol; +typedef struct _hfsfile_ hfsfile; +typedef struct _hfsdir_ hfsdir; + +typedef struct { + char name[HFS_MAX_VLEN + 1]; /* name of volume (MacOS Standard Roman) */ + int flags; /* volume flags */ + + unsigned long totbytes; /* total bytes on volume */ + unsigned long freebytes; /* free bytes on volume */ + + unsigned long alblocksz; /* volume allocation block size */ + unsigned long clumpsz; /* default file clump size */ + + unsigned long numfiles; /* number of files in volume */ + unsigned long numdirs; /* number of directories in volume */ + + time_t crdate; /* volume creation date */ + time_t mddate; /* last volume modification date */ + time_t bkdate; /* last volume backup date */ + + unsigned long blessed; /* CNID of MacOS System Folder */ +} hfsvolent; + +typedef struct { + char name[HFS_MAX_FLEN + 1]; /* catalog name (MacOS Standard Roman) */ + int flags; /* bit flags */ + unsigned long cnid; /* catalog node id (CNID) */ + unsigned long parid; /* CNID of parent directory */ + + time_t crdate; /* date of creation */ + time_t mddate; /* date of last modification */ + time_t bkdate; /* date of last backup */ + + short fdflags; /* Macintosh Finder flags */ + + struct { + signed short v; /* Finder icon vertical coordinate */ + signed short h; /* horizontal coordinate */ + } fdlocation; + + union { + struct { + unsigned long dsize; /* size of data fork */ + unsigned long rsize; /* size of resource fork */ + + char type[5]; /* file type code (plus null) */ + char creator[5]; /* file creator code (plus null) */ + } file; + + struct { + unsigned short valence; /* number of items in directory */ + + struct { + signed short top; /* top edge of folder's rectangle */ + signed short left; /* left edge */ + signed short bottom; /* bottom edge */ + signed short right; /* right edge */ + } rect; + } dir; + } u; +} hfsdirent; + +# define HFS_ISDIR 0x0001 +# define HFS_ISLOCKED 0x0002 + +# define HFS_CNID_ROOTPAR 1 +# define HFS_CNID_ROOTDIR 2 +# define HFS_CNID_EXT 3 +# define HFS_CNID_CAT 4 +# define HFS_CNID_BADALLOC 5 + +# define HFS_FNDR_ISONDESK (1 << 0) +# define HFS_FNDR_COLOR 0x0e +# define HFS_FNDR_COLORRESERVED (1 << 4) +# define HFS_FNDR_REQUIRESSWITCHLAUNCH (1 << 5) +# define HFS_FNDR_ISSHARED (1 << 6) +# define HFS_FNDR_HASNOINITS (1 << 7) +# define HFS_FNDR_HASBEENINITED (1 << 8) +# define HFS_FNDR_RESERVED (1 << 9) +# define HFS_FNDR_HASCUSTOMICON (1 << 10) +# define HFS_FNDR_ISSTATIONERY (1 << 11) +# define HFS_FNDR_NAMELOCKED (1 << 12) +# define HFS_FNDR_HASBUNDLE (1 << 13) +# define HFS_FNDR_ISINVISIBLE (1 << 14) +# define HFS_FNDR_ISALIAS (1 << 15) + +extern const char *hfs_error; +extern const unsigned char hfs_charorder[]; + +# define HFS_MODE_RDONLY 0 +# define HFS_MODE_RDWR 1 +# define HFS_MODE_ANY 2 + +# define HFS_MODE_MASK 0x0003 + +# define HFS_OPT_NOCACHE 0x0100 +# define HFS_OPT_2048 0x0200 +# define HFS_OPT_ZERO 0x0400 + +# define HFS_SEEK_SET 0 +# define HFS_SEEK_CUR 1 +# define HFS_SEEK_END 2 + +#ifdef CP_NO_STATIC +hfsvol *hfs_mount(const char *, int, int); +#endif +int hfs_flush(hfsvol *); +#ifdef CP_NO_STATIC +void hfs_flushall(void); +int hfs_umount(hfsvol *); +void hfs_umountall(void); +hfsvol *hfs_getvol(const char *); +void hfs_setvol(hfsvol *); +#endif + +int hfs_vstat(hfsvol *, hfsvolent *); +int hfs_vsetattr(hfsvol *, hfsvolent *); + +int hfs_chdir(hfsvol *, const char *); +unsigned long hfs_getcwd(hfsvol *); +int hfs_setcwd(hfsvol *, unsigned long); +int hfs_dirinfo(hfsvol *, unsigned long *, char *); + +hfsdir *hfs_opendir(hfsvol *, const char *); +int hfs_readdir(hfsdir *, hfsdirent *); +int hfs_closedir(hfsdir *); + +hfsfile *hfs_create(hfsvol *, const char *, const char *, const char *); +hfsfile *hfs_open(hfsvol *, const char *); +int hfs_setfork(hfsfile *, int); +int hfs_getfork(hfsfile *); +unsigned long hfs_read(hfsfile *, void *, unsigned long); +unsigned long hfs_write(hfsfile *, const void *, unsigned long); +int hfs_truncate(hfsfile *, unsigned long); +unsigned long hfs_seek(hfsfile *, long, int); +int hfs_close(hfsfile *); + +int hfs_stat(hfsvol *, const char *, hfsdirent *); +int hfs_fstat(hfsfile *, hfsdirent *); +int hfs_setattr(hfsvol *, const char *, const hfsdirent *); +int hfs_fsetattr(hfsfile *, const hfsdirent *); + +int hfs_mkdir(hfsvol *, const char *); +int hfs_rmdir(hfsvol *, const char *); + +int hfs_delete(hfsvol *, const char *); +int hfs_rename(hfsvol *, const char *, const char *); + +int hfs_zero(const char *, unsigned int, unsigned long *); +int hfs_mkpart(const char *, unsigned long); +int hfs_nparts(const char *); + +int hfs_format(const char *, int, int, + const char *, unsigned int, const unsigned long []); + +/* CiderPress callback interface */ +enum { HFS_CB_VOLSIZE, HFS_CB_READ, HFS_CB_WRITE, HFS_CB_SEEK }; +typedef unsigned long (*oscallback)(void* cookie, int op, unsigned long arg1, + void* arg2); +hfsvol* hfs_callback_open(oscallback func, void* cookie, int mode); +int hfs_callback_close(hfsvol* vol); +int hfs_callback_format(oscallback func, void* cookie, int mode, + const char* vname); + +#ifdef __cplusplus +}; +#endif diff --git a/diskimg/libhfs/libhfs.dsp b/diskimg/libhfs/libhfs.dsp new file mode 100644 index 0000000..4b82514 --- /dev/null +++ b/diskimg/libhfs/libhfs.dsp @@ -0,0 +1,200 @@ +# Microsoft Developer Studio Project File - Name="libhfs" - Package Owner=<4> +# Microsoft Developer Studio Generated Build File, Format Version 6.00 +# ** DO NOT EDIT ** + +# TARGTYPE "Win32 (x86) Static Library" 0x0104 + +CFG=libhfs - Win32 Debug +!MESSAGE This is not a valid makefile. To build this project using NMAKE, +!MESSAGE use the Export Makefile command and run +!MESSAGE +!MESSAGE NMAKE /f "libhfs.mak". +!MESSAGE +!MESSAGE You can specify a configuration when running NMAKE +!MESSAGE by defining the macro CFG on the command line. For example: +!MESSAGE +!MESSAGE NMAKE /f "libhfs.mak" CFG="libhfs - Win32 Debug" +!MESSAGE +!MESSAGE Possible choices for configuration are: +!MESSAGE +!MESSAGE "libhfs - Win32 Release" (based on "Win32 (x86) Static Library") +!MESSAGE "libhfs - Win32 Debug" (based on "Win32 (x86) Static Library") +!MESSAGE + +# Begin Project +# PROP AllowPerConfigDependencies 0 +# PROP Scc_ProjName "" +# PROP Scc_LocalPath "" +CPP=cl.exe +RSC=rc.exe + +!IF "$(CFG)" == "libhfs - Win32 Release" + +# PROP BASE Use_MFC 0 +# PROP BASE Use_Debug_Libraries 0 +# PROP BASE Output_Dir "Release" +# PROP BASE Intermediate_Dir "Release" +# PROP BASE Target_Dir "" +# PROP Use_MFC 0 +# PROP Use_Debug_Libraries 0 +# PROP Output_Dir "Release" +# PROP Intermediate_Dir "Release" +# PROP Target_Dir "" +# ADD BASE CPP /nologo /W3 /GX /O2 /D "WIN32" /D "NDEBUG" /D "_MBCS" /D "_LIB" /YX /FD /c +# ADD CPP /nologo /MD /W2 /GX /O2 /D "WIN32" /D "NDEBUG" /D "_MBCS" /D "_LIB" /D "HAVE_CONFIG_H" /YX /FD /c +# ADD BASE RSC /l 0x409 /d "NDEBUG" +# ADD RSC /l 0x409 /d "NDEBUG" +BSC32=bscmake.exe +# ADD BASE BSC32 /nologo +# ADD BSC32 /nologo +LIB32=link.exe -lib +# ADD BASE LIB32 /nologo +# ADD LIB32 /nologo + +!ELSEIF "$(CFG)" == "libhfs - Win32 Debug" + +# PROP BASE Use_MFC 0 +# PROP BASE Use_Debug_Libraries 1 +# PROP BASE Output_Dir "Debug" +# PROP BASE Intermediate_Dir "Debug" +# PROP BASE Target_Dir "" +# PROP Use_MFC 0 +# PROP Use_Debug_Libraries 1 +# PROP Output_Dir "Debug" +# PROP Intermediate_Dir "Debug" +# PROP Target_Dir "" +# ADD BASE CPP /nologo /W3 /Gm /GX /ZI /Od /D "WIN32" /D "_DEBUG" /D "_MBCS" /D "_LIB" /YX /FD /GZ /c +# ADD CPP /nologo /MDd /W2 /Gm /GX /ZI /Od /D "WIN32" /D "_DEBUG" /D "_MBCS" /D "_LIB" /D "HAVE_CONFIG_H" /FR /YX /FD /GZ /c +# ADD BASE RSC /l 0x409 /d "_DEBUG" +# ADD RSC /l 0x409 /d "_DEBUG" +BSC32=bscmake.exe +# ADD BASE BSC32 /nologo +# ADD BSC32 /nologo +LIB32=link.exe -lib +# ADD BASE LIB32 /nologo +# ADD LIB32 /nologo + +!ENDIF + +# Begin Target + +# Name "libhfs - Win32 Release" +# Name "libhfs - Win32 Debug" +# Begin Group "Source Files" + +# PROP Default_Filter "cpp;c;cxx;rc;def;r;odl;idl;hpj;bat" +# Begin Source File + +SOURCE=.\block.c +# End Source File +# Begin Source File + +SOURCE=.\btree.c +# End Source File +# Begin Source File + +SOURCE=.\data.c +# End Source File +# Begin Source File + +SOURCE=.\file.c +# End Source File +# Begin Source File + +SOURCE=.\hfs.c +# End Source File +# Begin Source File + +SOURCE=.\low.c +# End Source File +# Begin Source File + +SOURCE=.\medium.c +# End Source File +# Begin Source File + +SOURCE=.\node.c +# End Source File +# Begin Source File + +SOURCE=.\os.c +# End Source File +# Begin Source File + +SOURCE=.\record.c +# End Source File +# Begin Source File + +SOURCE=.\version.c +# End Source File +# Begin Source File + +SOURCE=.\volume.c +# End Source File +# End Group +# Begin Group "Header Files" + +# PROP Default_Filter "h;hpp;hxx;hm;inl" +# Begin Source File + +SOURCE=.\apple.h +# End Source File +# Begin Source File + +SOURCE=.\block.h +# End Source File +# Begin Source File + +SOURCE=.\btree.h +# End Source File +# Begin Source File + +SOURCE=.\config.h +# End Source File +# Begin Source File + +SOURCE=.\data.h +# End Source File +# Begin Source File + +SOURCE=.\file.h +# End Source File +# Begin Source File + +SOURCE=.\hfs.h +# End Source File +# Begin Source File + +SOURCE=.\libhfs.h +# End Source File +# Begin Source File + +SOURCE=.\low.h +# End Source File +# Begin Source File + +SOURCE=.\medium.h +# End Source File +# Begin Source File + +SOURCE=.\node.h +# End Source File +# Begin Source File + +SOURCE=.\os.h +# End Source File +# Begin Source File + +SOURCE=.\record.h +# End Source File +# Begin Source File + +SOURCE=.\version.h +# End Source File +# Begin Source File + +SOURCE=.\volume.h +# End Source File +# End Group +# End Target +# End Project diff --git a/diskimg/libhfs/libhfs.h b/diskimg/libhfs/libhfs.h new file mode 100644 index 0000000..b9131a1 --- /dev/null +++ b/diskimg/libhfs/libhfs.h @@ -0,0 +1,227 @@ +/* + * libhfs - library for reading and writing Macintosh HFS volumes + * Copyright (C) 1996-1998 Robert Leslie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + * + * $Id$ + */ + +# include "hfs.h" +# include "apple.h" + +//extern int errno; +# include + +# define ERROR(code, str) \ + do { hfs_error = (str), errno = (code); goto fail; } while (0) + +# ifdef DEBUG +# define ASSERT(cond) do { if (! (cond)) abort(); } while (0) +# else +# define ASSERT(cond) /* nothing */ +# endif + +# define SIZE(type, n) ((size_t) (sizeof(type) * (n))) +# define ALLOC(type, n) ((type *) malloc(SIZE(type, n))) +# define ALLOCX(type, n) ((n) ? ALLOC(type, n) : (type *) 0) +# define FREE(ptr) ((ptr) ? (void) free((void *) ptr) : (void) 0) + +# define REALLOC(ptr, type, n) \ + ((type *) ((ptr) ? realloc(ptr, SIZE(type, n)) : malloc(SIZE(type, n)))) +# define REALLOCX(ptr, type, n) \ + ((n) ? REALLOC(ptr, type, n) : (FREE(ptr), (type *) 0)) + +# define BMTST(bm, num) \ + (((const byte *) (bm))[(num) >> 3] & (0x80 >> ((num) & 0x07))) +# define BMSET(bm, num) \ + (((byte *) (bm))[(num) >> 3] |= (0x80 >> ((num) & 0x07))) +# define BMCLR(bm, num) \ + (((byte *) (bm))[(num) >> 3] &= ~(0x80 >> ((num) & 0x07))) + +# define STRINGIZE(x) #x +# define STR(x) STRINGIZE(x) + +typedef unsigned char byte; +typedef byte block[HFS_BLOCKSZ]; + +typedef struct _bucket_ { + int flags; /* bit flags */ + unsigned int count; /* number of times this block is requested */ + + unsigned long bnum; /* logical block number */ + block *data; /* pointer to block contents */ + + struct _bucket_ *cnext; /* next bucket in cache chain */ + struct _bucket_ *cprev; /* previous bucket in cache chain */ + + struct _bucket_ *hnext; /* next bucket in hash chain */ + struct _bucket_ **hprev; /* previous bucket's pointer to this bucket */ +} bucket; + +# define HFS_BUCKET_INUSE 0x01 +# define HFS_BUCKET_DIRTY 0x02 + +# define HFS_CACHESZ 128 +# define HFS_HASHSZ 32 +# define HFS_BLOCKBUFSZ 16 + +typedef struct { + struct _hfsvol_ *vol; /* volume to which cache belongs */ + bucket *tail; /* end of bucket chain */ + + unsigned int hits; /* number of cache hits */ + unsigned int misses; /* number of cache misses */ + + bucket chain[HFS_CACHESZ]; /* cache bucket chain */ + bucket *hash[HFS_HASHSZ]; /* hash table for bucket chain */ + + block pool[HFS_CACHESZ]; /* physical blocks in cache */ +} bcache; + +# define HFS_MAP1SZ 256 +# define HFS_MAPXSZ 492 + +# define HFS_NODEREC(nd, rnum) ((nd).data + (nd).roff[rnum]) +# define HFS_RECLEN(nd, rnum) ((nd).roff[(rnum) + 1] - (nd).roff[rnum]) + +# define HFS_RECKEYLEN(ptr) (*(const byte *) (ptr)) +# define HFS_RECKEYSKIP(ptr) ((size_t) ((1 + HFS_RECKEYLEN(ptr) + 1) & ~1)) +# define HFS_RECDATA(ptr) ((ptr) + HFS_RECKEYSKIP(ptr)) + +# define HFS_SETKEYLEN(ptr, x) (*(byte *) (ptr) = (x)) + +# define HFS_CATDATALEN sizeof(CatDataRec) +# define HFS_EXTDATALEN sizeof(ExtDataRec) +# define HFS_MAX_DATALEN (HFS_CATDATALEN > HFS_EXTDATALEN ? \ + HFS_CATDATALEN : HFS_EXTDATALEN) + +# define HFS_CATKEYLEN sizeof(CatKeyRec) +# define HFS_EXTKEYLEN sizeof(ExtKeyRec) +# define HFS_MAX_KEYLEN (HFS_CATKEYLEN > HFS_EXTKEYLEN ? \ + HFS_CATKEYLEN : HFS_EXTKEYLEN) + +# define HFS_MAX_CATRECLEN (HFS_CATKEYLEN + HFS_CATDATALEN) +# define HFS_MAX_EXTRECLEN (HFS_EXTKEYLEN + HFS_EXTDATALEN) +# define HFS_MAX_RECLEN (HFS_MAX_KEYLEN + HFS_MAX_DATALEN) + +# define HFS_SIGWORD 0x4244 +# define HFS_SIGWORD_MFS ((Integer) 0xd2d7) + +# define HFS_ATRB_BUSY (1 << 6) +# define HFS_ATRB_HLOCKED (1 << 7) +# define HFS_ATRB_UMOUNTED (1 << 8) +# define HFS_ATRB_BBSPARED (1 << 9) +# define HFS_ATRB_BVINCONSIS (1 << 11) +# define HFS_ATRB_COPYPROT (1 << 14) +# define HFS_ATRB_SLOCKED (1 << 15) + +struct _hfsfile_ { + struct _hfsvol_ *vol; /* pointer to volume descriptor */ + unsigned long parid; /* parent directory ID of this file */ + char name[HFS_MAX_FLEN + 1]; /* catalog name of this file */ + CatDataRec cat; /* catalog information */ + ExtDataRec ext; /* current extent record */ + unsigned int fabn; /* starting file allocation block number */ + int fork; /* current selected fork for I/O */ + unsigned long pos; /* current file seek pointer */ + int flags; /* bit flags */ + + struct _hfsfile_ *prev; + struct _hfsfile_ *next; +}; + +# define HFS_FILE_UPDATE_CATREC 0x01 + +# define HFS_MAX_NRECS 35 /* maximum based on minimum record size */ + +typedef struct _node_ { + struct _btree_ *bt; /* btree to which this node belongs */ + unsigned long nnum; /* node index */ + NodeDescriptor nd; /* node descriptor */ + int rnum; /* current record index */ + UInteger roff[HFS_MAX_NRECS + 1]; + /* record offsets */ + block data; /* raw contents of node */ +} node; + +struct _hfsdir_ { + struct _hfsvol_ *vol; /* associated volume */ + unsigned long dirid; /* directory ID of interest (or 0) */ + + node n; /* current B*-tree node */ + struct _hfsvol_ *vptr; /* current volume pointer */ + + struct _hfsdir_ *prev; + struct _hfsdir_ *next; +}; + +typedef void (*keyunpackfunc)(const byte *, void *); +typedef int (*keycomparefunc)(const void *, const void *); + +typedef struct _btree_ { + hfsfile f; /* subset file information */ + node hdrnd; /* header node */ + BTHdrRec hdr; /* header record */ + byte *map; /* usage bitmap */ + unsigned long mapsz; /* number of bytes in bitmap */ + int flags; /* bit flags */ + + keyunpackfunc keyunpack; /* key unpacking function */ + keycomparefunc keycompare; /* key comparison function */ +} btree; + +# define HFS_BT_UPDATE_HDR 0x01 + +struct _hfsvol_ { + void *priv; /* OS-dependent private descriptor data */ + int flags; /* bit flags */ + + int pnum; /* ordinal HFS partition number */ + unsigned long vstart; /* logical block offset to start of volume */ + unsigned long vlen; /* number of logical blocks in volume */ + unsigned int lpa; /* number of logical blocks per allocation block */ + + bcache *cache; /* cache of recently used blocks */ + + MDB mdb; /* master directory block */ + block *vbm; /* volume bitmap */ + unsigned short vbmsz; /* number of blocks in bitmap */ + + btree ext; /* B*-tree control block for extents overflow file */ + btree cat; /* B*-tree control block for catalog file */ + + unsigned long cwd; /* directory id of current working directory */ + + int refs; /* number of external references to this volume */ + hfsfile *files; /* list of open files */ + hfsdir *dirs; /* list of open directories */ + + struct _hfsvol_ *prev; + struct _hfsvol_ *next; +}; + +# define HFS_VOL_OPEN 0x0001 +# define HFS_VOL_MOUNTED 0x0002 +# define HFS_VOL_READONLY 0x0004 +# define HFS_VOL_USINGCACHE 0x0008 + +# define HFS_VOL_UPDATE_MDB 0x0010 +# define HFS_VOL_UPDATE_ALTMDB 0x0020 +# define HFS_VOL_UPDATE_VBM 0x0040 + +# define HFS_VOL_OPT_MASK 0xff00 + +extern hfsvol *hfs_mounts; diff --git a/diskimg/libhfs/libhfs.vcproj b/diskimg/libhfs/libhfs.vcproj new file mode 100644 index 0000000..fb852bd --- /dev/null +++ b/diskimg/libhfs/libhfs.vcproj @@ -0,0 +1,402 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/diskimg/libhfs/low.c b/diskimg/libhfs/low.c new file mode 100644 index 0000000..a5c2727 --- /dev/null +++ b/diskimg/libhfs/low.c @@ -0,0 +1,470 @@ +/* + * libhfs - library for reading and writing Macintosh HFS volumes + * Copyright (C) 1996-1998 Robert Leslie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + * + * $Id$ + */ + +# ifdef HAVE_CONFIG_H +# include "config.h" +# endif + +# include +# include +# include + +# include "libhfs.h" +# include "low.h" +# include "data.h" +# include "block.h" +# include "file.h" + +/* + * NAME: low->getddr() + * DESCRIPTION: read a driver descriptor record + */ +int l_getddr(hfsvol *vol, Block0 *ddr) +{ + block b; + const byte *ptr = b; + int i; + + if (b_readpb(vol, 0, &b, 1) == -1) + goto fail; + + d_fetchsw(&ptr, &ddr->sbSig); + d_fetchsw(&ptr, &ddr->sbBlkSize); + d_fetchsl(&ptr, &ddr->sbBlkCount); + d_fetchsw(&ptr, &ddr->sbDevType); + d_fetchsw(&ptr, &ddr->sbDevId); + d_fetchsl(&ptr, &ddr->sbData); + d_fetchsw(&ptr, &ddr->sbDrvrCount); + d_fetchsl(&ptr, &ddr->ddBlock); + d_fetchsw(&ptr, &ddr->ddSize); + d_fetchsw(&ptr, &ddr->ddType); + + for (i = 0; i < 243; ++i) + d_fetchsw(&ptr, &ddr->ddPad[i]); + + ASSERT(ptr - b == HFS_BLOCKSZ); + + return 0; + +fail: + return -1; +} + +/* + * NAME: low->putddr() + * DESCRIPTION: write a driver descriptor record + */ +int l_putddr(hfsvol *vol, const Block0 *ddr) +{ + block b; + byte *ptr = b; + int i; + + d_storesw(&ptr, ddr->sbSig); + d_storesw(&ptr, ddr->sbBlkSize); + d_storesl(&ptr, ddr->sbBlkCount); + d_storesw(&ptr, ddr->sbDevType); + d_storesw(&ptr, ddr->sbDevId); + d_storesl(&ptr, ddr->sbData); + d_storesw(&ptr, ddr->sbDrvrCount); + d_storesl(&ptr, ddr->ddBlock); + d_storesw(&ptr, ddr->ddSize); + d_storesw(&ptr, ddr->ddType); + + for (i = 0; i < 243; ++i) + d_storesw(&ptr, ddr->ddPad[i]); + + ASSERT(ptr - b == HFS_BLOCKSZ); + + if (b_writepb(vol, 0, &b, 1) == -1) + goto fail; + + return 0; + +fail: + return -1; +} + +/* + * NAME: low->getpmentry() + * DESCRIPTION: read a partition map entry + */ +int l_getpmentry(hfsvol *vol, Partition *map, unsigned long bnum) +{ + block b; + const byte *ptr = b; + int i; + + if (b_readpb(vol, bnum, &b, 1) == -1) + goto fail; + + d_fetchsw(&ptr, &map->pmSig); + d_fetchsw(&ptr, &map->pmSigPad); + d_fetchsl(&ptr, &map->pmMapBlkCnt); + d_fetchsl(&ptr, &map->pmPyPartStart); + d_fetchsl(&ptr, &map->pmPartBlkCnt); + + strncpy((char *) map->pmPartName, (const char *) ptr, 32); + map->pmPartName[32] = 0; + ptr += 32; + + strncpy((char *) map->pmParType, (const char *) ptr, 32); + map->pmParType[32] = 0; + ptr += 32; + + d_fetchsl(&ptr, &map->pmLgDataStart); + d_fetchsl(&ptr, &map->pmDataCnt); + d_fetchsl(&ptr, &map->pmPartStatus); + d_fetchsl(&ptr, &map->pmLgBootStart); + d_fetchsl(&ptr, &map->pmBootSize); + d_fetchsl(&ptr, &map->pmBootAddr); + d_fetchsl(&ptr, &map->pmBootAddr2); + d_fetchsl(&ptr, &map->pmBootEntry); + d_fetchsl(&ptr, &map->pmBootEntry2); + d_fetchsl(&ptr, &map->pmBootCksum); + + strncpy((char *) map->pmProcessor, (const char *) ptr, 16); + map->pmProcessor[16] = 0; + ptr += 16; + + for (i = 0; i < 188; ++i) + d_fetchsw(&ptr, &map->pmPad[i]); + + ASSERT(ptr - b == HFS_BLOCKSZ); + + return 0; + +fail: + return -1; +} + +/* + * NAME: low->putpmentry() + * DESCRIPTION: write a partition map entry + */ +int l_putpmentry(hfsvol *vol, const Partition *map, unsigned long bnum) +{ + block b; + byte *ptr = b; + int i; + + d_storesw(&ptr, map->pmSig); + d_storesw(&ptr, map->pmSigPad); + d_storesl(&ptr, map->pmMapBlkCnt); + d_storesl(&ptr, map->pmPyPartStart); + d_storesl(&ptr, map->pmPartBlkCnt); + + memset(ptr, 0, 32); + strncpy((char *) ptr, (const char *) map->pmPartName, 32); + ptr += 32; + + memset(ptr, 0, 32); + strncpy((char *) ptr, (const char *) map->pmParType, 32); + ptr += 32; + + d_storesl(&ptr, map->pmLgDataStart); + d_storesl(&ptr, map->pmDataCnt); + d_storesl(&ptr, map->pmPartStatus); + d_storesl(&ptr, map->pmLgBootStart); + d_storesl(&ptr, map->pmBootSize); + d_storesl(&ptr, map->pmBootAddr); + d_storesl(&ptr, map->pmBootAddr2); + d_storesl(&ptr, map->pmBootEntry); + d_storesl(&ptr, map->pmBootEntry2); + d_storesl(&ptr, map->pmBootCksum); + + memset(ptr, 0, 16); + strncpy((char *) ptr, (const char *) map->pmProcessor, 16); + ptr += 16; + + for (i = 0; i < 188; ++i) + d_storesw(&ptr, map->pmPad[i]); + + ASSERT(ptr - b == HFS_BLOCKSZ); + + if (b_writepb(vol, bnum, &b, 1) == -1) + goto fail; + + return 0; + +fail: + return -1; +} + +/* + * NAME: low->getbb() + * DESCRIPTION: read a volume's boot blocks + */ +int l_getbb(hfsvol *vol, BootBlkHdr *bb, byte *bootcode) +{ + block b; + const byte *ptr = b; + + if (b_readlb(vol, 0, &b) == -1) + goto fail; + + d_fetchsw(&ptr, &bb->bbID); + d_fetchsl(&ptr, &bb->bbEntry); + d_fetchsw(&ptr, &bb->bbVersion); + d_fetchsw(&ptr, &bb->bbPageFlags); + + d_fetchstr(&ptr, bb->bbSysName, sizeof(bb->bbSysName)); + d_fetchstr(&ptr, bb->bbShellName, sizeof(bb->bbShellName)); + d_fetchstr(&ptr, bb->bbDbg1Name, sizeof(bb->bbDbg1Name)); + d_fetchstr(&ptr, bb->bbDbg2Name, sizeof(bb->bbDbg2Name)); + d_fetchstr(&ptr, bb->bbScreenName, sizeof(bb->bbScreenName)); + d_fetchstr(&ptr, bb->bbHelloName, sizeof(bb->bbHelloName)); + d_fetchstr(&ptr, bb->bbScrapName, sizeof(bb->bbScrapName)); + + d_fetchsw(&ptr, &bb->bbCntFCBs); + d_fetchsw(&ptr, &bb->bbCntEvts); + d_fetchsl(&ptr, &bb->bb128KSHeap); + d_fetchsl(&ptr, &bb->bb256KSHeap); + d_fetchsl(&ptr, &bb->bbSysHeapSize); + d_fetchsw(&ptr, &bb->filler); + d_fetchsl(&ptr, &bb->bbSysHeapExtra); + d_fetchsl(&ptr, &bb->bbSysHeapFract); + + ASSERT(ptr - b == 148); + + if (bootcode) + { + memcpy(bootcode, ptr, HFS_BOOTCODE1LEN); + + if (b_readlb(vol, 1, &b) == -1) + goto fail; + + memcpy(bootcode + HFS_BOOTCODE1LEN, b, HFS_BOOTCODE2LEN); + } + + return 0; + +fail: + return -1; +} + +/* + * NAME: low->putbb() + * DESCRIPTION: write a volume's boot blocks + */ +int l_putbb(hfsvol *vol, const BootBlkHdr *bb, const byte *bootcode) +{ + block b; + byte *ptr = b; + + d_storesw(&ptr, bb->bbID); + d_storesl(&ptr, bb->bbEntry); + d_storesw(&ptr, bb->bbVersion); + d_storesw(&ptr, bb->bbPageFlags); + + d_storestr(&ptr, bb->bbSysName, sizeof(bb->bbSysName)); + d_storestr(&ptr, bb->bbShellName, sizeof(bb->bbShellName)); + d_storestr(&ptr, bb->bbDbg1Name, sizeof(bb->bbDbg1Name)); + d_storestr(&ptr, bb->bbDbg2Name, sizeof(bb->bbDbg2Name)); + d_storestr(&ptr, bb->bbScreenName, sizeof(bb->bbScreenName)); + d_storestr(&ptr, bb->bbHelloName, sizeof(bb->bbHelloName)); + d_storestr(&ptr, bb->bbScrapName, sizeof(bb->bbScrapName)); + + d_storesw(&ptr, bb->bbCntFCBs); + d_storesw(&ptr, bb->bbCntEvts); + d_storesl(&ptr, bb->bb128KSHeap); + d_storesl(&ptr, bb->bb256KSHeap); + d_storesl(&ptr, bb->bbSysHeapSize); + d_storesw(&ptr, bb->filler); + d_storesl(&ptr, bb->bbSysHeapExtra); + d_storesl(&ptr, bb->bbSysHeapFract); + + ASSERT(ptr - b == 148); + + if (bootcode) + memcpy(ptr, bootcode, HFS_BOOTCODE1LEN); + else + memset(ptr, 0, HFS_BOOTCODE1LEN); + + if (b_writelb(vol, 0, &b) == -1) + goto fail; + + if (bootcode) + memcpy(&b, bootcode + HFS_BOOTCODE1LEN, HFS_BOOTCODE2LEN); + else + memset(&b, 0, HFS_BOOTCODE2LEN); + + if (b_writelb(vol, 1, &b) == -1) + goto fail; + + return 0; + +fail: + return -1; +} + +/* + * NAME: low->getmdb() + * DESCRIPTION: read a master directory block + */ +int l_getmdb(hfsvol *vol, MDB *mdb, int backup) +{ + block b; + const byte *ptr = b; + int i; + + if (b_readlb(vol, backup ? vol->vlen - 2 : 2, &b) == -1) + goto fail; + + d_fetchsw(&ptr, &mdb->drSigWord); + d_fetchsl(&ptr, &mdb->drCrDate); + d_fetchsl(&ptr, &mdb->drLsMod); + d_fetchsw(&ptr, &mdb->drAtrb); + d_fetchuw(&ptr, &mdb->drNmFls); + d_fetchuw(&ptr, &mdb->drVBMSt); + d_fetchuw(&ptr, &mdb->drAllocPtr); + d_fetchuw(&ptr, &mdb->drNmAlBlks); + d_fetchul(&ptr, &mdb->drAlBlkSiz); + d_fetchul(&ptr, &mdb->drClpSiz); + d_fetchuw(&ptr, &mdb->drAlBlSt); + d_fetchsl(&ptr, &mdb->drNxtCNID); + d_fetchuw(&ptr, &mdb->drFreeBks); + + d_fetchstr(&ptr, mdb->drVN, sizeof(mdb->drVN)); + + ASSERT(ptr - b == 64); + + d_fetchsl(&ptr, &mdb->drVolBkUp); + d_fetchsw(&ptr, &mdb->drVSeqNum); + d_fetchul(&ptr, &mdb->drWrCnt); + d_fetchul(&ptr, &mdb->drXTClpSiz); + d_fetchul(&ptr, &mdb->drCTClpSiz); + d_fetchuw(&ptr, &mdb->drNmRtDirs); + d_fetchul(&ptr, &mdb->drFilCnt); + d_fetchul(&ptr, &mdb->drDirCnt); + + for (i = 0; i < 8; ++i) + d_fetchsl(&ptr, &mdb->drFndrInfo[i]); + + ASSERT(ptr - b == 124); + + d_fetchuw(&ptr, &mdb->drEmbedSigWord); + d_fetchuw(&ptr, &mdb->drEmbedExtent.xdrStABN); + d_fetchuw(&ptr, &mdb->drEmbedExtent.xdrNumABlks); + + d_fetchul(&ptr, &mdb->drXTFlSize); + + for (i = 0; i < 3; ++i) + { + d_fetchuw(&ptr, &mdb->drXTExtRec[i].xdrStABN); + d_fetchuw(&ptr, &mdb->drXTExtRec[i].xdrNumABlks); + } + + ASSERT(ptr - b == 146); + + d_fetchul(&ptr, &mdb->drCTFlSize); + + for (i = 0; i < 3; ++i) + { + d_fetchuw(&ptr, &mdb->drCTExtRec[i].xdrStABN); + d_fetchuw(&ptr, &mdb->drCTExtRec[i].xdrNumABlks); + } + + ASSERT(ptr - b == 162); + + return 0; + +fail: + return -1; +} + +/* + * NAME: low->putmdb() + * DESCRIPTION: write master directory block(s) + */ +int l_putmdb(hfsvol *vol, const MDB *mdb, int backup) +{ + block b; + byte *ptr = b; + int i; + + d_storesw(&ptr, mdb->drSigWord); + d_storesl(&ptr, mdb->drCrDate); + d_storesl(&ptr, mdb->drLsMod); + d_storesw(&ptr, mdb->drAtrb); + d_storeuw(&ptr, mdb->drNmFls); + d_storeuw(&ptr, mdb->drVBMSt); + d_storeuw(&ptr, mdb->drAllocPtr); + d_storeuw(&ptr, mdb->drNmAlBlks); + d_storeul(&ptr, mdb->drAlBlkSiz); + d_storeul(&ptr, mdb->drClpSiz); + d_storeuw(&ptr, mdb->drAlBlSt); + d_storesl(&ptr, mdb->drNxtCNID); + d_storeuw(&ptr, mdb->drFreeBks); + + d_storestr(&ptr, mdb->drVN, sizeof(mdb->drVN)); + + ASSERT(ptr - b == 64); + + d_storesl(&ptr, mdb->drVolBkUp); + d_storesw(&ptr, mdb->drVSeqNum); + d_storeul(&ptr, mdb->drWrCnt); + d_storeul(&ptr, mdb->drXTClpSiz); + d_storeul(&ptr, mdb->drCTClpSiz); + d_storeuw(&ptr, mdb->drNmRtDirs); + d_storeul(&ptr, mdb->drFilCnt); + d_storeul(&ptr, mdb->drDirCnt); + + for (i = 0; i < 8; ++i) + d_storesl(&ptr, mdb->drFndrInfo[i]); + + ASSERT(ptr - b == 124); + + d_storeuw(&ptr, mdb->drEmbedSigWord); + d_storeuw(&ptr, mdb->drEmbedExtent.xdrStABN); + d_storeuw(&ptr, mdb->drEmbedExtent.xdrNumABlks); + + d_storeul(&ptr, mdb->drXTFlSize); + + for (i = 0; i < 3; ++i) + { + d_storeuw(&ptr, mdb->drXTExtRec[i].xdrStABN); + d_storeuw(&ptr, mdb->drXTExtRec[i].xdrNumABlks); + } + + ASSERT(ptr - b == 146); + + d_storeul(&ptr, mdb->drCTFlSize); + + for (i = 0; i < 3; ++i) + { + d_storeuw(&ptr, mdb->drCTExtRec[i].xdrStABN); + d_storeuw(&ptr, mdb->drCTExtRec[i].xdrNumABlks); + } + + ASSERT(ptr - b == 162); + + memset(ptr, 0, HFS_BLOCKSZ - (ptr - b)); + + if (b_writelb(vol, 2, &b) == -1 || + (backup && b_writelb(vol, vol->vlen - 2, &b) == -1)) + goto fail; + + return 0; + +fail: + return -1; +} diff --git a/diskimg/libhfs/low.h b/diskimg/libhfs/low.h new file mode 100644 index 0000000..0e59d83 --- /dev/null +++ b/diskimg/libhfs/low.h @@ -0,0 +1,44 @@ +/* + * libhfs - library for reading and writing Macintosh HFS volumes + * Copyright (C) 1996-1998 Robert Leslie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + * + * $Id$ + */ + +# define HFS_DDR_SIGWORD 0x4552 + +# define HFS_PM_SIGWORD 0x504d +# define HFS_PM_SIGWORD_OLD 0x5453 + +# define HFS_BB_SIGWORD 0x4c4b + +# define HFS_BOOTCODE1LEN (HFS_BLOCKSZ - 148) +# define HFS_BOOTCODE2LEN HFS_BLOCKSZ + +# define HFS_BOOTCODELEN (HFS_BOOTCODE1LEN + HFS_BOOTCODE2LEN) + +int l_getddr(hfsvol *, Block0 *); +int l_putddr(hfsvol *, const Block0 *); + +int l_getpmentry(hfsvol *, Partition *, unsigned long); +int l_putpmentry(hfsvol *, const Partition *, unsigned long); + +int l_getbb(hfsvol *, BootBlkHdr *, byte *); +int l_putbb(hfsvol *, const BootBlkHdr *, const byte *); + +int l_getmdb(hfsvol *, MDB *, int); +int l_putmdb(hfsvol *, const MDB *, int); diff --git a/diskimg/libhfs/medium.c b/diskimg/libhfs/medium.c new file mode 100644 index 0000000..bcd070d --- /dev/null +++ b/diskimg/libhfs/medium.c @@ -0,0 +1,318 @@ +/* + * libhfs - library for reading and writing Macintosh HFS volumes + * Copyright (C) 1996-1998 Robert Leslie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + * + * $Id$ + */ + +# ifdef HAVE_CONFIG_H +# include "config.h" +# endif + +# include +# include +# include + +# include "libhfs.h" +# include "block.h" +# include "low.h" +# include "medium.h" + +/* Driver Descriptor Record Routines ======================================= */ + +/* + * NAME: medium->zeroddr() + * DESCRIPTION: write a new/empty driver descriptor record + */ +int m_zeroddr(hfsvol *vol) +{ + Block0 ddr; + int i; + + ASSERT(vol->pnum == 0 && vol->vlen != 0); + + ddr.sbSig = HFS_DDR_SIGWORD; + ddr.sbBlkSize = HFS_BLOCKSZ; + ddr.sbBlkCount = vol->vlen; + + ddr.sbDevType = 0; + ddr.sbDevId = 0; + ddr.sbData = 0; + + ddr.sbDrvrCount = 0; + + ddr.ddBlock = 0; + ddr.ddSize = 0; + ddr.ddType = 0; + + for (i = 0; i < 243; ++i) + ddr.ddPad[i] = 0; + + return l_putddr(vol, &ddr); +} + +/* Partition Map Routines ================================================== */ + +/* + * NAME: medium->zeropm() + * DESCRIPTION: write new/empty partition map + */ +int m_zeropm(hfsvol *vol, unsigned int maxparts) +{ + Partition map; + unsigned int i; + + ASSERT(vol->pnum == 0 && vol->vlen != 0); + + if (maxparts < 2) + ERROR(EINVAL, "must allow at least 2 partitions"); + + /* first entry: partition map itself */ + + map.pmSig = HFS_PM_SIGWORD; + map.pmSigPad = 0; + map.pmMapBlkCnt = 2; + + map.pmPyPartStart = 1; + map.pmPartBlkCnt = maxparts; + + strcpy((char *) map.pmPartName, "Apple"); + strcpy((char *) map.pmParType, "Apple_partition_map"); + + map.pmLgDataStart = 0; + map.pmDataCnt = map.pmPartBlkCnt; + + map.pmPartStatus = 0; + + map.pmLgBootStart = 0; + map.pmBootSize = 0; + map.pmBootAddr = 0; + map.pmBootAddr2 = 0; + map.pmBootEntry = 0; + map.pmBootEntry2 = 0; + map.pmBootCksum = 0; + + strcpy((char *) map.pmProcessor, ""); + + for (i = 0; i < 188; ++i) + map.pmPad[i] = 0; + + if (l_putpmentry(vol, &map, 1) == -1) + goto fail; + + /* second entry: rest of medium */ + + map.pmPyPartStart = 1 + maxparts; + map.pmPartBlkCnt = vol->vlen - 1 - maxparts; + + strcpy((char *) map.pmPartName, "Extra"); + strcpy((char *) map.pmParType, "Apple_Free"); + + map.pmDataCnt = map.pmPartBlkCnt; + + if (l_putpmentry(vol, &map, 2) == -1) + goto fail; + + /* zero rest of partition map's partition */ + + if (maxparts > 2) + { + block b; + + memset(&b, 0, sizeof(b)); + + for (i = 3; i <= maxparts; ++i) + { + if (b_writepb(vol, i, &b, 1) == -1) + goto fail; + } + } + + return 0; + +fail: + return -1; +} + +/* + * NAME: medium->findpmentry() + * DESCRIPTION: locate a partition map entry + */ +int m_findpmentry(hfsvol *vol, const char *type, + Partition *map, unsigned long *start) +{ + unsigned long bnum; + int found = 0; + + if (start && *start > 0) + { + bnum = *start; + + if (bnum++ >= (unsigned long) map->pmMapBlkCnt) + ERROR(EINVAL, "partition not found"); + } + else + bnum = 1; + + while (1) + { + if (l_getpmentry(vol, map, bnum) == -1) + { + found = -1; + goto fail; + } + + if (map->pmSig != HFS_PM_SIGWORD) + { + found = -1; + + if (map->pmSig == HFS_PM_SIGWORD_OLD) + ERROR(EINVAL, "old partition map format not supported"); + else + ERROR(EINVAL, "invalid partition map"); + } + + if (strcmp((char *) map->pmParType, type) == 0) + { + found = 1; + goto done; + } + + if (bnum++ >= (unsigned long) map->pmMapBlkCnt) + ERROR(EINVAL, "partition not found"); + } + +done: + if (start) + *start = bnum; + +fail: + return found; +} + +/* + * NAME: medium->mkpart() + * DESCRIPTION: create a new partition from available free space + */ +int m_mkpart(hfsvol *vol, + const char *name, const char *type, unsigned long len) +{ + Partition map; + unsigned int nparts, maxparts; + unsigned long bnum, start, remain; + int found; + + if (strlen(name) > 32 || + strlen(type) > 32) + ERROR(EINVAL, "partition name/type can each be at most 32 chars"); + + if (len == 0) + ERROR(EINVAL, "partition length must be > 0"); + + found = m_findpmentry(vol, "Apple_partition_map", &map, 0); + if (found == -1) + goto fail; + + if (! found) + ERROR(EIO, "cannot find partition map's partition"); + + nparts = map.pmMapBlkCnt; + maxparts = map.pmPartBlkCnt; + + bnum = 0; + do + { + found = m_findpmentry(vol, "Apple_Free", &map, &bnum); + if (found == -1) + goto fail; + + if (! found) + ERROR(ENOSPC, "no available partitions"); + } + while (len > (unsigned long) map.pmPartBlkCnt); + + start = (unsigned long) map.pmPyPartStart + len; + remain = (unsigned long) map.pmPartBlkCnt - len; + + if (remain && nparts >= maxparts) + ERROR(EINVAL, "must allocate all blocks in free space"); + + map.pmPartBlkCnt = len; + + strcpy((char *) map.pmPartName, name); + strcpy((char *) map.pmParType, type); + + map.pmLgDataStart = 0; + map.pmDataCnt = len; + + map.pmPartStatus = 0; + + if (l_putpmentry(vol, &map, bnum) == -1) + goto fail; + + if (remain) + { + map.pmPyPartStart = start; + map.pmPartBlkCnt = remain; + + strcpy((char *) map.pmPartName, "Extra"); + strcpy((char *) map.pmParType, "Apple_Free"); + + map.pmDataCnt = remain; + + if (l_putpmentry(vol, &map, ++nparts) == -1) + goto fail; + + for (bnum = 1; bnum <= nparts; ++bnum) + { + if (l_getpmentry(vol, &map, bnum) == -1) + goto fail; + + map.pmMapBlkCnt = nparts; + + if (l_putpmentry(vol, &map, bnum) == -1) + goto fail; + } + } + + return 0; + +fail: + return -1; +} + +/* Boot Blocks Routines ==================================================== */ + +/* + * NAME: medium->zerobb() + * DESCRIPTION: write new/empty volume boot blocks + */ +int m_zerobb(hfsvol *vol) +{ + block b; + + memset(&b, 0, sizeof(b)); + + if (b_writelb(vol, 0, &b) == -1 || + b_writelb(vol, 1, &b) == -1) + goto fail; + + return 0; + +fail: + return -1; +} diff --git a/diskimg/libhfs/medium.h b/diskimg/libhfs/medium.h new file mode 100644 index 0000000..912f40c --- /dev/null +++ b/diskimg/libhfs/medium.h @@ -0,0 +1,42 @@ +/* + * libhfs - library for reading and writing Macintosh HFS volumes + * Copyright (C) 1996-1998 Robert Leslie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + * + * $Id$ + */ + +/* + * Partition Types: + * + * "Apple_partition_map" partition map + * "Apple_Driver" device driver + * "Apple_Driver43" SCSI Manager 4.3 device driver + * "Apple_MFS" Macintosh 64K ROM filesystem + * "Apple_HFS" Macintosh hierarchical filesystem + * "Apple_Unix_SVR2" Unix filesystem + * "Apple_PRODOS" ProDOS filesystem + * "Apple_Free" unused + * "Apple_Scratch" empty + */ + +int m_zeroddr(hfsvol *); + +int m_zeropm(hfsvol *, unsigned int); +int m_findpmentry(hfsvol *, const char *, Partition *, unsigned long *); +int m_mkpart(hfsvol *, const char *, const char *, unsigned long); + +int m_zerobb(hfsvol *); diff --git a/diskimg/libhfs/memcmp.c b/diskimg/libhfs/memcmp.c new file mode 100644 index 0000000..037b5b2 --- /dev/null +++ b/diskimg/libhfs/memcmp.c @@ -0,0 +1,50 @@ +/* + * libhfs - library for reading and writing Macintosh HFS volumes + * Copyright (C) 1996-1998 Robert Leslie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + * + * $Id$ + */ + +# ifdef HAVE_CONFIG_H +# include "config.h" +# endif + +# include + +/* + * NAME: memcmp() + * DESCRIPTION: compare memory areas + */ +int memcmp(const void *s1, const void *s2, size_t n) +{ + register const unsigned char *c1, *c2; + + c1 = s1; + c2 = s2; + + while (n--) + { + register int diff; + + diff = *c1++ - *c2++; + + if (diff) + return diff; + } + + return 0; +} diff --git a/diskimg/libhfs/node.c b/diskimg/libhfs/node.c new file mode 100644 index 0000000..835bbc0 --- /dev/null +++ b/diskimg/libhfs/node.c @@ -0,0 +1,473 @@ +/* + * libhfs - library for reading and writing Macintosh HFS volumes + * Copyright (C) 1996-1998 Robert Leslie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + * + * $Id$ + */ + +# ifdef HAVE_CONFIG_H +# include "config.h" +# endif + +# include +# include +# include + +# include "libhfs.h" +# include "node.h" +# include "data.h" +# include "btree.h" + +/* total bytes used by records (NOT including record offsets) */ + +# define NODEUSED(n) \ + ((size_t) ((n).roff[(n).nd.ndNRecs] - (n).roff[0])) + +/* total bytes available for new records (INCLUDING record offsets) */ + +# define NODEFREE(n) \ + ((size_t) (HFS_BLOCKSZ - (n).roff[(n).nd.ndNRecs] - \ + 2 * ((n).nd.ndNRecs + 1))) + +/* + * NAME: node->init() + * DESCRIPTION: construct an empty node + */ +void n_init(node *np, btree *bt, int type, int height) +{ + np->bt = bt; + np->nnum = (unsigned long) -1; + + np->nd.ndFLink = 0; + np->nd.ndBLink = 0; + np->nd.ndType = type; + np->nd.ndNHeight = height; + np->nd.ndNRecs = 0; + np->nd.ndResv2 = 0; + + np->rnum = -1; + np->roff[0] = 0x00e; + + memset(&np->data, 0, sizeof(np->data)); +} + +/* + * NAME: node->new() + * DESCRIPTION: allocate a new b*-tree node + */ +int n_new(node *np) +{ + btree *bt = np->bt; + unsigned long num; + + if (bt->hdr.bthFree == 0) + ERROR(EIO, "b*-tree full"); + + num = 0; + while (num < bt->hdr.bthNNodes && BMTST(bt->map, num)) + ++num; + + if (num == bt->hdr.bthNNodes) + ERROR(EIO, "free b*-tree node not found"); + + np->nnum = num; + + BMSET(bt->map, num); + --bt->hdr.bthFree; + + bt->flags |= HFS_BT_UPDATE_HDR; + + return 0; + +fail: + return -1; +} + +/* + * NAME: node->free() + * DESCRIPTION: deallocate and remove a b*-tree node + */ +int n_free(node *np) +{ + btree *bt = np->bt; + node sib; + + if (bt->hdr.bthFNode == np->nnum) + bt->hdr.bthFNode = np->nd.ndFLink; + + if (bt->hdr.bthLNode == np->nnum) + bt->hdr.bthLNode = np->nd.ndBLink; + + if (np->nd.ndFLink > 0) + { + if (bt_getnode(&sib, bt, np->nd.ndFLink) == -1) + goto fail; + + sib.nd.ndBLink = np->nd.ndBLink; + + if (bt_putnode(&sib) == -1) + goto fail; + } + + if (np->nd.ndBLink > 0) + { + if (bt_getnode(&sib, bt, np->nd.ndBLink) == -1) + goto fail; + + sib.nd.ndFLink = np->nd.ndFLink; + + if (bt_putnode(&sib) == -1) + goto fail; + } + + BMCLR(bt->map, np->nnum); + ++bt->hdr.bthFree; + + bt->flags |= HFS_BT_UPDATE_HDR; + + return 0; + +fail: + return -1; +} + +/* + * NAME: compact() + * DESCRIPTION: clean up a node, removing deleted records + */ +static +void compact(node *np) +{ + byte *ptr; + int offset, nrecs, i; + + offset = 0x00e; + ptr = np->data + offset; + nrecs = 0; + + for (i = 0; i < np->nd.ndNRecs; ++i) + { + const byte *rec; + int reclen; + + rec = HFS_NODEREC(*np, i); + reclen = HFS_RECLEN(*np, i); + + if (HFS_RECKEYLEN(rec) > 0) + { + np->roff[nrecs++] = offset; + offset += reclen; + + if (ptr == rec) + ptr += reclen; + else + { + while (reclen--) + *ptr++ = *rec++; + } + } + } + + np->roff[nrecs] = offset; + np->nd.ndNRecs = nrecs; +} + +/* + * NAME: node->search() + * DESCRIPTION: locate a record in a node, or the record it should follow + */ +int n_search(node *np, const byte *pkey) +{ + const btree *bt = np->bt; + byte key1[HFS_MAX_KEYLEN], key2[HFS_MAX_KEYLEN]; + int i, comp = -1; + + bt->keyunpack(pkey, key2); + + for (i = np->nd.ndNRecs; i--; ) + { + const byte *rec; + + rec = HFS_NODEREC(*np, i); + + if (HFS_RECKEYLEN(rec) == 0) + continue; /* deleted record */ + + bt->keyunpack(rec, key1); + comp = bt->keycompare(key1, key2); + + if (comp <= 0) + break; + } + + np->rnum = i; + + return comp == 0; +} + +/* + * NAME: node->index() + * DESCRIPTION: create an index record from a key and node pointer + */ +void n_index(const node *np, byte *record, unsigned int *reclen) +{ + const byte *key = HFS_NODEREC(*np, 0); + + if (np->bt == &np->bt->f.vol->cat) + { + /* force the key length to be 0x25 */ + + HFS_SETKEYLEN(record, 0x25); + memset(record + 1, 0, 0x25); + memcpy(record + 1, key + 1, HFS_RECKEYLEN(key)); + } + else + memcpy(record, key, HFS_RECKEYSKIP(key)); + + d_putul(HFS_RECDATA(record), np->nnum); + + if (reclen) + *reclen = HFS_RECKEYSKIP(record) + 4; +} + +/* + * NAME: split() + * DESCRIPTION: divide a node into two and insert a record + */ +static +int split(node *left, byte *record, unsigned int *reclen) +{ + btree *bt = left->bt; + node n, *right = &n, *side = 0; + int mark, i; + + /* create a second node by cloning the first */ + + *right = *left; + + if (n_new(right) == -1) + goto fail; + + left->nd.ndFLink = right->nnum; + right->nd.ndBLink = left->nnum; + + /* divide all records evenly between the two nodes */ + + mark = (NODEUSED(*left) + 2 * left->nd.ndNRecs + *reclen + 2) >> 1; + + if (left->rnum == -1) + { + side = left; + mark -= *reclen + 2; + } + + for (i = 0; i < left->nd.ndNRecs; ++i) + { + node *np; + byte *rec; + + np = (mark > 0) ? right : left; + rec = HFS_NODEREC(*np, i); + + mark -= HFS_RECLEN(*np, i) + 2; + + HFS_SETKEYLEN(rec, 0); + + if (left->rnum == i) + { + side = (mark > 0) ? left : right; + mark -= *reclen + 2; + } + } + + compact(left); + compact(right); + + /* insert the new record and store the modified nodes */ + + ASSERT(side); + + n_search(side, record); + n_insertx(side, record, *reclen); + + if (bt_putnode(left) == -1 || + bt_putnode(right) == -1) + goto fail; + + /* create an index record in the parent for the new node */ + + n_index(right, record, reclen); + + /* update link pointers */ + + if (bt->hdr.bthLNode == left->nnum) + { + bt->hdr.bthLNode = right->nnum; + bt->flags |= HFS_BT_UPDATE_HDR; + } + + if (right->nd.ndFLink > 0) + { + node sib; + + if (bt_getnode(&sib, right->bt, right->nd.ndFLink) == -1) + goto fail; + + sib.nd.ndBLink = right->nnum; + + if (bt_putnode(&sib) == -1) + goto fail; + } + + return 0; + +fail: + return -1; +} + +/* + * NAME: node->insertx() + * DESCRIPTION: insert a record into a node (which must already have room) + */ +void n_insertx(node *np, const byte *record, unsigned int reclen) +{ + int rnum, i; + byte *ptr; + + rnum = np->rnum + 1; + + /* push other records down to make room */ + + for (ptr = HFS_NODEREC(*np, np->nd.ndNRecs) + reclen; + ptr > HFS_NODEREC(*np, rnum) + reclen; --ptr) + *(ptr - 1) = *(ptr - 1 - reclen); + + ++np->nd.ndNRecs; + + for (i = np->nd.ndNRecs; i > rnum; --i) + np->roff[i] = np->roff[i - 1] + reclen; + + /* write the new record */ + + memcpy(HFS_NODEREC(*np, rnum), record, reclen); +} + +/* + * NAME: node->insert() + * DESCRIPTION: insert a new record into a node; return a record for parent + */ +int n_insert(node *np, byte *record, unsigned int *reclen) +{ + /* check for free space */ + + if (np->nd.ndNRecs >= HFS_MAX_NRECS || + *reclen + 2 > NODEFREE(*np)) + return split(np, record, reclen); + + n_insertx(np, record, *reclen); + *reclen = 0; + + return bt_putnode(np); +} + +/* + * NAME: join() + * DESCRIPTION: combine two nodes into a single node + */ +static +int join(node *left, node *right, byte *record, int *flag) +{ + int i, offset; + + /* copy records and offsets */ + + memcpy(HFS_NODEREC(*left, left->nd.ndNRecs), + HFS_NODEREC(*right, 0), NODEUSED(*right)); + + offset = left->roff[left->nd.ndNRecs] - right->roff[0]; + + for (i = 1; i <= right->nd.ndNRecs; ++i) + left->roff[++left->nd.ndNRecs] = offset + right->roff[i]; + + if (bt_putnode(left) == -1) + goto fail; + + /* eliminate node and update link pointers */ + + if (n_free(right) == -1) + goto fail; + + HFS_SETKEYLEN(record, 0); + *flag = 1; + + return 0; + +fail: + return -1; +} + +/* + * NAME: node->delete() + * DESCRIPTION: remove a record from a node + */ +int n_delete(node *np, byte *record, int *flag) +{ + byte *rec; + + rec = HFS_NODEREC(*np, np->rnum); + + HFS_SETKEYLEN(rec, 0); + compact(np); + + if (np->nd.ndNRecs == 0) + { + if (n_free(np) == -1) + goto fail; + + HFS_SETKEYLEN(record, 0); + *flag = 1; + + return 0; + } + + /* see if we can join with our left sibling */ + + if (np->nd.ndBLink > 0) + { + node left; + + if (bt_getnode(&left, np->bt, np->nd.ndBLink) == -1) + goto fail; + + if (np->nd.ndNRecs + left.nd.ndNRecs <= HFS_MAX_NRECS && + NODEUSED(*np) + 2 * np->nd.ndNRecs <= NODEFREE(left)) + return join(&left, np, record, flag); + } + + if (np->rnum == 0) + { + /* special case: first record changed; update parent record key */ + + n_index(np, record, 0); + *flag = 1; + } + + return bt_putnode(np); + +fail: + return -1; +} diff --git a/diskimg/libhfs/node.h b/diskimg/libhfs/node.h new file mode 100644 index 0000000..12272ce --- /dev/null +++ b/diskimg/libhfs/node.h @@ -0,0 +1,34 @@ +/* + * libhfs - library for reading and writing Macintosh HFS volumes + * Copyright (C) 1996-1998 Robert Leslie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + * + * $Id$ + */ + +void n_init(node *, btree *, int, int); + +int n_new(node *); +int n_free(node *); + +int n_search(node *, const byte *); + +void n_index(const node *, byte *, unsigned int *); + +void n_insertx(node *, const byte *, unsigned int); +int n_insert(node *, byte *, unsigned int *); + +int n_delete(node *, byte *, int *); diff --git a/diskimg/libhfs/os.c b/diskimg/libhfs/os.c new file mode 100644 index 0000000..22b8d3a --- /dev/null +++ b/diskimg/libhfs/os.c @@ -0,0 +1,182 @@ +/* + * libhfs - library for reading and writing Macintosh HFS volumes + * Copyright (C) 1996-1998 Robert Leslie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + * + * $Id$ + */ + +# ifdef HAVE_CONFIG_H +# include "config.h" +# endif + +# ifdef HAVE_FCNTL_H +# include +# else +int open(const char *, int, ...); +int fcntl(int, int, ...); +# endif + +# ifdef HAVE_UNISTD_H +# include +# else +#ifndef _WIN32 +int close(int); +off_t lseek(int, off_t, int); +ssize_t read(int, void *, size_t); +ssize_t write(int, const char *, size_t); +int stat(const char *, struct stat *); +int fstat(int, struct stat *); +#endif +# endif + +# include +# include +# include +# include /* debug */ + +# include "libhfs.h" +# include "os.h" + +typedef struct cp_private { + oscallback func; /* function to call */ + void* cookie; /* magic cookie to pass in */ + long cur_block; /* current seek offset */ +} cp_private; + +/* + * NAME: os->callback_open() + * DESCRIPTION: open and lock a new descriptor from the given path and mode + */ +int os_callback_open(void **priv, oscallback func, void* cookie) +{ + cp_private* mypriv; + + mypriv = malloc(sizeof(*mypriv)); + mypriv->func = func; + mypriv->cookie = cookie; + mypriv->cur_block = 0; + + *priv = mypriv; + //fprintf(stderr, "ALLOC %p->%p\n", priv, *priv); + + return 0; +} + +/* + * NAME: os->close() + * DESCRIPTION: close an open descriptor + */ +int os_close(void **priv) +{ + //fprintf(stderr, "FREEING %p->%p\n", priv, *priv); + free(*priv); + *priv = 0; + + return 0; +} + +#ifdef CP_NOT_USED +/* + * NAME: os->same() + * DESCRIPTION: return 1 iff path is same as the open descriptor + */ +int os_same(void **priv, const char *path) +{ + return 0; + + int fd = (int) *priv; + struct stat fdev, dev; + + if (fstat(fd, &fdev) == -1 || + stat(path, &dev) == -1) + ERROR(errno, "can't get path information"); + + return fdev.st_dev == dev.st_dev && + fdev.st_ino == dev.st_ino; + +fail: + return -1; +} +#endif + +/* + * NAME: os->seek() + * DESCRIPTION: set a descriptor's seek pointer (offset in blocks) + */ +unsigned long os_seek(void **priv, unsigned long offset) +{ + cp_private* mypriv = (cp_private*) *priv; + unsigned long result; + + if (offset == (unsigned long) -1) { + result = (*mypriv->func)(mypriv->cookie, HFS_CB_VOLSIZE, 0, 0); + } else { + result = (*mypriv->func)(mypriv->cookie, HFS_CB_SEEK, offset, 0); + if (result != -1) + mypriv->cur_block = offset; + } + + return result; +} + +/* + * NAME: os->read() + * DESCRIPTION: read blocks from an open descriptor + */ +unsigned long os_read(void **priv, void *buf, unsigned long len) +{ + cp_private* mypriv = (cp_private*) *priv; + unsigned long result; + unsigned long success = 0; + + while (len--) { + result = (*mypriv->func)(mypriv->cookie, HFS_CB_READ, + mypriv->cur_block, buf); + if (result == -1) + break; + + mypriv->cur_block++; + buf = ((unsigned char*) buf) + HFS_BLOCKSZ; + success++; + } + + return success; +} + +/* + * NAME: os->write() + * DESCRIPTION: write blocks to an open descriptor + */ +unsigned long os_write(void **priv, const void *buf, unsigned long len) +{ + cp_private* mypriv = (cp_private*) *priv; + unsigned long result; + unsigned long success = 0; + + while (len--) { + result = (*mypriv->func)(mypriv->cookie, HFS_CB_WRITE, + mypriv->cur_block, (void*)buf); + if (result == -1) + break; + + mypriv->cur_block++; + buf = ((unsigned char*) buf) + HFS_BLOCKSZ; + success++; + } + + return success; +} diff --git a/diskimg/libhfs/os.h b/diskimg/libhfs/os.h new file mode 100644 index 0000000..a40c74f --- /dev/null +++ b/diskimg/libhfs/os.h @@ -0,0 +1,34 @@ +/* + * libhfs - library for reading and writing Macintosh HFS volumes + * Copyright (C) 1996-1998 Robert Leslie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + * + * $Id$ + */ + +#ifdef CP_NOT_USED +int os_open(void **, const char *, int); +#endif +int os_callback_open(void **priv, oscallback func, void* cookie); +int os_close(void **); + +#ifdef CP_NOT_USED +int os_same(void **, const char *); +#endif + +unsigned long os_seek(void **, unsigned long); +unsigned long os_read(void **, void *, unsigned long); +unsigned long os_write(void **, const void *, unsigned long); diff --git a/diskimg/libhfs/record.c b/diskimg/libhfs/record.c new file mode 100644 index 0000000..fe269a3 --- /dev/null +++ b/diskimg/libhfs/record.c @@ -0,0 +1,557 @@ +/* + * libhfs - library for reading and writing Macintosh HFS volumes + * Copyright (C) 1996-1998 Robert Leslie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + * + * $Id$ + */ + +# ifdef HAVE_CONFIG_H +# include "config.h" +# endif + +# include + +# include "libhfs.h" +# include "record.h" +# include "data.h" + +/* + * NAME: record->packcatkey() + * DESCRIPTION: pack a catalog record key + */ +void r_packcatkey(const CatKeyRec *key, byte *pkey, unsigned int *len) +{ + const byte *start = pkey; + + d_storesb(&pkey, key->ckrKeyLen); + d_storesb(&pkey, key->ckrResrv1); + d_storeul(&pkey, key->ckrParID); + + d_storestr(&pkey, key->ckrCName, sizeof(key->ckrCName)); + + if (len) + *len = HFS_RECKEYSKIP(start); +} + +/* + * NAME: record->unpackcatkey() + * DESCRIPTION: unpack a catalog record key + */ +void r_unpackcatkey(const byte *pkey, CatKeyRec *key) +{ + d_fetchsb(&pkey, &key->ckrKeyLen); + d_fetchsb(&pkey, &key->ckrResrv1); + d_fetchul(&pkey, &key->ckrParID); + + d_fetchstr(&pkey, key->ckrCName, sizeof(key->ckrCName)); +} + +/* + * NAME: record->packextkey() + * DESCRIPTION: pack an extents record key + */ +void r_packextkey(const ExtKeyRec *key, byte *pkey, unsigned int *len) +{ + const byte *start = pkey; + + d_storesb(&pkey, key->xkrKeyLen); + d_storesb(&pkey, key->xkrFkType); + d_storeul(&pkey, key->xkrFNum); + d_storeuw(&pkey, key->xkrFABN); + + if (len) + *len = HFS_RECKEYSKIP(start); +} + +/* + * NAME: record->unpackextkey() + * DESCRIPTION: unpack an extents record key + */ +void r_unpackextkey(const byte *pkey, ExtKeyRec *key) +{ + d_fetchsb(&pkey, &key->xkrKeyLen); + d_fetchsb(&pkey, &key->xkrFkType); + d_fetchul(&pkey, &key->xkrFNum); + d_fetchuw(&pkey, &key->xkrFABN); +} + +/* + * NAME: record->comparecatkeys() + * DESCRIPTION: compare two (packed) catalog record keys + */ +int r_comparecatkeys(const CatKeyRec *key1, const CatKeyRec *key2) +{ + int diff; + + diff = key1->ckrParID - key2->ckrParID; + if (diff) + return diff; + + return d_relstring(key1->ckrCName, key2->ckrCName); +} + +/* + * NAME: record->compareextkeys() + * DESCRIPTION: compare two (packed) extents record keys + */ +int r_compareextkeys(const ExtKeyRec *key1, const ExtKeyRec *key2) +{ + int diff; + + diff = key1->xkrFNum - key2->xkrFNum; + if (diff) + return diff; + + diff = (unsigned char) key1->xkrFkType - + (unsigned char) key2->xkrFkType; + if (diff) + return diff; + + return key1->xkrFABN - key2->xkrFABN; +} + +/* + * NAME: record->packcatdata() + * DESCRIPTION: pack catalog record data + */ +void r_packcatdata(const CatDataRec *data, byte *pdata, unsigned int *len) +{ + const byte *start = pdata; + int i; + + d_storesb(&pdata, data->cdrType); + d_storesb(&pdata, data->cdrResrv2); + + switch (data->cdrType) + { + case cdrDirRec: + d_storesw(&pdata, data->u.dir.dirFlags); + d_storeuw(&pdata, data->u.dir.dirVal); + d_storeul(&pdata, data->u.dir.dirDirID); + d_storesl(&pdata, data->u.dir.dirCrDat); + d_storesl(&pdata, data->u.dir.dirMdDat); + d_storesl(&pdata, data->u.dir.dirBkDat); + + d_storesw(&pdata, data->u.dir.dirUsrInfo.frRect.top); + d_storesw(&pdata, data->u.dir.dirUsrInfo.frRect.left); + d_storesw(&pdata, data->u.dir.dirUsrInfo.frRect.bottom); + d_storesw(&pdata, data->u.dir.dirUsrInfo.frRect.right); + d_storesw(&pdata, data->u.dir.dirUsrInfo.frFlags); + d_storesw(&pdata, data->u.dir.dirUsrInfo.frLocation.v); + d_storesw(&pdata, data->u.dir.dirUsrInfo.frLocation.h); + d_storesw(&pdata, data->u.dir.dirUsrInfo.frView); + + d_storesw(&pdata, data->u.dir.dirFndrInfo.frScroll.v); + d_storesw(&pdata, data->u.dir.dirFndrInfo.frScroll.h); + d_storesl(&pdata, data->u.dir.dirFndrInfo.frOpenChain); + d_storesw(&pdata, data->u.dir.dirFndrInfo.frUnused); + d_storesw(&pdata, data->u.dir.dirFndrInfo.frComment); + d_storesl(&pdata, data->u.dir.dirFndrInfo.frPutAway); + + for (i = 0; i < 4; ++i) + d_storesl(&pdata, data->u.dir.dirResrv[i]); + + break; + + case cdrFilRec: + d_storesb(&pdata, data->u.fil.filFlags); + d_storesb(&pdata, data->u.fil.filTyp); + + d_storesl(&pdata, data->u.fil.filUsrWds.fdType); + d_storesl(&pdata, data->u.fil.filUsrWds.fdCreator); + d_storesw(&pdata, data->u.fil.filUsrWds.fdFlags); + d_storesw(&pdata, data->u.fil.filUsrWds.fdLocation.v); + d_storesw(&pdata, data->u.fil.filUsrWds.fdLocation.h); + d_storesw(&pdata, data->u.fil.filUsrWds.fdFldr); + + d_storeul(&pdata, data->u.fil.filFlNum); + + d_storeuw(&pdata, data->u.fil.filStBlk); + d_storeul(&pdata, data->u.fil.filLgLen); + d_storeul(&pdata, data->u.fil.filPyLen); + + d_storeuw(&pdata, data->u.fil.filRStBlk); + d_storeul(&pdata, data->u.fil.filRLgLen); + d_storeul(&pdata, data->u.fil.filRPyLen); + + d_storesl(&pdata, data->u.fil.filCrDat); + d_storesl(&pdata, data->u.fil.filMdDat); + d_storesl(&pdata, data->u.fil.filBkDat); + + d_storesw(&pdata, data->u.fil.filFndrInfo.fdIconID); + for (i = 0; i < 4; ++i) + d_storesw(&pdata, data->u.fil.filFndrInfo.fdUnused[i]); + d_storesw(&pdata, data->u.fil.filFndrInfo.fdComment); + d_storesl(&pdata, data->u.fil.filFndrInfo.fdPutAway); + + d_storeuw(&pdata, data->u.fil.filClpSize); + + for (i = 0; i < 3; ++i) + { + d_storeuw(&pdata, data->u.fil.filExtRec[i].xdrStABN); + d_storeuw(&pdata, data->u.fil.filExtRec[i].xdrNumABlks); + } + + for (i = 0; i < 3; ++i) + { + d_storeuw(&pdata, data->u.fil.filRExtRec[i].xdrStABN); + d_storeuw(&pdata, data->u.fil.filRExtRec[i].xdrNumABlks); + } + + d_storesl(&pdata, data->u.fil.filResrv); + + break; + + case cdrThdRec: + for (i = 0; i < 2; ++i) + d_storesl(&pdata, data->u.dthd.thdResrv[i]); + + d_storeul(&pdata, data->u.dthd.thdParID); + + d_storestr(&pdata, data->u.dthd.thdCName, + sizeof(data->u.dthd.thdCName)); + + break; + + case cdrFThdRec: + for (i = 0; i < 2; ++i) + d_storesl(&pdata, data->u.fthd.fthdResrv[i]); + + d_storeul(&pdata, data->u.fthd.fthdParID); + + d_storestr(&pdata, data->u.fthd.fthdCName, + sizeof(data->u.fthd.fthdCName)); + + break; + + default: + ASSERT(0); + } + + if (len) + *len += pdata - start; +} + +/* + * NAME: record->unpackcatdata() + * DESCRIPTION: unpack catalog record data + */ +void r_unpackcatdata(const byte *pdata, CatDataRec *data) +{ + int i; + + d_fetchsb(&pdata, &data->cdrType); + d_fetchsb(&pdata, &data->cdrResrv2); + + switch (data->cdrType) + { + case cdrDirRec: + d_fetchsw(&pdata, &data->u.dir.dirFlags); + d_fetchuw(&pdata, &data->u.dir.dirVal); + d_fetchul(&pdata, &data->u.dir.dirDirID); + d_fetchsl(&pdata, &data->u.dir.dirCrDat); + d_fetchsl(&pdata, &data->u.dir.dirMdDat); + d_fetchsl(&pdata, &data->u.dir.dirBkDat); + + d_fetchsw(&pdata, &data->u.dir.dirUsrInfo.frRect.top); + d_fetchsw(&pdata, &data->u.dir.dirUsrInfo.frRect.left); + d_fetchsw(&pdata, &data->u.dir.dirUsrInfo.frRect.bottom); + d_fetchsw(&pdata, &data->u.dir.dirUsrInfo.frRect.right); + d_fetchsw(&pdata, &data->u.dir.dirUsrInfo.frFlags); + d_fetchsw(&pdata, &data->u.dir.dirUsrInfo.frLocation.v); + d_fetchsw(&pdata, &data->u.dir.dirUsrInfo.frLocation.h); + d_fetchsw(&pdata, &data->u.dir.dirUsrInfo.frView); + + d_fetchsw(&pdata, &data->u.dir.dirFndrInfo.frScroll.v); + d_fetchsw(&pdata, &data->u.dir.dirFndrInfo.frScroll.h); + d_fetchsl(&pdata, &data->u.dir.dirFndrInfo.frOpenChain); + d_fetchsw(&pdata, &data->u.dir.dirFndrInfo.frUnused); + d_fetchsw(&pdata, &data->u.dir.dirFndrInfo.frComment); + d_fetchsl(&pdata, &data->u.dir.dirFndrInfo.frPutAway); + + for (i = 0; i < 4; ++i) + d_fetchsl(&pdata, &data->u.dir.dirResrv[i]); + + break; + + case cdrFilRec: + d_fetchsb(&pdata, &data->u.fil.filFlags); + d_fetchsb(&pdata, &data->u.fil.filTyp); + + d_fetchsl(&pdata, &data->u.fil.filUsrWds.fdType); + d_fetchsl(&pdata, &data->u.fil.filUsrWds.fdCreator); + d_fetchsw(&pdata, &data->u.fil.filUsrWds.fdFlags); + d_fetchsw(&pdata, &data->u.fil.filUsrWds.fdLocation.v); + d_fetchsw(&pdata, &data->u.fil.filUsrWds.fdLocation.h); + d_fetchsw(&pdata, &data->u.fil.filUsrWds.fdFldr); + + d_fetchul(&pdata, &data->u.fil.filFlNum); + + d_fetchuw(&pdata, &data->u.fil.filStBlk); + d_fetchul(&pdata, &data->u.fil.filLgLen); + d_fetchul(&pdata, &data->u.fil.filPyLen); + + d_fetchuw(&pdata, &data->u.fil.filRStBlk); + d_fetchul(&pdata, &data->u.fil.filRLgLen); + d_fetchul(&pdata, &data->u.fil.filRPyLen); + + d_fetchsl(&pdata, &data->u.fil.filCrDat); + d_fetchsl(&pdata, &data->u.fil.filMdDat); + d_fetchsl(&pdata, &data->u.fil.filBkDat); + + d_fetchsw(&pdata, &data->u.fil.filFndrInfo.fdIconID); + for (i = 0; i < 4; ++i) + d_fetchsw(&pdata, &data->u.fil.filFndrInfo.fdUnused[i]); + d_fetchsw(&pdata, &data->u.fil.filFndrInfo.fdComment); + d_fetchsl(&pdata, &data->u.fil.filFndrInfo.fdPutAway); + + d_fetchuw(&pdata, &data->u.fil.filClpSize); + + for (i = 0; i < 3; ++i) + { + d_fetchuw(&pdata, &data->u.fil.filExtRec[i].xdrStABN); + d_fetchuw(&pdata, &data->u.fil.filExtRec[i].xdrNumABlks); + } + + for (i = 0; i < 3; ++i) + { + d_fetchuw(&pdata, &data->u.fil.filRExtRec[i].xdrStABN); + d_fetchuw(&pdata, &data->u.fil.filRExtRec[i].xdrNumABlks); + } + + d_fetchsl(&pdata, &data->u.fil.filResrv); + + break; + + case cdrThdRec: + for (i = 0; i < 2; ++i) + d_fetchsl(&pdata, &data->u.dthd.thdResrv[i]); + + d_fetchul(&pdata, &data->u.dthd.thdParID); + + d_fetchstr(&pdata, data->u.dthd.thdCName, + sizeof(data->u.dthd.thdCName)); + + break; + + case cdrFThdRec: + for (i = 0; i < 2; ++i) + d_fetchsl(&pdata, &data->u.fthd.fthdResrv[i]); + + d_fetchul(&pdata, &data->u.fthd.fthdParID); + + d_fetchstr(&pdata, data->u.fthd.fthdCName, + sizeof(data->u.fthd.fthdCName)); + + break; + + default: + ASSERT(0); + } +} + +/* + * NAME: record->packextdata() + * DESCRIPTION: pack extent record data + */ +void r_packextdata(const ExtDataRec *data, byte *pdata, unsigned int *len) +{ + const byte *start = pdata; + int i; + + for (i = 0; i < 3; ++i) + { + d_storeuw(&pdata, (*data)[i].xdrStABN); + d_storeuw(&pdata, (*data)[i].xdrNumABlks); + } + + if (len) + *len += pdata - start; +} + +/* + * NAME: record->unpackextdata() + * DESCRIPTION: unpack extent record data + */ +void r_unpackextdata(const byte *pdata, ExtDataRec *data) +{ + int i; + + for (i = 0; i < 3; ++i) + { + d_fetchuw(&pdata, &(*data)[i].xdrStABN); + d_fetchuw(&pdata, &(*data)[i].xdrNumABlks); + } +} + +/* + * NAME: record->makecatkey() + * DESCRIPTION: construct a catalog record key + */ +void r_makecatkey(CatKeyRec *key, unsigned long parid, const char *name) +{ + int len; + + len = strlen(name) + 1; + + key->ckrKeyLen = 0x05 + len + (len & 1); + key->ckrResrv1 = 0; + key->ckrParID = parid; + + strcpy(key->ckrCName, name); +} + +/* + * NAME: record->makeextkey() + * DESCRIPTION: construct an extents record key + */ +void r_makeextkey(ExtKeyRec *key, + int fork, unsigned long fnum, unsigned int fabn) +{ + key->xkrKeyLen = 0x07; + key->xkrFkType = fork; + key->xkrFNum = fnum; + key->xkrFABN = fabn; +} + +/* + * NAME: record->packcatrec() + * DESCRIPTION: create a packed catalog record + */ +void r_packcatrec(const CatKeyRec *key, const CatDataRec *data, + byte *precord, unsigned int *len) +{ + r_packcatkey(key, precord, len); + r_packcatdata(data, HFS_RECDATA(precord), len); +} + +/* + * NAME: record->packextrec() + * DESCRIPTION: create a packed extents record + */ +void r_packextrec(const ExtKeyRec *key, const ExtDataRec *data, + byte *precord, unsigned int *len) +{ + r_packextkey(key, precord, len); + r_packextdata(data, HFS_RECDATA(precord), len); +} + +/* + * NAME: record->packdirent() + * DESCRIPTION: make changes to a catalog record + */ +void r_packdirent(CatDataRec *data, const hfsdirent *ent) +{ + switch (data->cdrType) + { + case cdrDirRec: + data->u.dir.dirCrDat = d_mtime(ent->crdate); + data->u.dir.dirMdDat = d_mtime(ent->mddate); + data->u.dir.dirBkDat = d_mtime(ent->bkdate); + + data->u.dir.dirUsrInfo.frFlags = ent->fdflags; + data->u.dir.dirUsrInfo.frLocation.v = ent->fdlocation.v; + data->u.dir.dirUsrInfo.frLocation.h = ent->fdlocation.h; + + data->u.dir.dirUsrInfo.frRect.top = ent->u.dir.rect.top; + data->u.dir.dirUsrInfo.frRect.left = ent->u.dir.rect.left; + data->u.dir.dirUsrInfo.frRect.bottom = ent->u.dir.rect.bottom; + data->u.dir.dirUsrInfo.frRect.right = ent->u.dir.rect.right; + + break; + + case cdrFilRec: + if (ent->flags & HFS_ISLOCKED) + data->u.fil.filFlags |= (1 << 0); + else + data->u.fil.filFlags &= ~(1 << 0); + + data->u.fil.filCrDat = d_mtime(ent->crdate); + data->u.fil.filMdDat = d_mtime(ent->mddate); + data->u.fil.filBkDat = d_mtime(ent->bkdate); + + data->u.fil.filUsrWds.fdFlags = ent->fdflags; + data->u.fil.filUsrWds.fdLocation.v = ent->fdlocation.v; + data->u.fil.filUsrWds.fdLocation.h = ent->fdlocation.h; + + data->u.fil.filUsrWds.fdType = + d_getsl((const unsigned char *) ent->u.file.type); + data->u.fil.filUsrWds.fdCreator = + d_getsl((const unsigned char *) ent->u.file.creator); + + break; + } +} + +/* + * NAME: record->unpackdirent() + * DESCRIPTION: unpack catalog information into hfsdirent structure + */ +void r_unpackdirent(unsigned long parid, const char *name, + const CatDataRec *data, hfsdirent *ent) +{ + strcpy(ent->name, name); + ent->parid = parid; + + switch (data->cdrType) + { + case cdrDirRec: + ent->flags = HFS_ISDIR; + ent->cnid = data->u.dir.dirDirID; + + ent->crdate = d_ltime(data->u.dir.dirCrDat); + ent->mddate = d_ltime(data->u.dir.dirMdDat); + ent->bkdate = d_ltime(data->u.dir.dirBkDat); + + ent->fdflags = data->u.dir.dirUsrInfo.frFlags; + ent->fdlocation.v = data->u.dir.dirUsrInfo.frLocation.v; + ent->fdlocation.h = data->u.dir.dirUsrInfo.frLocation.h; + + ent->u.dir.valence = data->u.dir.dirVal; + + ent->u.dir.rect.top = data->u.dir.dirUsrInfo.frRect.top; + ent->u.dir.rect.left = data->u.dir.dirUsrInfo.frRect.left; + ent->u.dir.rect.bottom = data->u.dir.dirUsrInfo.frRect.bottom; + ent->u.dir.rect.right = data->u.dir.dirUsrInfo.frRect.right; + + break; + + case cdrFilRec: + ent->flags = (data->u.fil.filFlags & (1 << 0)) ? HFS_ISLOCKED : 0; + ent->cnid = data->u.fil.filFlNum; + + ent->crdate = d_ltime(data->u.fil.filCrDat); + ent->mddate = d_ltime(data->u.fil.filMdDat); + ent->bkdate = d_ltime(data->u.fil.filBkDat); + + ent->fdflags = data->u.fil.filUsrWds.fdFlags; + ent->fdlocation.v = data->u.fil.filUsrWds.fdLocation.v; + ent->fdlocation.h = data->u.fil.filUsrWds.fdLocation.h; + + ent->u.file.dsize = data->u.fil.filLgLen; + ent->u.file.rsize = data->u.fil.filRLgLen; + + d_putsl((unsigned char *) ent->u.file.type, + data->u.fil.filUsrWds.fdType); + d_putsl((unsigned char *) ent->u.file.creator, + data->u.fil.filUsrWds.fdCreator); + + ent->u.file.type[4] = ent->u.file.creator[4] = 0; + + break; + } +} diff --git a/diskimg/libhfs/record.h b/diskimg/libhfs/record.h new file mode 100644 index 0000000..74b3067 --- /dev/null +++ b/diskimg/libhfs/record.h @@ -0,0 +1,47 @@ +/* + * libhfs - library for reading and writing Macintosh HFS volumes + * Copyright (C) 1996-1998 Robert Leslie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + * + * $Id$ + */ + +void r_packcatkey(const CatKeyRec *, byte *, unsigned int *); +void r_unpackcatkey(const byte *, CatKeyRec *); + +void r_packextkey(const ExtKeyRec *, byte *, unsigned int *); +void r_unpackextkey(const byte *, ExtKeyRec *); + +int r_comparecatkeys(const CatKeyRec *, const CatKeyRec *); +int r_compareextkeys(const ExtKeyRec *, const ExtKeyRec *); + +void r_packcatdata(const CatDataRec *, byte *, unsigned int *); +void r_unpackcatdata(const byte *, CatDataRec *); + +void r_packextdata(const ExtDataRec *, byte *, unsigned int *); +void r_unpackextdata(const byte *, ExtDataRec *); + +void r_makecatkey(CatKeyRec *, unsigned long, const char *); +void r_makeextkey(ExtKeyRec *, int, unsigned long, unsigned int); + +void r_packcatrec(const CatKeyRec *, const CatDataRec *, + byte *, unsigned int *); +void r_packextrec(const ExtKeyRec *, const ExtDataRec *, + byte *, unsigned int *); + +void r_packdirent(CatDataRec *, const hfsdirent *); +void r_unpackdirent(unsigned long, const char *, + const CatDataRec *, hfsdirent *); diff --git a/diskimg/libhfs/version.c b/diskimg/libhfs/version.c new file mode 100644 index 0000000..e88e560 --- /dev/null +++ b/diskimg/libhfs/version.c @@ -0,0 +1,29 @@ +/* + * libhfs - library for reading and writing Macintosh HFS volumes + * Copyright (C) 1996-1998 Robert Leslie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + * + * $Id$ + */ + +# include "version.h" + +const char libhfs_rcsid[] = + "$Id$"; + +const char libhfs_version[] = "libhfs version 3.2.6"; +const char libhfs_copyright[] = "Copyright (C) 1996-1998 Robert Leslie"; +const char libhfs_author[] = "Robert Leslie "; diff --git a/diskimg/libhfs/version.h b/diskimg/libhfs/version.h new file mode 100644 index 0000000..8717e07 --- /dev/null +++ b/diskimg/libhfs/version.h @@ -0,0 +1,26 @@ +/* + * libhfs - library for reading and writing Macintosh HFS volumes + * Copyright (C) 1996-1998 Robert Leslie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + * + * $Id$ + */ + +extern const char libhfs_rcsid[]; + +extern const char libhfs_version[]; +extern const char libhfs_copyright[]; +extern const char libhfs_author[]; diff --git a/diskimg/libhfs/volume.c b/diskimg/libhfs/volume.c new file mode 100644 index 0000000..19fd6a5 --- /dev/null +++ b/diskimg/libhfs/volume.c @@ -0,0 +1,1250 @@ +/* + * libhfs - library for reading and writing Macintosh HFS volumes + * Copyright (C) 1996-1998 Robert Leslie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + * + * $Id$ + */ + +# ifdef HAVE_CONFIG_H +# include "config.h" +# endif + +# include +# include +# include +# include +# include +# include /* debug */ + +# include "libhfs.h" +# include "volume.h" +# include "data.h" +# include "block.h" +# include "low.h" +# include "medium.h" +# include "file.h" +# include "btree.h" +# include "record.h" +# include "os.h" + +/* + * NAME: vol->init() + * DESCRIPTION: initialize volume structure + */ +void v_init(hfsvol *vol, int flags) +{ + btree *ext = &vol->ext; + btree *cat = &vol->cat; + + vol->priv = 0; + vol->flags = flags & HFS_VOL_OPT_MASK; + + vol->pnum = -1; + vol->vstart = 0; + vol->vlen = 0; + vol->lpa = 0; + + vol->cache = 0; + + vol->vbm = 0; + vol->vbmsz = 0; + + f_init(&ext->f, vol, HFS_CNID_EXT, "extents overflow"); + + ext->map = 0; + ext->mapsz = 0; + ext->flags = 0; + + ext->keyunpack = (keyunpackfunc) r_unpackextkey; + ext->keycompare = (keycomparefunc) r_compareextkeys; + + f_init(&cat->f, vol, HFS_CNID_CAT, "catalog"); + + cat->map = 0; + cat->mapsz = 0; + cat->flags = 0; + + cat->keyunpack = (keyunpackfunc) r_unpackcatkey; + cat->keycompare = (keycomparefunc) r_comparecatkeys; + + vol->cwd = HFS_CNID_ROOTDIR; + + vol->refs = 0; + vol->files = 0; + vol->dirs = 0; + + vol->prev = 0; + vol->next = 0; +} + +#ifdef CP_NOT_USED +/* + * NAME: vol->open() + * DESCRIPTION: open volume source and lock against concurrent updates + */ +int v_open(hfsvol *vol, const char *path, int mode) +{ + if (vol->flags & HFS_VOL_OPEN) + ERROR(EINVAL, "volume already open"); + + if (os_open(&vol->priv, path, mode) == -1) + goto fail; + + vol->flags |= HFS_VOL_OPEN; + + /* initialize volume block cache (OK to fail) */ + + if (! (vol->flags & HFS_OPT_NOCACHE) && + b_init(vol) != -1) + vol->flags |= HFS_VOL_USINGCACHE; + + return 0; + +fail: + return -1; +} +#endif + +/* + * NAME: vol->opencallback() + * DESCRIPTION: open volume source and lock against concurrent updates + */ +int v_callback_open(hfsvol* vol, oscallback func, void* cookie) +{ + if (vol->flags & HFS_VOL_OPEN) + ERROR(EINVAL, "volume already open"); + + if (os_callback_open(&vol->priv, func, cookie) == -1) + goto fail; + + vol->flags |= HFS_VOL_OPEN; + + /* initialize volume block cache (OK to fail) */ + + if (! (vol->flags & HFS_OPT_NOCACHE) && + b_init(vol) != -1) + vol->flags |= HFS_VOL_USINGCACHE; + + return 0; + +fail: + return -1; +} + +/* + * NAME: flushvol() + * DESCRIPTION: flush all pending changes (B*-tree, MDB, VBM) to volume + */ +static +int flushvol(hfsvol *vol, int umount) +{ + if (vol->flags & HFS_VOL_READONLY) + goto done; + + if ((vol->ext.flags & HFS_BT_UPDATE_HDR) && + bt_writehdr(&vol->ext) == -1) + goto fail; + + if ((vol->cat.flags & HFS_BT_UPDATE_HDR) && + bt_writehdr(&vol->cat) == -1) + goto fail; + + if ((vol->flags & HFS_VOL_UPDATE_VBM) && + v_writevbm(vol) == -1) + goto fail; + + /* + * CiderPress note: this causes the MDB to be written when we are + * unmounting the volume if changes have been made at any point. + * This means we ALWAYS write something when we're closing the disk + * if we touched something. That's not great for us, since we + * might be using removable media. We can "fix" this by removing + * the part that marks the volume as mounted (earlier). + */ + if (umount && ! (vol->mdb.drAtrb & HFS_ATRB_UMOUNTED)) + { + vol->mdb.drAtrb |= HFS_ATRB_UMOUNTED; + vol->flags |= HFS_VOL_UPDATE_MDB; + } + + if ((vol->flags & (HFS_VOL_UPDATE_MDB | HFS_VOL_UPDATE_ALTMDB)) && + v_writemdb(vol) == -1) + goto fail; + +done: + return 0; + +fail: + return -1; +} + +/* + * NAME: vol->flush() + * DESCRIPTION: commit all pending changes to volume device + */ +int v_flush(hfsvol *vol) +{ + if (flushvol(vol, 0) == -1) + goto fail; + + if ((vol->flags & HFS_VOL_USINGCACHE) && + b_flush(vol) == -1) + goto fail; + + return 0; + +fail: + return -1; +} + +/* + * NAME: vol->close() + * DESCRIPTION: close access path to volume source + */ +int v_close(hfsvol *vol) +{ + int result = 0; + + if (! (vol->flags & HFS_VOL_OPEN)) + goto done; + + if ((vol->flags & HFS_VOL_MOUNTED) && + flushvol(vol, 1) == -1) + result = -1; + + if ((vol->flags & HFS_VOL_USINGCACHE) && + b_finish(vol) == -1) + result = -1; + + if (os_close(&vol->priv) == -1) + result = -1; + + vol->flags &= ~(HFS_VOL_OPEN | HFS_VOL_MOUNTED | HFS_VOL_USINGCACHE); + + /* free dynamically allocated structures */ + + FREE(vol->vbm); + + vol->vbm = 0; + vol->vbmsz = 0; + + FREE(vol->ext.map); + FREE(vol->cat.map); + + vol->ext.map = 0; + vol->cat.map = 0; + +done: + return result; +} + +#ifdef CP_NOT_USED +/* + * NAME: vol->same() + * DESCRIPTION: return 1 iff path is same as open volume + */ +int v_same(hfsvol *vol, const char *path) +{ + return os_same(&vol->priv, path); +} +#endif + +/* + * NAME: vol->geometry() + * DESCRIPTION: determine volume location and size (possibly in a partition) + */ +int v_geometry(hfsvol *vol, int pnum) +{ + Partition map; + unsigned long bnum = 0; + int found; + + vol->pnum = pnum; + + if (pnum == 0) + { + vol->vstart = 0; + vol->vlen = b_size(vol); + + if (vol->vlen == 0) + goto fail; + } + else + { + while (pnum--) + { + found = m_findpmentry(vol, "Apple_HFS", &map, &bnum); + if (found == -1 || ! found) + goto fail; + } + + vol->vstart = map.pmPyPartStart; + vol->vlen = map.pmPartBlkCnt; + + if (map.pmDataCnt) + { + if ((unsigned long) map.pmLgDataStart + + (unsigned long) map.pmDataCnt > vol->vlen) + ERROR(EINVAL, "partition data overflows partition"); + + vol->vstart += (unsigned long) map.pmLgDataStart; + vol->vlen = map.pmDataCnt; + } + + if (vol->vlen == 0) + ERROR(EINVAL, "volume partition is empty"); + } + + if (vol->vlen < 800 * (1024 >> HFS_BLOCKSZ_BITS)) + ERROR(EINVAL, "volume is smaller than 800K"); + + return 0; + +fail: + return -1; +} + +/* + * NAME: vol->readmdb() + * DESCRIPTION: load Master Directory Block into memory + */ +int v_readmdb(hfsvol *vol) +{ + if (l_getmdb(vol, &vol->mdb, 0) == -1) + goto fail; + + if (vol->mdb.drSigWord != HFS_SIGWORD) + { + if (vol->mdb.drSigWord == HFS_SIGWORD_MFS) + ERROR(EINVAL, "MFS volume format not supported"); + else + ERROR(EINVAL, "not a Macintosh HFS volume"); + } + + if (vol->mdb.drAlBlkSiz % HFS_BLOCKSZ != 0) + ERROR(EINVAL, "bad volume allocation block size"); + + vol->lpa = vol->mdb.drAlBlkSiz >> HFS_BLOCKSZ_BITS; + + /* extents pseudo-file structs */ + + vol->ext.f.cat.u.fil.filStBlk = vol->mdb.drXTExtRec[0].xdrStABN; + vol->ext.f.cat.u.fil.filLgLen = vol->mdb.drXTFlSize; + vol->ext.f.cat.u.fil.filPyLen = vol->mdb.drXTFlSize; + + vol->ext.f.cat.u.fil.filCrDat = vol->mdb.drCrDate; + vol->ext.f.cat.u.fil.filMdDat = vol->mdb.drLsMod; + + memcpy(&vol->ext.f.cat.u.fil.filExtRec, + &vol->mdb.drXTExtRec, sizeof(ExtDataRec)); + + f_selectfork(&vol->ext.f, fkData); + + /* catalog pseudo-file structs */ + + vol->cat.f.cat.u.fil.filStBlk = vol->mdb.drCTExtRec[0].xdrStABN; + vol->cat.f.cat.u.fil.filLgLen = vol->mdb.drCTFlSize; + vol->cat.f.cat.u.fil.filPyLen = vol->mdb.drCTFlSize; + + vol->cat.f.cat.u.fil.filCrDat = vol->mdb.drCrDate; + vol->cat.f.cat.u.fil.filMdDat = vol->mdb.drLsMod; + + memcpy(&vol->cat.f.cat.u.fil.filExtRec, + &vol->mdb.drCTExtRec, sizeof(ExtDataRec)); + + f_selectfork(&vol->cat.f, fkData); + + return 0; + +fail: + return -1; +} + +/* + * NAME: vol->writemdb() + * DESCRIPTION: flush Master Directory Block to medium + */ +int v_writemdb(hfsvol *vol) +{ + vol->mdb.drLsMod = d_mtime(time(0)); + + vol->mdb.drXTFlSize = vol->ext.f.cat.u.fil.filPyLen; + memcpy(&vol->mdb.drXTExtRec, + &vol->ext.f.cat.u.fil.filExtRec, sizeof(ExtDataRec)); + + vol->mdb.drCTFlSize = vol->cat.f.cat.u.fil.filPyLen; + memcpy(&vol->mdb.drCTExtRec, + &vol->cat.f.cat.u.fil.filExtRec, sizeof(ExtDataRec)); + + if (l_putmdb(vol, &vol->mdb, vol->flags & HFS_VOL_UPDATE_ALTMDB) == -1) + goto fail; + + vol->flags &= ~(HFS_VOL_UPDATE_MDB | HFS_VOL_UPDATE_ALTMDB); + + return 0; + +fail: + return -1; +} + +/* + * NAME: vol->readvbm() + * DESCRIPTION: read volume bitmap into memory + */ +int v_readvbm(hfsvol *vol) +{ + unsigned int vbmst = vol->mdb.drVBMSt; + unsigned int vbmsz = (vol->mdb.drNmAlBlks + 0x0fff) >> 12; + block *bp; + + ASSERT(vol->vbm == 0); + + if (vol->mdb.drAlBlSt - vbmst < vbmsz) + ERROR(EIO, "volume bitmap collides with volume data"); + + vol->vbm = ALLOC(block, vbmsz); + if (vol->vbm == 0) + ERROR(ENOMEM, 0); + + vol->vbmsz = vbmsz; + + for (bp = vol->vbm; vbmsz--; ++bp) + { + if (b_readlb(vol, vbmst++, bp) == -1) + goto fail; + } + + return 0; + +fail: + FREE(vol->vbm); + + vol->vbm = 0; + vol->vbmsz = 0; + + return -1; +} + +/* + * NAME: vol->writevbm() + * DESCRIPTION: flush volume bitmap to medium + */ +int v_writevbm(hfsvol *vol) +{ + unsigned int vbmst = vol->mdb.drVBMSt; + unsigned int vbmsz = vol->vbmsz; + const block *bp; + + for (bp = vol->vbm; vbmsz--; ++bp) + { + if (b_writelb(vol, vbmst++, bp) == -1) + goto fail; + } + + vol->flags &= ~HFS_VOL_UPDATE_VBM; + + return 0; + +fail: + return -1; +} + +/* + * NAME: vol->mount() + * DESCRIPTION: load volume information into memory + */ +int v_mount(hfsvol *vol) +{ + /* read the MDB, volume bitmap, and extents/catalog B*-tree headers */ + + if (v_readmdb(vol) == -1 || + v_readvbm(vol) == -1 || + bt_readhdr(&vol->ext) == -1 || + bt_readhdr(&vol->cat) == -1) + goto fail; + + if (! (vol->mdb.drAtrb & HFS_ATRB_UMOUNTED) && + v_scavenge(vol) == -1) + goto fail; + + if (vol->mdb.drAtrb & HFS_ATRB_SLOCKED) + vol->flags |= HFS_VOL_READONLY; + else if (vol->flags & HFS_VOL_READONLY) + vol->mdb.drAtrb |= HFS_ATRB_HLOCKED; + else + vol->mdb.drAtrb &= ~HFS_ATRB_HLOCKED; + + vol->flags |= HFS_VOL_MOUNTED; + + return 0; + +fail: + return -1; +} + +/* + * NAME: vol->dirty() + * DESCRIPTION: ensure the volume is marked "in use" before we make changes + */ +int v_dirty(hfsvol *vol) +{ +#ifdef NOT_FOR_CP // see notes in flushvol() + if (vol->mdb.drAtrb & HFS_ATRB_UMOUNTED) + { + vol->mdb.drAtrb &= ~HFS_ATRB_UMOUNTED; + ++vol->mdb.drWrCnt; + + if (v_writemdb(vol) == -1) + goto fail; + + if ((vol->flags & HFS_VOL_USINGCACHE) && + b_flush(vol) == -1) + goto fail; + } +#endif + + return 0; + +fail: + return -1; +} + +/* + * NAME: vol->catsearch() + * DESCRIPTION: search catalog tree + */ +int v_catsearch(hfsvol *vol, unsigned long parid, const char *name, + CatDataRec *data, char *cname, node *np) +{ + CatKeyRec key; + byte pkey[HFS_CATKEYLEN]; + const byte *ptr; + node n; + int found; + + if (np == 0) + np = &n; + + r_makecatkey(&key, parid, name); + r_packcatkey(&key, pkey, 0); + + found = bt_search(&vol->cat, pkey, np); + if (found <= 0) + return found; + + ptr = HFS_NODEREC(*np, np->rnum); + + if (cname) + { + r_unpackcatkey(ptr, &key); + strcpy(cname, key.ckrCName); + } + + if (data) + r_unpackcatdata(HFS_RECDATA(ptr), data); + + return 1; +} + +/* + * NAME: vol->extsearch() + * DESCRIPTION: search extents tree + */ +int v_extsearch(hfsfile *file, unsigned int fabn, + ExtDataRec *data, node *np) +{ + ExtKeyRec key; + ExtDataRec extsave; + unsigned int fabnsave; + byte pkey[HFS_EXTKEYLEN]; + const byte *ptr; + node n; + int found; + + if (np == 0) + np = &n; + + r_makeextkey(&key, file->fork, file->cat.u.fil.filFlNum, fabn); + r_packextkey(&key, pkey, 0); + + /* in case bt_search() clobbers these */ + + memcpy(&extsave, &file->ext, sizeof(ExtDataRec)); + fabnsave = file->fabn; + + found = bt_search(&file->vol->ext, pkey, np); + + memcpy(&file->ext, &extsave, sizeof(ExtDataRec)); + file->fabn = fabnsave; + + if (found <= 0) + return found; + + if (data) + { + ptr = HFS_NODEREC(*np, np->rnum); + r_unpackextdata(HFS_RECDATA(ptr), data); + } + + return 1; +} + +/* + * NAME: vol->getthread() + * DESCRIPTION: retrieve catalog thread information for a file or directory + */ +int v_getthread(hfsvol *vol, unsigned long id, + CatDataRec *thread, node *np, int type) +{ + CatDataRec rec; + int found; + + if (thread == 0) + thread = &rec; + + found = v_catsearch(vol, id, "", thread, 0, np); + if (found == 1 && thread->cdrType != type) + ERROR(EIO, "bad thread record"); + + return found; + +fail: + return -1; +} + +/* + * NAME: vol->putcatrec() + * DESCRIPTION: store catalog information + */ +int v_putcatrec(const CatDataRec *data, node *np) +{ + byte pdata[HFS_CATDATALEN], *ptr; + unsigned int len = 0; + + r_packcatdata(data, pdata, &len); + + ptr = HFS_NODEREC(*np, np->rnum); + memcpy(HFS_RECDATA(ptr), pdata, len); + + return bt_putnode(np); +} + +/* + * NAME: vol->putextrec() + * DESCRIPTION: store extent information + */ +int v_putextrec(const ExtDataRec *data, node *np) +{ + byte pdata[HFS_EXTDATALEN], *ptr; + unsigned int len = 0; + + r_packextdata(data, pdata, &len); + + ptr = HFS_NODEREC(*np, np->rnum); + memcpy(HFS_RECDATA(ptr), pdata, len); + + return bt_putnode(np); +} + +/* + * NAME: vol->allocblocks() + * DESCRIPTION: allocate a contiguous range of blocks + */ +int v_allocblocks(hfsvol *vol, ExtDescriptor *blocks) +{ + unsigned int request, found, foundat, start, end; + register unsigned int pt; + block *vbm; + int wrap = 0; + + if (vol->mdb.drFreeBks == 0) + ERROR(ENOSPC, "volume full"); + + request = blocks->xdrNumABlks; + found = 0; + foundat = 0; + start = vol->mdb.drAllocPtr; + end = vol->mdb.drNmAlBlks; + vbm = vol->vbm; + + ASSERT(request > 0); + + /* backtrack the start pointer to recover unused space */ + + if (! BMTST(vbm, start)) + { + while (start > 0 && ! BMTST(vbm, start - 1)) + --start; + } + + /* find largest unused block which satisfies request */ + + pt = start; + + while (1) + { + unsigned int mark; + + /* skip blocks in use */ + + while (pt < end && BMTST(vbm, pt)) + ++pt; + + if (wrap && pt >= start) + break; + + /* count blocks not in use */ + + mark = pt; + while (pt < end && pt - mark < request && ! BMTST(vbm, pt)) + ++pt; + + if (pt - mark > found) + { + found = pt - mark; + foundat = mark; + } + + if (wrap && pt >= start) + break; + + if (pt == end) + pt = 0, wrap = 1; + + if (found == request) + break; + } + + if (found == 0 || found > vol->mdb.drFreeBks) + ERROR(EIO, "bad volume bitmap or free block count"); + + blocks->xdrStABN = foundat; + blocks->xdrNumABlks = found; + + if (v_dirty(vol) == -1) + goto fail; + + vol->mdb.drAllocPtr = pt; + vol->mdb.drFreeBks -= found; + + for (pt = foundat; pt < foundat + found; ++pt) + BMSET(vbm, pt); + + vol->flags |= HFS_VOL_UPDATE_MDB | HFS_VOL_UPDATE_VBM; + + if (vol->flags & HFS_OPT_ZERO) + { + block b; + unsigned int i; + + memset(&b, 0, sizeof(b)); + + for (pt = foundat; pt < foundat + found; ++pt) + { + for (i = 0; i < vol->lpa; ++i) + b_writeab(vol, pt, i, &b); + } + } + + return 0; + +fail: + return -1; +} + +/* + * NAME: vol->freeblocks() + * DESCRIPTION: deallocate a contiguous range of blocks + */ +int v_freeblocks(hfsvol *vol, const ExtDescriptor *blocks) +{ + unsigned int start, len, pt; + block *vbm; + + start = blocks->xdrStABN; + len = blocks->xdrNumABlks; + vbm = vol->vbm; + + if (v_dirty(vol) == -1) + goto fail; + + vol->mdb.drFreeBks += len; + + for (pt = start; pt < start + len; ++pt) + BMCLR(vbm, pt); + + vol->flags |= HFS_VOL_UPDATE_MDB | HFS_VOL_UPDATE_VBM; + + return 0; + +fail: + return -1; +} + +/* + * NAME: vol->resolve() + * DESCRIPTION: translate a pathname; return catalog information + */ +int v_resolve(hfsvol **vol, const char *path, + CatDataRec *data, long *parid, char *fname, node *np) +{ + unsigned long dirid; + char name[HFS_MAX_FLEN + 1], *nptr; + int found = 0; + + if (*path == 0) + ERROR(ENOENT, "empty path"); + + if (parid) + *parid = 0; + + nptr = strchr(path, ':'); + + if (*path == ':' || nptr == 0) + { + dirid = (*vol)->cwd; /* relative path */ + + if (*path == ':') + ++path; + + if (*path == 0) + { + found = v_getdthread(*vol, dirid, data, 0); + if (found == -1) + goto fail; + + if (found) + { + if (parid) + *parid = data->u.dthd.thdParID; + + found = v_catsearch(*vol, data->u.dthd.thdParID, + data->u.dthd.thdCName, data, fname, np); + if (found == -1) + goto fail; + } + + goto done; + } + } + else + { +#ifdef CP_NO_STATIC + hfsvol *check; +#endif + + dirid = HFS_CNID_ROOTPAR; /* absolute path */ + + if (nptr - path > HFS_MAX_VLEN) + ERROR(ENAMETOOLONG, 0); + + strncpy(name, path, nptr - path); + name[nptr - path] = 0; + +#ifdef CP_NO_STATIC + for (check = hfs_mounts; check; check = check->next) + { + if (d_relstring(check->mdb.drVN, name) == 0) + { + *vol = check; + break; + } + } +#else + assert(*vol != 0); +#endif + } + + while (1) + { + while (*path == ':') + { + ++path; + + found = v_getdthread(*vol, dirid, data, 0); + if (found == -1) + goto fail; + else if (! found) + goto done; + + dirid = data->u.dthd.thdParID; + } + + if (*path == 0) + { + found = v_getdthread(*vol, dirid, data, 0); + if (found == -1) + goto fail; + + if (found) + { + if (parid) + *parid = data->u.dthd.thdParID; + + found = v_catsearch(*vol, data->u.dthd.thdParID, + data->u.dthd.thdCName, data, fname, np); + if (found == -1) + goto fail; + } + + goto done; + } + + nptr = name; + while (nptr < name + sizeof(name) - 1 && *path && *path != ':') + *nptr++ = *path++; + + if (*path && *path != ':') + ERROR(ENAMETOOLONG, 0); + + *nptr = 0; + if (*path == ':') + ++path; + + if (parid) + *parid = dirid; + + found = v_catsearch(*vol, dirid, name, data, fname, np); + if (found == -1) + goto fail; + + if (! found) + { + if (*path && parid) + *parid = 0; + + if (*path == 0 && fname) + strcpy(fname, name); + + goto done; + } + + switch (data->cdrType) + { + case cdrDirRec: + if (*path == 0) + goto done; + + dirid = data->u.dir.dirDirID; + break; + + case cdrFilRec: + if (*path == 0) + goto done; + + ERROR(ENOTDIR, "invalid pathname"); + + default: + ERROR(EIO, "unexpected catalog record"); + } + } + +done: + return found; + +fail: + return -1; +} + +/* + * NAME: vol->adjvalence() + * DESCRIPTION: update a volume's valence counts + */ +int v_adjvalence(hfsvol *vol, unsigned long parid, int isdir, int adj) +{ + node n; + CatDataRec data; + int result = 0; + + if (isdir) + vol->mdb.drDirCnt += adj; + else + vol->mdb.drFilCnt += adj; + + vol->flags |= HFS_VOL_UPDATE_MDB; + + if (parid == HFS_CNID_ROOTDIR) + { + if (isdir) + vol->mdb.drNmRtDirs += adj; + else + vol->mdb.drNmFls += adj; + } + else if (parid == HFS_CNID_ROOTPAR) + goto done; + + if (v_getdthread(vol, parid, &data, 0) <= 0 || + v_catsearch(vol, data.u.dthd.thdParID, data.u.dthd.thdCName, + &data, 0, &n) <= 0 || + data.cdrType != cdrDirRec) + ERROR(EIO, "can't find parent directory"); + + data.u.dir.dirVal += adj; + data.u.dir.dirMdDat = d_mtime(time(0)); + + result = v_putcatrec(&data, &n); + +done: + return result; + +fail: + return -1; +} + +/* + * NAME: vol->mkdir() + * DESCRIPTION: create a new HFS directory + */ +int v_mkdir(hfsvol *vol, unsigned long parid, const char *name) +{ + CatKeyRec key; + CatDataRec data; + unsigned long id; + byte record[HFS_MAX_CATRECLEN]; + unsigned int reclen; + int i; + + if (bt_space(&vol->cat, 2) == -1) + goto fail; + + id = vol->mdb.drNxtCNID++; + vol->flags |= HFS_VOL_UPDATE_MDB; + + /* create directory record */ + + data.cdrType = cdrDirRec; + data.cdrResrv2 = 0; + + data.u.dir.dirFlags = 0; + data.u.dir.dirVal = 0; + data.u.dir.dirDirID = id; + data.u.dir.dirCrDat = d_mtime(time(0)); + data.u.dir.dirMdDat = data.u.dir.dirCrDat; + data.u.dir.dirBkDat = 0; + + memset(&data.u.dir.dirUsrInfo, 0, sizeof(data.u.dir.dirUsrInfo)); + memset(&data.u.dir.dirFndrInfo, 0, sizeof(data.u.dir.dirFndrInfo)); + for (i = 0; i < 4; ++i) + data.u.dir.dirResrv[i] = 0; + + r_makecatkey(&key, parid, name); + r_packcatrec(&key, &data, record, &reclen); + + if (bt_insert(&vol->cat, record, reclen) == -1) + goto fail; + + /* create thread record */ + + data.cdrType = cdrThdRec; + data.cdrResrv2 = 0; + + data.u.dthd.thdResrv[0] = 0; + data.u.dthd.thdResrv[1] = 0; + data.u.dthd.thdParID = parid; + strcpy(data.u.dthd.thdCName, name); + + r_makecatkey(&key, id, ""); + r_packcatrec(&key, &data, record, &reclen); + + if (bt_insert(&vol->cat, record, reclen) == -1 || + v_adjvalence(vol, parid, 1, 1) == -1) + goto fail; + + return 0; + +fail: + return -1; +} + +/* + * NAME: markexts() + * DESCRIPTION: set bits from an extent record in the volume bitmap + */ +static +void markexts(block *vbm, const ExtDataRec *exts) +{ + int i; + unsigned int pt, len; + + for (i = 0; i < 3; ++i) + { + for ( pt = (*exts)[i].xdrStABN, + len = (*exts)[i].xdrNumABlks; len--; ++pt) + BMSET(vbm, pt); + } +} + +/* + * NAME: vol->scavenge() + * DESCRIPTION: safeguard blocks in the volume bitmap + */ +int v_scavenge(hfsvol *vol) +{ + block *vbm = vol->vbm; + node n; + unsigned int pt, blks; + unsigned long lastcnid = 15; + +# ifdef DEBUG + fprintf(stderr, "VOL: \"%s\" not cleanly unmounted\n", + vol->mdb.drVN); +# endif + + if (vol->flags & HFS_VOL_READONLY) + goto done; + +# ifdef DEBUG + fprintf(stderr, "VOL: scavenging...\n"); +# endif + + /* reset MDB by marking it dirty again */ + + vol->mdb.drAtrb |= HFS_ATRB_UMOUNTED; + if (v_dirty(vol) == -1) + goto fail; + + /* begin by marking extents in MDB */ + + markexts(vbm, &vol->mdb.drXTExtRec); + markexts(vbm, &vol->mdb.drCTExtRec); + + vol->flags |= HFS_VOL_UPDATE_VBM; + + /* scavenge the extents overflow file */ + + if (vol->ext.hdr.bthFNode > 0) + { + if (bt_getnode(&n, &vol->ext, vol->ext.hdr.bthFNode) == -1) + goto fail; + + n.rnum = 0; + + while (1) + { + ExtDataRec data; + const byte *ptr; + + while (n.rnum >= n.nd.ndNRecs && n.nd.ndFLink > 0) + { + if (bt_getnode(&n, &vol->ext, n.nd.ndFLink) == -1) + goto fail; + + n.rnum = 0; + } + + if (n.rnum >= n.nd.ndNRecs && n.nd.ndFLink == 0) + break; + + ptr = HFS_NODEREC(n, n.rnum); + r_unpackextdata(HFS_RECDATA(ptr), &data); + + markexts(vbm, &data); + + ++n.rnum; + } + } + + /* scavenge the catalog file */ + + if (vol->cat.hdr.bthFNode > 0) + { + if (bt_getnode(&n, &vol->cat, vol->cat.hdr.bthFNode) == -1) + goto fail; + + n.rnum = 0; + + while (1) + { + CatDataRec data; + const byte *ptr; + + while (n.rnum >= n.nd.ndNRecs && n.nd.ndFLink > 0) + { + if (bt_getnode(&n, &vol->cat, n.nd.ndFLink) == -1) + goto fail; + + n.rnum = 0; + } + + if (n.rnum >= n.nd.ndNRecs && n.nd.ndFLink == 0) + break; + + ptr = HFS_NODEREC(n, n.rnum); + r_unpackcatdata(HFS_RECDATA(ptr), &data); + + switch (data.cdrType) + { + case cdrFilRec: + markexts(vbm, &data.u.fil.filExtRec); + markexts(vbm, &data.u.fil.filRExtRec); + + if (data.u.fil.filFlNum > lastcnid) + lastcnid = data.u.fil.filFlNum; + break; + + case cdrDirRec: + if (data.u.dir.dirDirID > lastcnid) + lastcnid = data.u.dir.dirDirID; + break; + } + + ++n.rnum; + } + } + + /* count free blocks */ + + for (blks = 0, pt = vol->mdb.drNmAlBlks; pt--; ) + { + if (! BMTST(vbm, pt)) + ++blks; + } + + if (vol->mdb.drFreeBks != blks) + { +# ifdef DEBUG + fprintf(stderr, "VOL: updating free blocks from %u to %u\n", + vol->mdb.drFreeBks, blks); +# endif + + vol->mdb.drFreeBks = blks; + vol->flags |= HFS_VOL_UPDATE_MDB; + } + + /* ensure next CNID is sane */ + + if ((unsigned long) vol->mdb.drNxtCNID <= lastcnid) + { +# ifdef DEBUG + fprintf(stderr, "VOL: updating next CNID from %lu to %lu\n", + vol->mdb.drNxtCNID, lastcnid + 1); +# endif + + vol->mdb.drNxtCNID = lastcnid + 1; + vol->flags |= HFS_VOL_UPDATE_MDB; + } + +# ifdef DEBUG + fprintf(stderr, "VOL: scavenging complete\n"); +# endif + +done: + return 0; + +fail: + return -1; +} diff --git a/diskimg/libhfs/volume.h b/diskimg/libhfs/volume.h new file mode 100644 index 0000000..c5ded74 --- /dev/null +++ b/diskimg/libhfs/volume.h @@ -0,0 +1,67 @@ +/* + * libhfs - library for reading and writing Macintosh HFS volumes + * Copyright (C) 1996-1998 Robert Leslie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + * + * $Id$ + */ + +void v_init(hfsvol *, int); + +#ifdef CP_NOT_USED +int v_open(hfsvol *, const char *, int); +#endif +int v_callback_open(hfsvol *, oscallback, void*); +int v_flush(hfsvol *); +int v_close(hfsvol *); + +#ifdef CP_NOT_USED +int v_same(hfsvol *, const char *); +#endif +int v_geometry(hfsvol *, int); + +int v_readmdb(hfsvol *); +int v_writemdb(hfsvol *); + +int v_readvbm(hfsvol *); +int v_writevbm(hfsvol *); + +int v_mount(hfsvol *); +int v_dirty(hfsvol *); + +int v_catsearch(hfsvol *, unsigned long, const char *, + CatDataRec *, char *, node *); +int v_extsearch(hfsfile *, unsigned int, ExtDataRec *, node *); + +int v_getthread(hfsvol *, unsigned long, CatDataRec *, node *, int); + +# define v_getdthread(vol, id, thread, np) \ + v_getthread(vol, id, thread, np, cdrThdRec) +# define v_getfthread(vol, id, thread, np) \ + v_getthread(vol, id, thread, np, cdrFThdRec) + +int v_putcatrec(const CatDataRec *, node *); +int v_putextrec(const ExtDataRec *, node *); + +int v_allocblocks(hfsvol *, ExtDescriptor *); +int v_freeblocks(hfsvol *, const ExtDescriptor *); + +int v_resolve(hfsvol **, const char *, CatDataRec *, long *, char *, node *); + +int v_adjvalence(hfsvol *, unsigned long, int, int); +int v_mkdir(hfsvol *, unsigned long, const char *); + +int v_scavenge(hfsvol *); diff --git a/linux/Convert.cpp b/linux/Convert.cpp new file mode 100644 index 0000000..758faad --- /dev/null +++ b/linux/Convert.cpp @@ -0,0 +1,491 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Convert from one image format to another. + */ +#include +#include +#include +#include +#include +#include "../diskimg/DiskImg.h" +#include "../prebuilt/NufxLib.h" + +using namespace DiskImgLib; + +#define nil NULL +#define ASSERT assert +#define NELEM(x) (sizeof(x) / sizeof((x)[0])) + +FILE* gLog = nil; +pid_t gPid = getpid(); + + +/* + * Handle a debug message from the DiskImg library. + */ +/*static*/ void +MsgHandler(const char* file, int line, const char* msg) +{ + ASSERT(file != nil); + ASSERT(msg != nil); + + fprintf(gLog, "%05u %s", gPid, msg); +} +/* + * Handle a global error message from the NufxLib library by shoving it + * through the DiskImgLib message function. + */ +NuResult +NufxErrorMsgHandler(NuArchive* /*pArchive*/, void* vErrorMessage) +{ + const NuErrorMessage* pErrorMessage = (const NuErrorMessage*) vErrorMessage; + + if (pErrorMessage->isDebug) { + Global::PrintDebugMsg(pErrorMessage->file, pErrorMessage->line, + " [D] %s\n", pErrorMessage->message); + } else { + Global::PrintDebugMsg(pErrorMessage->file, pErrorMessage->line, + " %s\n", pErrorMessage->message); + } + + return kNuOK; +} + +/* + * Convert one disk image to another. + */ +DIError +Convert(const char* infile, const char* outfile) +{ + DIError dierr = kDIErrNone; + DiskImg srcImg, dstImg; + const char* storageName = nil; + + printf("Converting in='%s' out='%s'\n", infile, outfile); + + /* + * Prepare the source image. + */ + dierr = srcImg.OpenImage(infile, '/', true); + if (dierr != kDIErrNone) { + fprintf(stderr, "Unable to open disk image: %s.\n", + DiskImgLib::DIStrError(dierr)); + goto bail; + } + + dierr = srcImg.AnalyzeImage(); + if (dierr != kDIErrNone) { + fprintf(stderr, "Unable to determine source image format.\n"); + goto bail; + } + + if (!srcImg.GetHasBlocks() && !srcImg.GetHasSectors()) { + /* add nibble tracks someday */ + fprintf(stderr, + "Sorry, only block- or sector-addressable images allowed.\n"); + dierr = kDIErrUnsupportedPhysicalFmt; + goto bail; + } + if (srcImg.GetHasBlocks()) { + assert(srcImg.GetNumBlocks() > 0); + } else { + assert(srcImg.GetNumTracks() > 0); + } + + if (srcImg.GetSectorOrder() == DiskImg::kSectorOrderUnknown) { + fprintf(stderr, "(QUERY) don't know sector order\n"); + dierr = kDIErrFilesystemNotFound; + goto bail; + } + + storageName = "MyHappyDisk"; + + /* force the access to be ProDOS-ordered */ + dierr = srcImg.OverrideFormat(srcImg.GetPhysicalFormat(), + DiskImg::kFormatGenericProDOSOrd, srcImg.GetSectorOrder()); + if (dierr != kDIErrNone) { + fprintf(stderr, "Couldn't switch to generic ProDOS: %s.\n", + DiskImgLib::DIStrError(dierr)); + goto bail; + } + + /* transfer the DOS volume num, if one was set */ + printf("DOS volume number set to %d\n", srcImg.GetDOSVolumeNum()); + dstImg.SetDOSVolumeNum(srcImg.GetDOSVolumeNum()); + + const DiskImg::NibbleDescr* pNibbleDescr; + pNibbleDescr = nil; + + /* + * Prepare the destination image. + * + * We *always* use DiskImg::kFormatGenericProDOSOrd here, because it + * must match up with what we selected above. + * + * We could enable "skipFormat" on all of these but the nibble images, + * but we go ahead and set it to "false" on all of them just for fun. + */ + switch (18) { + case 0: + /* 16-sector nibble image, by blocks */ + pNibbleDescr= DiskImg::GetStdNibbleDescr(DiskImg::kNibbleDescrDOS33Std); + dierr = dstImg.CreateImage(outfile, storageName, + DiskImg::kOuterFormatNone, + DiskImg::kFileFormatUnadorned, + DiskImg::kPhysicalFormatNib525_6656, + pNibbleDescr, + DiskImg::kSectorOrderPhysical, + DiskImg::kFormatGenericProDOSOrd, + srcImg.GetNumBlocks(), + false); + break; + case 1: + /* 16-sector nibble image, by tracks/sectors */ + pNibbleDescr= DiskImg::GetStdNibbleDescr(DiskImg::kNibbleDescrDOS33Std); + dierr = dstImg.CreateImage(outfile, storageName, + DiskImg::kOuterFormatNone, + DiskImg::kFileFormatUnadorned, + DiskImg::kPhysicalFormatNib525_6656, + pNibbleDescr, + DiskImg::kSectorOrderPhysical, + DiskImg::kFormatGenericProDOSOrd, + 35, 16, + false); + break; + case 2: + /* 16-sector NB2 nibble image, by tracks/sectors */ + pNibbleDescr= DiskImg::GetStdNibbleDescr(DiskImg::kNibbleDescrDOS33Std); + dierr = dstImg.CreateImage(outfile, storageName, + DiskImg::kOuterFormatNone, + DiskImg::kFileFormatUnadorned, + DiskImg::kPhysicalFormatNib525_6384, + pNibbleDescr, + DiskImg::kSectorOrderPhysical, + DiskImg::kFormatGenericProDOSOrd, + 35, 16, + false); + break; + case 3: + /* 13-sector nibble image, by tracks/sectors */ + pNibbleDescr= DiskImg::GetStdNibbleDescr(DiskImg::kNibbleDescrDOS32Std); + dierr = dstImg.CreateImage(outfile, storageName, + DiskImg::kOuterFormatNone, + DiskImg::kFileFormatUnadorned, + DiskImg::kPhysicalFormatNib525_6656, + pNibbleDescr, + DiskImg::kSectorOrderPhysical, + DiskImg::kFormatGenericProDOSOrd, + 35, 13, + false); + break; + case 4: + /* 16-sector nb2 image, by tracks/sectors */ + pNibbleDescr= DiskImg::GetStdNibbleDescr(DiskImg::kNibbleDescrDOS33Std); + dierr = dstImg.CreateImage(outfile, storageName, + DiskImg::kOuterFormatNone, + DiskImg::kFileFormatUnadorned, + DiskImg::kPhysicalFormatNib525_6384, + pNibbleDescr, + DiskImg::kSectorOrderPhysical, + DiskImg::kFormatGenericProDOSOrd, + 35, 16, + false); + break; + case 5: + /* sector image, by blocks, ProDOS order */ + dierr = dstImg.CreateImage(outfile, storageName, + DiskImg::kOuterFormatNone, + DiskImg::kFileFormatUnadorned, + DiskImg::kPhysicalFormatSectors, + pNibbleDescr, + DiskImg::kSectorOrderProDOS, + DiskImg::kFormatGenericProDOSOrd, + srcImg.GetNumBlocks(), + false); + break; + case 6: + /* sector image, by blocks, DOS order */ + dierr = dstImg.CreateImage(outfile, storageName, + DiskImg::kOuterFormatNone, + DiskImg::kFileFormatUnadorned, + DiskImg::kPhysicalFormatSectors, + pNibbleDescr, + DiskImg::kSectorOrderDOS, + DiskImg::kFormatGenericProDOSOrd, + srcImg.GetNumBlocks(), + false); + break; + case 7: + /* sector image, by blocks, ProDOS order, Sim2e */ + dierr = dstImg.CreateImage(outfile, storageName, + DiskImg::kOuterFormatNone, + DiskImg::kFileFormatSim2eHDV, + DiskImg::kPhysicalFormatSectors, + pNibbleDescr, + DiskImg::kSectorOrderProDOS, + DiskImg::kFormatGenericProDOSOrd, + srcImg.GetNumBlocks(), + false); + break; + case 8: + /* odd-length HUGE sector image, by blocks */ + dierr = dstImg.CreateImage(outfile, storageName, + DiskImg::kOuterFormatNone, + DiskImg::kFileFormatUnadorned, + DiskImg::kPhysicalFormatSectors, + pNibbleDescr, + DiskImg::kSectorOrderProDOS, + DiskImg::kFormatGenericProDOSOrd, + 65535, + false); + break; + case 9: + /* sector image, by blocks, physical order, with gzip */ + dierr = dstImg.CreateImage(outfile, storageName, + DiskImg::kOuterFormatGzip, + DiskImg::kFileFormatUnadorned, + DiskImg::kPhysicalFormatSectors, + pNibbleDescr, + DiskImg::kSectorOrderPhysical, + DiskImg::kFormatGenericProDOSOrd, + srcImg.GetNumBlocks(), + false); + break; + case 10: + /* sector image, by blocks, ProDOS order, with gzip */ + dierr = dstImg.CreateImage(outfile, storageName, + DiskImg::kOuterFormatGzip, + DiskImg::kFileFormatSim2eHDV, + DiskImg::kPhysicalFormatSectors, + pNibbleDescr, + DiskImg::kSectorOrderProDOS, + DiskImg::kFormatGenericProDOSOrd, + srcImg.GetNumBlocks(), + false); + break; + case 11: + /* sector image, by blocks, ProDOS order, 2MG */ + dierr = dstImg.CreateImage(outfile, storageName, + DiskImg::kOuterFormatNone, + DiskImg::kFileFormat2MG, + DiskImg::kPhysicalFormatSectors, + pNibbleDescr, + DiskImg::kSectorOrderProDOS, + DiskImg::kFormatGenericProDOSOrd, + srcImg.GetNumBlocks(), + false); + break; + case 12: + /* 16-sector nibble image, by tracks/sectors, 2MG */ + pNibbleDescr= DiskImg::GetStdNibbleDescr(DiskImg::kNibbleDescrDOS33Std); + dierr = dstImg.CreateImage(outfile, storageName, + DiskImg::kOuterFormatNone, + DiskImg::kFileFormat2MG, + DiskImg::kPhysicalFormatNib525_6656, + pNibbleDescr, + DiskImg::kSectorOrderPhysical, + DiskImg::kFormatGenericProDOSOrd, + 35, 16, + false); + break; + case 13: + /* 16-sector nibble image, by tracks/sectors, 2MG, gzip */ + pNibbleDescr= DiskImg::GetStdNibbleDescr(DiskImg::kNibbleDescrDOS33Std); + dierr = dstImg.CreateImage(outfile, storageName, + DiskImg::kOuterFormatGzip, + DiskImg::kFileFormat2MG, + DiskImg::kPhysicalFormatNib525_6656, + pNibbleDescr, + DiskImg::kSectorOrderPhysical, + DiskImg::kFormatGenericProDOSOrd, + 35, 16, + false); + break; + case 14: + /* sector image, by blocks, for DC42 (800K only) */ + dierr = dstImg.CreateImage(outfile, storageName, + DiskImg::kOuterFormatNone, + DiskImg::kFileFormatDiskCopy42, + DiskImg::kPhysicalFormatSectors, + pNibbleDescr, + DiskImg::kSectorOrderProDOS, + DiskImg::kFormatGenericProDOSOrd, + srcImg.GetNumBlocks(), + false); + break; + case 15: + /* sector image, by blocks, for NuFX */ + dierr = dstImg.CreateImage(outfile, storageName, + DiskImg::kOuterFormatNone, + DiskImg::kFileFormatNuFX, + DiskImg::kPhysicalFormatSectors, + pNibbleDescr, + DiskImg::kSectorOrderProDOS, + DiskImg::kFormatGenericProDOSOrd, + srcImg.GetNumBlocks(), + false); + break; + case 16: + /* sector image, by blocks, for DDD */ + dierr = dstImg.CreateImage(outfile, storageName, + DiskImg::kOuterFormatNone, + DiskImg::kFileFormatDDD, + DiskImg::kPhysicalFormatSectors, + pNibbleDescr, + DiskImg::kSectorOrderDOS, + DiskImg::kFormatGenericProDOSOrd, + srcImg.GetNumBlocks(), + false); + break; + case 17: + /* sector image, by blocks, ProDOS order, stored in ZIP (.po.zip) */ + dierr = dstImg.CreateImage(outfile, storageName, + DiskImg::kOuterFormatZip, + DiskImg::kFileFormatUnadorned, + DiskImg::kPhysicalFormatSectors, + pNibbleDescr, + DiskImg::kSectorOrderProDOS, + DiskImg::kFormatGenericProDOSOrd, + srcImg.GetNumBlocks(), + false); + break; + case 18: + /* 13-sector nibble image, by tracks/sectors */ + pNibbleDescr= DiskImg::GetStdNibbleDescr(DiskImg::kNibbleDescrDOS33Std); + dierr = dstImg.CreateImage(outfile, storageName, + DiskImg::kOuterFormatNone, + DiskImg::kFileFormatUnadorned, + DiskImg::kPhysicalFormatSectors, + pNibbleDescr, + DiskImg::kSectorOrderProDOS, + DiskImg::kFormatGenericProDOSOrd, + 35, 13, + false); + break; + default: + fprintf(stderr, "UNEXPECTED NUMBER\n"); + abort(); + } + if (dierr != kDIErrNone) { + fprintf(stderr, "Couldn't create new image file '%s': %s.\n", + outfile, DiskImgLib::DIStrError(dierr)); + goto bail; + } + + /* + * Copy blocks or sectors from source to destination. + */ + if (srcImg.GetHasBlocks()) { + int numBlocks; + numBlocks = srcImg.GetNumBlocks(); + if (dstImg.GetNumBlocks() < srcImg.GetNumBlocks()) + numBlocks = dstImg.GetNumBlocks(); + printf("Copying %d blocks\n", numBlocks); + + unsigned char blkBuf[512]; + for (int block = 0; block < numBlocks; block++) { + dierr = srcImg.ReadBlock(block, blkBuf); + if (dierr != kDIErrNone) { + fprintf(stderr, "ERROR: ReadBlock failed (err=%d)\n", dierr); + goto bail; + } + dierr = dstImg.WriteBlock(block, blkBuf); + if (dierr != kDIErrNone) { + fprintf(stderr, "ERROR: WriteBlock failed (err=%d)\n", dierr); + goto bail; + } + } + } else { + int numTracks, numSectPerTrack; + numTracks = srcImg.GetNumTracks(); + numSectPerTrack = srcImg.GetNumSectPerTrack(); + if (dstImg.GetNumTracks() < srcImg.GetNumTracks()) + numTracks = dstImg.GetNumTracks(); + if (dstImg.GetNumSectPerTrack() < srcImg.GetNumSectPerTrack()) + numSectPerTrack = dstImg.GetNumSectPerTrack(); + printf("Copying %d tracks of %d sectors\n", numTracks, numSectPerTrack); + + unsigned char sctBuf[256]; + for (int track = 0; track < numTracks; track++) { + for (int sector = 0; sector < numSectPerTrack; sector++) { + dierr = srcImg.ReadTrackSector(track, sector, sctBuf); + if (dierr != kDIErrNone) { + fprintf(stderr, + "WARNING: ReadTrackSector failed on T=%d S=%d (err=%d)\n", + track, sector, dierr); + dierr = kDIErrNone; // allow bad blocks + memset(sctBuf, 0, sizeof(sctBuf)); + } + dierr = dstImg.WriteTrackSector(track, sector, sctBuf); + if (dierr != kDIErrNone) { + fprintf(stderr, + "ERROR: WriteBlock failed on T=%d S=%d (err=%d)\n", + track, sector, dierr); + goto bail; + } + } + } + } + + dierr = srcImg.CloseImage(); + if (dierr != kDIErrNone) { + fprintf(stderr, "ERROR: srcImg close failed?!\n"); + goto bail; + } + + dierr = dstImg.CloseImage(); + if (dierr != kDIErrNone) { + fprintf(stderr, "ERROR: dstImg close failed (err=%d)\n", dierr); + goto bail; + } + + assert(dierr == kDIErrNone); +bail: + return dierr; +} + +/* + * Process every argument. + */ +int +main(int argc, char** argv) +{ + const char* kLogFile = "iconv-log.txt"; + + if (argc != 3) { + fprintf(stderr, "%s: infile outfile\n", argv[0]); + exit(2); + } + + gLog = fopen(kLogFile, "w"); + if (gLog == nil) { + fprintf(stderr, "ERROR: unable to open log file\n"); + exit(1); + } + + printf("Image Converter for Linux v1.0\n"); + printf("Copyright (C) 2004 by faddenSoft, LLC. All rights reserved.\n"); + long major, minor, bug; + Global::GetVersion(&major, &minor, &bug); + printf("Linked against DiskImg library v%ld.%ld.%ld\n", + major, minor, bug); + printf("Log file is '%s'\n", kLogFile); + printf("\n"); + + Global::SetDebugMsgHandler(MsgHandler); + Global::AppInit(); + + NuSetGlobalErrorMessageHandler(NufxErrorMsgHandler); + + Convert(argv[1], argv[2]); + + Global::AppCleanup(); + fclose(gLog); + + exit(0); +} + diff --git a/linux/GetFile.cpp b/linux/GetFile.cpp new file mode 100644 index 0000000..ab4eb98 --- /dev/null +++ b/linux/GetFile.cpp @@ -0,0 +1,220 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Get a file from a disk image. + */ +#include +#include +#include +#include +#include +#include +#include +#include "../diskimg/DiskImg.h" + +using namespace DiskImgLib; + +#define nil NULL +#define NELEM(x) (sizeof(x) / sizeof((x)[0])) + +/* + * Globals. + */ +FILE* gLog = nil; +pid_t gPid = getpid(); + +/* + * Show usage info. + */ +void +Usage(const char* argv0) +{ + fprintf(stderr, "Usage: %s image-filename file\n", argv0); + + fprintf(stderr, "\n"); + fprintf(stderr, "The file will be written to stdout.\n"); +} + +/* + * Copy a file from "src" to "dst". + */ +int +CopyFile(A2FileDescr* src, FILE* dst) +{ + DIError dierr; + size_t actual; + char buf[4096]; + + while (1) { + dierr = src->Read(buf, sizeof(buf), &actual); + if (dierr != kDIErrNone) { + fprintf(stderr, "Error: read failed: %s\n", DIStrError(dierr)); + return -1; + } + + if (actual == 0) // EOF hit + break; + + fwrite(buf, 1, actual, dst); + } + + return 0; +} + +/* + * Extract the named file from the specified image. + */ +int +Process(const char* imageName, const char* wantedFileName) +{ + DIError dierr; + DiskImg diskImg; + DiskFS* pDiskFS = nil; + A2File* pFile = nil; + A2FileDescr* pDescr = nil; + int result = -1; + + /* open read-only */ + dierr = diskImg.OpenImage(imageName, '/', true); + if (dierr != kDIErrNone) { + fprintf(stderr, "Unable to open '%s': %s\n", imageName, + DIStrError(dierr)); + goto bail; + } + + /* figure out the format */ + dierr = diskImg.AnalyzeImage(); + if (dierr != kDIErrNone) { + fprintf(stderr, "Analysis of '%s' failed: %s\n", imageName, + DIStrError(dierr)); + goto bail; + } + + /* recognized? */ + if (diskImg.GetFSFormat() == DiskImg::kFormatUnknown || + diskImg.GetSectorOrder() == DiskImg::kSectorOrderUnknown) + { + fprintf(stderr, "Unable to identify filesystem on '%s'\n", imageName); + goto bail; + } + + /* create an appropriate DiskFS object */ + pDiskFS = diskImg.OpenAppropriateDiskFS(); + if (pDiskFS == nil) { + /* unknown FS should've been caught above! */ + assert(false); + fprintf(stderr, "Format of '%s' not recognized.\n", imageName); + goto bail; + } + + /* go ahead and load up volumes mounted inside volumes */ + pDiskFS->SetScanForSubVolumes(DiskFS::kScanSubEnabled); + + /* do a full scan */ + dierr = pDiskFS->Initialize(&diskImg, DiskFS::kInitFull); + if (dierr != kDIErrNone) { + fprintf(stderr, "Error reading list of files from disk: %s\n", + DIStrError(dierr)); + goto bail; + } + + /* + * Find the file. This comes out of a list of entries, so don't + * delete "pFile" when we're done. + */ + pFile = pDiskFS->GetFileByName(wantedFileName); + if (pFile == nil) { + fprintf(stderr, "File '%s' not found in '%s'\n", wantedFileName, + imageName); + goto bail; + } + + /* + * Open the file read-only. + */ + dierr = pFile->Open(&pDescr, true); + if (dierr != kDIErrNone) { + fprintf(stderr, "Error opening '%s': %s\n", wantedFileName, + DIStrError(dierr)); + goto bail; + } + + /* + * Copy the file to stdout. + */ + result = CopyFile(pDescr, stdout); + +bail: + if (pDescr != nil) { + pDescr->Close(); + //delete pDescr; -- don't do this (double free) + } + delete pDiskFS; + return result; +} + +/* + * Handle a debug message from the DiskImg library. + */ +/*static*/ void +MsgHandler(const char* file, int line, const char* msg) +{ + assert(file != nil); + assert(msg != nil); + +#ifdef _DEBUG + fprintf(gLog, "%05u %s", gPid, msg); +#endif +} + +/* + * Process args. + */ +int +main(int argc, char** argv) +{ +#ifdef _DEBUG + const char* kLogFile = "makedisk-log.txt"; + gLog = fopen(kLogFile, "w"); + if (gLog == nil) { + fprintf(stderr, "ERROR: unable to open log file\n"); + exit(1); + } +#endif + +#ifdef _DEBUG + fprintf(stderr, "Log file is '%s'\n", kLogFile); +#endif + + Global::SetDebugMsgHandler(MsgHandler); + Global::AppInit(); + + if (argc != 3) { + Usage(argv[0]); + exit(2); + } + + const char* imageName; + const char* getFileName; + + argv++; + imageName = *argv++; + getFileName = *argv++; + argc -= 2; + + if (Process(imageName, getFileName) == 0) + fprintf(stderr, "Success!\n"); + else + fprintf(stderr, "Failed.\n"); + + Global::AppCleanup(); +#ifdef _DEBUG + fclose(gLog); +#endif + + exit(0); +} + diff --git a/linux/MDC.cpp b/linux/MDC.cpp new file mode 100644 index 0000000..0e6c96a --- /dev/null +++ b/linux/MDC.cpp @@ -0,0 +1,886 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Drive diskimglib. Similar to MDC. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "zlib.h" +#include "../diskimg/DiskImg.h" +#include "../prebuilt/NufxLib.h" +#include "StringArray.h" + +using namespace DiskImgLib; + +#define nil NULL +#define ASSERT assert +#define NELEM(x) (sizeof(x) / sizeof((x)[0])) +typedef const char* LPCTSTR; + +#define UNIX_LIKE +#define HAVE_DIRENT_H // linux +#define MAX_PATH_LEN 1024 + +/* get a grip on this opendir/readdir stuff */ +#if defined(UNIX_LIKE) +# if defined(HAVE_DIRENT_H) +# include +# define DIR_NAME_LEN(dirent) ((int)strlen((dirent)->d_name)) + typedef struct dirent DIR_TYPE; +# elif defined(HAVE_SYS_DIR_H) +# include +# define DIR_NAME_LEN(direct) ((direct)->d_namlen) + typedef struct direct DIR_TYPE; +# elif defined(HAVE_NDIR_H) +# include +# define DIR_NAME_LEN(direct) ((direct)->d_namlen) + typedef struct direct DIR_TYPE; +# else +# error "Port this?" +# endif +#endif + +/* + * Globals. + */ +FILE* gLog = nil; +pid_t gPid = getpid(); + +struct Stats { + long numFiles; + long numDirectories; + long goodDiskImages; +} gStats = { 0 }; + +typedef struct ScanOpts { + FILE* outfp; +} ScanOpts; + +typedef enum RecordKind { + kRecordKindUnknown = 0, + kRecordKindDisk, + kRecordKindFile, + kRecordKindForkedFile, + kRecordKindDirectory, + kRecordKindVolumeDirectory, +} RecordKind; + + +//#define kFilenameExtDelim '.' /* separates extension from filename */ + +/* time_t values for bad dates */ +#define kDateNone ((time_t) -2) +#define kDateInvalid ((time_t) -1) // should match return from mktime() + +/* + * "buf" must hold at least 64 chars. + */ +void +FormatDate(time_t when, char* buf) +{ + if (when == kDateNone) { + strcpy(buf, "[No Date]"); + } else if (when == kDateInvalid) { + strcpy(buf, ""); + } else { + struct tm* ptm; + + ptm = localtime(&when); + strftime(buf, 64, "%d-%b-%y %H:%M", ptm); + } +} + +#if 0 +/* + * Find the filename component of a local pathname. Uses the fssep passed + * in. If the fssep is '\0' (as is the case for DOS 3.3), then the entire + * pathname is returned. + * + * Always returns a pointer to a string; never returns nil. + */ +const char* +FilenameOnly(const char* pathname, char fssep) +{ + const char* retstr; + const char* pSlash; + char* tmpStr = nil; + + ASSERT(pathname != nil); + if (fssep == '\0') { + retstr = pathname; + goto bail; + } + + pSlash = strrchr(pathname, fssep); + if (pSlash == nil) { + retstr = pathname; /* whole thing is the filename */ + goto bail; + } + + pSlash++; + if (*pSlash == '\0') { + if (strlen(pathname) < 2) { + retstr = pathname; /* the pathname is just "/"? Whatever */ + goto bail; + } + + /* some bonehead put an fssep on the very end; back up before it */ + /* (not efficient, but this should be rare, and I'm feeling lazy) */ + tmpStr = strdup(pathname); + tmpStr[strlen(pathname)-1] = '\0'; + pSlash = strrchr(tmpStr, fssep); + + if (pSlash == nil) { + retstr = pathname; /* just a filename with a '/' after it */ + goto bail; + } + + pSlash++; + if (*pSlash == '\0') { + retstr = pathname; /* I give up! */ + goto bail; + } + + retstr = pathname + (pSlash - tmpStr); + + } else { + retstr = pSlash; + } + +bail: + free(tmpStr); + return retstr; +} + +/* + * Return the filename extension found in a full pathname. + * + * An extension is the stuff following the last '.' in the filename. If + * there is nothing following the last '.', then there is no extension. + * + * Returns a pointer to the '.' preceding the extension, or nil if no + * extension was found. + * + * We guarantee that there is at least one character after the '.'. + */ +const char* +FindExtension(const char* pathname, char fssep) +{ + const char* pFilename; + const char* pExt; + + /* + * We have to isolate the filename so that we don't get excited + * about "/foo.bar/file". + */ + pFilename = FilenameOnly(pathname, fssep); + ASSERT(pFilename != nil); + pExt = strrchr(pFilename, kFilenameExtDelim); + + /* also check for "/blah/foo.", which doesn't count */ + if (pExt != nil && *(pExt+1) != '\0') + return pExt; + + return nil; +} +#endif + + +/* + * Analyze a file's characteristics. + */ +void +AnalyzeFile(const A2File* pFile, RecordKind* pRecordKind, + unsigned long* pTotalLen, unsigned long* pTotalCompLen) +{ + if (pFile->IsVolumeDirectory()) { + /* volume directory entry */ + ASSERT(pFile->GetRsrcLength() < 0); + *pRecordKind = kRecordKindVolumeDirectory; + *pTotalLen = pFile->GetDataLength(); + *pTotalCompLen = pFile->GetDataLength(); + } else if (pFile->IsDirectory()) { + /* directory entry */ + ASSERT(pFile->GetRsrcLength() < 0); + *pRecordKind = kRecordKindDirectory; + *pTotalLen = pFile->GetDataLength(); + *pTotalCompLen = pFile->GetDataLength(); + } else if (pFile->GetRsrcLength() >= 0) { + /* has resource fork */ + *pRecordKind = kRecordKindForkedFile; + *pTotalLen = pFile->GetDataLength() + pFile->GetRsrcLength(); + *pTotalCompLen = + pFile->GetDataSparseLength() + pFile->GetRsrcSparseLength(); + } else { + /* just data fork */ + *pRecordKind = kRecordKindFile; + *pTotalLen = pFile->GetDataLength(); + *pTotalCompLen = pFile->GetDataSparseLength(); + } +} + +/* + * Determine whether the access bits on the record make it a read-only + * file or not. + * + * Uses a simplified view of the access flags. + */ +bool +IsRecordReadOnly(int accessBits) +{ + if (accessBits == 0x21L || accessBits == 0x01L) + return true; + else + return false; +} + +/* ProDOS file type names; must be entirely in upper case */ +static const char gFileTypeNames[256][4] = { + "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", + "$80", "$81", "$82", "$83", "$84", "$85", "$86", "$87", + "$88", "$89", "$8A", "$8B", "$8C", "$8D", "$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" +}; + +/* + * Return a pointer to the three-letter representation of the file type name. + * + * Note to self: code down below tests first char for '?'. + */ +/*static*/ const char* +GetFileTypeString(unsigned long fileType) +{ + if (fileType < NELEM(gFileTypeNames)) + return gFileTypeNames[fileType]; + else + return "???"; +} + +/* + * Sanitize a string. The Mac likes to stick control characters into + * things, e.g. ^C and ^M. + */ +static void +MacSanitize(char* str) +{ + while (*str != '\0') { + if (*str < 0x20 || *str >= 0x7f) + *str = '?'; + str++; + } +} + + +/* + * Load the contents of a DiskFS. + * + * Recursively handle sub-volumes. + */ +int +LoadDiskFSContents(DiskFS* pDiskFS, const char* volName, + ScanOpts* pScanOpts) +{ + static const char* kBlankFileName = ""; + DiskFS::SubVolume* pSubVol = nil; + A2File* pFile; + + ASSERT(pDiskFS != nil); + pFile = pDiskFS->GetNextFile(nil); + while (pFile != nil) { + char subVolName[128] = ""; + char dispName[128] = ""; + //CString subVolName, dispName; + RecordKind recordKind; + unsigned long totalLen, totalCompLen; + char tmpbuf[16]; + + AnalyzeFile(pFile, &recordKind, &totalLen, &totalCompLen); + if (recordKind == kRecordKindVolumeDirectory) { + /* skip these */ + pFile = pDiskFS->GetNextFile(pFile); + continue; + } + + /* prepend volName for sub-volumes; must be valid Win32 dirname */ + if (volName[0] != '\0') + sprintf(subVolName, "_%s", volName); + + const char* ccp = pFile->GetPathName(); + ASSERT(ccp != nil); + if (strlen(ccp) == 0) + ccp = kBlankFileName; + + if (subVolName[0] == '\0') + strcpy(dispName, ccp); + else { + sprintf(dispName, "%s:%s", subVolName, ccp); + //dispName = subVolName; + //dispName += ':'; + //dispName += ccp; + } + ccp = dispName; + + int len = strlen(ccp); + if (len <= 32) { + fprintf(pScanOpts->outfp, "%c%-32.32s ", + IsRecordReadOnly(pFile->GetAccess()) ? '*' : ' ', + ccp); + } else { + fprintf(pScanOpts->outfp, "%c..%-30.30s ", + IsRecordReadOnly(pFile->GetAccess()) ? '*' : ' ', + ccp + len - 30); + } + switch (recordKind) { + case kRecordKindUnknown: + fprintf(pScanOpts->outfp, "%s- $%04lX ", + GetFileTypeString(pFile->GetFileType()), + pFile->GetAuxType()); + break; + case kRecordKindDisk: + sprintf(tmpbuf, "%ldk", totalLen / 1024); + fprintf(pScanOpts->outfp, "Disk %-6s ", tmpbuf); + break; + case kRecordKindFile: + case kRecordKindForkedFile: + case kRecordKindDirectory: + if (pDiskFS->GetDiskImg()->GetFSFormat() == DiskImg::kFormatMacHFS) + { + if (recordKind != kRecordKindDirectory && + pFile->GetFileType() >= 0 && pFile->GetFileType() <= 0xff && + pFile->GetAuxType() >= 0 && pFile->GetAuxType() <= 0xffff) + { + /* ProDOS type embedded in HFS */ + fprintf(pScanOpts->outfp, "%s%c $%04lX ", + GetFileTypeString(pFile->GetFileType()), + recordKind == kRecordKindForkedFile ? '+' : ' ', + pFile->GetAuxType()); + } else { + char typeStr[5]; + char creatorStr[5]; + unsigned long val; + + val = pFile->GetAuxType(); + creatorStr[0] = (unsigned char) (val >> 24); + creatorStr[1] = (unsigned char) (val >> 16); + creatorStr[2] = (unsigned char) (val >> 8); + creatorStr[3] = (unsigned char) val; + creatorStr[4] = '\0'; + + val = pFile->GetFileType(); + typeStr[0] = (unsigned char) (val >> 24); + typeStr[1] = (unsigned char) (val >> 16); + typeStr[2] = (unsigned char) (val >> 8); + typeStr[3] = (unsigned char) val; + typeStr[4] = '\0'; + + MacSanitize(creatorStr); + MacSanitize(typeStr); + + if (recordKind == kRecordKindDirectory) { + fprintf(pScanOpts->outfp, "DIR %-4s ", creatorStr); + } else { + fprintf(pScanOpts->outfp, "%-4s%c %-4s ", + typeStr, + pFile->GetRsrcLength() > 0 ? '+' : ' ', + creatorStr); + } + } + } else { + fprintf(pScanOpts->outfp, "%s%c $%04lX ", + GetFileTypeString(pFile->GetFileType()), + recordKind == kRecordKindForkedFile ? '+' : ' ', + pFile->GetAuxType()); + } + break; + default: + ASSERT(0); + fprintf(pScanOpts->outfp, "ERROR "); + break; + } + + char date[64]; + if (pFile->GetModWhen() == 0) + FormatDate(kDateNone, date); + else + FormatDate(pFile->GetModWhen(), date); + fprintf(pScanOpts->outfp, "%-15s ", (LPCTSTR) date); + + const char* fmtStr; + switch (pFile->GetFSFormat()) { + case DiskImg::kFormatDOS33: + case DiskImg::kFormatDOS32: + case DiskImg::kFormatUNIDOS: + fmtStr = "DOS "; + break; + case DiskImg::kFormatProDOS: + fmtStr = "ProDOS"; + break; + case DiskImg::kFormatPascal: + fmtStr = "Pascal"; + break; + case DiskImg::kFormatMacHFS: + fmtStr = "HFS "; + break; + case DiskImg::kFormatCPM: + fmtStr = "CP/M "; + break; + case DiskImg::kFormatMSDOS: + fmtStr = "MS-DOS"; + break; + case DiskImg::kFormatRDOS33: + case DiskImg::kFormatRDOS32: + case DiskImg::kFormatRDOS3: + fmtStr = "RDOS "; + break; + default: + fmtStr = "??? "; + break; + } + if (pFile->GetQuality() == A2File::kQualityDamaged) + fmtStr = "BROKEN"; + + fprintf(pScanOpts->outfp, "%s ", fmtStr); + + +#if 0 + /* compute the percent size */ + if ((!totalLen && totalCompLen) || (totalLen && !totalCompLen)) + fprintf(pScanOpts->outfp, "--- "); /* weird */ + else if (totalLen < totalCompLen) + fprintf(pScanOpts->outfp, ">100%% "); /* compression failed? */ + else { + sprintf(tmpbuf, "%02d%%", ComputePercent(totalCompLen, totalLen)); + fprintf(pScanOpts->outfp, "%4s ", tmpbuf); + } +#endif + + if (!totalLen && totalCompLen) + fprintf(pScanOpts->outfp, " ????"); /* weird */ + else + fprintf(pScanOpts->outfp, "%8ld", totalLen); + + fprintf(pScanOpts->outfp, "\n"); + + pFile = pDiskFS->GetNextFile(pFile); + } + + /* + * Load all sub-volumes. + */ + pSubVol = pDiskFS->GetNextSubVolume(nil); + while (pSubVol != nil) { + const char* subVolName; + int ret; + + subVolName = pSubVol->GetDiskFS()->GetVolumeName(); + if (subVolName == nil) + subVolName = "+++"; // could probably do better than this + + ret = LoadDiskFSContents(pSubVol->GetDiskFS(), subVolName, pScanOpts); + if (ret != 0) + return ret; + pSubVol = pDiskFS->GetNextSubVolume(pSubVol); + } + + return 0; +} + +/* + * Open a disk image and dump the contents. + * + * Returns 0 on success, nonzero on failure. + */ +int +ScanDiskImage(const char* pathName, ScanOpts* pScanOpts) +{ + ASSERT(pathName != nil); + ASSERT(pScanOpts != nil); + ASSERT(pScanOpts->outfp != nil); + + DIError dierr; + char errMsg[256] = ""; + DiskImg diskImg; + DiskFS* pDiskFS = nil; + + dierr = diskImg.OpenImage(pathName, '/', true); + if (dierr != kDIErrNone) { + sprintf(errMsg, "Unable to open '%s': %s", pathName, + DIStrError(dierr)); + goto bail; + } + + dierr = diskImg.AnalyzeImage(); + if (dierr != kDIErrNone) { + sprintf(errMsg, "Analysis of '%s' failed: %s", pathName, + DIStrError(dierr)); + goto bail; + } + + if (diskImg.GetFSFormat() == DiskImg::kFormatUnknown || + diskImg.GetSectorOrder() == DiskImg::kSectorOrderUnknown) + { + sprintf(errMsg, "Unable to identify filesystem on '%s'", pathName); + goto bail; + } + + /* create an appropriate DiskFS object */ + pDiskFS = diskImg.OpenAppropriateDiskFS(); + if (pDiskFS == nil) { + /* unknown FS should've been caught above! */ + ASSERT(false); + sprintf(errMsg, "Format of '%s' not recognized.", pathName); + goto bail; + } + + pDiskFS->SetScanForSubVolumes(DiskFS::kScanSubEnabled); + + /* object created; prep it */ + dierr = pDiskFS->Initialize(&diskImg, DiskFS::kInitFull); + if (dierr != kDIErrNone) { + sprintf(errMsg, "Error reading list of files from disk: %s", + DIStrError(dierr)); + goto bail; + } + + fprintf(pScanOpts->outfp, "File: %s\n", pathName); + + int kbytes; + if (pDiskFS->GetDiskImg()->GetHasBlocks()) + kbytes = pDiskFS->GetDiskImg()->GetNumBlocks() / 2; + else if (pDiskFS->GetDiskImg()->GetHasSectors()) + kbytes = (pDiskFS->GetDiskImg()->GetNumTracks() * + pDiskFS->GetDiskImg()->GetNumSectPerTrack()) / 4; + else + kbytes = 0; + fprintf(pScanOpts->outfp, "Disk: %s%s (%dKB)\n", pDiskFS->GetVolumeID(), + pDiskFS->GetFSDamaged() ? " [*]" : "", kbytes); + + fprintf(pScanOpts->outfp, + " Name Type Auxtyp Modified" + " Format Length\n"); + fprintf(pScanOpts->outfp, + "------------------------------------------------------" + "------------------------\n"); + if (LoadDiskFSContents(pDiskFS, "", pScanOpts) != 0) { + sprintf(errMsg, "Failed while loading contents of '%s'.", pathName); + goto bail; + } + fprintf(pScanOpts->outfp, + "------------------------------------------------------" + "------------------------\n\n"); + + gStats.goodDiskImages++; + +bail: + delete pDiskFS; + + if (errMsg[0] != '\0') { + fprintf(pScanOpts->outfp, "Unable to process '%s'\n", pathName); + fprintf(pScanOpts->outfp, " %s\n\n", (LPCTSTR) errMsg); + return -1; + } else { + return 0; + } +} + + +/* + * Check a file's status. + * + * [ Someday we may want to modify this to handle symbolic links. ] + */ +int +CheckFileStatus(const char* pathname, struct stat* psb, bool* pExists, + bool* pIsReadable, bool* pIsDir) +{ + int result = 0; + int cc; + + assert(pathname != nil); + assert(psb != nil); + assert(pExists != nil); + assert(pIsReadable != nil); + assert(pIsDir != nil); + + *pExists = true; + *pIsReadable = true; + *pIsDir = false; + + cc = stat(pathname, psb); + if (cc) { + if (errno == ENOENT) + *pExists = false; + else + result = -1; // stat failed + goto bail; + } + + if (S_ISDIR(psb->st_mode)) + *pIsDir = true; + + /* + * Test if we can read this file. How do we do that? The easy but slow + * way is to call access(2), the harder way is to figure out + * what user/group we are and compare the appropriate file mode. + */ + if (access(pathname, R_OK) < 0) + *pIsReadable = false; + +bail: + return result; +} + + +/* forward decl */ +int ProcessFile(const char* pathname, ScanOpts* pScanOpts); + +/* + * UNIX-style recursive directory descent. Scan the contents of a directory. + * If a subdirectory is found, follow it; otherwise, call ProcessFile to + * handle the file. + */ +int +ProcessDirectory(const char* dirName, ScanOpts* pScanOpts) +{ + StringArray strArray; + int result = -1; + DIR* dirp = nil; + DIR_TYPE* entry; + char nbuf[MAX_PATH_LEN]; /* malloc might be better; this soaks stack */ + char fssep; + int len; + + assert(pScanOpts != nil); + assert(dirName != nil); + +#ifdef _DEBUG + fprintf(gLog, "+++ Processing directory '%s'\n", dirName); +#endif + + dirp = opendir(dirName); + if (dirp == nil) { + //err = errno ? errno : -1; + goto bail; + } + + fssep = '/'; + + /* could use readdir_r, but we don't care about reentrancy here */ + while ((entry = readdir(dirp)) != nil) { + /* skip the dotsies */ + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) + continue; + + len = strlen(dirName); + if (len + DIR_NAME_LEN(entry) +2 > MAX_PATH_LEN) { + fprintf(stderr, "ERROR: Filename exceeds %d bytes: %s%c%s\n", + MAX_PATH_LEN, dirName, fssep, entry->d_name); + goto bail; + } + + /* form the new name, inserting an fssep if needed */ + strcpy(nbuf, dirName); + if (dirName[len-1] != fssep) + nbuf[len++] = fssep; + strcpy(nbuf+len, entry->d_name); + + strArray.Add(nbuf); + } + + /* sort the list, then process the files */ + strArray.Sort(StringArray::CmpAscendingAlpha); + for (int i = 0; i < strArray.GetCount(); i++) + (void) ProcessFile(strArray.GetEntry(i), pScanOpts); + + result = 0; + +bail: + if (dirp != nil) + (void)closedir(dirp); + return result; +} + +/* + * Process a file. + * + * Returns with an error if the file doesn't exist or isn't readable. + */ +int +ProcessFile(const char* pathname, ScanOpts* pScanOpts) +{ + int result = -1; + bool exists, isDir, isReadable; + struct stat sb; + + assert(pathname != nil); + assert(pScanOpts != nil); + +#ifdef _DEBUG + fprintf(gLog, "+++ Processing file or dir '%s'\n", pathname); +#endif + + if (CheckFileStatus(pathname, &sb, &exists, &isReadable, &isDir) != 0) { + fprintf(stderr, "ERROR: unexpected error while examining '%s'\n", + pathname); + goto bail; + } + + if (!exists) { + fprintf(stderr, "ERROR: couldn't find '%s'\n", pathname); + goto bail; + } + if (!isReadable) { + fprintf(stderr, "ERROR: file '%s' isn't readable\n", pathname); + goto bail; + } + + if (isDir) { + result = ProcessDirectory(pathname, pScanOpts); + gStats.numDirectories++; + } else { + result = ScanDiskImage(pathname, pScanOpts); + gStats.numFiles++; + } + +bail: + return result; +} + + +/* + * Handle a debug message from the DiskImg library. + */ +/*static*/ void +MsgHandler(const char* file, int line, const char* msg) +{ + ASSERT(file != nil); + ASSERT(msg != nil); + +#ifdef _DEBUG + fprintf(gLog, "%05u %s", gPid, msg); +#endif +} +/* + * Handle a global error message from the NufxLib library by shoving it + * through the DiskImgLib message function. + */ +NuResult +NufxErrorMsgHandler(NuArchive* /*pArchive*/, void* vErrorMessage) +{ + const NuErrorMessage* pErrorMessage = (const NuErrorMessage*) vErrorMessage; + + if (pErrorMessage->isDebug) { + Global::PrintDebugMsg(pErrorMessage->file, pErrorMessage->line, + " [D] %s\n", pErrorMessage->message); + } else { + Global::PrintDebugMsg(pErrorMessage->file, pErrorMessage->line, + " %s\n", pErrorMessage->message); + } + + return kNuOK; +} + +/* + * Process every argument. + */ +int +main(int argc, char** argv) +{ + ScanOpts scanOpts; + scanOpts.outfp = stdout; + +#ifdef _DEBUG + const char* kLogFile = "mdc-log.txt"; + gLog = fopen(kLogFile, "w"); + if (gLog == nil) { + fprintf(stderr, "ERROR: unable to open log file\n"); + exit(1); + } +#endif + + long major, minor, bug; + Global::GetVersion(&major, &minor, &bug); + + printf("MDC for Linux v2.2.0 (DiskImg library v%ld.%ld.%ld)\n", + major, minor, bug); + printf("Copyright (C) 2006 by faddenSoft, LLC. All rights reserved.\n"); + printf("MDC is part of CiderPress, available from http://www.faddensoft.com/.\n"); + NuGetVersion(&major, &minor, &bug, nil, nil); + printf("Linked against NufxLib v%ld.%ld.%ld and zlib version %s.\n", + major, minor, bug, zlibVersion()); + + if (argc == 1) { + fprintf(stderr, "\nUsage: mdc file ...\n"); + goto done; + } + +#ifdef _DEBUG + printf("Log file is '%s'\n", kLogFile); +#endif + printf("\n"); + + Global::SetDebugMsgHandler(MsgHandler); + Global::AppInit(); + + NuSetGlobalErrorMessageHandler(NufxErrorMsgHandler); + + time_t start = time(NULL); + printf("Run started at %.24s\n\n", ctime(&start)); + + while (--argc) { + ProcessFile(*++argv, &scanOpts); + } + + printf("Scan completed in %ld seconds:\n", time(NULL) - start); + printf(" Directories : %ld\n", gStats.numDirectories); + printf(" Files : %ld (%ld good disk images)\n", gStats.numFiles, + gStats.goodDiskImages); + + Global::AppCleanup(); + +done: +#ifdef _DEBUG + fclose(gLog); +#endif + + return 0; +} + diff --git a/linux/MakeDisk.cpp b/linux/MakeDisk.cpp new file mode 100644 index 0000000..0e3757f --- /dev/null +++ b/linux/MakeDisk.cpp @@ -0,0 +1,388 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Create a blank disk image, format it, and copy some files onto it. + */ +#include +#include +#include +#include +#include +#include +#include +#include "../diskimg/DiskImg.h" + +using namespace DiskImgLib; + +#define nil NULL +#define NELEM(x) (sizeof(x) / sizeof((x)[0])) + +/* + * Globals. + */ +FILE* gLog = nil; +pid_t gPid = getpid(); + +/* + * Show usage info. + */ +void +Usage(const char* argv0) +{ + fprintf(stderr, + "Usage: %s {dos|prodos|pascal} size image-filename.po input-file1 ...\n", + argv0); + + fprintf(stderr, "\n"); + fprintf(stderr, "Example: makedisk prodos 800k foo.po file1.txt file2.txt\n"); +} + + +/* + * Create a ProDOS-ordered disk image. + * + * Returns a DiskImg pointer on success, or nil on failure. + */ +DiskImg* +CreateDisk(const char* fileName, long blockCount) +{ + DIError dierr; + DiskImg* pDiskImg = nil; + + pDiskImg = new DiskImg; + dierr = pDiskImg->CreateImage( + fileName, + nil, // storageName + DiskImg::kOuterFormatNone, + DiskImg::kFileFormatUnadorned, + DiskImg::kPhysicalFormatSectors, + nil, // pNibbleDescr + DiskImg::kSectorOrderProDOS, + DiskImg::kFormatGenericProDOSOrd, + blockCount, + true); // no need to format the image + + if (dierr != kDIErrNone) { + fprintf(stderr, "ERROR: CreateImage failed: %s\n", + DIStrError(dierr)); + delete pDiskImg; + pDiskImg = nil; + } + + return pDiskImg; +} + +/* + * Copy files to the disk. + */ +int +CopyFiles(DiskFS* pDiskFS, int argc, char** argv) +{ + DIError dierr; + DiskFS::CreateParms parms; + A2File* pNewFile; + + typedef struct CreateParms { + const char* pathName; // full pathname + char fssep; + int storageType; // determines normal, subdir, or forked + long fileType; + long auxType; + int access; + time_t createWhen; + time_t modWhen; + } CreateParms; + + + while (argc--) { + printf("+++ Adding '%s'\n", *argv); + + /* + * Use external pathname as internal pathname. This isn't quite + * right, since things like "../" will end up getting converted + * to something we don't want, but it'll do for now. + */ + parms.pathName = *argv; + parms.fssep = '/'; // UNIX fssep + parms.storageType = DiskFS::kStorageSeedling; // not forked, not dir + parms.fileType = 0; // NON + parms.auxType = 0; // $0000 + parms.access = DiskFS::kFileAccessUnlocked; + parms.createWhen = time(nil); + parms.modWhen = time(nil); + + /* + * Create a new, empty file. The "pNewFile" pointer does not belong + * to us, so we should not delete it later, or try to access it + * after the underlying file is deleted. + */ + dierr = pDiskFS->CreateFile(&parms, &pNewFile); + if (dierr != kDIErrNone) { + fprintf(stderr, "ERROR: unable to create '%s': %s\n", + *argv, DIStrError(dierr)); + return -1; + } + + /* + * Load the input file into memory. + */ + FILE* fp; + char* buf; + long len; + + fp = fopen(*argv, "r"); + if (fp == nil) { + fprintf(stderr, "ERROR: unable to open input file '%s': %s\n", + *argv, strerror(errno)); + return -1; + } + + if (fseek(fp, 0, SEEK_END) != 0) { + fprintf(stderr, "ERROR: unable to seek input file '%s': %s\n", + *argv, strerror(errno)); + fclose(fp); + return -1; + } + + len = ftell(fp); + rewind(fp); + + buf = new char[len]; + if (buf == nil) { + fprintf(stderr, "ERROR: unable to alloc %ld bytes\n", len); + fclose(fp); + return -1; + } + + if (fread(buf, len, 1, fp) != 1) { + fprintf(stderr, "ERROR: fread of %ld bytes from '%s' failed: %s\n", + len, *argv, strerror(errno)); + fclose(fp); + delete[] buf; + return -1; + } + fclose(fp); + + /* + * Write the buffer to the disk image. + * + * The A2FileDescr object is created by "Open" and deleted by + * "Close". + */ + A2FileDescr* pFD; + + dierr = pNewFile->Open(&pFD, true); + if (dierr != kDIErrNone) { + fprintf(stderr, "ERROR: unable to open new file '%s': %s\n", + pNewFile->GetPathName(), DIStrError(dierr)); + delete[] buf; + return -1; + } + + dierr = pFD->Write(buf, len); + if (dierr != kDIErrNone) { + fprintf(stderr, "ERROR: failed writing to '%s': %s\n", + pNewFile->GetPathName(), DIStrError(dierr)); + pFD->Close(); + pDiskFS->DeleteFile(pNewFile); + delete[] buf; + return -1; + } + delete[] buf; + + dierr = pFD->Close(); + if (dierr != kDIErrNone) { + fprintf(stderr, "ERROR: failed while closing '%s': %s\n", + pNewFile->GetPathName(), DIStrError(dierr)); + return -1; + } + + /* + * On to the next file. + */ + argv++; + } + + return 0; +} + +/* + * Process the request. + * + * Returns 0 on success, -1 on failure. + */ +int +Process(const char* formatName, const char* sizeStr, + const char* outputFileName, int argc, char** argv) +{ + DiskImg::FSFormat format; + long blockCount; + + if (strcasecmp(formatName, "dos") == 0) + format = DiskImg::kFormatDOS33; + else if (strcasecmp(formatName, "prodos") == 0) + format = DiskImg::kFormatProDOS; + else if (strcasecmp(formatName, "pascal") == 0) + format = DiskImg::kFormatPascal; + else { + fprintf(stderr, "ERROR: invalid format '%s'\n", formatName); + return -1; + } + + if (strcasecmp(sizeStr, "140k") == 0) + blockCount = 280; + else if (strcasecmp(sizeStr, "800k") == 0) + blockCount = 1600; + else { + blockCount = atoi(sizeStr); + if (blockCount <= 0 || blockCount > 65536) { + fprintf(stderr, "ERROR: invalid size '%s'\n", sizeStr); + return -1; + } + } + + if (access(outputFileName, F_OK) == 0) { + fprintf(stderr, "ERROR: output file '%s' already exists\n", + outputFileName); + return -1; + } + + assert(argc >= 1); + assert(*argv != nil); + + + const char* volName; + DiskImg* pDiskImg; + DiskFS* pDiskFS; + DIError dierr; + + /* + * Prepare the disk image file. + */ + pDiskImg = CreateDisk(outputFileName, blockCount); + if (pDiskImg == nil) + return -1; + + if (format == DiskImg::kFormatDOS33) + volName = "DOS"; // put DOS 3.3 in tracks 0-2 + else + volName = "TEST"; + dierr = pDiskImg->FormatImage(format, volName); + if (dierr != kDIErrNone) { + fprintf(stderr, "ERROR: unable to format disk: %s\n", + DIStrError(dierr)); + delete pDiskImg; + return -1; + } + + /* + * Prepare to access the image as a filesystem. + */ + pDiskFS = pDiskImg->OpenAppropriateDiskFS(false); + if (pDiskFS == nil) { + fprintf(stderr, "ERROR: unable to open appropriate DiskFS\n"); + delete pDiskImg; + return -1; + } + + dierr = pDiskFS->Initialize(pDiskImg, DiskFS::kInitFull); + if (dierr != kDIErrNone) { + fprintf(stderr, "ERROR: unable to initialize DiskFS: %s\n", + DIStrError(dierr)); + delete pDiskFS; + delete pDiskImg; + return -1; + } + + /* + * Copy the files over. + */ + if (CopyFiles(pDiskFS, argc, argv) != 0) { + delete pDiskFS; + delete pDiskImg; + return -1; + } + + /* + * Clean up. Note "CloseImage" isn't strictly necessary, but it gives + * us an opportunity to detect failures. + */ + delete pDiskFS; + + if (pDiskImg->CloseImage() != 0) { + fprintf(stderr, "WARNING: CloseImage failed: %s\n", + DIStrError(dierr)); + } + + delete pDiskImg; + return 0; +} + + +/* + * Handle a debug message from the DiskImg library. + */ +/*static*/ void +MsgHandler(const char* file, int line, const char* msg) +{ + assert(file != nil); + assert(msg != nil); + +#ifdef _DEBUG + fprintf(gLog, "%05u %s", gPid, msg); +#endif +} + +/* + * Process args. + */ +int +main(int argc, char** argv) +{ +#ifdef _DEBUG + const char* kLogFile = "makedisk-log.txt"; + gLog = fopen(kLogFile, "w"); + if (gLog == nil) { + fprintf(stderr, "ERROR: unable to open log file\n"); + exit(1); + } +#endif + +#ifdef _DEBUG + printf("Log file is '%s'\n", kLogFile); +#endif + + Global::SetDebugMsgHandler(MsgHandler); + Global::AppInit(); + + if (argc < 5) { + Usage(argv[0]); + exit(2); + } + + const char* formatName; + const char* sizeStr; + const char* outputFileName; + + argv++; + formatName = *argv++; + sizeStr = *argv++; + outputFileName = *argv++; + argc -= 4; + + if (Process(formatName, sizeStr, outputFileName, argc, argv) == 0) + fprintf(stderr, "Success!\n"); + else + fprintf(stderr, "Failed.\n"); + + Global::AppCleanup(); +#ifdef _DEBUG + fclose(gLog); +#endif + + exit(0); +} + diff --git a/linux/Makefile b/linux/Makefile new file mode 100644 index 0000000..6a74941 --- /dev/null +++ b/linux/Makefile @@ -0,0 +1,81 @@ +# +# CiderPress +# Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. +# See the file LICENSE for distribution terms. +# +# DiskImg makefile for Linux. +# +SHELL = /bin/sh +CC = gcc +CXX = g++ +#OPT = -g -D_DEBUG +OPT = -g -O2 +GCC_FLAGS = -Wall -Wwrite-strings -Wpointer-arith -Wshadow +CXXFLAGS = $(OPT) $(GCC_FLAGS) -D_FILE_OFFSET_BITS=64 + +SRCS1 = MDC.cpp +SRCS2 = Convert.cpp +SRCS3 = SSTAsm.cpp +SRCS4 = PackDDD.cpp +SRCS5 = MakeDisk.cpp +SRCS5 = GetFile.cpp + +OBJS1 = MDC.o +OBJS2 = Convert.o +OBJS3 = SSTAsm.o +OBJS4 = PackDDD.o +OBJS5 = MakeDisk.o +OBJS6 = GetFile.o + +PRODUCT1 = mdc +PRODUCT2 = iconv +PRODUCT3 = sstasm +PRODUCT4 = packddd +PRODUCT5 = makedisk +PRODUCT6 = getfile + +DISKIMGLIB = ../diskimg/libdiskimg.a ../diskimg/libhfs/libhfs.a +NUFXLIB = ../prebuilt/libnufx.a + +all: $(PRODUCT1) $(PRODUCT2) $(PRODUCT3) $(PRODUCT4) $(PRODUCT5) $(PRODUCT6) + @true + +$(PRODUCT1): $(OBJS1) $(DISKIMGLIB) + $(CXX) -o $@ $(OBJS1) $(DISKIMGLIB) $(NUFXLIB) -lz + +$(PRODUCT2): $(OBJS2) $(DISKIMGLIB) + $(CXX) -o $@ $(OBJS2) $(DISKIMGLIB) $(NUFXLIB) -lz + +$(PRODUCT3): $(OBJS3) $(DISKIMGLIB) + $(CXX) -o $@ $(OBJS3) $(DISKIMGLIB) $(NUFXLIB) -lz + +$(PRODUCT4): $(OBJS4) $(DISKIMGLIB) + $(CXX) -o $@ $(OBJS4) $(DISKIMGLIB) $(NUFXLIB) -lz + +$(PRODUCT5): $(OBJS5) $(DISKIMGLIB) + $(CXX) -o $@ $(OBJS5) $(DISKIMGLIB) $(NUFXLIB) -lz + +$(PRODUCT6): $(OBJS6) $(DISKIMGLIB) + $(CXX) -o $@ $(OBJS6) $(DISKIMGLIB) $(NUFXLIB) -lz + +../diskimg/libdiskimg.a: + (cd ../diskimg ; make) + +../diskimg/libhfs/libhfs.a: + (cd ../diskimg/libhfs ; make) + +clean: + -rm -f *.o core + -rm -f $(PRODUCT1) $(PRODUCT2) $(PRODUCT3) $(PRODUCT4) $(PRODUCT5) + -rm -f $(PRODUCT6) + -rm -f Makefile.bak tags + -rm -f mdc-log.txt iconv-log.txt makedisk-log.txt + +tags:: + @ctags -R --totals * + +depend: + makedepend -- $(CFLAGS) -- $(SRCS1) $(SRCS2) $(SRCS3) $(SRCS4) $(SRCS5) $(SRCS6) + +# DO NOT DELETE THIS LINE -- make depend depends on it. + diff --git a/linux/PackDDD.cpp b/linux/PackDDD.cpp new file mode 100644 index 0000000..d035772 --- /dev/null +++ b/linux/PackDDD.cpp @@ -0,0 +1,766 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Pack DDD. + +The trouble with unpacking DOS DDD 2.x files: + +The files are stored as binary files with no length. DDD v2.0 stored +a copy of the length in sectors in the filename (e.g. "<397>"). This +means that, when CiderPress goes to extract or view the file, it just +sees an empty binary file. + +CiderPress could make an exception and assume that any binary file with +zero length and more than one sector allocated has a length equal to the +number of sectors times 256. This could cause problems for other things, +but it's probably pretty safe. However, we still don't have an accurate +idea of where the end of the file is. + +Knowing where the file ends is important because there is no identifying +information or checksum in a DDD file. The only way to know that it's a +DDD compressed disk is to try to unpack it and see if you end up at exactly +140K at the same time that you run out of input. Without knowing where the +file really ends, this test is much less certain. + +The only safe way to make this work would be to skip the automatic format +detection and tell CiderPress that the file is definitely DDD format. +There's currently no easy way to do that without complicating the user +interface. Filename extensions might be useful, but they're rare under +DOS 3.3, and I don't think the "<397>" convention is common to all versions +of DDD. + +Complicating the matter is that, if a DOS DDD file (type 'B') is converted +to ProDOS, the first 4 bytes will be stripped off. Without unpacking +the file and knowing to within a byte where it ends, there's no way to +automatically tell whether to start at byte 0 or byte 4. (DDD Pro files +have four bytes of garbage at the very start, probably in an attempt to +retain compatibility with the DOS version. Because it uses REL files the +4 bytes of extra DOS stuff aren't added when the files are copied around, +so this was a reasonably smart thing to do, but it complicates matters +for CiderPress because a file extracted from DOS and a file extracted +from ProDOS will come out differently due to the 4 bytes of type 'B' +gunk getting stripped. This can be avoided if the DOS file uses the 'R' +or 'S' file type.) + +All this would have been much easier if the DOS files had a length word. + +To unpack a file created by DOS DDD v2.x: + - Copy the file to a ProDOS disk, using something that guesses at the + actual length when one isn't provided (Copy ][+ 9.0 may work). + - Reduce the length to within a byte or two of the actual end of file. + Removing all but the last couple of trailing zero bytes usually does + the trick. + - Insert 4 bytes of garbage at the front of the file. My copy of DDD + Pro 1.1 seems to like 03 c9 bf d0. +Probably not worth the effort. + */ +#include +#include +#include +#include +#include +#include "../diskimg/DiskImg.h" +#include "../prebuilt/NufxLib.h" + +using namespace DiskImgLib; + +#define nil NULL +#define NELEM(x) (sizeof(x) / sizeof((x)[0])) + +FILE* gLog = nil; +pid_t gPid = getpid(); + +const int kTrackLen = 4096; +const int kNumSymbols = 256; +const int kNumFavorites = 20; +const int kRLEDelim = 0x97; // value MUST have high bit set +const int kNumTracks = 35; + +/* I suspect this is random garbage, but it's consistent for me */ +const unsigned long kDDDProSignature = 0xd0bfc903; + + +/* + * Class for getting and putting bits to and from a file. + */ +class BitBuffer { +public: + BitBuffer(void) : fFp(nil), fBits(0), fBitCount(0) {} + ~BitBuffer(void) {} + + void SetFile(FILE* fp) { fFp = fp; } + void PutBits(unsigned char bits, int numBits); + //void FlushBits(void); + unsigned char GetBits(int numBits); + + static unsigned char Reverse(unsigned char val); + +private: + FILE* fFp; + unsigned char fBits; + int fBitCount; +}; + +/* + * Add bits to the buffer. + * + * We roll the low bits out of "bits" and shift them to the left (in the + * reverse order in which they were passed in). As soon as we get 8 bits + * we flush. + */ +void +BitBuffer::PutBits(unsigned char bits, int numBits) +{ + assert(fBitCount >= 0 && fBitCount < 8); + assert(numBits > 0 && numBits <= 8); + assert(fFp != nil); + + while (numBits--) { + fBits = (fBits << 1) | (bits & 0x01); + fBitCount++; + + if (fBitCount == 8) { + putc(fBits, fFp); + fBitCount = 0; + } + + bits >>= 1; + } +} + +/* + * Get bits from the buffer. + * + * These come out in the order in which they appear in the file, which + * means that in some cases they will have to be reversed. + */ +unsigned char +BitBuffer::GetBits(int numBits) +{ + assert(fBitCount >= 0 && fBitCount < 8); + assert(numBits > 0 && numBits <= 8); + assert(fFp != nil); + + unsigned char retVal; + + if (fBitCount == 0) { + /* have no bits */ + fBits = getc(fFp); + fBitCount = 8; + } + + if (numBits <= fBitCount) { + /* just serve up what we've already got */ + retVal = fBits >> (8 - numBits); + fBits <<= numBits; + fBitCount -= numBits; + } else { + /* some old, some new; load what we have right-aligned */ + retVal = fBits >> (8 - fBitCount); + numBits -= fBitCount; + + fBits = getc(fFp); + fBitCount = 8; + + /* make room for the rest (also zeroes out the low bits) */ + retVal <<= numBits; + + /* add the high bits from the new byte */ + retVal |= fBits >> (8 - numBits); + fBits <<= numBits; + fBitCount -= numBits; + } + + return retVal; +} + +/* + * Utility function to reverse the order of bits in a byte. + */ +/*static*/ unsigned char +BitBuffer::Reverse(unsigned char val) +{ + int i; + unsigned char result = 0; // init to make compiler happy + + for (i = 0; i < 8; i++) { + result = (result << 1) + (val & 0x01); + val >>= 1; + } + + return result; +} + +#if 0 +/* + * Flush any remaining bits out. Call this at the very end. + */ +void +BitBuffer::FlushBits(void) +{ + if (fBitCount) { + fBits <<= 8 - fBitCount; + putc(fBits, fFp); + + fBitCount = 0; + } +} +#endif + + +/* + * Compute the #of times each byte appears in trackBuf. Runs of four + * bytes or longer are completely ignored. + * + * "trackBuf" holds kTrackLen bytes of data, and "freqCounts" holds + * kNumSymbols (256) unsigned shorts. + */ +void +ComputeFreqCounts(const unsigned char* trackBuf, unsigned short* freqCounts) +{ + const unsigned char* ucp; + int i; + + memset(freqCounts, 0, 256 * sizeof(unsigned short)); + + ucp = trackBuf; + for (i = 0; i < kTrackLen; i++, ucp++) { + if (i < (kTrackLen-3) && + *ucp == *(ucp+1) && + *ucp == *(ucp+2) && + *ucp == *(ucp+3)) + { + int runLen = 4; // DEBUG only + i += 3; + ucp += 3; + + while (*ucp == *(ucp+1) && i < kTrackLen) { + runLen++; + ucp++; + i++; + + if (runLen == 256) { + runLen = 0; + break; + } + } + + //printf("Found run of %d of 0x%02x\n", runLen, *ucp); + } else { + /* not a run, just update stats */ + freqCounts[*ucp]++; + } + } +} + +/* + * Find the 20 most frequently occurring symbols, in order. + * + * Modifies "freqCounts". + */ +void +ComputeFavorites(unsigned short* freqCounts, unsigned char* favorites) +{ + int i, fav; + + for (fav = 0; fav < kNumFavorites; fav++) { + unsigned short bestCount = 0; + unsigned char bestSym = 0; + + for (i = 0; i < kNumSymbols; i++) { + if (freqCounts[i] >= bestCount) { + bestSym = (unsigned char) i; + bestCount = freqCounts[i]; + } + } + + favorites[fav] = bestSym; + freqCounts[bestSym] = 0; + } + + //printf("FAVORITES: "); + //for (fav = 0; fav < kNumFavorites; fav++) + // printf("%02x ", favorites[fav]); + //printf("\n"); +} + +/* + * These are all odd, which when they're written in reverse order means + * they all have their hi bits set. + */ +static const unsigned char kFavoriteBitEnc[kNumFavorites] = { + 0x03, 0x09, 0x1f, 0x0f, 0x07, 0x1b, 0x0b, 0x0d, 0x15, 0x37, + 0x3d, 0x25, 0x05, 0xb1, 0x11, 0x21, 0x01, 0x57, 0x5d, 0x1d +}; +static const int kFavoriteBitEncLen[kNumFavorites] = { + 4, 4, 5, 5, 5, 5, 5, 5, 5, 6, + 6, 6, 6, 6, 6, 6, 6, 7, 7, 7 +}; + + +/* + * Compress a track full of data. + */ +void +CompressTrack(const unsigned char* trackBuf, BitBuffer* pBitBuf) +{ + unsigned short freqCounts[kNumSymbols]; + unsigned char favorites[kNumFavorites]; + int i, fav; + + ComputeFreqCounts(trackBuf, freqCounts); + ComputeFavorites(freqCounts, favorites); + + /* write favorites */ + for (fav = 0; fav < kNumFavorites; fav++) + pBitBuf->PutBits(favorites[fav], 8); + + /* + * Compress track data. Store runs as { 0x97 char count }, where + * a count of zero means 256. + */ + const unsigned char* ucp = trackBuf; + for (i = 0; i < kTrackLen; i++, ucp++) { + if (i < (kTrackLen-3) && + *ucp == *(ucp+1) && + *ucp == *(ucp+2) && + *ucp == *(ucp+3)) + { + int runLen = 4; + i += 3; + ucp += 3; + + while (*ucp == *(ucp+1) && i < kTrackLen) { + runLen++; + ucp++; + i++; + + if (runLen == 256) { + runLen = 0; + break; + } + } + + pBitBuf->PutBits(kRLEDelim, 8); // note kRLEDelim has hi bit set + pBitBuf->PutBits(*ucp, 8); + pBitBuf->PutBits(runLen, 8); + + } else { + /* + * Not a run, see if it's one of our favorites. + */ + for (fav = 0; fav < kNumFavorites; fav++) { + if (*ucp == favorites[fav]) + break; + } + if (fav == kNumFavorites) { + /* just a plain byte */ + pBitBuf->PutBits(0x00, 1); + pBitBuf->PutBits(*ucp, 8); + } else { + /* found a favorite; leading hi bit is implied */ + pBitBuf->PutBits(kFavoriteBitEnc[fav], kFavoriteBitEncLen[fav]); + } + } + } +} + + + +/* + * Handle a debug message from the DiskImg library. + */ +/*static*/ void +MsgHandler(const char* file, int line, const char* msg) +{ + assert(file != nil); + assert(msg != nil); + + fprintf(gLog, "%05u %s", gPid, msg); +} +/* + * Handle a global error message from the NufxLib library by shoving it + * through the DiskImgLib message function. + */ +NuResult +NufxErrorMsgHandler(NuArchive* /*pArchive*/, void* vErrorMessage) +{ + const NuErrorMessage* pErrorMessage = (const NuErrorMessage*) vErrorMessage; + + if (pErrorMessage->isDebug) { + Global::PrintDebugMsg(pErrorMessage->file, pErrorMessage->line, + " [D] %s\n", pErrorMessage->message); + } else { + Global::PrintDebugMsg(pErrorMessage->file, pErrorMessage->line, + " %s\n", pErrorMessage->message); + } + + return kNuOK; +} + +/* + * Pack a disk image with DDD. + */ +DIError +Pack(const char* infile, const char* outfile) +{ + DIError dierr = kDIErrNone; + DiskImg srcImg; + FILE* outfp = nil; + BitBuffer bitBuffer; + + printf("Packing in='%s' out='%s'\n", infile, outfile); + + /* + * Prepare the source image. + */ + dierr = srcImg.OpenImage(infile, '/', true); + if (dierr != kDIErrNone) { + fprintf(stderr, "Unable to open disk image: %s.\n", + DiskImgLib::DIStrError(dierr)); + goto bail; + } + + dierr = srcImg.AnalyzeImage(); + if (dierr != kDIErrNone) { + fprintf(stderr, "Unable to determine source image format.\n"); + goto bail; + } + + if (!srcImg.GetHasSectors()) { + fprintf(stderr, "Sorry, only sector-addressable images allowed.\n"); + dierr = kDIErrUnsupportedPhysicalFmt; + goto bail; + } + assert(srcImg.GetNumSectPerTrack() > 0); + + if (srcImg.GetSectorOrder() == DiskImg::kSectorOrderUnknown) { + fprintf(stderr, "(QUERY) don't know sector order\n"); + dierr = kDIErrFilesystemNotFound; + goto bail; + } + + /* force the access to be DOS-ordered */ + dierr = srcImg.OverrideFormat(srcImg.GetPhysicalFormat(), + DiskImg::kFormatGenericDOSOrd, srcImg.GetSectorOrder()); + if (dierr != kDIErrNone) { + fprintf(stderr, "Couldn't switch to generic ProDOS: %s.\n", + DiskImgLib::DIStrError(dierr)); + goto bail; + } + + /* transfer the DOS volume num, if one was set */ + printf("DOS volume number set to %d\n", srcImg.GetDOSVolumeNum()); + if (srcImg.GetDOSVolumeNum() == DiskImg::kVolumeNumNotSet) + srcImg.SetDOSVolumeNum(kDefaultNibbleVolumeNum); + + /* + * Open the output file. + */ + outfp = fopen(outfile, "w"); + if (outfp == nil) { + perror("unable to open output file"); + dierr = kDIErrGeneric; + goto bail; + } + + /* write four zeroes to replace the DOS addr/len bytes */ + /* (let's write the apparent DDD Pro v1.1 signature instead) */ + putc(kDDDProSignature, outfp); + putc(kDDDProSignature >> 8, outfp); + putc(kDDDProSignature >> 16, outfp); + putc(kDDDProSignature >> 24, outfp); + + bitBuffer.SetFile(outfp); + + bitBuffer.PutBits(0x00, 3); + bitBuffer.PutBits(srcImg.GetDOSVolumeNum(), 8); + + /* + * Process all tracks. + */ + for (int track = 0; track < srcImg.GetNumTracks(); track++) { + unsigned char trackBuf[kTrackLen]; + + /* + * Read the track. + */ + for (int sector = 0; sector < srcImg.GetNumSectPerTrack(); sector++) { + dierr = srcImg.ReadTrackSector(track, sector, + trackBuf + sector * 256); + if (dierr != kDIErrNone) { + fprintf(stderr, "ERROR: ReadBlock failed (err=%d)\n", dierr); + goto bail; + } + } + + //printf("Got track %d (0x%02x %02x %02x %02x %02x %02x ...)\n", + // track, trackBuf[0], trackBuf[1], trackBuf[2], trackBuf[3], + // trackBuf[4], trackBuf[5]); + CompressTrack(trackBuf, &bitBuffer); + } + + /* write 8 bits of zeroes to flush remaining data out of buffer */ + bitBuffer.PutBits(0x00, 8); + + /* write another zero byte because that's what DDD Pro v1.1 does */ + long zero; + zero = 0; + fwrite(&zero, 1, 1, outfp); + + dierr = srcImg.CloseImage(); + if (dierr != kDIErrNone) { + fprintf(stderr, "ERROR: srcImg close failed?!\n"); + goto bail; + } + + assert(dierr == kDIErrNone); +bail: + return dierr; +} + +/* + * This is the reverse of the kFavoriteBitEnc table. The bits are + * reversed and lack the high bit. + */ +static const unsigned char kFavoriteBitDec[kNumFavorites] = { + 0x04, 0x01, 0x0f, 0x0e, 0x0c, 0x0b, 0x0a, 0x06, 0x05, 0x1b, + 0x0f, 0x09, 0x08, 0x03, 0x02, 0x01, 0x00, 0x35, 0x1d, 0x1c +}; + +/* + * Unpack a single track. + * + * Returns "true" if all went well, "false" if something failed. + */ +bool +UnpackTrack(BitBuffer* pBitBuffer, unsigned char* trackBuf) +{ + unsigned char favorites[kNumFavorites]; + unsigned char val; + unsigned char* trackPtr; + int fav; + + /* + * Start by pulling our favorites out, in reverse order. + */ + for (fav = 0; fav < kNumFavorites; fav++) { + val = pBitBuffer->GetBits(8); + val = pBitBuffer->Reverse(val); + favorites[fav] = val; + } + + trackPtr = trackBuf; + + /* + * Keep pulling data out until the track is full. + */ + while (trackPtr < trackBuf + kTrackLen) { + val = pBitBuffer->GetBits(1); + if (!val) { + /* simple byte */ + val = pBitBuffer->GetBits(8); + val = pBitBuffer->Reverse(val); + *trackPtr++ = val; + } else { + /* try for a prefix match */ + int extraBits; + + val = pBitBuffer->GetBits(2); + + for (extraBits = 0; extraBits < 4; extraBits++) { + val = (val << 1) | pBitBuffer->GetBits(1); + int start, end; + + if (extraBits == 0) { + start = 0; + end = 2; + } else if (extraBits == 1) { + start = 2; + end = 9; + } else if (extraBits == 2) { + start = 9; + end = 17; + } else { + start = 17; + end = 20; + } + + while (start < end) { + if (val == kFavoriteBitDec[start]) { + /* winner! */ + *trackPtr++ = favorites[start]; + break; + } + start++; + } + if (start != end) + break; // we got it, break out of for loop + } + if (extraBits == 4) { + /* we didn't get it, this must be RLE */ + unsigned char rleChar; + int rleCount; + + (void) pBitBuffer->GetBits(1); // get last bit of 0x97 + val = pBitBuffer->GetBits(8); + rleChar = pBitBuffer->Reverse(val); + val = pBitBuffer->GetBits(8); + rleCount = pBitBuffer->Reverse(val); + printf("Found run of %d of 0x%02x\n", rleCount, rleChar); + + if (rleCount == 0) + rleCount = 256; + + /* make sure we won't overrun */ + if (trackPtr + rleCount > trackBuf + kTrackLen) { + printf("Overrun in RLE\n"); + return false; + } + while (rleCount--) + *trackPtr++ = rleChar; + } + } + } + + return true; +} + +/* + * Unpack a disk image compressed with DDD. + * + * The result is an unadorned DOS-ordered image. + */ +DIError +Unpack(const char* infile, const char* outfile) +{ + DIError dierr = kDIErrNone; + FILE* infp = nil; + FILE* outfp = nil; + BitBuffer bitBuffer; + unsigned char val; + + printf("Unpacking in='%s' out='%s'\n", infile, outfile); + + /* + * Open the input file. + */ + infp = fopen(infile, "r"); + if (infp == nil) { + perror("unable to open input file"); + dierr = kDIErrGeneric; + goto bail; + } + + /* + * Open the output file. + */ + outfp = fopen(outfile, "w"); + if (outfp == nil) { + perror("unable to open output file"); + dierr = kDIErrGeneric; + goto bail; + } + + /* read four zeroes to skip the DOS addr/len bytes */ + (void) getc(infp); + (void) getc(infp); + (void) getc(infp); + (void) getc(infp); + + bitBuffer.SetFile(infp); + + val = bitBuffer.GetBits(3); + if (val != 0) { + printf("HEY: this isn't a DDD II file (%d)\n", val); + dierr = kDIErrGeneric; + goto bail; + } + val = bitBuffer.GetBits(8); + val = bitBuffer.Reverse(val); + printf("GOT disk volume num = %d\n", val); + + for (int track = 0; track < kNumTracks; track++) { + unsigned char trackBuf[kTrackLen]; + + if (!UnpackTrack(&bitBuffer, trackBuf)) { + fprintf(stderr, "FAILED on track %d\n", track); + dierr = kDIErrBadCompressedData; + goto bail; + } + if (feof(infp) || ferror(infp)) { + fprintf(stderr, "Failure or EOF on input file\n"); + dierr = kDIErrBadCompressedData; + goto bail; + } + fwrite(trackBuf, 1, 4096, outfp); + } + + /* + * We should be within a byte or two of the end of the file. + */ + (void) getc(infp); + (void) getc(infp); + (void) getc(infp); + (void) getc(infp); + if (!feof(infp)) { + fprintf(stderr, "Looks like too much data in input file\n"); + dierr = kDIErrBadCompressedData; + goto bail; + } + + assert(dierr == kDIErrNone); +bail: + return dierr; +} + +/* + * Process every argument. + */ +int +main(int argc, char** argv) +{ +// const char* kLogFile = "iconv-log.txt"; + + if (argc != 3) { + fprintf(stderr, "%s: infile outfile\n", argv[0]); + exit(2); + } + + gLog = stdout; +// gLog = fopen(kLogFile, "w"); +// if (gLog == nil) { +// fprintf(stderr, "ERROR: unable to open log file\n"); +// exit(1); +// } + + printf("DDD Converter for Linux v1.0\n"); + printf("Copyright (C) 2003 by faddenSoft, LLC. All rights reserved.\n"); + long major, minor, bug; + Global::GetVersion(&major, &minor, &bug); + printf("Linked against DiskImg library v%ld.%ld.%ld\n", + major, minor, bug); +// printf("Log file is '%s'\n", kLogFile); + printf("\n"); + + Global::SetDebugMsgHandler(MsgHandler); + Global::AppInit(); + + NuSetGlobalErrorMessageHandler(NufxErrorMsgHandler); + + int len = strlen(argv[2]); + if (len > 3 && strcasecmp(argv[2] + len - 3, ".do") == 0) { + Unpack(argv[1], argv[2]); + } else { + Pack(argv[1], argv[2]); + } + + Global::AppCleanup(); + fclose(gLog); + + exit(0); +} + diff --git a/linux/SSTAsm.cpp b/linux/SSTAsm.cpp new file mode 100644 index 0000000..4c46de2 --- /dev/null +++ b/linux/SSTAsm.cpp @@ -0,0 +1,325 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Reassemble SST disk images into a .NIB file. + */ +#include +#include +#include +#include +#include "../diskimg/DiskImg.h" + +using namespace DiskImgLib; + +#define nil NULL + + +#if 0 +inline int +ConvOddEven(unsigned char val1, unsigned char val2) +{ + return ((val1 & 0x55) << 1) | (val2 & 0x55); +} +#endif + + +const int kSSTNumTracks = 35; +const int kSSTTrackLen = 6656; // or 6384 for .NB2 + +/* + * Compute the destination file offset for a particular source track. The + * track number ranges from 0 to 69 inclusive. Sectors from two adjacent + * "cooked" tracks are combined into a single "raw nibbilized" track. + * + * The data is ordered like this: + * track 1 sector 15 --> track 1 sector 4 (12 sectors) + * track 0 sector 13 --> track 0 sector 0 (14 sectors) + * + * Total of 26 sectors, or $1a00 bytes. + */ +long +GetBufOffset(int track) +{ + assert(track >= 0 && track < kSSTNumTracks*2); + + long offset; + + if (track & 0x01) { + /* odd, use start of data */ + offset = (track / 2) * kSSTTrackLen; + } else { + /* even, start of data plus 12 sectors */ + offset = (track / 2) * kSSTTrackLen + 12 * 256; + } + + assert(offset >= 0 && offset < kSSTTrackLen * kSSTNumTracks); + + return offset; +} + +/* + * Copy 17.5 tracks of data from the SST image to a .NIB image. + * + * Data is stored in all 16 sectors of track 0, followed by the first + * 12 sectors of track 1, then on to track 2. Total of $1a00 bytes. + */ +int +LoadSSTData(DiskImg* pDiskImg, int seqNum, unsigned char* trackBuf) +{ + DIError dierr; + char sctBuf[256]; + int track, sector; + long bufOffset; + + for (track = 0; track < kSSTNumTracks; track++) { + int virtualTrack = track + (seqNum * kSSTNumTracks); + bufOffset = GetBufOffset(virtualTrack); + //fprintf(stderr, "USING offset=%ld (track=%d / %d)\n", + // bufOffset, track, virtualTrack); + + if (virtualTrack & 0x01) { + /* odd-numbered track, sectors 15-4 */ + for (sector = 15; sector >= 4; sector--) { + dierr = pDiskImg->ReadTrackSector(track, sector, sctBuf); + if (dierr != kDIErrNone) { + fprintf(stderr, "ERROR: on track=%d sector=%d\n", + track, sector); + return -1; + } + + memcpy(trackBuf + bufOffset, sctBuf, 256); + bufOffset += 256; + } + } else { + for (sector = 13; sector >= 0; sector--) { + dierr = pDiskImg->ReadTrackSector(track, sector, sctBuf); + if (dierr != kDIErrNone) { + fprintf(stderr, "ERROR: on track=%d sector=%d\n", + track, sector); + return -1; + } + + memcpy(trackBuf + bufOffset, sctBuf, 256); + bufOffset += 256; + } + } + } + +#if 0 + int i; + for (i = 0; (size_t) i < sizeof(trackBuf)-10; i++) { + if ((trackBuf[i] | 0x80) == 0xd5 && + (trackBuf[i+1] | 0x80) == 0xaa && + (trackBuf[i+2] | 0x80) == 0x96) + { + fprintf(stderr, "off=%5d vol=%d trk=%d sct=%d chk=%d\n", i, + ConvOddEven(trackBuf[i+3], trackBuf[i+4]), + ConvOddEven(trackBuf[i+5], trackBuf[i+6]), + ConvOddEven(trackBuf[i+7], trackBuf[i+8]), + ConvOddEven(trackBuf[i+9], trackBuf[i+10])); + i += 10; + if ((size_t)i < sizeof(trackBuf)-3) { + fprintf(stderr, " 0x%02x 0x%02x 0x%02x\n", + trackBuf[i+1], trackBuf[i+2], trackBuf[i+3]); + } + } + } +#endif + + return 0; +} + +/* + * Copy sectors from a single image. + */ +int +HandleSSTImage(const char* fileName, int seqNum, unsigned char* trackBuf) +{ + DIError dierr; + DiskImg diskImg; + int result = -1; + + fprintf(stderr, "Handling '%s'\n", fileName); + + dierr = diskImg.OpenImage(fileName, '/', true); + if (dierr != kDIErrNone) { + fprintf(stderr, "ERROR: unable to open '%s'\n", fileName); + goto bail; + } + + dierr = diskImg.AnalyzeImage(); + if (dierr != kDIErrNone) { + fprintf(stderr, "ERROR: image analysis failed\n"); + goto bail; + } + + if (diskImg.GetSectorOrder() == DiskImg::kSectorOrderUnknown) { + fprintf(stderr, "ERROR: sector order not set\n"); + goto bail; + } + if (diskImg.GetFSFormat() != DiskImg::kFormatUnknown) { + fprintf(stderr, "WARNING: file format *was* recognized!\n"); + goto bail; + } + if (diskImg.GetNumTracks() != kSSTNumTracks || + diskImg.GetNumSectPerTrack() != 16) + { + fprintf(stderr, "ERROR: only 140K floppies can be SST inputs\n"); + goto bail; + } + + dierr = diskImg.OverrideFormat(diskImg.GetPhysicalFormat(), + DiskImg::kFormatGenericDOSOrd, diskImg.GetSectorOrder()); + if (dierr != kDIErrNone) { + fprintf(stderr, "ERROR: format override failed\n"); + goto bail; + } + + /* + * We have the image open successfully, now do something with it. + */ + result = LoadSSTData(&diskImg, seqNum, trackBuf); + +bail: + return result; +} + +/* + * Run through the data, adding 0x80 everywhere and re-aligning the + * tracks so that the big clump of sync bytes is at the end. + */ +int +ProcessTrackData(unsigned char* trackBuf) +{ + unsigned char* trackPtr; + int track; + + for (track = 0, trackPtr = trackBuf; track < kSSTNumTracks; + track++, trackPtr += kSSTTrackLen) + { + bool inRun; + int start = 0; + int longestStart = -1; + int count7f = 0; + int longest = -1; + int i; + + inRun = false; + for (i = 0; i < kSSTTrackLen; i++) { + if (trackPtr[i] == 0x7f) { + if (inRun) { + count7f++; + } else { + count7f = 1; + start = i; + inRun = true; + } + } else { + if (inRun) { + if (count7f > longest) { + longest = count7f; + longestStart = start; + } + inRun = false; + } else { + /* do nothing */ + } + } + + trackPtr[i] |= 0x80; + } + + + if (longest == -1) { + fprintf(stderr, "HEY: couldn't find any 0x7f in track %d\n", + track); + } else { + fprintf(stderr, "Found run of %d at %d in track %d\n", + longest, longestStart, track); + + int bkpt = longestStart + longest; + assert(bkpt < kSSTTrackLen); + + char oneTrack[kSSTTrackLen]; + memcpy(oneTrack, trackPtr, kSSTTrackLen); + + /* copy it back so sync bytes are at end of track */ + memcpy(trackPtr, oneTrack + bkpt, kSSTTrackLen - bkpt); + memcpy(trackPtr + (kSSTTrackLen - bkpt), oneTrack, bkpt); + } + } + + return 0; +} + +/* + * Read sectors from file1 and file2, and write them in the correct + * sequence to outfp. + */ +int +ReassembleSST(const char* file1, const char* file2, FILE* outfp) +{ + unsigned char* trackBuf = nil; + int result; + + trackBuf = new unsigned char[kSSTNumTracks * kSSTTrackLen]; + if (trackBuf == nil) { + fprintf(stderr, "ERROR: malloc failed\n"); + return -1; + } + + result = HandleSSTImage(file1, 0, trackBuf); + if (result != 0) + return result; + + result = HandleSSTImage(file2, 1, trackBuf); + if (result != 0) + return result; + + result = ProcessTrackData(trackBuf); + + fprintf(stderr, "Writing %d bytes\n", kSSTNumTracks * kSSTTrackLen); + fwrite(trackBuf, 1, kSSTNumTracks * kSSTTrackLen, outfp); + + delete[] trackBuf; + return result; +} + + +/* + * Handle a debug message from the DiskImg library. + */ +/*static*/ void +MsgHandler(const char* file, int line, const char* msg) +{ + assert(file != nil); + assert(msg != nil); + + fprintf(stderr, "%s", msg); +} + +/* + * Parse args, go. + */ +int +main(int argc, char** argv) +{ + int result; + + if (argc != 3) { + fprintf(stderr, "Usage: %s file1 file2 > outfile\n", argv[0]); + exit(2); + } + + Global::SetDebugMsgHandler(MsgHandler); + Global::AppInit(); + + result = ReassembleSST(argv[1], argv[2], stdout); + + Global::AppCleanup(); + exit(result != 0); +} + diff --git a/linux/StringArray.h b/linux/StringArray.h new file mode 100644 index 0000000..33feaf3 --- /dev/null +++ b/linux/StringArray.h @@ -0,0 +1,101 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * An expandable array of strings. + */ +#ifndef __STRING_ARRAY__ +#define __STRING_ARRAY__ + +#include +#include +#include + +// +// This is a simple container for an array of strings. You can add strings +// to the list and sort them. +// +class StringArray { +public: + StringArray() + : mMax(0), mCurrent(0), mArray(NULL) + {} + virtual ~StringArray() + { + for (int i = 0; i < mCurrent; i++) + delete[] mArray[i]; + delete[] mArray; + } + + // + // Add a string. A copy of the string is made. + // + bool Add(const char* str) + { + if (mCurrent >= mMax) { + char** tmp; + + if (mMax == 0) + mMax = 16; + else + mMax *= 2; + + tmp = new char*[mMax]; + if (tmp == NULL) + return false; + + memcpy(tmp, mArray, mCurrent * sizeof(char*)); + delete[] mArray; + mArray = tmp; + } + + int len = strlen(str); + mArray[mCurrent] = new char[len+1]; + memcpy(mArray[mCurrent], str, len+1); + mCurrent++; + + return true; + } + + // + // Sort the array. Supply a sort function that takes two strings + // and returns <0, 0, or >0 if the first argument is less than, + // equal to, or greater than the second argument. (strcmp works.) + // + void Sort(int (*compare)(const void*, const void*)) + { + qsort(mArray, mCurrent, sizeof(char*), compare); + } + + // + // Use this as an argument to the sort routine. + // + static int CmpAscendingAlpha(const void* pstr1, const void* pstr2) + { + return strcmp(*(const char**)pstr1, *(const char**)pstr2); + } + + // + // Get the #of items in the array. + // + inline int GetCount(void) const { return mCurrent; } + + // + // Get entry N. + // + const char* GetEntry(int idx) const + { + if (idx < 0 || idx >= mCurrent) + return NULL; + return mArray[idx]; + } + +private: + int mMax; + int mCurrent; + char** mArray; +}; + +#endif /*__STRING_ARRAY__*/ diff --git a/mdc/AboutDlg.cpp b/mdc/AboutDlg.cpp new file mode 100644 index 0000000..d5b83fb --- /dev/null +++ b/mdc/AboutDlg.cpp @@ -0,0 +1,56 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +// AboutDlg.cpp : implementation file +// + +#include "stdafx.h" +#include "mdc.h" +#include "AboutDlg.h" + +#ifdef _DEBUG +#define new DEBUG_NEW +#undef THIS_FILE +static char THIS_FILE[] = __FILE__; +#endif + +///////////////////////////////////////////////////////////////////////////// +// AboutDlg dialog + + +AboutDlg::AboutDlg(CWnd* pParent /*=NULL*/) + : CDialog(AboutDlg::IDD, pParent) +{ + //{{AFX_DATA_INIT(AboutDlg) + // NOTE: the ClassWizard will add member initialization here + //}}AFX_DATA_INIT +} + + +BEGIN_MESSAGE_MAP(AboutDlg, CDialog) + //{{AFX_MSG_MAP(AboutDlg) + //}}AFX_MSG_MAP +END_MESSAGE_MAP() + +///////////////////////////////////////////////////////////////////////////// +// AboutDlg message handlers + +BOOL AboutDlg::OnInitDialog() +{ + CDialog::OnInitDialog(); + + // TODO: Add extra initialization here + CWnd* pWnd = GetDlgItem(IDC_ABOUT_VERS); + ASSERT(pWnd != nil); + CString fmt, newText; + + pWnd->GetWindowText(fmt); + newText.Format(fmt, kAppMajorVersion, kAppMinorVersion, kAppBugVersion); + pWnd->SetWindowText(newText); + WMSG1("STR is '%s'\n", newText); + + return TRUE; // return TRUE unless you set the focus to a control + // EXCEPTION: OCX Property Pages should return FALSE +} diff --git a/mdc/AboutDlg.h b/mdc/AboutDlg.h new file mode 100644 index 0000000..3b067ef --- /dev/null +++ b/mdc/AboutDlg.h @@ -0,0 +1,49 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +#if !defined(AFX_ABOUTDLG_H__340C7108_C3D5_4F24_98AF_C1C614460F6A__INCLUDED_) +#define AFX_ABOUTDLG_H__340C7108_C3D5_4F24_98AF_C1C614460F6A__INCLUDED_ + +#if _MSC_VER > 1000 +#pragma once +#endif // _MSC_VER > 1000 +// AboutDlg.h : header file +// + +///////////////////////////////////////////////////////////////////////////// +// AboutDlg dialog + +class AboutDlg : public CDialog +{ +// Construction +public: + AboutDlg(CWnd* pParent = NULL); // standard constructor + +// Dialog Data + //{{AFX_DATA(AboutDlg) + enum { IDD = IDD_ABOUTBOX }; + // NOTE: the ClassWizard will add data members here + //}}AFX_DATA + + +// Overrides + // ClassWizard generated virtual function overrides + //{{AFX_VIRTUAL(AboutDlg) + //}}AFX_VIRTUAL + +// Implementation +protected: + + // Generated message map functions + //{{AFX_MSG(AboutDlg) + virtual BOOL OnInitDialog(); + //}}AFX_MSG + DECLARE_MESSAGE_MAP() +}; + +//{{AFX_INSERT_LOCATION}} +// Microsoft Visual C++ will insert additional declarations immediately before the previous line. + +#endif // !defined(AFX_ABOUTDLG_H__340C7108_C3D5_4F24_98AF_C1C614460F6A__INCLUDED_) diff --git a/mdc/ChooseFilesDlg.cpp b/mdc/ChooseFilesDlg.cpp new file mode 100644 index 0000000..ed00c35 --- /dev/null +++ b/mdc/ChooseFilesDlg.cpp @@ -0,0 +1,29 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Support for the "choose files" dialog. + */ +#include "stdafx.h" +#include "ChooseFilesDlg.h" + + +/* + * Override base class version so we can move our stuff around. + * + * It's important that the base class be called last, because it calls + * Invalidate to redraw the dialog. + */ +void +ChooseFilesDlg::ShiftControls(int deltaX, int deltaY) +{ + /* + * These only need to be here so that the initial move puts them + * where they belong. Once the dialog has been created, the + * CFileDialog will move things where they need to go. + */ + MoveControl(this, IDC_CHOOSEFILES_STATIC1, 0, deltaY, false); + SelectFilesDialog::ShiftControls(deltaX, deltaY); +} diff --git a/mdc/ChooseFilesDlg.h b/mdc/ChooseFilesDlg.h new file mode 100644 index 0000000..eaa442e --- /dev/null +++ b/mdc/ChooseFilesDlg.h @@ -0,0 +1,39 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Choose files and directories. + */ +#ifndef __CHOOSEFILESDIALOG__ +#define __CHOOSEFILESDIALOG__ + +#include "../util/UtilLib.h" +#include "resource.h" + +class ChooseFilesDlg : public SelectFilesDialog { +public: + ChooseFilesDlg(CWnd* pParentWnd = NULL) : + SelectFilesDialog("IDD_CHOOSE_FILES", pParentWnd) + { + SetWindowTitle(_T("Choose Files...")); + + fAcceptButtonID = IDC_SELECT_ACCEPT; + } + virtual ~ChooseFilesDlg(void) {} + +private: + //virtual bool MyDataExchange(bool saveAndValidate); + virtual void ShiftControls(int deltaX, int deltaY); + //virtual UINT MyOnCommand(WPARAM wParam, LPARAM lParam); + + //void OnIDHelp(void); + + //int GetButtonCheck(int id); + //void SetButtonCheck(int id, int checkVal); + + //DECLARE_MESSAGE_MAP() +}; + +#endif /*__CHOOSEFILESDIALOG__*/ \ No newline at end of file diff --git a/mdc/Main.cpp b/mdc/Main.cpp new file mode 100644 index 0000000..899a9a3 --- /dev/null +++ b/mdc/Main.cpp @@ -0,0 +1,1044 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Main window management. + */ +#include "stdafx.h" +#include "Main.h" +#include "mdc.h" +#include "AboutDlg.h" +#include "ChooseFilesDlg.h" +#include "ProgressDlg.h" +#include "resource.h" +#include "../diskimg/DiskImg.h" +#include "../prebuilt/zlib.h" + +const char* kWebSiteURL = "http://www.faddensoft.com/"; + + +BEGIN_MESSAGE_MAP(MainWindow, CFrameWnd) + ON_COMMAND(IDM_FILE_SCAN, OnFileScan) + ON_COMMAND(IDM_FILE_EXIT, OnFileExit) + ON_COMMAND(IDM_HELP_WEBSITE, OnHelpWebSite) + ON_COMMAND(IDM_HELP_ABOUT, OnHelpAbout) +END_MESSAGE_MAP() + + +/* + * MainWindow constructor. Creates the main window and sets + * its properties. + */ +MainWindow::MainWindow() +{ + static const char* kAppName = "MDC"; + + CString wndClass = AfxRegisterWndClass( + CS_DBLCLKS /*| CS_HREDRAW | CS_VREDRAW*/, + gMyApp.LoadStandardCursor(IDC_ARROW), + (HBRUSH) (COLOR_WINDOW + 1), + gMyApp.LoadIcon(IDI_MDC) ); + + Create(wndClass, kAppName, WS_OVERLAPPEDWINDOW /*| WS_CLIPCHILDREN*/, + rectDefault, NULL, MAKEINTRESOURCE(IDC_MDC)); + + LoadAccelTable(MAKEINTRESOURCE(IDC_MDC)); + + // initialize some OLE garbage + //AfxOleInit(); + + // required by MFC if Rich Edit controls are used + //AfxInitRichEdit(); + + DiskImgLib::Global::SetDebugMsgHandler(DebugMsgHandler); + DiskImgLib::Global::AppInit(); + + NuSetGlobalErrorMessageHandler(NufxErrorMsgHandler); + + //fTitleAnimation = 0; + fCancelFlag = false; +} + +/* + * MainWindow destructor. Close the archive if one is open, but don't try + * to shut down any controls in child windows. By this point, Windows has + * already snuffed them. + */ +MainWindow::~MainWindow() +{ +// int cc; +// cc = ::WinHelp(m_hWnd, ::AfxGetApp()->m_pszHelpFilePath, HELP_QUIT, 0); +// WMSG1("Turning off WinHelp returned %d\n", cc); + + DiskImgLib::Global::AppCleanup(); +} + +/* + * Handle Exit item by sending a close request. + */ +void +MainWindow::OnFileExit(void) +{ + SendMessage(WM_CLOSE, 0, 0); +} + +/* + * Go to the faddenSoft web site. + */ +void +MainWindow::OnHelpWebSite(void) +{ + int err; + + err = (int) ::ShellExecute(m_hWnd, _T("open"), kWebSiteURL, NULL, NULL, + SW_SHOWNORMAL); + if (err <= 32) { + CString msg; + msg.Format("Unable to launch web browser (err=%d).", err); + ShowFailureMsg(this, msg, IDS_FAILED); + } +} + +/* + * Pop up the About box. + */ +void +MainWindow::OnHelpAbout(void) +{ + int result; + + AboutDlg dlg(this); + + result = dlg.DoModal(); + WMSG1("HelpAbout returned %d\n", result); +} + + +/* + * Handle "scan" item. + */ +void +MainWindow::OnFileScan(void) +{ + if (0) { + CString msg; + msg.LoadString(IDS_MUST_REGISTER); + ShowFailureMsg(this, msg, IDS_APP_TITLE); + } else { + ScanFiles(); + } +} + +/* + * Allow events to flow through the message queue whenever the + * progress meter gets updated. This will allow us to redraw with + * reasonable frequency. + * + * Calling this can result in other code being called, such as Windows + * message handlers, which can lead to reentrancy problems. Make sure + * you're adequately semaphored before calling here. + * + * Returns TRUE if all is well, FALSE if we're trying to quit. + */ +BOOL +MainWindow::PeekAndPump(void) +{ + MSG msg; + + while (::PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE)) { + if (!AfxGetApp()->PumpMessage()) { + ::PostQuitMessage(0); + return FALSE; + } + } + + LONG lIdle = 0; + while (AfxGetApp()->OnIdle(lIdle++)) + ; + return TRUE; +} + + +/* + * ========================================================================== + * Disk image processing + * ========================================================================== + */ + +/* + * Handle a debug message from the DiskImg library. + */ +/*static*/ void +MainWindow::DebugMsgHandler(const char* file, int line, const char* msg) +{ + ASSERT(file != nil); + ASSERT(msg != nil); + +#if defined(_DEBUG_LOG) + //fprintf(gLog, "%s(%d) : %s", file, line, msg); + fprintf(gLog, "%05u %s", gPid, msg); +#elif defined(_DEBUG) + _CrtDbgReport(_CRT_WARN, file, line, NULL, "%s", msg); +#else + /* do nothing */ +#endif +} +/* + * Handle a global error message from the NufxLib library. + */ +/*static*/ NuResult +MainWindow::NufxErrorMsgHandler(NuArchive* /*pArchive*/, void* vErrorMessage) +{ + const NuErrorMessage* pErrorMessage = (const NuErrorMessage*) vErrorMessage; + +#if defined(_DEBUG_LOG) + if (pErrorMessage->isDebug) { + fprintf(gLog, "%05u [D] %s\n", gPid, pErrorMessage->message); + } else { + fprintf(gLog, "%05u %s\n", gPid, pErrorMessage->message); + } +#elif defined(_DEBUG) + if (pErrorMessage->isDebug) { + _CrtDbgReport(_CRT_WARN, pErrorMessage->file, pErrorMessage->line, + NULL, " [D] %s\n", pErrorMessage->message); + } else { + _CrtDbgReport(_CRT_WARN, pErrorMessage->file, pErrorMessage->line, + NULL, " %s\n", pErrorMessage->message); + } +#else + /* do nothing */ +#endif + + return kNuOK; +} + +const int kLocalFssep = '\\'; + +typedef struct ScanOpts { + FILE* outfp; + ProgressDlg* pProgress; +} ScanOpts; + +/* + * Scan a set of files. + */ +void +MainWindow::ScanFiles(void) +{ + ChooseFilesDlg chooseFiles; + ScanOpts scanOpts; + char curDir[MAX_PATH] = ""; + CString errMsg; + CString outPath; + bool doResetDir = false; + + memset(&scanOpts, 0, sizeof(scanOpts)); + + /* choose input files */ + chooseFiles.DoModal(); + if (chooseFiles.GetExitStatus() != IDOK) + return; + + const char* buf = chooseFiles.GetFileNames(); + WMSG2("Selected path = '%s' (offset=%d)\n", buf, + chooseFiles.GetFileNameOffset()); + + /* choose output file */ + CFileDialog dlg(FALSE, _T("txt"), NULL, + OFN_OVERWRITEPROMPT|OFN_NOREADONLYRETURN, + "Text Files (*.txt)|*.txt|All Files (*.*)|*.*||", this); + + dlg.m_ofn.lpstrTitle = "Save Output As..."; + strcpy(dlg.m_ofn.lpstrFile, "mdc-out.txt"); + + if (dlg.DoModal() != IDOK) { + goto bail; + } + + outPath = dlg.GetPathName(); + WMSG1("NEW FILE '%s'\n", (LPCTSTR) outPath); + + scanOpts.outfp = fopen(outPath, "w"); + if (scanOpts.outfp == nil) { + ShowFailureMsg(this, "Unable to open output file", IDS_FAILED); + goto bail; + } + + long major, minor, bug; + DiskImgLib::Global::GetVersion(&major, &minor, &bug); + fprintf(scanOpts.outfp, "MDC for Windows v%d.%d.%d (DiskImg library v%ld.%ld.%ld)\n", + kAppMajorVersion, kAppMinorVersion, kAppBugVersion, + major, minor, bug); + fprintf(scanOpts.outfp, + "Copyright (C) 2006 by faddenSoft, LLC. All rights reserved.\n"); + fprintf(scanOpts.outfp, + "MDC is part of CiderPress, available from http://www.faddensoft.com/.\n"); + NuGetVersion(&major, &minor, &bug, NULL, NULL); + fprintf(scanOpts.outfp, + "Linked against NufxLib v%ld.%ld.%ld and zlib v%s\n", + major, minor, bug, zlibVersion()); + fprintf(scanOpts.outfp, "\n"); + + /* change to base directory */ + if (GetCurrentDirectory(sizeof(curDir), curDir) == 0) { + errMsg = "Unable to get current directory."; + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + if (SetCurrentDirectory(buf) == false) { + errMsg.Format("Unable to set current directory to '%s'.", buf); + ShowFailureMsg(this, errMsg, IDS_FAILED); + goto bail; + } + doResetDir = true; + + time_t now; + now = time(nil); + fprintf(scanOpts.outfp, + "Run started at %.24s in '%s'\n\n", ctime(&now), buf); + + /* obstruct input to the main window */ + EnableWindow(FALSE); + + + /* create a modeless dialog with a cancel button */ + scanOpts.pProgress = new ProgressDlg; + if (scanOpts.pProgress == nil) + goto bail; + scanOpts.pProgress->fpCancelFlag = &fCancelFlag; + fCancelFlag = false; + if (scanOpts.pProgress->Create(this) == FALSE) { + WMSG0("WARNING: ProgressDlg init failed\n"); + ASSERT(false); + } else { + scanOpts.pProgress->CenterWindow(this); + } + + time_t start, end; + start = time(nil); + + /* start cranking */ + buf += chooseFiles.GetFileNameOffset(); + while (*buf != '\0') { + if (Process(buf, &scanOpts, &errMsg) != 0) { + WMSG2("Skipping '%s': %s.\n", buf, (LPCTSTR) errMsg); + } + + if (fCancelFlag) { + WMSG0("CANCELLED by user\n"); + MessageBox("Cancelled!", "MDC", MB_OK); + goto bail; + } + + buf += strlen(buf)+1; + } + end = time(nil); + fprintf(scanOpts.outfp, "\nScan completed in %ld seconds.\n", + end - start); + + { + SetWindowText(_T("MDC Done!")); + + CString doneMsg; + CString appName; + + appName.LoadString(IDS_APP_TITLE); +#ifdef _DEBUG_LOG + doneMsg.Format("Processing completed.\r\n\r\n" + "Output is in '%s', log messages in '%s'.", + outPath, kDebugLog); +#else + doneMsg.Format("Processing completed."); +#endif + scanOpts.pProgress->MessageBox(doneMsg, appName, MB_OK|MB_ICONINFORMATION); + } + +bail: + if (scanOpts.outfp != nil) + fclose(scanOpts.outfp); + + if (doResetDir && SetCurrentDirectory(curDir) == false) { + errMsg.Format("Unable to reset current directory to '%s'.\n", curDir); + ShowFailureMsg(this, errMsg, IDS_FAILED); + // bummer + } + + // restore the main window + EnableWindow(TRUE); + + if (scanOpts.pProgress != nil) + scanOpts.pProgress->DestroyWindow(); + + SetWindowText(_T("MDC")); +} + +/* + * Directory structure and functions, based on zDIR in Info-Zip sources. + */ +typedef struct Win32dirent { + char d_attr; + char d_name[MAX_PATH]; + int d_first; + HANDLE d_hFindFile; +} Win32dirent; + +static const char* kWildMatchAll = "*.*"; + +/* + * Prepare a directory for reading. + * + * Allocates a Win32dirent struct that must be freed by the caller. + */ +Win32dirent* +MainWindow::OpenDir(const char* name) +{ + Win32dirent* dir = nil; + char* tmpStr = nil; + char* cp; + WIN32_FIND_DATA fnd; + + dir = (Win32dirent*) malloc(sizeof(*dir)); + tmpStr = (char*) malloc(strlen(name) + (2 + sizeof(kWildMatchAll))); + if (dir == nil || tmpStr == nil) + goto failed; + + strcpy(tmpStr, name); + cp = tmpStr + strlen(tmpStr); + + /* don't end in a colon (e.g. "C:") */ + if ((cp - tmpStr) > 0 && strrchr(tmpStr, ':') == (cp - 1)) + *cp++ = '.'; + /* must end in a slash */ + if ((cp - tmpStr) > 0 && + strrchr(tmpStr, kLocalFssep) != (cp - 1)) + *cp++ = kLocalFssep; + + strcpy(cp, kWildMatchAll); + + dir->d_hFindFile = FindFirstFile(tmpStr, &fnd); + if (dir->d_hFindFile == INVALID_HANDLE_VALUE) + goto failed; + + strcpy(dir->d_name, fnd.cFileName); + dir->d_attr = (unsigned char) fnd.dwFileAttributes; + dir->d_first = 1; + +bail: + free(tmpStr); + return dir; + +failed: + free(dir); + dir = nil; + goto bail; +} + +/* + * Get an entry from an open directory. + * + * Returns a nil pointer after the last entry has been read. + */ +Win32dirent* +MainWindow::ReadDir(Win32dirent* dir) +{ + if (dir->d_first) + dir->d_first = 0; + else { + WIN32_FIND_DATA fnd; + + if (!FindNextFile(dir->d_hFindFile, &fnd)) + return nil; + strcpy(dir->d_name, fnd.cFileName); + dir->d_attr = (unsigned char) fnd.dwFileAttributes; + } + + return dir; +} + +/* + * Close a directory. + */ +void +MainWindow::CloseDir(Win32dirent* dir) +{ + if (dir == nil) + return; + + FindClose(dir->d_hFindFile); + free(dir); +} + +/* might as well blend in with the UNIX version */ +#define DIR_NAME_LEN(dirent) ((int)strlen((dirent)->d_name)) + + +/* + * Process a file or directory. These are expected to be names of files in + * the current directory. + * + * Returns 0 on success, nonzero on error with a message in "*pErrMsg". + */ +int +MainWindow::Process(const char* pathname, ScanOpts* pScanOpts, + CString* pErrMsg) +{ + bool exists, isDir, isReadable; + struct stat sb; + int result = -1; + + if (fCancelFlag) + return -1; + + ASSERT(pathname != nil); + ASSERT(pErrMsg != nil); + + PathName checkPath(pathname); + int ierr = checkPath.CheckFileStatus(&sb, &exists, &isReadable, &isDir); + if (ierr != 0) { + pErrMsg->Format("Unexpected error while examining '%s': %s", pathname, + strerror(ierr)); + goto bail; + } + + if (!exists) { + pErrMsg->Format("Couldn't find '%s'", pathname); + goto bail; + } + if (!isReadable) { + pErrMsg->Format("File '%s' isn't readable", pathname); + goto bail; + } + if (isDir) { + result = ProcessDirectory(pathname, pScanOpts, pErrMsg); + goto bail; + } + + (void) ScanDiskImage(pathname, pScanOpts); + + result = 0; + +bail: + if (result != 0 && pErrMsg->IsEmpty()) { + pErrMsg->Format("Unable to add file '%s'", pathname); + } + return result; +} + +/* + * Win32 recursive directory descent. Scan the contents of a directory. + * If a subdirectory is found, follow it; otherwise, call Win32AddFile to + * add the file. + */ +int +MainWindow::ProcessDirectory(const char* dirName, ScanOpts* pScanOpts, + CString* pErrMsg) +{ + Win32dirent* dirp = nil; + Win32dirent* entry; + char nbuf[MAX_PATH]; /* malloc might be better; this soaks stack */ + char fssep; + int len; + int result = -1; + + ASSERT(dirName != nil); + ASSERT(pErrMsg != nil); + + WMSG1("+++ DESCEND: '%s'\n", dirName); + + dirp = OpenDir(dirName); + if (dirp == nil) { + pErrMsg->Format("Failed on '%s': %s", dirName, strerror(errno)); + goto bail; + } + + fssep = kLocalFssep; + + /* could use readdir_r, but we don't care about reentrancy here */ + while ((entry = ReadDir(dirp)) != nil) { + /* skip the dotsies */ + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) + continue; + + len = strlen(dirName); + if (len + DIR_NAME_LEN(entry) +2 > MAX_PATH) { + WMSG4("ERROR: Filename exceeds %d bytes: %s%c%s", + MAX_PATH, dirName, fssep, entry->d_name); + goto bail; + } + + /* form the new name, inserting an fssep if needed */ + strcpy(nbuf, dirName); + if (dirName[len-1] != fssep) + nbuf[len++] = fssep; + strcpy(nbuf+len, entry->d_name); + + result = Process(nbuf, pScanOpts, pErrMsg); + if (result != 0) + goto bail; + } + + result = 0; + +bail: + if (dirp != nil) + (void)CloseDir(dirp); + return result; +} + + +/* + * Open a disk image and dump the contents. + * + * Returns 0 on success, nonzero on failure. + */ +int +MainWindow::ScanDiskImage(const char* pathName, ScanOpts* pScanOpts) +{ + ASSERT(pathName != nil); + ASSERT(pScanOpts != nil); + ASSERT(pScanOpts->outfp != nil); + + DIError dierr; + CString errMsg; + DiskImg diskImg; + DiskFS* pDiskFS = nil; + PathName path(pathName); + CString ext = path.GetExtension(); + + /* first, some housekeeping */ + PeekAndPump(); + pScanOpts->pProgress->SetCurrentFile(pathName); + if (fCancelFlag) + return -1; + + CString title; + title = _T("MDC "); + title += FilenameOnly(pathName, '\\'); + SetWindowText(title); + + fprintf(pScanOpts->outfp, "File: %s\n", pathName); + fflush(pScanOpts->outfp); // in case we crash + + if (!ext.IsEmpty()) { + /* delete the leading '.' */ + ext.Delete(0, 1); + } + + dierr = diskImg.OpenImage(pathName, '\\', true); + if (dierr != kDIErrNone) { + errMsg.Format("Unable to open '%s': %s", pathName, + DiskImgLib::DIStrError(dierr)); + goto bail; + } + + dierr = diskImg.AnalyzeImage(); + if (dierr != kDIErrNone) { + errMsg.Format("Analysis of '%s' failed: %s", pathName, + DiskImgLib::DIStrError(dierr)); + goto bail; + } + + if (diskImg.GetFSFormat() == DiskImg::kFormatUnknown || + diskImg.GetSectorOrder() == DiskImg::kSectorOrderUnknown) + { + errMsg.Format("Unable to identify filesystem on '%s'", pathName); + goto bail; + } + + /* create an appropriate DiskFS object */ + pDiskFS = diskImg.OpenAppropriateDiskFS(); + if (pDiskFS == nil) { + /* unknown FS should've been caught above! */ + ASSERT(false); + errMsg.Format("Format of '%s' not recognized.", pathName); + goto bail; + } + + pDiskFS->SetScanForSubVolumes(DiskFS::kScanSubEnabled); + + /* object created; prep it */ + dierr = pDiskFS->Initialize(&diskImg, DiskFS::kInitFull); + if (dierr != kDIErrNone) { + errMsg.Format("Error reading list of files from disk: %s", + DiskImgLib::DIStrError(dierr)); + goto bail; + } + + int kbytes; + if (pDiskFS->GetDiskImg()->GetHasBlocks()) + kbytes = pDiskFS->GetDiskImg()->GetNumBlocks() / 2; + else if (pDiskFS->GetDiskImg()->GetHasSectors()) + kbytes = (pDiskFS->GetDiskImg()->GetNumTracks() * + pDiskFS->GetDiskImg()->GetNumSectPerTrack()) / 4; + else + kbytes = 0; + fprintf(pScanOpts->outfp, "Disk: %s%s (%dKB)\n", pDiskFS->GetVolumeID(), + pDiskFS->GetFSDamaged() ? " [*]" : "", kbytes); + fprintf(pScanOpts->outfp, + " Name Type Auxtyp Modified" + " Format Length\n"); + fprintf(pScanOpts->outfp, + "------------------------------------------------------" + "------------------------\n"); + if (LoadDiskFSContents(pDiskFS, "", pScanOpts) != 0) { + errMsg.Format("Failed while loading contents of '%s'.", pathName); + goto bail; + } + fprintf(pScanOpts->outfp, + "------------------------------------------------------" + "------------------------\n\n"); + +bail: + delete pDiskFS; + + //PeekAndPump(); + + if (!errMsg.IsEmpty()) { + fprintf(pScanOpts->outfp, "Failed: %s\n\n", (LPCTSTR) errMsg); + return -1; + } else { + return 0; + } +} + +/* + * Analyze a file's characteristics. + */ +void +MainWindow::AnalyzeFile(const A2File* pFile, RecordKind* pRecordKind, + LONGLONG* pTotalLen, LONGLONG* pTotalCompLen) +{ + if (pFile->IsVolumeDirectory()) { + /* volume dir entry */ + ASSERT(pFile->GetRsrcLength() < 0); + *pRecordKind = kRecordKindVolumeDir; + *pTotalLen = pFile->GetDataLength(); + *pTotalCompLen = pFile->GetDataLength(); + } else if (pFile->IsDirectory()) { + /* directory entry */ + ASSERT(pFile->GetRsrcLength() < 0); + *pRecordKind = kRecordKindDirectory; + *pTotalLen = pFile->GetDataLength(); + *pTotalCompLen = pFile->GetDataLength(); + } else if (pFile->GetRsrcLength() >= 0) { + /* has resource fork */ + *pRecordKind = kRecordKindForkedFile; + *pTotalLen = pFile->GetDataLength() + pFile->GetRsrcLength(); + *pTotalCompLen = + pFile->GetDataSparseLength() + pFile->GetRsrcSparseLength(); + } else { + /* just data fork */ + *pRecordKind = kRecordKindFile; + *pTotalLen = pFile->GetDataLength(); + *pTotalCompLen = pFile->GetDataSparseLength(); + } +} + +/* + * Determine whether the access bits on the record make it a read-only + * file or not. + * + * Uses a simplified view of the access flags. + */ +bool +MainWindow::IsRecordReadOnly(int access) +{ + if (access == 0x21L || access == 0x01L) + return true; + else + return false; +} + +/* ProDOS file type names; must be entirely in upper case */ +static const char gFileTypeNames[256][4] = { + "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", + "$80", "$81", "$82", "$83", "$84", "$85", "$86", "$87", + "$88", "$89", "$8A", "$8B", "$8C", "$8D", "$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" +}; + +/* + * Return a pointer to the three-letter representation of the file type name. + * + * Note to self: code down below tests first char for '?'. + */ +/*static*/ const char* +MainWindow::GetFileTypeString(unsigned long fileType) +{ + if (fileType < NELEM(gFileTypeNames)) + return gFileTypeNames[fileType]; + else + return "???"; +} + +/* + * Sanitize a string. The Mac likes to stick control characters into + * things, e.g. ^C and ^M, and uses high ASCII for special characters. + */ +static void +MacSanitize(char* str) +{ + while (*str != '\0') { + *str = DiskImg::MacToASCII(*str); + str++; + } +} + +/* + * Load the contents of a DiskFS. + * + * Recursively handle sub-volumes. + */ +int +MainWindow::LoadDiskFSContents(DiskFS* pDiskFS, const char* volName, + ScanOpts* pScanOpts) +{ + static const char* kBlankFileName = ""; + DiskFS::SubVolume* pSubVol = nil; + A2File* pFile; + + ASSERT(pDiskFS != nil); + pFile = pDiskFS->GetNextFile(nil); + for ( ; pFile != nil; pFile = pDiskFS->GetNextFile(pFile)) { + CString subVolName, dispName; + RecordKind recordKind; + LONGLONG totalLen, totalCompLen; + char tmpbuf[16]; + + AnalyzeFile(pFile, &recordKind, &totalLen, &totalCompLen); + + if (recordKind == kRecordKindVolumeDir) { + /* this is a volume directory */ + WMSG1("Not displaying volume dir '%s'\n", pFile->GetPathName()); + continue; + } + + /* prepend volName for sub-volumes; must be valid Win32 dirname */ + if (volName[0] != '\0') + subVolName.Format("_%s", volName); + + const char* ccp = pFile->GetPathName(); + ASSERT(ccp != nil); + if (strlen(ccp) == 0) + ccp = kBlankFileName; + + CString path(ccp); + if (DiskImg::UsesDOSFileStructure(pFile->GetFSFormat()) && 0) + { + InjectLowercase(&path); + } + + if (subVolName.IsEmpty()) + dispName = path; + else { + dispName = subVolName; + dispName += ':'; + dispName += path; + } + + /* strip out ctrl chars and high ASCII in HFS names */ + MacSanitize(dispName.GetBuffer(0)); + dispName.ReleaseBuffer(); + + ccp = dispName; + + int len = strlen(ccp); + if (len <= 32) { + fprintf(pScanOpts->outfp, "%c%-32.32s ", + IsRecordReadOnly(pFile->GetAccess()) ? '*' : ' ', + ccp); + } else { + fprintf(pScanOpts->outfp, "%c..%-30.30s ", + IsRecordReadOnly(pFile->GetAccess()) ? '*' : ' ', + ccp + len - 30); + } + switch (recordKind) { + case kRecordKindUnknown: + fprintf(pScanOpts->outfp, "%s- $%04lX ", + GetFileTypeString(pFile->GetFileType()), + pFile->GetAuxType()); + break; + case kRecordKindDisk: + sprintf(tmpbuf, "%ldk", totalLen / 1024); + fprintf(pScanOpts->outfp, "Disk %-6s ", tmpbuf); + break; + case kRecordKindFile: + case kRecordKindForkedFile: + case kRecordKindDirectory: + if (pDiskFS->GetDiskImg()->GetFSFormat() == DiskImg::kFormatMacHFS) + { + if (recordKind != kRecordKindDirectory && + pFile->GetFileType() >= 0 && pFile->GetFileType() <= 0xff && + pFile->GetAuxType() >= 0 && pFile->GetAuxType() <= 0xffff) + { + /* ProDOS type embedded in HFS */ + fprintf(pScanOpts->outfp, "%s%c $%04lX ", + GetFileTypeString(pFile->GetFileType()), + recordKind == kRecordKindForkedFile ? '+' : ' ', + pFile->GetAuxType()); + } else { + char typeStr[5]; + char creatorStr[5]; + unsigned long val; + + val = pFile->GetAuxType(); + creatorStr[0] = (unsigned char) (val >> 24); + creatorStr[1] = (unsigned char) (val >> 16); + creatorStr[2] = (unsigned char) (val >> 8); + creatorStr[3] = (unsigned char) val; + creatorStr[4] = '\0'; + + val = pFile->GetFileType(); + typeStr[0] = (unsigned char) (val >> 24); + typeStr[1] = (unsigned char) (val >> 16); + typeStr[2] = (unsigned char) (val >> 8); + typeStr[3] = (unsigned char) val; + typeStr[4] = '\0'; + + MacSanitize(creatorStr); + MacSanitize(typeStr); + + if (recordKind == kRecordKindDirectory) { + fprintf(pScanOpts->outfp, "DIR %-4s ", creatorStr); + } else { + fprintf(pScanOpts->outfp, "%-4s%c %-4s ", + typeStr, + pFile->GetRsrcLength() > 0 ? '+' : ' ', + creatorStr); + } + } + } else { + fprintf(pScanOpts->outfp, "%s%c $%04lX ", + GetFileTypeString(pFile->GetFileType()), + recordKind == kRecordKindForkedFile ? '+' : ' ', + pFile->GetAuxType()); + } + break; + case kRecordKindVolumeDir: + /* should've trapped this earlier */ + ASSERT(0); + fprintf(pScanOpts->outfp, "ERROR "); + break; + default: + ASSERT(0); + fprintf(pScanOpts->outfp, "ERROR "); + break; + } + + CString date; + if (pFile->GetModWhen() == 0) + FormatDate(kDateNone, &date); + else + FormatDate(pFile->GetModWhen(), &date); + fprintf(pScanOpts->outfp, "%-15s ", (LPCTSTR) date); + + const char* fmtStr; + switch (pFile->GetFSFormat()) { + case DiskImg::kFormatDOS33: + case DiskImg::kFormatDOS32: + case DiskImg::kFormatUNIDOS: + case DiskImg::kFormatOzDOS: + fmtStr = "DOS "; + break; + case DiskImg::kFormatProDOS: + fmtStr = "ProDOS"; + break; + case DiskImg::kFormatPascal: + fmtStr = "Pascal"; + break; + case DiskImg::kFormatCPM: + fmtStr = "CP/M "; + break; + case DiskImg::kFormatRDOS33: + case DiskImg::kFormatRDOS32: + case DiskImg::kFormatRDOS3: + fmtStr = "RDOS "; + break; + case DiskImg::kFormatMacHFS: + fmtStr = "HFS "; + break; + case DiskImg::kFormatMSDOS: + fmtStr = "MS-DOS"; + break; + default: + fmtStr = "??? "; + break; + } + if (pFile->GetQuality() == A2File::kQualityDamaged) + fmtStr = "BROKEN"; + else if (pFile->GetQuality() == A2File::kQualitySuspicious) + fmtStr = "BAD? "; + + fprintf(pScanOpts->outfp, "%s ", fmtStr); + + +#if 0 + /* compute the percent size */ + if ((!totalLen && totalCompLen) || (totalLen && !totalCompLen)) + fprintf(pScanOpts->outfp, "--- "); /* weird */ + else if (totalLen < totalCompLen) + fprintf(pScanOpts->outfp, ">100%% "); /* compression failed? */ + else { + sprintf(tmpbuf, "%02d%%", ComputePercent(totalCompLen, totalLen)); + fprintf(pScanOpts->outfp, "%4s ", tmpbuf); + } +#endif + + if (!totalLen && totalCompLen) + fprintf(pScanOpts->outfp, " ????"); /* weird */ + else + fprintf(pScanOpts->outfp, "%8ld", totalLen); + + fprintf(pScanOpts->outfp, "\n"); + } + + /* + * Load all sub-volumes. + */ + pSubVol = pDiskFS->GetNextSubVolume(nil); + while (pSubVol != nil) { + const char* subVolName; + int ret; + + subVolName = pSubVol->GetDiskFS()->GetVolumeName(); + if (subVolName == nil) + subVolName = "+++"; // could probably do better than this + + ret = LoadDiskFSContents(pSubVol->GetDiskFS(), subVolName, pScanOpts); + if (ret != 0) + return ret; + pSubVol = pDiskFS->GetNextSubVolume(pSubVol); + } + + return 0; +} diff --git a/mdc/Main.h b/mdc/Main.h new file mode 100644 index 0000000..a0565e2 --- /dev/null +++ b/mdc/Main.h @@ -0,0 +1,72 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Main frame window declarations. + */ +#ifndef __MAIN__ +#define __MAIN__ + +#include "../diskimg/DiskImg.h" +#include "../prebuilt/NufxLib.h" +using namespace DiskImgLib; + +struct Win32dirent; +struct ScanOpts; + +typedef enum RecordKind { + kRecordKindUnknown = 0, + kRecordKindDisk, + kRecordKindFile, + kRecordKindForkedFile, + kRecordKindDirectory, + kRecordKindVolumeDir, +} RecordKind; + + +/* + * The main UI window. + */ +class MainWindow : public CFrameWnd { +public: + MainWindow(void); + ~MainWindow(void); + +private: + afx_msg void OnFileScan(void); + afx_msg void OnFileExit(void); + afx_msg void OnHelpWebSite(void); + afx_msg void OnHelpAbout(void); + + BOOL PeekAndPump(void); + + static void DebugMsgHandler(const char* file, int line, + const char* msg); + static NuResult NufxErrorMsgHandler(NuArchive* /*pArchive*/, + void* vErrorMessage); + + void ScanFiles(void); + Win32dirent* OpenDir(const char* name); + Win32dirent* ReadDir(Win32dirent* dir); + void CloseDir(Win32dirent* dir); + int Process(const char* pathname, ScanOpts* pScanOpts, + CString* pErrMsg); + int ProcessDirectory(const char* dirName, ScanOpts* pScanOpts, + CString* pErrMsg); + int ScanDiskImage(const char* pathName, ScanOpts* pScanOpts); + int LoadDiskFSContents(DiskFS* pDiskFS, const char* volName, + ScanOpts* pScanOpts); + void AnalyzeFile(const A2File* pFile, RecordKind* pRecordKind, + LONGLONG* pTotalLen, LONGLONG* pTotalCompLen); + bool IsRecordReadOnly(int access); + static const char* GetFileTypeString(unsigned long fileType); + + bool fCancelFlag; + //int fTitleAnimation; + + DECLARE_MESSAGE_MAP() +}; + +#endif /*__MAIN__*/ \ No newline at end of file diff --git a/mdc/ProgressDlg.cpp b/mdc/ProgressDlg.cpp new file mode 100644 index 0000000..a98b5e7 --- /dev/null +++ b/mdc/ProgressDlg.cpp @@ -0,0 +1,78 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +// ProgressDlg.cpp : implementation file +// + +#include "stdafx.h" +#include "mdc.h" +#include "ProgressDlg.h" + +#ifdef _DEBUG +#define new DEBUG_NEW +#undef THIS_FILE +static char THIS_FILE[] = __FILE__; +#endif + +///////////////////////////////////////////////////////////////////////////// +// ProgressDlg dialog + +#if 0 +ProgressDlg::ProgressDlg(CWnd* pParent /*=NULL*/) + : CDialog(ProgressDlg::IDD, pParent) +{ + //{{AFX_DATA_INIT(ProgressDlg) + // NOTE: the ClassWizard will add member initialization here + //}}AFX_DATA_INIT + + fpCancelFlag = nil; +} +#endif + + +BEGIN_MESSAGE_MAP(ProgressDlg, CDialog) + //{{AFX_MSG_MAP(ProgressDlg) + //}}AFX_MSG_MAP +END_MESSAGE_MAP() + +///////////////////////////////////////////////////////////////////////////// +// ProgressDlg message handlers + +BOOL ProgressDlg::Create(LPCTSTR lpszClassName, LPCTSTR lpszWindowName, DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID, CCreateContext* pContext) +{ + // TODO: Add your specialized code here and/or call the base class + + return CDialog::Create(IDD, pParentWnd); +} + +void ProgressDlg::PostNcDestroy() +{ + // TODO: Add your specialized code here and/or call the base class + delete this; + + //CDialog::PostNcDestroy(); +} + +/* + * Update the progress display with the name of the file we're currently + * working on. + */ +void +ProgressDlg::SetCurrentFile(const char* fileName) +{ + CWnd* pWnd = GetDlgItem(IDC_PROGRESS_FILENAME); + ASSERT(pWnd != nil); + pWnd->SetWindowText(fileName); +} + +void ProgressDlg::OnCancel() +{ + // TODO: Add extra cleanup here + WMSG0("Cancel button pushed\n"); + ASSERT(fpCancelFlag != nil); + *fpCancelFlag = true; + + //CDialog::OnCancel(); +} diff --git a/mdc/ProgressDlg.h b/mdc/ProgressDlg.h new file mode 100644 index 0000000..97a9dbc --- /dev/null +++ b/mdc/ProgressDlg.h @@ -0,0 +1,59 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +#if !defined(AFX_PROGRESSDLG_H__94B5552B_1337_4FA4_A336_32FA9544ADAC__INCLUDED_) +#define AFX_PROGRESSDLG_H__94B5552B_1337_4FA4_A336_32FA9544ADAC__INCLUDED_ + +#if _MSC_VER > 1000 +#pragma once +#endif // _MSC_VER > 1000 +// ProgressDlg.h : header file +// + +///////////////////////////////////////////////////////////////////////////// +// ProgressDlg dialog + +class ProgressDlg : public CDialog +{ +// Construction +public: + /*ProgressDlg(CWnd* pParent = NULL); // standard constructor*/ + +// Dialog Data + //{{AFX_DATA(ProgressDlg) + enum { IDD = IDD_PROGRESS }; + // NOTE: the ClassWizard will add data members here + //}}AFX_DATA + + bool* fpCancelFlag; + +// Overrides + // ClassWizard generated virtual function overrides + //{{AFX_VIRTUAL(ProgressDlg) + public: + virtual BOOL Create(LPCTSTR lpszClassName, LPCTSTR lpszWindowName, DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID, CCreateContext* pContext = NULL); + protected: + virtual void PostNcDestroy(); + //}}AFX_VIRTUAL + +public: + BOOL Create(CWnd* pParentWnd = NULL) { + return CDialog::Create(IDD_PROGRESS, pParentWnd); + } + void SetCurrentFile(const char* fileName); + +// Implementation +protected: + // Generated message map functions + //{{AFX_MSG(ProgressDlg) + virtual void OnCancel(); + //}}AFX_MSG + DECLARE_MESSAGE_MAP() +}; + +//{{AFX_INSERT_LOCATION}} +// Microsoft Visual C++ will insert additional declarations immediately before the previous line. + +#endif // !defined(AFX_PROGRESSDLG_H__94B5552B_1337_4FA4_A336_32FA9544ADAC__INCLUDED_) diff --git a/mdc/StdAfx.cpp b/mdc/StdAfx.cpp new file mode 100644 index 0000000..1da4078 --- /dev/null +++ b/mdc/StdAfx.cpp @@ -0,0 +1,13 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +// stdafx.cpp : source file that includes just the standard includes +// mdc.pch will be the pre-compiled header +// stdafx.obj will contain the pre-compiled type information + +#include "stdafx.h" + +// TODO: reference any additional headers you need in STDAFX.H +// and not in this file diff --git a/mdc/StdAfx.h b/mdc/StdAfx.h new file mode 100644 index 0000000..4ba244b --- /dev/null +++ b/mdc/StdAfx.h @@ -0,0 +1,49 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +// stdafx.h : include file for standard system include files, +// or project specific include files that are used frequently, but +// are changed infrequently +// + +#if !defined(AFX_STDAFX_H__A9DB83DB_A9FD_11D0_BFD1_444553540000__INCLUDED_) +#define AFX_STDAFX_H__A9DB83DB_A9FD_11D0_BFD1_444553540000__INCLUDED_ + +#if _MSC_VER > 1000 +#pragma once +#endif // _MSC_VER > 1000 + +#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers + + +// Windows Header Files: +//#include + +// C RunTime Header Files +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include "..\util\UtilLib.h" + +// Local Header Files + +// TODO: reference additional headers your program requires here + +//{{AFX_INSERT_LOCATION}} +// Microsoft Visual C++ will insert additional declarations immediately before the previous line. + +#endif // !defined(AFX_STDAFX_H__A9DB83DB_A9FD_11D0_BFD1_444553540000__INCLUDED_) diff --git a/mdc/mdc.ICO b/mdc/mdc.ICO new file mode 100644 index 0000000000000000000000000000000000000000..0004723a69fab4fd355beb052461ac1e641bb595 GIT binary patch literal 4846 zcmd^@%}XRl6u{qP1V@pqUStmvjVHYc1qq%u7}(1q*^6flLj40dcnNHaAUOyT0z#eQ zAz2b37yp9sl0dApULqlQ*dRjm65NfX@_SX&Jv}q+xNeAqq{gcEQLoL!>5k znVK3E9lyRM@*Z+@G}$vXk$37Mb8~T9zIj{Z@qow-<`nY-W=(J~GR{_yj7@g3ED2Ur zW+u&MlZ_C6Bm~u%eAK1O>yP2Ry}feu4fbamqf2YQ+b&YvaK39lLdGS)v$or?i&XO}socDugBhlL}y3Jwc{g~7t0t+Q}g z7%U7vezW`3Lh7Usc5JV92P$ei;$o|*?O<{gbrf(gIchsL9km^`9kt`@h_&%k-%;OD zUtf`BD&d%iW@=~s@uH@p6AYbk7#_u|+#~+fZ#?-t> zmf`yu=jWYuO7^SLD1u7+(O0rd;jiv3z9`hIRPywO6`ucGy#BGAMta8F3L)A(&)c;> z+-3jm_@dL*N>Bdku)aspF>2UVjXK z{R3o<>F-vdw`?EJo$y;YdAhFq+kKxktRwxc@!Fm|IkIoO#&$%P4pymm46jP@CcfM5 z+rNGPO`iGxa;ES7qzU)Ef2Y1DIR5EV)6ea4ouP3X2O9?}bGzq!J*)T2r*(BdmmTN% z|D9+Lw&a5(!?RTHdNS3&!mssze?RU2`KTnRe@(pR$>l{zb{(m82U}AAF4$4OPam}2 Z`g*Fragge(-E&@_KYxyjd|F@Y{skz%3tj*K literal 0 HcmV?d00001 diff --git a/mdc/mdc.clw b/mdc/mdc.clw new file mode 100644 index 0000000..a2992f9 --- /dev/null +++ b/mdc/mdc.clw @@ -0,0 +1,82 @@ +; CLW file contains information for the MFC ClassWizard + +[General Info] +Version=1 +LastClass=AboutDlg +LastTemplate=CDialog +NewFileInclude1=#include "stdafx.h" +NewFileInclude2=#include "mdc.h" +LastPage=0 + +ClassCount=2 + +ResourceCount=4 +Resource1=IDC_MDC +Class1=AboutDlg +Resource2="IDD_CHOOSE_FILES" +Resource3=IDD_ABOUTBOX +Class2=ProgressDlg +Resource4=IDD_PROGRESS + +[DLG:IDD_ABOUTBOX] +Type=1 +Class=AboutDlg +ControlCount=6 +Control1=IDC_MYICON,static,1342177283 +Control2=IDC_ABOUT_VERS,static,1342308480 +Control3=IDC_STATIC,static,1342308352 +Control4=IDOK,button,1342242817 +Control5=IDC_STATIC,static,1342308352 +Control6=IDC_STATIC,static,1342308352 + +[MNU:IDC_MDC] +Type=1 +Class=? +Command1=IDM_FILE_SCAN +Command2=IDM_FILE_EXIT +Command3=IDM_HELP_WEBSITE +Command4=IDM_HELP_ABOUT +CommandCount=4 + +[ACL:IDC_MDC] +Type=1 +Class=? +Command1=IDM_ABOUT +Command2=IDM_ABOUT +CommandCount=2 + +[CLS:AboutDlg] +Type=0 +HeaderFile=AboutDlg.h +ImplementationFile=AboutDlg.cpp +BaseClass=CDialog +Filter=D +VirtualFilter=dWC +LastObject=AboutDlg + +[DLG:"IDD_CHOOSE_FILES"] +Type=1 +Class=? +ControlCount=4 +Control1=1119,static,1342177280 +Control2=IDC_SELECT_ACCEPT,button,1342242816 +Control3=IDCANCEL,button,1342242816 +Control4=IDC_CHOOSEFILES_STATIC1,static,1342308352 + +[DLG:IDD_PROGRESS] +Type=1 +Class=ProgressDlg +ControlCount=3 +Control1=IDCANCEL,button,1342242816 +Control2=IDC_STATIC,static,1342308352 +Control3=IDC_PROGRESS_FILENAME,static,1342308352 + +[CLS:ProgressDlg] +Type=0 +HeaderFile=ProgressDlg.h +ImplementationFile=ProgressDlg.cpp +BaseClass=CDialog +Filter=C +LastObject=IDCANCEL +VirtualFilter=dWC + diff --git a/mdc/mdc.cpp b/mdc/mdc.cpp new file mode 100644 index 0000000..f477d38 --- /dev/null +++ b/mdc/mdc.cpp @@ -0,0 +1,77 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * The application object. + */ +#include "stdafx.h" +#include "mdc.h" +#include "Main.h" +#include + +/* magic global that MFC finds (or that finds MFC) */ +MyApp gMyApp; + +#if defined(_DEBUG_LOG) +FILE* gLog = nil; +int gPid = -1; +#endif + +/* + * Constructor. This is the closest thing to "main" that we have, but we + * should wait for InitInstance for most things. + */ +MyApp::MyApp(LPCTSTR lpszAppName) : CWinApp(lpszAppName) +{ + time_t now; + now = time(nil); + +#ifdef _DEBUG_LOG + gLog = fopen(kDebugLog, "w"); + if (gLog == nil) + abort(); + ::setvbuf(gLog, nil, _IONBF, 0); + + gPid = ::getpid(); + fprintf(gLog, "\n"); +#endif + + WMSG1("MDC started at %.24s\n", ctime(&now)); + + int tmpDbgFlag; + // enable memory leak detection + tmpDbgFlag = _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG); + tmpDbgFlag |= _CRTDBG_LEAK_CHECK_DF; + _CrtSetDbgFlag(tmpDbgFlag); + WMSG0("Leak detection enabled\n"); +} + +/* + * This is the last point of control we have. + */ +MyApp::~MyApp(void) +{ + WMSG0("MDC SHUTTING DOWN\n\n"); +#ifdef _DEBUG_LOG + if (gLog != nil) + fclose(gLog); +#endif +} + + +/* + * It all begins here. + * + * Create a main window. + */ +BOOL +MyApp::InitInstance(void) +{ + m_pMainWnd = new MainWindow; + m_pMainWnd->ShowWindow(m_nCmdShow); + m_pMainWnd->UpdateWindow(); + + return TRUE; +} diff --git a/mdc/mdc.dsp b/mdc/mdc.dsp new file mode 100644 index 0000000..fc0b8e2 --- /dev/null +++ b/mdc/mdc.dsp @@ -0,0 +1,174 @@ +# Microsoft Developer Studio Project File - Name="mdc" - Package Owner=<4> +# Microsoft Developer Studio Generated Build File, Format Version 6.00 +# ** DO NOT EDIT ** + +# TARGTYPE "Win32 (x86) Application" 0x0101 + +CFG=mdc - Win32 Debug +!MESSAGE This is not a valid makefile. To build this project using NMAKE, +!MESSAGE use the Export Makefile command and run +!MESSAGE +!MESSAGE NMAKE /f "mdc.mak". +!MESSAGE +!MESSAGE You can specify a configuration when running NMAKE +!MESSAGE by defining the macro CFG on the command line. For example: +!MESSAGE +!MESSAGE NMAKE /f "mdc.mak" CFG="mdc - Win32 Debug" +!MESSAGE +!MESSAGE Possible choices for configuration are: +!MESSAGE +!MESSAGE "mdc - Win32 Release" (based on "Win32 (x86) Application") +!MESSAGE "mdc - Win32 Debug" (based on "Win32 (x86) Application") +!MESSAGE + +# Begin Project +# PROP AllowPerConfigDependencies 0 +# PROP Scc_ProjName "" +# PROP Scc_LocalPath "" +CPP=cl.exe +MTL=midl.exe +RSC=rc.exe + +!IF "$(CFG)" == "mdc - Win32 Release" + +# PROP BASE Use_MFC 0 +# PROP BASE Use_Debug_Libraries 0 +# PROP BASE Output_Dir "Release" +# PROP BASE Intermediate_Dir "Release" +# PROP BASE Target_Dir "" +# PROP Use_MFC 2 +# PROP Use_Debug_Libraries 0 +# PROP Output_Dir "Release" +# PROP Intermediate_Dir "Release" +# PROP Ignore_Export_Lib 0 +# PROP Target_Dir "" +# ADD BASE CPP /nologo /W3 /GX /O2 /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /D "_MBCS" /Yu"stdafx.h" /FD /c +# ADD CPP /nologo /MD /W3 /GX /O2 /D "WIN32" /D "NDEBUGX" /D "_WINDOWS" /D "_MBCS" /D "_AFXDLL" /Yu"stdafx.h" /FD /c +# ADD BASE MTL /nologo /D "NDEBUG" /mktyplib203 /win32 +# ADD MTL /nologo /D "NDEBUG" /mktyplib203 /win32 +# ADD BASE RSC /l 0x409 /d "NDEBUG" +# ADD RSC /l 0x409 /d "NDEBUG" /d "_AFXDLL" +BSC32=bscmake.exe +# ADD BASE BSC32 /nologo +# ADD BSC32 /nologo +LINK32=link.exe +# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /subsystem:windows /machine:I386 +# ADD LINK32 ../prebuilt/nufxlib2.lib ../prebuilt/zdll.lib /nologo /subsystem:windows /map /machine:I386 +# Begin Special Build Tool +SOURCE="$(InputPath)" +PostBuild_Desc=Copy DLLs in +PostBuild_Cmds=copy ..\prebuilt\nufxlib2.dll . copy ..\prebuilt\zlib1.dll . +# End Special Build Tool + +!ELSEIF "$(CFG)" == "mdc - Win32 Debug" + +# PROP BASE Use_MFC 0 +# PROP BASE Use_Debug_Libraries 1 +# PROP BASE Output_Dir "Debug" +# PROP BASE Intermediate_Dir "Debug" +# PROP BASE Target_Dir "" +# PROP Use_MFC 2 +# PROP Use_Debug_Libraries 1 +# PROP Output_Dir "Debug" +# PROP Intermediate_Dir "Debug" +# PROP Ignore_Export_Lib 0 +# PROP Target_Dir "" +# ADD BASE CPP /nologo /W3 /Gm /GX /ZI /Od /D "WIN32" /D "_DEBUG" /D "_WINDOWS" /D "_MBCS" /Yu"stdafx.h" /FD /GZ /c +# ADD CPP /nologo /MDd /W3 /Gm /GX /ZI /Od /D "WIN32" /D "_DEBUG" /D "_WINDOWS" /D "_MBCS" /D "_AFXDLL" /FR /Yu"stdafx.h" /FD /GZ /c +# ADD BASE MTL /nologo /D "_DEBUG" /mktyplib203 /win32 +# ADD MTL /nologo /D "_DEBUG" /mktyplib203 /win32 +# ADD BASE RSC /l 0x409 /d "_DEBUG" +# ADD RSC /l 0x409 /d "_DEBUG" /d "_AFXDLL" +BSC32=bscmake.exe +# ADD BASE BSC32 /nologo +# ADD BSC32 /nologo +LINK32=link.exe +# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /subsystem:windows /debug /machine:I386 /pdbtype:sept +# ADD LINK32 ../prebuilt/nufxlib2D.lib ../prebuilt/zdll.lib /nologo /subsystem:windows /debug /machine:I386 /pdbtype:sept +# Begin Special Build Tool +SOURCE="$(InputPath)" +PostBuild_Desc=Copy debug DLLs in +PostBuild_Cmds=copy ..\prebuilt\nufxlib2D.dll . copy ..\prebuilt\zlib1.dll . +# End Special Build Tool + +!ENDIF + +# Begin Target + +# Name "mdc - Win32 Release" +# Name "mdc - Win32 Debug" +# Begin Group "Source Files" + +# PROP Default_Filter "cpp;c;cxx;rc;def;r;odl;idl;hpj;bat" +# Begin Source File + +SOURCE=.\AboutDlg.cpp +# End Source File +# Begin Source File + +SOURCE=.\ChooseFilesDlg.cpp +# End Source File +# Begin Source File + +SOURCE=.\Main.cpp +# End Source File +# Begin Source File + +SOURCE=.\mdc.cpp +# End Source File +# Begin Source File + +SOURCE=.\mdc.rc +# End Source File +# Begin Source File + +SOURCE=.\ProgressDlg.cpp +# End Source File +# Begin Source File + +SOURCE=.\StdAfx.cpp +# ADD CPP /Yc"stdafx.h" +# End Source File +# End Group +# Begin Group "Header Files" + +# PROP Default_Filter "h;hpp;hxx;hm;inl" +# Begin Source File + +SOURCE=.\AboutDlg.h +# End Source File +# Begin Source File + +SOURCE=.\ChooseFilesDlg.h +# End Source File +# Begin Source File + +SOURCE=.\Main.h +# End Source File +# Begin Source File + +SOURCE=.\mdc.h +# End Source File +# Begin Source File + +SOURCE=.\ProgressDlg.h +# End Source File +# Begin Source File + +SOURCE=.\resource.h +# End Source File +# Begin Source File + +SOURCE=.\StdAfx.h +# End Source File +# End Group +# Begin Group "Resource Files" + +# PROP Default_Filter "ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe" +# Begin Source File + +SOURCE=.\mdc.ico +# End Source File +# End Group +# End Target +# End Project diff --git a/mdc/mdc.h b/mdc/mdc.h new file mode 100644 index 0000000..893151b --- /dev/null +++ b/mdc/mdc.h @@ -0,0 +1,43 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +#if !defined(AFX_MDC_H__15BEB7EF_BB49_4E23_BDD1_7F7B0220F3DB__INCLUDED_) +#define AFX_MDC_H__15BEB7EF_BB49_4E23_BDD1_7F7B0220F3DB__INCLUDED_ + +#if _MSC_VER > 1000 +#pragma once +#endif // _MSC_VER > 1000 + +#include "resource.h" + +/* + * Application object. + */ + +#if defined(_DEBUG_LOG) +#define kDebugLog "C:\\mdclog.txt" +#endif + +/* MDC version numbers */ +#define kAppMajorVersion 2 +#define kAppMinorVersion 2 +#define kAppBugVersion 0 + +/* + * Windows application object. + */ +class MyApp : public CWinApp { +public: + MyApp(LPCTSTR lpszAppName = NULL); + virtual ~MyApp(void); + + // Overridden functions + virtual BOOL InitInstance(void); + //virtual BOOL OnIdle(LONG lCount); +}; + +extern MyApp gMyApp; + +#endif // !defined(AFX_MDC_H__15BEB7EF_BB49_4E23_BDD1_7F7B0220F3DB__INCLUDED_) diff --git a/mdc/mdc.rc b/mdc/mdc.rc new file mode 100644 index 0000000..8cc523c --- /dev/null +++ b/mdc/mdc.rc @@ -0,0 +1,207 @@ +//Microsoft Developer Studio generated resource script. +// +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#define APSTUDIO_HIDDEN_SYMBOLS +#include "afxres.h" +#undef APSTUDIO_HIDDEN_SYMBOLS +#include "resource.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (U.S.) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +#ifdef _WIN32 +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) +#endif //_WIN32 + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_MDC ICON DISCARDABLE "mdc.ICO" + +///////////////////////////////////////////////////////////////////////////// +// +// Menu +// + +IDC_MDC MENU DISCARDABLE +BEGIN + POPUP "&File" + BEGIN + MENUITEM "&Scan disk images...", IDM_FILE_SCAN + MENUITEM "E&xit", IDM_FILE_EXIT + END + POPUP "&Help" + BEGIN + MENUITEM "Visit faddenSoft &web site", IDM_HELP_WEBSITE + MENUITEM "&About ...", IDM_HELP_ABOUT + END +END + + +///////////////////////////////////////////////////////////////////////////// +// +// Accelerator +// + +IDC_MDC ACCELERATORS MOVEABLE PURE +BEGIN + "?", IDM_ABOUT, ASCII, ALT + "/", IDM_ABOUT, ASCII, ALT +END + + +///////////////////////////////////////////////////////////////////////////// +// +// Dialog +// + +IDD_ABOUTBOX DIALOG DISCARDABLE 22, 17, 133, 111 +STYLE DS_MODALFRAME | WS_CAPTION | WS_SYSMENU +CAPTION "About" +FONT 10, "MS Sans Serif" +BEGIN + ICON IDI_MDC,IDC_MYICON,7,7,20,20 + LTEXT "MDC version %d.%d.%d",IDC_ABOUT_VERS,33,7,93,8, + SS_NOPREFIX + LTEXT "Copyright © 2007 by FaddenSoft, LLC.\rAll Rights Reserved.", + IDC_STATIC,7,31,119,19 + DEFPUSHBUTTON "OK",IDOK,41,90,50,14 + LTEXT "This utility scans Apple II disk images and creates formatted listings of the files in them.", + IDC_STATIC,7,58,119,26 + LTEXT "Multi-Disk Catalog",IDC_STATIC,33,15,55,8 +END + +IDD_CHOOSE_FILES DIALOGEX 0, 0, 272, 131 +STYLE DS_3DLOOK | WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS +FONT 8, "MS Sans Serif", 0, 0, 0x1 +BEGIN + LTEXT "",1119,0,0,272,87,NOT WS_GROUP,WS_EX_STATICEDGE + PUSHBUTTON "&Accept",IDC_SELECT_ACCEPT,220,88,50,14 + PUSHBUTTON "Cancel",IDCANCEL,220,107,50,14 + LTEXT "Select the disk images to scan. If you select a folder, all files in that folder will be processed.", + IDC_CHOOSEFILES_STATIC1,7,104,199,17 +END + +IDD_PROGRESS DIALOG DISCARDABLE 0, 0, 250, 66 +STYLE DS_MODALFRAME | DS_3DLOOK | WS_POPUP | WS_VISIBLE | WS_CAPTION +CAPTION "Progress..." +FONT 8, "MS Sans Serif" +BEGIN + PUSHBUTTON "Cancel",IDCANCEL,99,45,50,14 + LTEXT "Now scanning:",IDC_STATIC,7,7,96,8 + LTEXT ">filename<",IDC_PROGRESS_FILENAME,7,20,236,8 +END + + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +2 TEXTINCLUDE DISCARDABLE +BEGIN + "#define APSTUDIO_HIDDEN_SYMBOLS\r\n" + "#include ""afxres.h""\r\n" + "#undef APSTUDIO_HIDDEN_SYMBOLS\r\n" + "#include ""resource.h""\r\n" + "\0" +END + +3 TEXTINCLUDE DISCARDABLE +BEGIN + "\r\n" + "\0" +END + +1 TEXTINCLUDE DISCARDABLE +BEGIN + "resource.h\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// DESIGNINFO +// + +#ifdef APSTUDIO_INVOKED +GUIDELINES DESIGNINFO DISCARDABLE +BEGIN + IDD_ABOUTBOX, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 126 + VERTGUIDE, 33 + TOPMARGIN, 7 + BOTTOMMARGIN, 104 + END + + "IDD_CHOOSE_FILES", DIALOG + BEGIN + VERTGUIDE, 7 + BOTTOMMARGIN, 121 + END + + IDD_PROGRESS, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 243 + TOPMARGIN, 7 + BOTTOMMARGIN, 59 + END +END +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// String Table +// + +STRINGTABLE DISCARDABLE +BEGIN + IDS_APP_TITLE "MDC" + IDC_MDC "MDC" + IDS_MUST_REGISTER "You must be a registered owner of CiderPress to use MDC.\r\n\r\nVisit http://www.faddensoft.com/." + IDS_FAILED "Failed" +END + +STRINGTABLE DISCARDABLE +BEGIN + IDM_FILE_SCAN "Select disk images to scan" + IDM_HELP_WEBSITE "Visit the CiderPress web site\nGo to web site" +END + +#endif // English (U.S.) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/mdc/mdc.vcproj b/mdc/mdc.vcproj new file mode 100644 index 0000000..34717be --- /dev/null +++ b/mdc/mdc.vcproj @@ -0,0 +1,315 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mdc/resource.h b/mdc/resource.h new file mode 100644 index 0000000..fecbe8a --- /dev/null +++ b/mdc/resource.h @@ -0,0 +1,32 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Developer Studio generated include file. +// Used by mdc.rc +// +#define IDC_MYICON 2 +#define IDD_ABOUTBOX 103 +#define IDS_APP_TITLE 103 +#define IDM_ABOUT 104 +#define IDM_FILE_EXIT 105 +#define IDI_MDC 107 +#define IDC_MDC 109 +#define IDS_MUST_REGISTER 110 +#define IDS_FAILED 111 +#define IDD_PROGRESS 131 +#define IDC_CHOOSEFILES_STATIC1 1002 +#define IDC_PROGRESS_FILENAME 1003 +#define IDC_ABOUT_VERS 1004 +#define IDC_SELECT_ACCEPT 1175 +#define IDM_HELP_ABOUT 32771 +#define IDM_FILE_SCAN 32772 +#define IDM_HELP_WEBSITE 32773 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 132 +#define _APS_NEXT_COMMAND_VALUE 32774 +#define _APS_NEXT_CONTROL_VALUE 1005 +#define _APS_NEXT_SYMED_VALUE 110 +#endif +#endif diff --git a/prebuilt/NufxLib.h b/prebuilt/NufxLib.h new file mode 100644 index 0000000..5408a0a --- /dev/null +++ b/prebuilt/NufxLib.h @@ -0,0 +1,845 @@ +/* + * NuFX archive manipulation library + * Copyright (C) 2000-2007 by Andy McFadden, All Rights Reserved. + * This is free software; you can redistribute it and/or modify it under the + * terms of the BSD License, see the file COPYING-LIB. + * + * External interface (types, defines, and function prototypes). + */ +#ifndef __NufxLib__ +#define __NufxLib__ + +#include + + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * NufxLib version number. Compare these values (which represent the + * version against which your application was compiled) to the values + * returned by NuGetVersion (representing the version against which + * your application is statically or dynamically linked). If the major + * number doesn't match exactly, an existing interface has changed and you + * should halt immediately. If the minor number from NuGetVersion is + * less, there may be new interfaces, new features, or bug fixes missing + * upon which your application depends, so you should halt immediately. + * (If the minor number is greater, there are new features, but your + * application will not be affected by them.) + * + * The "bug" version can usually be ignored, since it represents minor + * fixes. Unless, of course, your code depends upon that fix. + */ +#define kNuVersionMajor 2 +#define kNuVersionMinor 2 +#define kNuVersionBug 0 + + +/* + * =========================================================================== + * Types + * =========================================================================== + */ + +/* + * Error values returned from functions. + * + * These are negative so that they don't conflict with system-defined + * errors (like ENOENT). A NuError can hold either. + */ +typedef enum NuError { + kNuErrNone = 0, + + kNuErrGeneric = -1, + kNuErrInternal = -2, + kNuErrUsage = -3, + kNuErrSyntax = -4, + kNuErrMalloc = -5, + kNuErrInvalidArg = -6, + kNuErrBadStruct = -7, + kNuErrUnexpectedNil = -8, + kNuErrBusy = -9, + + kNuErrSkipped = -10, /* processing skipped by request */ + kNuErrAborted = -11, /* processing aborted by request */ + kNuErrRename = -12, /* user wants to rename before extracting */ + + kNuErrFile = -20, + kNuErrFileOpen = -21, + kNuErrFileClose = -22, + kNuErrFileRead = -23, + kNuErrFileWrite = -24, + kNuErrFileSeek = -25, + kNuErrFileExists = -26, /* existed when it shouldn't */ + kNuErrFileNotFound = -27, /* didn't exist when it should have */ + kNuErrFileStat = -28, /* some sort of GetFileInfo failure */ + kNuErrFileNotReadable = -29, /* bad access permissions */ + + kNuErrDirExists = -30, /* dir exists, don't need to create it */ + kNuErrNotDir = -31, /* expected a dir, got a regular file */ + kNuErrNotRegularFile = -32, /* expected regular file, got weirdness */ + kNuErrDirCreate = -33, /* unable to create a directory */ + kNuErrOpenDir = -34, /* error opening directory */ + kNuErrReadDir = -35, /* error reading directory */ + kNuErrFileSetDate = -36, /* unable to set file date */ + kNuErrFileSetAccess = -37, /* unable to set file access permissions */ + kNuErrFileAccessDenied = -38, /* equivalent to EACCES */ + + kNuErrNotNuFX = -40, /* 'NuFile' missing; not a NuFX archive? */ + kNuErrBadMHVersion = -41, /* bad master header version */ + kNuErrRecHdrNotFound = -42, /* 'NuFX' missing; corrupted archive? */ + kNuErrNoRecords = -43, /* archive doesn't have any records */ + kNuErrBadRecord = -44, /* something about the record looked bad */ + kNuErrBadMHCRC = -45, /* bad master header CRC */ + kNuErrBadRHCRC = -46, /* bad record header CRC */ + kNuErrBadThreadCRC = -47, /* bad thread header CRC */ + kNuErrBadDataCRC = -48, /* bad CRC detected in the data */ + + kNuErrBadFormat = -50, /* compression type not supported */ + kNuErrBadData = -51, /* expansion func didn't like input */ + kNuErrBufferOverrun = -52, /* overflowed a user buffer */ + kNuErrBufferUnderrun = -53, /* underflowed a user buffer */ + kNuErrOutMax = -54, /* output limit exceeded */ + + kNuErrNotFound = -60, /* (generic) search unsuccessful */ + kNuErrRecordNotFound = -61, /* search for specific record failed */ + kNuErrRecIdxNotFound = -62, /* search by NuRecordIdx failed */ + kNuErrThreadIdxNotFound = -63, /* search by NuThreadIdx failed */ + kNuErrThreadIDNotFound = -64, /* search by NuThreadID failed */ + kNuErrRecNameNotFound = -65, /* search by storageName failed */ + kNuErrRecordExists = -66, /* found existing record with same name */ + + kNuErrAllDeleted = -70, /* attempt to delete everything */ + kNuErrArchiveRO = -71, /* archive is open in read-only mode */ + kNuErrModRecChange = -72, /* tried to change modified record */ + kNuErrModThreadChange = -73, /* tried to change modified thread */ + kNuErrThreadAdd = -74, /* adding that thread creates a conflict */ + kNuErrNotPreSized = -75, /* tried to update a non-pre-sized thread */ + kNuErrPreSizeOverflow = -76, /* too much data */ + kNuErrInvalidFilename = -77, /* invalid filename */ + + kNuErrLeadingFssep = -80, /* names in archives must not start w/sep */ + kNuErrNotNewer = -81, /* item same age or older than existing */ + kNuErrDuplicateNotFound = -82, /* "must overwrite" was set, but item DNE */ + kNuErrDamaged = -83, /* original archive may have been damaged */ + + kNuErrIsBinary2 = -90, /* this looks like a Binary II archive */ + + kNuErrUnknownFeature =-100, /* attempt to test unknown feature */ + kNuErrUnsupFeature = -101, /* feature not supported */ +} NuError; + +/* + * Return values from callback functions. + */ +typedef enum NuResult { + kNuOK = 0, + kNuSkip = 1, + kNuAbort = 2, + /*kNuAbortAll = 3,*/ + kNuRetry = 4, + kNuIgnore = 5, + kNuRename = 6, + kNuOverwrite = 7 +} NuResult; + +/* + * NuRecordIdxs are assigned to records in an archive. You may assume that + * the values are unique, but that is all. + */ +typedef unsigned long NuRecordIdx; + +/* + * NuThreadIdxs are assigned to threads within a record. Again, you may + * assume that the values are unique within a record, but that is all. + */ +typedef unsigned long NuThreadIdx; + +/* + * Thread ID, a combination of thread_class and thread_kind. Standard + * values have explicit identifiers. + */ +typedef unsigned long NuThreadID; +#define NuMakeThreadID(class, kind) /* construct a NuThreadID */ \ + ((unsigned long)(class) << 16 | (unsigned long)(kind)) +#define NuGetThreadID(pThread) /* pull NuThreadID out of NuThread */ \ + (NuMakeThreadID((pThread)->thThreadClass, (pThread)->thThreadKind)) +#define NuThreadIDGetClass(threadID) /* get threadClass from NuThreadID */ \ + ((unsigned short) ((unsigned long)(threadID) >> 16)) +#define NuThreadIDGetKind(threadID) /* get threadKind from NuThreadID */ \ + ((unsigned short) ((threadID) & 0xffff)) +#define kNuThreadClassMessage 0x0000 +#define kNuThreadClassControl 0x0001 +#define kNuThreadClassData 0x0002 +#define kNuThreadClassFilename 0x0003 +#define kNuThreadIDOldComment NuMakeThreadID(kNuThreadClassMessage, 0x0000) +#define kNuThreadIDComment NuMakeThreadID(kNuThreadClassMessage, 0x0001) +#define kNuThreadIDIcon NuMakeThreadID(kNuThreadClassMessage, 0x0002) +#define kNuThreadIDMkdir NuMakeThreadID(kNuThreadClassControl, 0x0000) +#define kNuThreadIDDataFork NuMakeThreadID(kNuThreadClassData, 0x0000) +#define kNuThreadIDDiskImage NuMakeThreadID(kNuThreadClassData, 0x0001) +#define kNuThreadIDRsrcFork NuMakeThreadID(kNuThreadClassData, 0x0002) +#define kNuThreadIDFilename NuMakeThreadID(kNuThreadClassFilename, 0x0000) +#define kNuThreadIDWildcard NuMakeThreadID(0xffff, 0xffff) + +/* enumerate the possible values for thThreadFormat */ +typedef enum NuThreadFormat { + kNuThreadFormatUncompressed = 0x0000, + kNuThreadFormatHuffmanSQ = 0x0001, + kNuThreadFormatLZW1 = 0x0002, + kNuThreadFormatLZW2 = 0x0003, + kNuThreadFormatLZC12 = 0x0004, + kNuThreadFormatLZC16 = 0x0005, + kNuThreadFormatDeflate = 0x0006, /* NOTE: not in NuFX standard */ + kNuThreadFormatBzip2 = 0x0007, /* NOTE: not in NuFX standard */ +} NuThreadFormat; + + +/* extract the filesystem separator char from the "file_sys_info" field */ +#define NuGetSepFromSysInfo(sysInfo) \ + ((char) ((sysInfo) & 0xff)) +/* return a file_sys_info with a replaced filesystem separator */ +#define NuSetSepInSysInfo(sysInfo, newSep) \ + ((unsigned short) (((sysInfo) & 0xff00) | ((newSep) & 0xff)) ) + +/* GS/OS-defined file system identifiers; sadly, UNIX is not among them */ +typedef enum NuFileSysID { + kNuFileSysUnknown = 0, /* NuFX spec says use this */ + kNuFileSysProDOS = 1, + kNuFileSysDOS33 = 2, + kNuFileSysDOS32 = 3, + kNuFileSysPascal = 4, + kNuFileSysMacHFS = 5, + kNuFileSysMacMFS = 6, + kNuFileSysLisa = 7, + kNuFileSysCPM = 8, + kNuFileSysCharFST = 9, + kNuFileSysMSDOS = 10, + kNuFileSysHighSierra = 11, + kNuFileSysISO9660 = 12, + kNuFileSysAppleShare = 13 +} NuFileSysID; + +/* simplified definition of storage types */ +typedef enum NuStorageType { + kNuStorageUnknown = 0, /* (used by ProDOS for deleted files) */ + kNuStorageSeedling = 1, /* <= 512 bytes */ + kNuStorageSapling = 2, /* < 128KB */ + kNuStorageTree = 3, /* < 16MB */ + kNuStoragePascalVol = 4, /* (embedded pascal volume; rare) */ + kNuStorageExtended = 5, /* forked (any size) */ + kNuStorageDirectory = 13, /* directory */ + kNuStorageSubdirHeader = 14, /* (only used in subdir headers) */ + kNuStorageVolumeHeader = 15, /* (only used in volume dir header) */ +} NuStorageType; + +/* bit flags for NuOpenRW */ +enum { + kNuOpenCreat = 0x0001, + kNuOpenExcl = 0x0002 +}; + + +/* + * The actual NuArchive structure is opaque, and should only be visible + * to the library. We define it here as an ambiguous struct. + */ +typedef struct NuArchive NuArchive; + +/* + * Generic callback prototype. + */ +typedef NuResult (*NuCallback)(NuArchive* pArchive, void* args); + +/* + * Parameters that affect archive operations. + */ +typedef enum NuValueID { + kNuValueInvalid = 0, + kNuValueIgnoreCRC = 1, + kNuValueDataCompression = 2, + kNuValueDiscardWrapper = 3, + kNuValueEOL = 4, + kNuValueConvertExtractedEOL = 5, + kNuValueOnlyUpdateOlder = 6, + kNuValueAllowDuplicates = 7, + kNuValueHandleExisting = 8, + kNuValueModifyOrig = 9, + kNuValueMimicSHK = 10, + kNuValueMaskDataless = 11, + kNuValueStripHighASCII = 12, + kNuValueJunkSkipMax = 13, + kNuValueIgnoreLZW2Len = 14, + kNuValueHandleBadMac = 15 +} NuValueID; +typedef unsigned long NuValue; + +/* + * Enumerated values for things you pass in a NuValue. + */ +enum NuValueValue { + /* for the truly retentive */ + kNuValueFalse = 0, + kNuValueTrue = 1, + + /* for kNuValueDataCompression */ + kNuCompressNone = 10, + kNuCompressSQ = 11, + kNuCompressLZW1 = 12, + kNuCompressLZW2 = 13, + kNuCompressLZC12 = 14, + kNuCompressLZC16 = 15, + kNuCompressDeflate = 16, + kNuCompressBzip2 = 17, + + /* for kNuValueEOL */ + kNuEOLUnknown = 50, + kNuEOLCR = 51, + kNuEOLLF = 52, + kNuEOLCRLF = 53, + + /* for kNuValueConvertExtractedEOL */ + kNuConvertOff = 60, + kNuConvertOn = 61, + kNuConvertAuto = 62, + + /* for kNuValueHandleExisting */ + kNuMaybeOverwrite = 90, + kNuNeverOverwrite = 91, + kNuAlwaysOverwrite = 93, + kNuMustOverwrite = 94 +}; + + +/* + * Pull out archive attributes. + */ +typedef enum NuAttrID { + kNuAttrInvalid = 0, + kNuAttrArchiveType = 1, + kNuAttrNumRecords = 2, + kNuAttrHeaderOffset = 3, + kNuAttrJunkOffset = 4, +} NuAttrID; +typedef unsigned long NuAttr; + +/* + * Archive types. + */ +typedef enum NuArchiveType { + kNuArchiveUnknown, /* .??? */ + kNuArchiveNuFX, /* .SHK (sometimes .SDK) */ + kNuArchiveNuFXInBNY, /* .BXY */ + kNuArchiveNuFXSelfEx, /* .SEA */ + kNuArchiveNuFXSelfExInBNY, /* .BSE */ + + kNuArchiveBNY /* .BNY, .BQY - not supported */ +} NuArchiveType; + + +/* + * Some common values for "locked" and "unlocked". Under ProDOS each bit + * can be set independently, so don't use these defines to *interpret* + * what you see. They're reasonable things to *set* the access field to. + * + * The defined bits are: + * 0x80 'D' destroy enabled + * 0x40 'N' rename enabled + * 0x20 'B' file needs to be backed up + * 0x10 (reserved, must be zero) + * 0x08 (reserved, must be zero) + * 0x04 'I' file is invisible + * 0x02 'W' write enabled + * 0x01 'R' read enabled + */ +#define kNuAccessLocked 0x21 +#define kNuAccessUnlocked 0xe3 + + +/* + * NuFlush result flags. + */ +#define kNuFlushSucceeded (1L) +#define kNuFlushAborted (1L << 1) +#define kNuFlushCorrupted (1L << 2) +#define kNuFlushReadOnly (1L << 3) +#define kNuFlushInaccessible (1L << 4) + + +/* + * =========================================================================== + * NuFX archive defintions + * =========================================================================== + */ + +typedef struct NuThreadMod NuThreadMod; /* dummy def for internal struct */ +typedef union NuDataSource NuDataSource; /* dummy def for internal struct */ +typedef union NuDataSink NuDataSink; /* dummy def for internal struct */ + +/* + * NuFX Date/Time structure; same as TimeRec from IIgs "misctool.h". + */ +typedef struct NuDateTime { + unsigned char second; /* 0-59 */ + unsigned char minute; /* 0-59 */ + unsigned char hour; /* 0-23 */ + unsigned char year; /* year - 1900 */ + unsigned char day; /* 0-30 */ + unsigned char month; /* 0-11 */ + unsigned char extra; /* (must be zero) */ + unsigned char weekDay; /* 1-7 (1=sunday) */ +} NuDateTime; + +/* + * NuFX "thread" definition. + * + * Guaranteed not to have pointers in it. Can be copied with memcpy or + * assignment. + */ +typedef struct NuThread { + /* from the archive */ + unsigned short thThreadClass; + NuThreadFormat thThreadFormat; + unsigned short thThreadKind; + unsigned short thThreadCRC; /* comp or uncomp data; see rec vers */ + unsigned long thThreadEOF; + unsigned long thCompThreadEOF; + + /* extra goodies */ + NuThreadIdx threadIdx; + unsigned long actualThreadEOF; /* disk images might be off */ + long fileOffset; /* fseek offset to data in shk */ + + /* internal use only */ + unsigned short used; /* mark as uninteresting */ +} NuThread; + +/* + * NuFX "record" definition. + * + * (Note to developers: update Nu_AddRecord if this changes.) + */ +#define kNufxIDLen 4 /* len of 'NuFX' with funky MSBs */ +#define kNuReasonableAttribCount 256 +#define kNuReasonableFilenameLen 1024 +#define kNuReasonableTotalThreads 16 +#define kNuMaxRecordVersion 3 /* max we can handle */ +#define kNuOurRecordVersion 3 /* what we write */ +typedef struct NuRecord { + /* version 0+ */ + unsigned char recNufxID[kNufxIDLen]; + unsigned short recHeaderCRC; + unsigned short recAttribCount; + unsigned short recVersionNumber; + unsigned long recTotalThreads; + NuFileSysID recFileSysID; + unsigned short recFileSysInfo; + unsigned long recAccess; + unsigned long recFileType; + unsigned long recExtraType; + unsigned short recStorageType; /* NuStorage*,file_sys_block_size */ + NuDateTime recCreateWhen; + NuDateTime recModWhen; + NuDateTime recArchiveWhen; + + /* option lists only in version 1+ */ + unsigned short recOptionSize; + unsigned char* recOptionList; /* NULL if v0 or recOptionSize==0 */ + + /* data specified by recAttribCount, not accounted for by option list */ + long extraCount; + unsigned char* extraBytes; + + unsigned short recFilenameLength; /* usually zero */ + char* recFilename; /* doubles as disk volume_name */ + + /* extra goodies; "dirtyHeader" does not apply to anything below */ + NuRecordIdx recordIdx; /* session-unique record index */ + char* threadFilename; /* extracted from filename thread */ + char* newFilename; /* memorized during "add file" call */ + const char* filename; /* points at recFilen or threadFilen */ + unsigned long recHeaderLength; /* size of rec hdr, incl thread hdrs */ + unsigned long totalCompLength; /* total len of data in archive file */ + long fakeThreads; /* used by "MaskDataless" */ + int isBadMac; /* malformed "bad mac" header */ + + long fileOffset; /* file offset of record header */ + + /* use provided interface to access this */ + struct NuThread* pThreads; /* ptr to thread array */ + + /* private -- things the application shouldn't look at */ + struct NuRecord* pNext; /* used internally */ + NuThreadMod* pThreadMods; /* used internally */ + short dirtyHeader; /* set in "copy" when hdr fields uptd */ + short dropRecFilename; /* if set, we're dropping this name */ +} NuRecord; + +/* + * NuFX "master header" definition. + * + * The "mhReserved2" entry doesn't appear in my copy of the $e0/8002 File + * Type Note, but as best as I can recall the MH block must be 48 bytes. + */ +#define kNufileIDLen 6 /* length of 'NuFile' with funky MSBs */ +#define kNufileMasterReserved1Len 8 +#define kNufileMasterReserved2Len 6 +#define kNuMaxMHVersion 2 /* max we can handle */ +#define kNuOurMHVersion 2 /* what we write */ +typedef struct NuMasterHeader { + unsigned char mhNufileID[kNufileIDLen]; + unsigned short mhMasterCRC; + unsigned long mhTotalRecords; + NuDateTime mhArchiveCreateWhen; + NuDateTime mhArchiveModWhen; + unsigned short mhMasterVersion; + unsigned char mhReserved1[kNufileMasterReserved1Len]; + unsigned long mhMasterEOF; + unsigned char mhReserved2[kNufileMasterReserved2Len]; + + /* private -- internal use only */ + short isValid; +} NuMasterHeader; + + +/* + * =========================================================================== + * Misc declarations + * =========================================================================== + */ + +/* + * Record attributes that can be changed with NuSetRecordAttr. This is + * a small subset of the full record. + */ +typedef struct NuRecordAttr { + NuFileSysID fileSysID; + /*unsigned short fileSysInfo;*/ + unsigned long access; + unsigned long fileType; + unsigned long extraType; + NuDateTime createWhen; + NuDateTime modWhen; + NuDateTime archiveWhen; +} NuRecordAttr; + +/* + * Some additional details about a file. + */ +typedef struct NuFileDetails { + /* used during AddFile call */ + NuThreadID threadID; /* data, rsrc, disk img? */ + const char* origName; + + /* these go straight into the NuRecord */ + const char* storageName; + NuFileSysID fileSysID; + unsigned short fileSysInfo; + unsigned long access; + unsigned long fileType; + unsigned long extraType; + unsigned short storageType; /* use Unknown, or disk block size */ + NuDateTime createWhen; + NuDateTime modWhen; + NuDateTime archiveWhen; +} NuFileDetails; + + +/* + * Passed into the SelectionFilter callback. + */ +typedef struct NuSelectionProposal { + const NuRecord* pRecord; + const NuThread* pThread; +} NuSelectionProposal; + +/* + * Passed into the OutputPathnameFilter callback. + */ +typedef struct NuPathnameProposal { + const char* pathname; + char filenameSeparator; + const NuRecord* pRecord; + const NuThread* pThread; + + const char* newPathname; + unsigned char newFilenameSeparator; + /*NuThreadID newStorage;*/ + NuDataSink* newDataSink; +} NuPathnameProposal; + + +/* used by error handler and progress updater to indicate what we're doing */ +typedef enum NuOperation { + kNuOpUnknown = 0, + kNuOpAdd, + kNuOpExtract, + kNuOpTest, + kNuOpDelete, /* not used for progress updates */ + kNuOpContents /* not used for progress updates */ +} NuOperation; + +/* state of progress when adding or extracting */ +typedef enum NuProgressState { + kNuProgressPreparing, /* not started yet */ + kNuProgressOpening, /* opening files */ + + kNuProgressAnalyzing, /* analyzing data */ + kNuProgressCompressing, /* compressing data */ + kNuProgressStoring, /* storing (no compression) data */ + kNuProgressExpanding, /* expanding data */ + kNuProgressCopying, /* copying data (in or out) */ + + kNuProgressDone, /* all done, success */ + kNuProgressSkipped, /* all done, we skipped this one */ + kNuProgressAborted, /* all done, user cancelled the operation */ + kNuProgressFailed /* all done, failure */ +} NuProgressState; + +/* + * Passed into the ProgressUpdater callback. + * + * [ Thought for the day: add an optional flag that causes us to only + * call the progressFunc when the "percentComplete" changes by more + * than a specified amount. ] + */ +typedef struct NuProgressData { + /* what are we doing */ + NuOperation operation; + /* what specifically are we doing */ + NuProgressState state; + /* how far along are we */ + short percentComplete; /* 0-100 */ + + /* original pathname (in archive for expand, on disk for compress) */ + const char* origPathname; + /* processed pathname (PathnameFilter for expand, in-record for compress) */ + const char* pathname; + /* basename of "pathname" */ + const char* filename; + /* pointer to the record we're expanding from */ + const NuRecord* pRecord; + + unsigned long uncompressedLength; /* size of uncompressed data */ + unsigned long uncompressedProgress; /* #of bytes in/out */ + + struct { + NuThreadFormat threadFormat; /* compression being applied */ + } compress; + + struct { + unsigned long totalCompressedLength; /* all "data" threads */ + unsigned long totalUncompressedLength; + + /*unsigned long compressedLength; * size of compressed data */ + /*unsigned long compressedProgress; * #of compressed bytes in/out*/ + const NuThread* pThread; /* thread we're working on */ + NuValue convertEOL; /* set if LF/CR conv is on */ + } expand; + + /* pay no attention */ + NuCallback progressFunc; +} NuProgressData; + +/* + * Passed into the ErrorHandler callback. + */ +typedef struct NuErrorStatus { + NuOperation operation; /* were we adding, extracting, ?? */ + NuError err; /* library error code */ + int sysErr; /* system error code, if applicable */ + const char* message; /* (optional) message to user */ + const NuRecord* pRecord; /* relevant record, if any */ + const char* pathname; /* problematic pathname, if any */ + const char* origPathname; /* original pathname, if any */ + char filenameSeparator; /* fssep for pathname, if any */ + /*char origArchiveTouched;*/ + + char canAbort; /* give option to abort */ + /*char canAbortAll;*/ /* abort + discard all recent changes */ + char canRetry; /* give option to retry same op */ + char canIgnore; /* give option to ignore error */ + char canSkip; /* give option to skip this file/rec */ + char canRename; /* give option to rename file */ + char canOverwrite; /* give option to overwrite file */ +} NuErrorStatus; + +/* + * Error message callback gets one of these. + */ +typedef struct NuErrorMessage { + const char* message; /* the message itself */ + NuError err; /* relevant error code (may be none) */ + short isDebug; /* set for debug-only messages */ + + /* these identify where the message originated if lib built w/debug set */ + const char* file; /* source file */ + int line; /* line number */ + const char* function; /* function name (might be nil) */ +} NuErrorMessage; + + +/* + * Options for the NuTestFeature function. + */ +typedef enum NuFeature { + kNuFeatureUnknown = 0, + + kNuFeatureCompressSQ = 1, /* kNuThreadFormatHuffmanSQ */ + kNuFeatureCompressLZW = 2, /* kNuThreadFormatLZW1 and LZW2 */ + kNuFeatureCompressLZC = 3, /* kNuThreadFormatLZC12 and LZC16 */ + kNuFeatureCompressDeflate = 4, /* kNuThreadFormatDeflate */ + kNuFeatureCompressBzip2 = 5, /* kNuThreadFormatBzip2 */ +} NuFeature; + + +/* + * =========================================================================== + * Function prototypes + * =========================================================================== + */ + +/* + * Win32 dll magic. + */ +#if defined(_WIN32) +# include +# if defined(NUFXLIB_EXPORTS) + /* building the NufxLib DLL */ +# define NUFXLIB_API __declspec(dllexport) +# elif defined (NUFXLIB_DLL) + /* building to link against the NufxLib DLL */ +# define NUFXLIB_API __declspec(dllimport) +# else + /* using static libs */ +# define NUFXLIB_API +# endif +#else + /* not using Win32... hooray! */ +# define NUFXLIB_API +#endif + +/* streaming and non-streaming read-only interfaces */ +NUFXLIB_API NuError NuStreamOpenRO(FILE* infp, NuArchive** ppArchive); +NUFXLIB_API NuError NuContents(NuArchive* pArchive, NuCallback contentFunc); +NUFXLIB_API NuError NuExtract(NuArchive* pArchive); +NUFXLIB_API NuError NuTest(NuArchive* pArchive); + +/* strictly non-streaming read-only interfaces */ +NUFXLIB_API NuError NuOpenRO(const char* archivePathname,NuArchive** ppArchive); +NUFXLIB_API NuError NuExtractRecord(NuArchive* pArchive, NuRecordIdx recordIdx); +NUFXLIB_API NuError NuExtractThread(NuArchive* pArchive, NuThreadIdx threadIdx, + NuDataSink* pDataSink); +NUFXLIB_API NuError NuTestRecord(NuArchive* pArchive, NuRecordIdx recordIdx); +NUFXLIB_API NuError NuGetRecord(NuArchive* pArchive, NuRecordIdx recordIdx, + const NuRecord** ppRecord); +NUFXLIB_API NuError NuGetRecordIdxByName(NuArchive* pArchive, const char* name, + NuRecordIdx* pRecordIdx); +NUFXLIB_API NuError NuGetRecordIdxByPosition(NuArchive* pArchive, + unsigned long position, NuRecordIdx* pRecordIdx); + +/* read/write interfaces */ +NUFXLIB_API NuError NuOpenRW(const char* archivePathname, + const char* tempPathname, unsigned long flags, + NuArchive** ppArchive); +NUFXLIB_API NuError NuFlush(NuArchive* pArchive, long* pStatusFlags); +NUFXLIB_API NuError NuAddRecord(NuArchive* pArchive, + const NuFileDetails* pFileDetails, NuRecordIdx* pRecordIdx); +NUFXLIB_API NuError NuAddThread(NuArchive* pArchive, NuRecordIdx recordIdx, + NuThreadID threadID, NuDataSource* pDataSource, + NuThreadIdx* pThreadIdx); +NUFXLIB_API NuError NuAddFile(NuArchive* pArchive, const char* pathname, + const NuFileDetails* pFileDetails, short fromRsrcFork, + NuRecordIdx* pRecordIdx); +NUFXLIB_API NuError NuRename(NuArchive* pArchive, NuRecordIdx recordIdx, + const char* pathname, char fssep); +NUFXLIB_API NuError NuSetRecordAttr(NuArchive* pArchive, NuRecordIdx recordIdx, + const NuRecordAttr* pRecordAttr); +NUFXLIB_API NuError NuUpdatePresizedThread(NuArchive* pArchive, + NuThreadIdx threadIdx, NuDataSource* pDataSource, long* pMaxLen); +NUFXLIB_API NuError NuDelete(NuArchive* pArchive); +NUFXLIB_API NuError NuDeleteRecord(NuArchive* pArchive, NuRecordIdx recordIdx); +NUFXLIB_API NuError NuDeleteThread(NuArchive* pArchive, NuThreadIdx threadIdx); + +/* general interfaces */ +NUFXLIB_API NuError NuClose(NuArchive* pArchive); +NUFXLIB_API NuError NuAbort(NuArchive* pArchive); +NUFXLIB_API NuError NuGetMasterHeader(NuArchive* pArchive, + const NuMasterHeader** ppMasterHeader); +NUFXLIB_API NuError NuGetExtraData(NuArchive* pArchive, void** ppData); +NUFXLIB_API NuError NuSetExtraData(NuArchive* pArchive, void* pData); +NUFXLIB_API NuError NuGetValue(NuArchive* pArchive, NuValueID ident, + NuValue* pValue); +NUFXLIB_API NuError NuSetValue(NuArchive* pArchive, NuValueID ident, + NuValue value); +NUFXLIB_API NuError NuGetAttr(NuArchive* pArchive, NuAttrID ident, + NuAttr* pAttr); +NUFXLIB_API NuError NuDebugDumpArchive(NuArchive* pArchive); + +/* sources and sinks */ +NUFXLIB_API NuError NuCreateDataSourceForFile(NuThreadFormat threadFormat, + unsigned long otherLen, const char* pathname, + short isFromRsrcFork, NuDataSource** ppDataSource); +NUFXLIB_API NuError NuCreateDataSourceForFP(NuThreadFormat threadFormat, + unsigned long otherLen, FILE* fp, long offset, long length, + NuCallback closeFunc, NuDataSource** ppDataSource); +NUFXLIB_API NuError NuCreateDataSourceForBuffer(NuThreadFormat threadFormat, + unsigned long otherLen, const unsigned char* buffer, long offset, + long length, NuCallback freeFunc, NuDataSource** ppDataSource); +NUFXLIB_API NuError NuFreeDataSource(NuDataSource* pDataSource); +NUFXLIB_API NuError NuDataSourceSetRawCrc(NuDataSource* pDataSource, + unsigned short crc); +NUFXLIB_API NuError NuCreateDataSinkForFile(short doExpand, NuValue convertEOL, + const char* pathname, char fssep, NuDataSink** ppDataSink); +NUFXLIB_API NuError NuCreateDataSinkForFP(short doExpand, NuValue convertEOL, + FILE* fp, NuDataSink** ppDataSink); +NUFXLIB_API NuError NuCreateDataSinkForBuffer(short doExpand, + NuValue convertEOL, unsigned char* buffer, unsigned long bufLen, + NuDataSink** ppDataSink); +NUFXLIB_API NuError NuFreeDataSink(NuDataSink* pDataSink); +NUFXLIB_API NuError NuDataSinkGetOutCount(NuDataSink* pDataSink, + unsigned long* pOutCount); + +/* miscellaneous non-archive operations */ +NUFXLIB_API NuError NuGetVersion(long* pMajorVersion, long* pMinorVersion, + long* pBugVersion, const char** ppBuildDate, + const char** ppBuildFlags); +NUFXLIB_API const char* NuStrError(NuError err); +NUFXLIB_API NuError NuTestFeature(NuFeature feature); +NUFXLIB_API void NuRecordCopyAttr(NuRecordAttr* pRecordAttr, + const NuRecord* pRecord); +NUFXLIB_API NuError NuRecordCopyThreads(const NuRecord* pRecord, + NuThread** ppThreads); +NUFXLIB_API unsigned long NuRecordGetNumThreads(const NuRecord* pRecord); +NUFXLIB_API const NuThread* NuThreadGetByIdx(const NuThread* pThread, long idx); +NUFXLIB_API short NuIsPresizedThreadID(NuThreadID threadID); + +#define NuGetThread(pRecord, idx) ( (const NuThread*) \ + ((unsigned long) (idx) < (unsigned long) (pRecord)->recTotalThreads ? \ + &(pRecord)->pThreads[(idx)] : NULL) \ + ) + + +/* callback setters */ +#define kNuInvalidCallback ((NuCallback) 1) +NUFXLIB_API NuCallback NuSetSelectionFilter(NuArchive* pArchive, + NuCallback filterFunc); +NUFXLIB_API NuCallback NuSetOutputPathnameFilter(NuArchive* pArchive, + NuCallback filterFunc); +NUFXLIB_API NuCallback NuSetProgressUpdater(NuArchive* pArchive, + NuCallback updateFunc); +NUFXLIB_API NuCallback NuSetFreeHandler(NuArchive* pArchive, + NuCallback freeFunc); +NUFXLIB_API NuCallback NuSetErrorHandler(NuArchive* pArchive, + NuCallback errorFunc); +NUFXLIB_API NuCallback NuSetErrorMessageHandler(NuArchive* pArchive, + NuCallback messageHandlerFunc); +NUFXLIB_API NuCallback NuSetGlobalErrorMessageHandler(NuCallback messageHandlerFunc); + + +#ifdef __cplusplus +} +#endif + +#endif /*__NufxLib__*/ diff --git a/prebuilt/nufxlib2.dll b/prebuilt/nufxlib2.dll new file mode 100644 index 0000000000000000000000000000000000000000..b8230f5dc4ec300ef277305343d05686f654facd GIT binary patch literal 110592 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 literal 0 HcmV?d00001 diff --git a/prebuilt/nufxlib2.lib b/prebuilt/nufxlib2.lib new file mode 100644 index 0000000000000000000000000000000000000000..ff6135a58adbfc04bdb1f7606cbbdb02c45457b0 GIT binary patch literal 13282 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 F{{l9Z3O@h< literal 0 HcmV?d00001 diff --git a/prebuilt/nufxlib2D.dll b/prebuilt/nufxlib2D.dll new file mode 100644 index 0000000000000000000000000000000000000000..d0353d34ff2750819a746d9c247549eae3891521 GIT binary patch literal 331829 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 A`2YX_ literal 0 HcmV?d00001 diff --git a/prebuilt/nufxlib2D.lib b/prebuilt/nufxlib2D.lib new file mode 100644 index 0000000000000000000000000000000000000000..e3621810ad49de8930bbdccdc712130de8a9316f GIT binary patch literal 13344 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 b?p3RU(7t!X^bLRX!~f^F#|N))UDW>pd_^Q; literal 0 HcmV?d00001 diff --git a/prebuilt/zconf.h b/prebuilt/zconf.h new file mode 100644 index 0000000..e3b0c96 --- /dev/null +++ b/prebuilt/zconf.h @@ -0,0 +1,332 @@ +/* zconf.h -- configuration of the zlib compression library + * Copyright (C) 1995-2005 Jean-loup Gailly. + * For conditions of distribution and use, see copyright notice in zlib.h + */ + +/* @(#) $Id$ */ + +#ifndef ZCONF_H +#define ZCONF_H + +/* + * If you *really* need a unique prefix for all types and library functions, + * compile with -DZ_PREFIX. The "standard" zlib should be compiled without it. + */ +#ifdef Z_PREFIX +# define deflateInit_ z_deflateInit_ +# define deflate z_deflate +# define deflateEnd z_deflateEnd +# define inflateInit_ z_inflateInit_ +# define inflate z_inflate +# define inflateEnd z_inflateEnd +# define deflateInit2_ z_deflateInit2_ +# define deflateSetDictionary z_deflateSetDictionary +# define deflateCopy z_deflateCopy +# define deflateReset z_deflateReset +# define deflateParams z_deflateParams +# define deflateBound z_deflateBound +# define deflatePrime z_deflatePrime +# define inflateInit2_ z_inflateInit2_ +# define inflateSetDictionary z_inflateSetDictionary +# define inflateSync z_inflateSync +# define inflateSyncPoint z_inflateSyncPoint +# define inflateCopy z_inflateCopy +# define inflateReset z_inflateReset +# define inflateBack z_inflateBack +# define inflateBackEnd z_inflateBackEnd +# define compress z_compress +# define compress2 z_compress2 +# define compressBound z_compressBound +# define uncompress z_uncompress +# define adler32 z_adler32 +# define crc32 z_crc32 +# define get_crc_table z_get_crc_table +# define zError z_zError + +# define alloc_func z_alloc_func +# define free_func z_free_func +# define in_func z_in_func +# define out_func z_out_func +# define Byte z_Byte +# define uInt z_uInt +# define uLong z_uLong +# define Bytef z_Bytef +# define charf z_charf +# define intf z_intf +# define uIntf z_uIntf +# define uLongf z_uLongf +# define voidpf z_voidpf +# define voidp z_voidp +#endif + +#if defined(__MSDOS__) && !defined(MSDOS) +# define MSDOS +#endif +#if (defined(OS_2) || defined(__OS2__)) && !defined(OS2) +# define OS2 +#endif +#if defined(_WINDOWS) && !defined(WINDOWS) +# define WINDOWS +#endif +#if defined(_WIN32) || defined(_WIN32_WCE) || defined(__WIN32__) +# ifndef WIN32 +# define WIN32 +# endif +#endif +#if (defined(MSDOS) || defined(OS2) || defined(WINDOWS)) && !defined(WIN32) +# if !defined(__GNUC__) && !defined(__FLAT__) && !defined(__386__) +# ifndef SYS16BIT +# define SYS16BIT +# endif +# endif +#endif + +/* + * Compile with -DMAXSEG_64K if the alloc function cannot allocate more + * than 64k bytes at a time (needed on systems with 16-bit int). + */ +#ifdef SYS16BIT +# define MAXSEG_64K +#endif +#ifdef MSDOS +# define UNALIGNED_OK +#endif + +#ifdef __STDC_VERSION__ +# ifndef STDC +# define STDC +# endif +# if __STDC_VERSION__ >= 199901L +# ifndef STDC99 +# define STDC99 +# endif +# endif +#endif +#if !defined(STDC) && (defined(__STDC__) || defined(__cplusplus)) +# define STDC +#endif +#if !defined(STDC) && (defined(__GNUC__) || defined(__BORLANDC__)) +# define STDC +#endif +#if !defined(STDC) && (defined(MSDOS) || defined(WINDOWS) || defined(WIN32)) +# define STDC +#endif +#if !defined(STDC) && (defined(OS2) || defined(__HOS_AIX__)) +# define STDC +#endif + +#if defined(__OS400__) && !defined(STDC) /* iSeries (formerly AS/400). */ +# define STDC +#endif + +#ifndef STDC +# ifndef const /* cannot use !defined(STDC) && !defined(const) on Mac */ +# define const /* note: need a more gentle solution here */ +# endif +#endif + +/* Some Mac compilers merge all .h files incorrectly: */ +#if defined(__MWERKS__)||defined(applec)||defined(THINK_C)||defined(__SC__) +# define NO_DUMMY_DECL +#endif + +/* Maximum value for memLevel in deflateInit2 */ +#ifndef MAX_MEM_LEVEL +# ifdef MAXSEG_64K +# define MAX_MEM_LEVEL 8 +# else +# define MAX_MEM_LEVEL 9 +# endif +#endif + +/* Maximum value for windowBits in deflateInit2 and inflateInit2. + * WARNING: reducing MAX_WBITS makes minigzip unable to extract .gz files + * created by gzip. (Files created by minigzip can still be extracted by + * gzip.) + */ +#ifndef MAX_WBITS +# define MAX_WBITS 15 /* 32K LZ77 window */ +#endif + +/* The memory requirements for deflate are (in bytes): + (1 << (windowBits+2)) + (1 << (memLevel+9)) + that is: 128K for windowBits=15 + 128K for memLevel = 8 (default values) + plus a few kilobytes for small objects. For example, if you want to reduce + the default memory requirements from 256K to 128K, compile with + make CFLAGS="-O -DMAX_WBITS=14 -DMAX_MEM_LEVEL=7" + Of course this will generally degrade compression (there's no free lunch). + + The memory requirements for inflate are (in bytes) 1 << windowBits + that is, 32K for windowBits=15 (default value) plus a few kilobytes + for small objects. +*/ + + /* Type declarations */ + +#ifndef OF /* function prototypes */ +# ifdef STDC +# define OF(args) args +# else +# define OF(args) () +# endif +#endif + +/* The following definitions for FAR are needed only for MSDOS mixed + * model programming (small or medium model with some far allocations). + * This was tested only with MSC; for other MSDOS compilers you may have + * to define NO_MEMCPY in zutil.h. If you don't need the mixed model, + * just define FAR to be empty. + */ +#ifdef SYS16BIT +# if defined(M_I86SM) || defined(M_I86MM) + /* MSC small or medium model */ +# define SMALL_MEDIUM +# ifdef _MSC_VER +# define FAR _far +# else +# define FAR far +# endif +# endif +# if (defined(__SMALL__) || defined(__MEDIUM__)) + /* Turbo C small or medium model */ +# define SMALL_MEDIUM +# ifdef __BORLANDC__ +# define FAR _far +# else +# define FAR far +# endif +# endif +#endif + +#if defined(WINDOWS) || defined(WIN32) + /* If building or using zlib as a DLL, define ZLIB_DLL. + * This is not mandatory, but it offers a little performance increase. + */ +# ifdef ZLIB_DLL +# if defined(WIN32) && (!defined(__BORLANDC__) || (__BORLANDC__ >= 0x500)) +# ifdef ZLIB_INTERNAL +# define ZEXTERN extern __declspec(dllexport) +# else +# define ZEXTERN extern __declspec(dllimport) +# endif +# endif +# endif /* ZLIB_DLL */ + /* If building or using zlib with the WINAPI/WINAPIV calling convention, + * define ZLIB_WINAPI. + * Caution: the standard ZLIB1.DLL is NOT compiled using ZLIB_WINAPI. + */ +# ifdef ZLIB_WINAPI +# ifdef FAR +# undef FAR +# endif +# include + /* No need for _export, use ZLIB.DEF instead. */ + /* For complete Windows compatibility, use WINAPI, not __stdcall. */ +# define ZEXPORT WINAPI +# ifdef WIN32 +# define ZEXPORTVA WINAPIV +# else +# define ZEXPORTVA FAR CDECL +# endif +# endif +#endif + +#if defined (__BEOS__) +# ifdef ZLIB_DLL +# ifdef ZLIB_INTERNAL +# define ZEXPORT __declspec(dllexport) +# define ZEXPORTVA __declspec(dllexport) +# else +# define ZEXPORT __declspec(dllimport) +# define ZEXPORTVA __declspec(dllimport) +# endif +# endif +#endif + +#ifndef ZEXTERN +# define ZEXTERN extern +#endif +#ifndef ZEXPORT +# define ZEXPORT +#endif +#ifndef ZEXPORTVA +# define ZEXPORTVA +#endif + +#ifndef FAR +# define FAR +#endif + +#if !defined(__MACTYPES__) +typedef unsigned char Byte; /* 8 bits */ +#endif +typedef unsigned int uInt; /* 16 bits or more */ +typedef unsigned long uLong; /* 32 bits or more */ + +#ifdef SMALL_MEDIUM + /* Borland C/C++ and some old MSC versions ignore FAR inside typedef */ +# define Bytef Byte FAR +#else + typedef Byte FAR Bytef; +#endif +typedef char FAR charf; +typedef int FAR intf; +typedef uInt FAR uIntf; +typedef uLong FAR uLongf; + +#ifdef STDC + typedef void const *voidpc; + typedef void FAR *voidpf; + typedef void *voidp; +#else + typedef Byte const *voidpc; + typedef Byte FAR *voidpf; + typedef Byte *voidp; +#endif + +#if 0 /* HAVE_UNISTD_H -- this line is updated by ./configure */ +# include /* for off_t */ +# include /* for SEEK_* and off_t */ +# ifdef VMS +# include /* for off_t */ +# endif +# define z_off_t off_t +#endif +#ifndef SEEK_SET +# define SEEK_SET 0 /* Seek from beginning of file. */ +# define SEEK_CUR 1 /* Seek from current position. */ +# define SEEK_END 2 /* Set file pointer to EOF plus "offset" */ +#endif +#ifndef z_off_t +# define z_off_t long +#endif + +#if defined(__OS400__) +# define NO_vsnprintf +#endif + +#if defined(__MVS__) +# define NO_vsnprintf +# ifdef FAR +# undef FAR +# endif +#endif + +/* MVS linker does not support external names larger than 8 bytes */ +#if defined(__MVS__) +# pragma map(deflateInit_,"DEIN") +# pragma map(deflateInit2_,"DEIN2") +# pragma map(deflateEnd,"DEEND") +# pragma map(deflateBound,"DEBND") +# pragma map(inflateInit_,"ININ") +# pragma map(inflateInit2_,"ININ2") +# pragma map(inflateEnd,"INEND") +# pragma map(inflateSync,"INSY") +# pragma map(inflateSetDictionary,"INSEDI") +# pragma map(compressBound,"CMBND") +# pragma map(inflate_table,"INTABL") +# pragma map(inflate_fast,"INFA") +# pragma map(inflate_copyright,"INCOPY") +#endif + +#endif /* ZCONF_H */ diff --git a/prebuilt/zdll.lib b/prebuilt/zdll.lib new file mode 100644 index 0000000000000000000000000000000000000000..01f4e10e6ef05b1198d2e5b3410437611826e61a GIT binary patch literal 10590 zcmcgy&2L*b68||Ln?!Y6*G~NXAv;d2^uw_wImrTxz;&F(26a=zNrPUtiXy+r!YY;_ z$t@}r-FvYpiXM9^iaq7fbB;l7TlBDpoVq}fzo3VrXp0`&ogs&O#ycM||gK;H(?Uk3&r0;!)g z4gLr)4W~5CJO!9$ztc4OIlwgZSkq{lc%qTJnugvXInl`1n$CX&DEgjCqS^N}ogXBg z=ooVt@P2*Pqrs)+;$x(o5W=GTH52Qyl^+?m? zJn=+RcQj3W12E0>YMR(0o+$ZD=tC-rrpZQ3<6mkT`xaoDIHqZ=PCU`+Uo{PWM(;~B z{EN^hR1%&0N@$fzqS=I|IeH(aG2)q0G}crqm%H)i`db^fbF0^HUE8>^e*3MBT%%mN zw}@Qs=G$v)&CU3`YS*o||MK?DH*%}5-+rC+OAmH(>ft*b580JdO2yHUFSyRSfIttW7+a=u7LY?kQ$3ZzvHYNQd? z{FZdsL{+~{3~nCv?7zEh#K^gI-9xca*$K$KO=+Z?Ml2O0Cui9ae<(H_#17Dn?!?2l*<=xGAjb*8FBUsAkA9Y(>ip)dEYU&@$OeZfuw8IlHWd zYtl&jD$HsfSvB&U&oycAV~$EwFT8I2o*aqS+bvq=>lJbv+o2=Tnl!^a;9Na_ugv!K zS*_E!u8k~Z=lMa#<_DJmZ1e&3e&FE%@DoL&9|tKS4FgYSfbV93&ne129s<%--W>tn zq59WUesmuAp6ET&8Km;cS>OwzkExG$&j3$|Qqe<0bT zDPW$;J5+u%0rZl5i^^xD^C8hg(*Kg4`*sXCM)Eq9zn%s@qkey(cl?Ctt8>6AQG(v% z4q+uohodSbKIp*_yoNyxVHoo`j^~lUAFzOn=*0_o4yUk$SMf5&Z~?F29L{1IqnN@B zl9<38&SM-Y%wiHJFo4s@;3b?y8jI-1i|9iZBe;Y!IQD-+IZ7Vfai$3Vyl!O5~^D+I}tYR zRJ?boea|RFB8<01Ms~o+*6eTX#8JpFz3TX7xEA5X2^D^j@k3X1d-uaYiwNlN`_U-l z1wh#m%}Wa#muBekIEvo3EppdUMC3S%Nch3fQ5>Ol6p?xJFS3^NZ`zvku%;clIS(=W zu^!-FSrW@_MsC-lS#@55WP~O)(s59H+D1KT6|pv)Fv?_7H^y>UGLvv-B^lOBbj5no z7O`6-GPzORu99NIreY?CFzJEKSoAt5HuMTGsa%*;nbgRnQzktNlNXcTcsZTPVTujo z#WsPHxwbJ_`>aTrQPy*KKe%f{OS7wL3@uImQ({YLi`ddgqelYC6`OR`!QilI^laRe~N$wQxCf0h6;^%+;XXRuMC<3Zq&-`X^Z?`ymOS8Z-RsLZZ zDVNc=GGE%v*YmS!(?kIrbQH%k5ges*84Hg^kCPJhqvJ7tBYLJd{bw1;$498zN3olK zr|h6o80b4e67G$ECn(k*@9lrHRH#;JmHYK%YU4t3?Z(YFbaUh{T??ouxd+NMh)xg& zC}-fB1;H{ePI}Sfy?@2;zZ*2x=O4e>5t#8VQ{u#uEhAAQkrM)d&WAY7b+W zpzZ7x7OqXd$S#UoL@?+eMZV!+q#cmXM`D~WfG@@P?Nu}!{ zkgc^lM=_Rr-oeqytL;6sVVgKhdw5)uJnjVt$4`s*kC%<{y%!nJKfHuMo$sZ4jEvV( zpF#@n5JDJRYtw#l@s01J)SW8dOW2M(Uqx{k{Tf3b<_>}p6AuP72A>lTf)EoAP78uM zf_4exjKWYMFDex_N3zIXK-rwOnvex_fGQIf8VcQ%g*a|B8dt z%^d$#2j|do{20G`&BfzO_0YXL-htxQ(tHF*-`9s=w$|=iiaAYOWGv@PfbBaRT$?Cy j(Px2Cadler to the adler32 checksum of all input read + so far (that is, total_in bytes). + + deflate() may update strm->data_type if it can make a good guess about + the input data type (Z_BINARY or Z_TEXT). In doubt, the data is considered + binary. This field is only for information purposes and does not affect + the compression algorithm in any manner. + + deflate() returns Z_OK if some progress has been made (more input + processed or more output produced), Z_STREAM_END if all input has been + consumed and all output has been produced (only when flush is set to + Z_FINISH), Z_STREAM_ERROR if the stream state was inconsistent (for example + if next_in or next_out was NULL), Z_BUF_ERROR if no progress is possible + (for example avail_in or avail_out was zero). Note that Z_BUF_ERROR is not + fatal, and deflate() can be called again with more input and more output + space to continue compressing. +*/ + + +ZEXTERN int ZEXPORT deflateEnd OF((z_streamp strm)); +/* + All dynamically allocated data structures for this stream are freed. + This function discards any unprocessed input and does not flush any + pending output. + + deflateEnd returns Z_OK if success, Z_STREAM_ERROR if the + stream state was inconsistent, Z_DATA_ERROR if the stream was freed + prematurely (some input or output was discarded). In the error case, + msg may be set but then points to a static string (which must not be + deallocated). +*/ + + +/* +ZEXTERN int ZEXPORT inflateInit OF((z_streamp strm)); + + Initializes the internal stream state for decompression. The fields + next_in, avail_in, zalloc, zfree and opaque must be initialized before by + the caller. If next_in is not Z_NULL and avail_in is large enough (the exact + value depends on the compression method), inflateInit determines the + compression method from the zlib header and allocates all data structures + accordingly; otherwise the allocation will be deferred to the first call of + inflate. If zalloc and zfree are set to Z_NULL, inflateInit updates them to + use default allocation functions. + + inflateInit returns Z_OK if success, Z_MEM_ERROR if there was not enough + memory, Z_VERSION_ERROR if the zlib library version is incompatible with the + version assumed by the caller. msg is set to null if there is no error + message. inflateInit does not perform any decompression apart from reading + the zlib header if present: this will be done by inflate(). (So next_in and + avail_in may be modified, but next_out and avail_out are unchanged.) +*/ + + +ZEXTERN int ZEXPORT inflate OF((z_streamp strm, int flush)); +/* + inflate decompresses as much data as possible, and stops when the input + buffer becomes empty or the output buffer becomes full. It may introduce + some output latency (reading input without producing any output) except when + forced to flush. + + The detailed semantics are as follows. inflate performs one or both of the + following actions: + + - Decompress more input starting at next_in and update next_in and avail_in + accordingly. If not all input can be processed (because there is not + enough room in the output buffer), next_in is updated and processing + will resume at this point for the next call of inflate(). + + - Provide more output starting at next_out and update next_out and avail_out + accordingly. inflate() provides as much output as possible, until there + is no more input data or no more space in the output buffer (see below + about the flush parameter). + + Before the call of inflate(), the application should ensure that at least + one of the actions is possible, by providing more input and/or consuming + more output, and updating the next_* and avail_* values accordingly. + The application can consume the uncompressed output when it wants, for + example when the output buffer is full (avail_out == 0), or after each + call of inflate(). If inflate returns Z_OK and with zero avail_out, it + must be called again after making room in the output buffer because there + might be more output pending. + + The flush parameter of inflate() can be Z_NO_FLUSH, Z_SYNC_FLUSH, + Z_FINISH, or Z_BLOCK. Z_SYNC_FLUSH requests that inflate() flush as much + output as possible to the output buffer. Z_BLOCK requests that inflate() stop + if and when it gets to the next deflate block boundary. When decoding the + zlib or gzip format, this will cause inflate() to return immediately after + the header and before the first block. When doing a raw inflate, inflate() + will go ahead and process the first block, and will return when it gets to + the end of that block, or when it runs out of data. + + The Z_BLOCK option assists in appending to or combining deflate streams. + Also to assist in this, on return inflate() will set strm->data_type to the + number of unused bits in the last byte taken from strm->next_in, plus 64 + if inflate() is currently decoding the last block in the deflate stream, + plus 128 if inflate() returned immediately after decoding an end-of-block + code or decoding the complete header up to just before the first byte of the + deflate stream. The end-of-block will not be indicated until all of the + uncompressed data from that block has been written to strm->next_out. The + number of unused bits may in general be greater than seven, except when + bit 7 of data_type is set, in which case the number of unused bits will be + less than eight. + + inflate() should normally be called until it returns Z_STREAM_END or an + error. However if all decompression is to be performed in a single step + (a single call of inflate), the parameter flush should be set to + Z_FINISH. In this case all pending input is processed and all pending + output is flushed; avail_out must be large enough to hold all the + uncompressed data. (The size of the uncompressed data may have been saved + by the compressor for this purpose.) The next operation on this stream must + be inflateEnd to deallocate the decompression state. The use of Z_FINISH + is never required, but can be used to inform inflate that a faster approach + may be used for the single inflate() call. + + In this implementation, inflate() always flushes as much output as + possible to the output buffer, and always uses the faster approach on the + first call. So the only effect of the flush parameter in this implementation + is on the return value of inflate(), as noted below, or when it returns early + because Z_BLOCK is used. + + If a preset dictionary is needed after this call (see inflateSetDictionary + below), inflate sets strm->adler to the adler32 checksum of the dictionary + chosen by the compressor and returns Z_NEED_DICT; otherwise it sets + strm->adler to the adler32 checksum of all output produced so far (that is, + total_out bytes) and returns Z_OK, Z_STREAM_END or an error code as described + below. At the end of the stream, inflate() checks that its computed adler32 + checksum is equal to that saved by the compressor and returns Z_STREAM_END + only if the checksum is correct. + + inflate() will decompress and check either zlib-wrapped or gzip-wrapped + deflate data. The header type is detected automatically. Any information + contained in the gzip header is not retained, so applications that need that + information should instead use raw inflate, see inflateInit2() below, or + inflateBack() and perform their own processing of the gzip header and + trailer. + + inflate() returns Z_OK if some progress has been made (more input processed + or more output produced), Z_STREAM_END if the end of the compressed data has + been reached and all uncompressed output has been produced, Z_NEED_DICT if a + preset dictionary is needed at this point, Z_DATA_ERROR if the input data was + corrupted (input stream not conforming to the zlib format or incorrect check + value), Z_STREAM_ERROR if the stream structure was inconsistent (for example + if next_in or next_out was NULL), Z_MEM_ERROR if there was not enough memory, + Z_BUF_ERROR if no progress is possible or if there was not enough room in the + output buffer when Z_FINISH is used. Note that Z_BUF_ERROR is not fatal, and + inflate() can be called again with more input and more output space to + continue decompressing. If Z_DATA_ERROR is returned, the application may then + call inflateSync() to look for a good compression block if a partial recovery + of the data is desired. +*/ + + +ZEXTERN int ZEXPORT inflateEnd OF((z_streamp strm)); +/* + All dynamically allocated data structures for this stream are freed. + This function discards any unprocessed input and does not flush any + pending output. + + inflateEnd returns Z_OK if success, Z_STREAM_ERROR if the stream state + was inconsistent. In the error case, msg may be set but then points to a + static string (which must not be deallocated). +*/ + + /* Advanced functions */ + +/* + The following functions are needed only in some special applications. +*/ + +/* +ZEXTERN int ZEXPORT deflateInit2 OF((z_streamp strm, + int level, + int method, + int windowBits, + int memLevel, + int strategy)); + + This is another version of deflateInit with more compression options. The + fields next_in, zalloc, zfree and opaque must be initialized before by + the caller. + + The method parameter is the compression method. It must be Z_DEFLATED in + this version of the library. + + The windowBits parameter is the base two logarithm of the window size + (the size of the history buffer). It should be in the range 8..15 for this + version of the library. Larger values of this parameter result in better + compression at the expense of memory usage. The default value is 15 if + deflateInit is used instead. + + windowBits can also be -8..-15 for raw deflate. In this case, -windowBits + determines the window size. deflate() will then generate raw deflate data + with no zlib header or trailer, and will not compute an adler32 check value. + + windowBits can also be greater than 15 for optional gzip encoding. Add + 16 to windowBits to write a simple gzip header and trailer around the + compressed data instead of a zlib wrapper. The gzip header will have no + file name, no extra data, no comment, no modification time (set to zero), + no header crc, and the operating system will be set to 255 (unknown). If a + gzip stream is being written, strm->adler is a crc32 instead of an adler32. + + The memLevel parameter specifies how much memory should be allocated + for the internal compression state. memLevel=1 uses minimum memory but + is slow and reduces compression ratio; memLevel=9 uses maximum memory + for optimal speed. The default value is 8. See zconf.h for total memory + usage as a function of windowBits and memLevel. + + The strategy parameter is used to tune the compression algorithm. Use the + value Z_DEFAULT_STRATEGY for normal data, Z_FILTERED for data produced by a + filter (or predictor), Z_HUFFMAN_ONLY to force Huffman encoding only (no + string match), or Z_RLE to limit match distances to one (run-length + encoding). Filtered data consists mostly of small values with a somewhat + random distribution. In this case, the compression algorithm is tuned to + compress them better. The effect of Z_FILTERED is to force more Huffman + coding and less string matching; it is somewhat intermediate between + Z_DEFAULT and Z_HUFFMAN_ONLY. Z_RLE is designed to be almost as fast as + Z_HUFFMAN_ONLY, but give better compression for PNG image data. The strategy + parameter only affects the compression ratio but not the correctness of the + compressed output even if it is not set appropriately. Z_FIXED prevents the + use of dynamic Huffman codes, allowing for a simpler decoder for special + applications. + + deflateInit2 returns Z_OK if success, Z_MEM_ERROR if there was not enough + memory, Z_STREAM_ERROR if a parameter is invalid (such as an invalid + method). msg is set to null if there is no error message. deflateInit2 does + not perform any compression: this will be done by deflate(). +*/ + +ZEXTERN int ZEXPORT deflateSetDictionary OF((z_streamp strm, + const Bytef *dictionary, + uInt dictLength)); +/* + Initializes the compression dictionary from the given byte sequence + without producing any compressed output. This function must be called + immediately after deflateInit, deflateInit2 or deflateReset, before any + call of deflate. The compressor and decompressor must use exactly the same + dictionary (see inflateSetDictionary). + + The dictionary should consist of strings (byte sequences) that are likely + to be encountered later in the data to be compressed, with the most commonly + used strings preferably put towards the end of the dictionary. Using a + dictionary is most useful when the data to be compressed is short and can be + predicted with good accuracy; the data can then be compressed better than + with the default empty dictionary. + + Depending on the size of the compression data structures selected by + deflateInit or deflateInit2, a part of the dictionary may in effect be + discarded, for example if the dictionary is larger than the window size in + deflate or deflate2. Thus the strings most likely to be useful should be + put at the end of the dictionary, not at the front. In addition, the + current implementation of deflate will use at most the window size minus + 262 bytes of the provided dictionary. + + Upon return of this function, strm->adler is set to the adler32 value + of the dictionary; the decompressor may later use this value to determine + which dictionary has been used by the compressor. (The adler32 value + applies to the whole dictionary even if only a subset of the dictionary is + actually used by the compressor.) If a raw deflate was requested, then the + adler32 value is not computed and strm->adler is not set. + + deflateSetDictionary returns Z_OK if success, or Z_STREAM_ERROR if a + parameter is invalid (such as NULL dictionary) or the stream state is + inconsistent (for example if deflate has already been called for this stream + or if the compression method is bsort). deflateSetDictionary does not + perform any compression: this will be done by deflate(). +*/ + +ZEXTERN int ZEXPORT deflateCopy OF((z_streamp dest, + z_streamp source)); +/* + Sets the destination stream as a complete copy of the source stream. + + This function can be useful when several compression strategies will be + tried, for example when there are several ways of pre-processing the input + data with a filter. The streams that will be discarded should then be freed + by calling deflateEnd. Note that deflateCopy duplicates the internal + compression state which can be quite large, so this strategy is slow and + can consume lots of memory. + + deflateCopy returns Z_OK if success, Z_MEM_ERROR if there was not + enough memory, Z_STREAM_ERROR if the source stream state was inconsistent + (such as zalloc being NULL). msg is left unchanged in both source and + destination. +*/ + +ZEXTERN int ZEXPORT deflateReset OF((z_streamp strm)); +/* + This function is equivalent to deflateEnd followed by deflateInit, + but does not free and reallocate all the internal compression state. + The stream will keep the same compression level and any other attributes + that may have been set by deflateInit2. + + deflateReset returns Z_OK if success, or Z_STREAM_ERROR if the source + stream state was inconsistent (such as zalloc or state being NULL). +*/ + +ZEXTERN int ZEXPORT deflateParams OF((z_streamp strm, + int level, + int strategy)); +/* + Dynamically update the compression level and compression strategy. The + interpretation of level and strategy is as in deflateInit2. This can be + used to switch between compression and straight copy of the input data, or + to switch to a different kind of input data requiring a different + strategy. If the compression level is changed, the input available so far + is compressed with the old level (and may be flushed); the new level will + take effect only at the next call of deflate(). + + Before the call of deflateParams, the stream state must be set as for + a call of deflate(), since the currently available input may have to + be compressed and flushed. In particular, strm->avail_out must be non-zero. + + deflateParams returns Z_OK if success, Z_STREAM_ERROR if the source + stream state was inconsistent or if a parameter was invalid, Z_BUF_ERROR + if strm->avail_out was zero. +*/ + +ZEXTERN int ZEXPORT deflateTune OF((z_streamp strm, + int good_length, + int max_lazy, + int nice_length, + int max_chain)); +/* + Fine tune deflate's internal compression parameters. This should only be + used by someone who understands the algorithm used by zlib's deflate for + searching for the best matching string, and even then only by the most + fanatic optimizer trying to squeeze out the last compressed bit for their + specific input data. Read the deflate.c source code for the meaning of the + max_lazy, good_length, nice_length, and max_chain parameters. + + deflateTune() can be called after deflateInit() or deflateInit2(), and + returns Z_OK on success, or Z_STREAM_ERROR for an invalid deflate stream. + */ + +ZEXTERN uLong ZEXPORT deflateBound OF((z_streamp strm, + uLong sourceLen)); +/* + deflateBound() returns an upper bound on the compressed size after + deflation of sourceLen bytes. It must be called after deflateInit() + or deflateInit2(). This would be used to allocate an output buffer + for deflation in a single pass, and so would be called before deflate(). +*/ + +ZEXTERN int ZEXPORT deflatePrime OF((z_streamp strm, + int bits, + int value)); +/* + deflatePrime() inserts bits in the deflate output stream. The intent + is that this function is used to start off the deflate output with the + bits leftover from a previous deflate stream when appending to it. As such, + this function can only be used for raw deflate, and must be used before the + first deflate() call after a deflateInit2() or deflateReset(). bits must be + less than or equal to 16, and that many of the least significant bits of + value will be inserted in the output. + + deflatePrime returns Z_OK if success, or Z_STREAM_ERROR if the source + stream state was inconsistent. +*/ + +ZEXTERN int ZEXPORT deflateSetHeader OF((z_streamp strm, + gz_headerp head)); +/* + deflateSetHeader() provides gzip header information for when a gzip + stream is requested by deflateInit2(). deflateSetHeader() may be called + after deflateInit2() or deflateReset() and before the first call of + deflate(). The text, time, os, extra field, name, and comment information + in the provided gz_header structure are written to the gzip header (xflag is + ignored -- the extra flags are set according to the compression level). The + caller must assure that, if not Z_NULL, name and comment are terminated with + a zero byte, and that if extra is not Z_NULL, that extra_len bytes are + available there. If hcrc is true, a gzip header crc is included. Note that + the current versions of the command-line version of gzip (up through version + 1.3.x) do not support header crc's, and will report that it is a "multi-part + gzip file" and give up. + + If deflateSetHeader is not used, the default gzip header has text false, + the time set to zero, and os set to 255, with no extra, name, or comment + fields. The gzip header is returned to the default state by deflateReset(). + + deflateSetHeader returns Z_OK if success, or Z_STREAM_ERROR if the source + stream state was inconsistent. +*/ + +/* +ZEXTERN int ZEXPORT inflateInit2 OF((z_streamp strm, + int windowBits)); + + This is another version of inflateInit with an extra parameter. The + fields next_in, avail_in, zalloc, zfree and opaque must be initialized + before by the caller. + + The windowBits parameter is the base two logarithm of the maximum window + size (the size of the history buffer). It should be in the range 8..15 for + this version of the library. The default value is 15 if inflateInit is used + instead. windowBits must be greater than or equal to the windowBits value + provided to deflateInit2() while compressing, or it must be equal to 15 if + deflateInit2() was not used. If a compressed stream with a larger window + size is given as input, inflate() will return with the error code + Z_DATA_ERROR instead of trying to allocate a larger window. + + windowBits can also be -8..-15 for raw inflate. In this case, -windowBits + determines the window size. inflate() will then process raw deflate data, + not looking for a zlib or gzip header, not generating a check value, and not + looking for any check values for comparison at the end of the stream. This + is for use with other formats that use the deflate compressed data format + such as zip. Those formats provide their own check values. If a custom + format is developed using the raw deflate format for compressed data, it is + recommended that a check value such as an adler32 or a crc32 be applied to + the uncompressed data as is done in the zlib, gzip, and zip formats. For + most applications, the zlib format should be used as is. Note that comments + above on the use in deflateInit2() applies to the magnitude of windowBits. + + windowBits can also be greater than 15 for optional gzip decoding. Add + 32 to windowBits to enable zlib and gzip decoding with automatic header + detection, or add 16 to decode only the gzip format (the zlib format will + return a Z_DATA_ERROR). If a gzip stream is being decoded, strm->adler is + a crc32 instead of an adler32. + + inflateInit2 returns Z_OK if success, Z_MEM_ERROR if there was not enough + memory, Z_STREAM_ERROR if a parameter is invalid (such as a null strm). msg + is set to null if there is no error message. inflateInit2 does not perform + any decompression apart from reading the zlib header if present: this will + be done by inflate(). (So next_in and avail_in may be modified, but next_out + and avail_out are unchanged.) +*/ + +ZEXTERN int ZEXPORT inflateSetDictionary OF((z_streamp strm, + const Bytef *dictionary, + uInt dictLength)); +/* + Initializes the decompression dictionary from the given uncompressed byte + sequence. This function must be called immediately after a call of inflate, + if that call returned Z_NEED_DICT. The dictionary chosen by the compressor + can be determined from the adler32 value returned by that call of inflate. + The compressor and decompressor must use exactly the same dictionary (see + deflateSetDictionary). For raw inflate, this function can be called + immediately after inflateInit2() or inflateReset() and before any call of + inflate() to set the dictionary. The application must insure that the + dictionary that was used for compression is provided. + + inflateSetDictionary returns Z_OK if success, Z_STREAM_ERROR if a + parameter is invalid (such as NULL dictionary) or the stream state is + inconsistent, Z_DATA_ERROR if the given dictionary doesn't match the + expected one (incorrect adler32 value). inflateSetDictionary does not + perform any decompression: this will be done by subsequent calls of + inflate(). +*/ + +ZEXTERN int ZEXPORT inflateSync OF((z_streamp strm)); +/* + Skips invalid compressed data until a full flush point (see above the + description of deflate with Z_FULL_FLUSH) can be found, or until all + available input is skipped. No output is provided. + + inflateSync returns Z_OK if a full flush point has been found, Z_BUF_ERROR + if no more input was provided, Z_DATA_ERROR if no flush point has been found, + or Z_STREAM_ERROR if the stream structure was inconsistent. In the success + case, the application may save the current current value of total_in which + indicates where valid compressed data was found. In the error case, the + application may repeatedly call inflateSync, providing more input each time, + until success or end of the input data. +*/ + +ZEXTERN int ZEXPORT inflateCopy OF((z_streamp dest, + z_streamp source)); +/* + Sets the destination stream as a complete copy of the source stream. + + This function can be useful when randomly accessing a large stream. The + first pass through the stream can periodically record the inflate state, + allowing restarting inflate at those points when randomly accessing the + stream. + + inflateCopy returns Z_OK if success, Z_MEM_ERROR if there was not + enough memory, Z_STREAM_ERROR if the source stream state was inconsistent + (such as zalloc being NULL). msg is left unchanged in both source and + destination. +*/ + +ZEXTERN int ZEXPORT inflateReset OF((z_streamp strm)); +/* + This function is equivalent to inflateEnd followed by inflateInit, + but does not free and reallocate all the internal decompression state. + The stream will keep attributes that may have been set by inflateInit2. + + inflateReset returns Z_OK if success, or Z_STREAM_ERROR if the source + stream state was inconsistent (such as zalloc or state being NULL). +*/ + +ZEXTERN int ZEXPORT inflatePrime OF((z_streamp strm, + int bits, + int value)); +/* + This function inserts bits in the inflate input stream. The intent is + that this function is used to start inflating at a bit position in the + middle of a byte. The provided bits will be used before any bytes are used + from next_in. This function should only be used with raw inflate, and + should be used before the first inflate() call after inflateInit2() or + inflateReset(). bits must be less than or equal to 16, and that many of the + least significant bits of value will be inserted in the input. + + inflatePrime returns Z_OK if success, or Z_STREAM_ERROR if the source + stream state was inconsistent. +*/ + +ZEXTERN int ZEXPORT inflateGetHeader OF((z_streamp strm, + gz_headerp head)); +/* + inflateGetHeader() requests that gzip header information be stored in the + provided gz_header structure. inflateGetHeader() may be called after + inflateInit2() or inflateReset(), and before the first call of inflate(). + As inflate() processes the gzip stream, head->done is zero until the header + is completed, at which time head->done is set to one. If a zlib stream is + being decoded, then head->done is set to -1 to indicate that there will be + no gzip header information forthcoming. Note that Z_BLOCK can be used to + force inflate() to return immediately after header processing is complete + and before any actual data is decompressed. + + The text, time, xflags, and os fields are filled in with the gzip header + contents. hcrc is set to true if there is a header CRC. (The header CRC + was valid if done is set to one.) If extra is not Z_NULL, then extra_max + contains the maximum number of bytes to write to extra. Once done is true, + extra_len contains the actual extra field length, and extra contains the + extra field, or that field truncated if extra_max is less than extra_len. + If name is not Z_NULL, then up to name_max characters are written there, + terminated with a zero unless the length is greater than name_max. If + comment is not Z_NULL, then up to comm_max characters are written there, + terminated with a zero unless the length is greater than comm_max. When + any of extra, name, or comment are not Z_NULL and the respective field is + not present in the header, then that field is set to Z_NULL to signal its + absence. This allows the use of deflateSetHeader() with the returned + structure to duplicate the header. However if those fields are set to + allocated memory, then the application will need to save those pointers + elsewhere so that they can be eventually freed. + + If inflateGetHeader is not used, then the header information is simply + discarded. The header is always checked for validity, including the header + CRC if present. inflateReset() will reset the process to discard the header + information. The application would need to call inflateGetHeader() again to + retrieve the header from the next gzip stream. + + inflateGetHeader returns Z_OK if success, or Z_STREAM_ERROR if the source + stream state was inconsistent. +*/ + +/* +ZEXTERN int ZEXPORT inflateBackInit OF((z_streamp strm, int windowBits, + unsigned char FAR *window)); + + Initialize the internal stream state for decompression using inflateBack() + calls. The fields zalloc, zfree and opaque in strm must be initialized + before the call. If zalloc and zfree are Z_NULL, then the default library- + derived memory allocation routines are used. windowBits is the base two + logarithm of the window size, in the range 8..15. window is a caller + supplied buffer of that size. Except for special applications where it is + assured that deflate was used with small window sizes, windowBits must be 15 + and a 32K byte window must be supplied to be able to decompress general + deflate streams. + + See inflateBack() for the usage of these routines. + + inflateBackInit will return Z_OK on success, Z_STREAM_ERROR if any of + the paramaters are invalid, Z_MEM_ERROR if the internal state could not + be allocated, or Z_VERSION_ERROR if the version of the library does not + match the version of the header file. +*/ + +typedef unsigned (*in_func) OF((void FAR *, unsigned char FAR * FAR *)); +typedef int (*out_func) OF((void FAR *, unsigned char FAR *, unsigned)); + +ZEXTERN int ZEXPORT inflateBack OF((z_streamp strm, + in_func in, void FAR *in_desc, + out_func out, void FAR *out_desc)); +/* + inflateBack() does a raw inflate with a single call using a call-back + interface for input and output. This is more efficient than inflate() for + file i/o applications in that it avoids copying between the output and the + sliding window by simply making the window itself the output buffer. This + function trusts the application to not change the output buffer passed by + the output function, at least until inflateBack() returns. + + inflateBackInit() must be called first to allocate the internal state + and to initialize the state with the user-provided window buffer. + inflateBack() may then be used multiple times to inflate a complete, raw + deflate stream with each call. inflateBackEnd() is then called to free + the allocated state. + + A raw deflate stream is one with no zlib or gzip header or trailer. + This routine would normally be used in a utility that reads zip or gzip + files and writes out uncompressed files. The utility would decode the + header and process the trailer on its own, hence this routine expects + only the raw deflate stream to decompress. This is different from the + normal behavior of inflate(), which expects either a zlib or gzip header and + trailer around the deflate stream. + + inflateBack() uses two subroutines supplied by the caller that are then + called by inflateBack() for input and output. inflateBack() calls those + routines until it reads a complete deflate stream and writes out all of the + uncompressed data, or until it encounters an error. The function's + parameters and return types are defined above in the in_func and out_func + typedefs. inflateBack() will call in(in_desc, &buf) which should return the + number of bytes of provided input, and a pointer to that input in buf. If + there is no input available, in() must return zero--buf is ignored in that + case--and inflateBack() will return a buffer error. inflateBack() will call + out(out_desc, buf, len) to write the uncompressed data buf[0..len-1]. out() + should return zero on success, or non-zero on failure. If out() returns + non-zero, inflateBack() will return with an error. Neither in() nor out() + are permitted to change the contents of the window provided to + inflateBackInit(), which is also the buffer that out() uses to write from. + The length written by out() will be at most the window size. Any non-zero + amount of input may be provided by in(). + + For convenience, inflateBack() can be provided input on the first call by + setting strm->next_in and strm->avail_in. If that input is exhausted, then + in() will be called. Therefore strm->next_in must be initialized before + calling inflateBack(). If strm->next_in is Z_NULL, then in() will be called + immediately for input. If strm->next_in is not Z_NULL, then strm->avail_in + must also be initialized, and then if strm->avail_in is not zero, input will + initially be taken from strm->next_in[0 .. strm->avail_in - 1]. + + The in_desc and out_desc parameters of inflateBack() is passed as the + first parameter of in() and out() respectively when they are called. These + descriptors can be optionally used to pass any information that the caller- + supplied in() and out() functions need to do their job. + + On return, inflateBack() will set strm->next_in and strm->avail_in to + pass back any unused input that was provided by the last in() call. The + return values of inflateBack() can be Z_STREAM_END on success, Z_BUF_ERROR + if in() or out() returned an error, Z_DATA_ERROR if there was a format + error in the deflate stream (in which case strm->msg is set to indicate the + nature of the error), or Z_STREAM_ERROR if the stream was not properly + initialized. In the case of Z_BUF_ERROR, an input or output error can be + distinguished using strm->next_in which will be Z_NULL only if in() returned + an error. If strm->next is not Z_NULL, then the Z_BUF_ERROR was due to + out() returning non-zero. (in() will always be called before out(), so + strm->next_in is assured to be defined if out() returns non-zero.) Note + that inflateBack() cannot return Z_OK. +*/ + +ZEXTERN int ZEXPORT inflateBackEnd OF((z_streamp strm)); +/* + All memory allocated by inflateBackInit() is freed. + + inflateBackEnd() returns Z_OK on success, or Z_STREAM_ERROR if the stream + state was inconsistent. +*/ + +ZEXTERN uLong ZEXPORT zlibCompileFlags OF((void)); +/* Return flags indicating compile-time options. + + Type sizes, two bits each, 00 = 16 bits, 01 = 32, 10 = 64, 11 = other: + 1.0: size of uInt + 3.2: size of uLong + 5.4: size of voidpf (pointer) + 7.6: size of z_off_t + + Compiler, assembler, and debug options: + 8: DEBUG + 9: ASMV or ASMINF -- use ASM code + 10: ZLIB_WINAPI -- exported functions use the WINAPI calling convention + 11: 0 (reserved) + + One-time table building (smaller code, but not thread-safe if true): + 12: BUILDFIXED -- build static block decoding tables when needed + 13: DYNAMIC_CRC_TABLE -- build CRC calculation tables when needed + 14,15: 0 (reserved) + + Library content (indicates missing functionality): + 16: NO_GZCOMPRESS -- gz* functions cannot compress (to avoid linking + deflate code when not needed) + 17: NO_GZIP -- deflate can't write gzip streams, and inflate can't detect + and decode gzip streams (to avoid linking crc code) + 18-19: 0 (reserved) + + Operation variations (changes in library functionality): + 20: PKZIP_BUG_WORKAROUND -- slightly more permissive inflate + 21: FASTEST -- deflate algorithm with only one, lowest compression level + 22,23: 0 (reserved) + + The sprintf variant used by gzprintf (zero is best): + 24: 0 = vs*, 1 = s* -- 1 means limited to 20 arguments after the format + 25: 0 = *nprintf, 1 = *printf -- 1 means gzprintf() not secure! + 26: 0 = returns value, 1 = void -- 1 means inferred string length returned + + Remainder: + 27-31: 0 (reserved) + */ + + + /* utility functions */ + +/* + The following utility functions are implemented on top of the + basic stream-oriented functions. To simplify the interface, some + default options are assumed (compression level and memory usage, + standard memory allocation functions). The source code of these + utility functions can easily be modified if you need special options. +*/ + +ZEXTERN int ZEXPORT compress OF((Bytef *dest, uLongf *destLen, + const Bytef *source, uLong sourceLen)); +/* + Compresses the source buffer into the destination buffer. sourceLen is + the byte length of the source buffer. Upon entry, destLen is the total + size of the destination buffer, which must be at least the value returned + by compressBound(sourceLen). Upon exit, destLen is the actual size of the + compressed buffer. + This function can be used to compress a whole file at once if the + input file is mmap'ed. + compress returns Z_OK if success, Z_MEM_ERROR if there was not + enough memory, Z_BUF_ERROR if there was not enough room in the output + buffer. +*/ + +ZEXTERN int ZEXPORT compress2 OF((Bytef *dest, uLongf *destLen, + const Bytef *source, uLong sourceLen, + int level)); +/* + Compresses the source buffer into the destination buffer. The level + parameter has the same meaning as in deflateInit. sourceLen is the byte + length of the source buffer. Upon entry, destLen is the total size of the + destination buffer, which must be at least the value returned by + compressBound(sourceLen). Upon exit, destLen is the actual size of the + compressed buffer. + + compress2 returns Z_OK if success, Z_MEM_ERROR if there was not enough + memory, Z_BUF_ERROR if there was not enough room in the output buffer, + Z_STREAM_ERROR if the level parameter is invalid. +*/ + +ZEXTERN uLong ZEXPORT compressBound OF((uLong sourceLen)); +/* + compressBound() returns an upper bound on the compressed size after + compress() or compress2() on sourceLen bytes. It would be used before + a compress() or compress2() call to allocate the destination buffer. +*/ + +ZEXTERN int ZEXPORT uncompress OF((Bytef *dest, uLongf *destLen, + const Bytef *source, uLong sourceLen)); +/* + Decompresses the source buffer into the destination buffer. sourceLen is + the byte length of the source buffer. Upon entry, destLen is the total + size of the destination buffer, which must be large enough to hold the + entire uncompressed data. (The size of the uncompressed data must have + been saved previously by the compressor and transmitted to the decompressor + by some mechanism outside the scope of this compression library.) + Upon exit, destLen is the actual size of the compressed buffer. + This function can be used to decompress a whole file at once if the + input file is mmap'ed. + + uncompress returns Z_OK if success, Z_MEM_ERROR if there was not + enough memory, Z_BUF_ERROR if there was not enough room in the output + buffer, or Z_DATA_ERROR if the input data was corrupted or incomplete. +*/ + + +typedef voidp gzFile; + +ZEXTERN gzFile ZEXPORT gzopen OF((const char *path, const char *mode)); +/* + Opens a gzip (.gz) file for reading or writing. The mode parameter + is as in fopen ("rb" or "wb") but can also include a compression level + ("wb9") or a strategy: 'f' for filtered data as in "wb6f", 'h' for + Huffman only compression as in "wb1h", or 'R' for run-length encoding + as in "wb1R". (See the description of deflateInit2 for more information + about the strategy parameter.) + + gzopen can be used to read a file which is not in gzip format; in this + case gzread will directly read from the file without decompression. + + gzopen returns NULL if the file could not be opened or if there was + insufficient memory to allocate the (de)compression state; errno + can be checked to distinguish the two cases (if errno is zero, the + zlib error is Z_MEM_ERROR). */ + +ZEXTERN gzFile ZEXPORT gzdopen OF((int fd, const char *mode)); +/* + gzdopen() associates a gzFile with the file descriptor fd. File + descriptors are obtained from calls like open, dup, creat, pipe or + fileno (in the file has been previously opened with fopen). + The mode parameter is as in gzopen. + The next call of gzclose on the returned gzFile will also close the + file descriptor fd, just like fclose(fdopen(fd), mode) closes the file + descriptor fd. If you want to keep fd open, use gzdopen(dup(fd), mode). + gzdopen returns NULL if there was insufficient memory to allocate + the (de)compression state. +*/ + +ZEXTERN int ZEXPORT gzsetparams OF((gzFile file, int level, int strategy)); +/* + Dynamically update the compression level or strategy. See the description + of deflateInit2 for the meaning of these parameters. + gzsetparams returns Z_OK if success, or Z_STREAM_ERROR if the file was not + opened for writing. +*/ + +ZEXTERN int ZEXPORT gzread OF((gzFile file, voidp buf, unsigned len)); +/* + Reads the given number of uncompressed bytes from the compressed file. + If the input file was not in gzip format, gzread copies the given number + of bytes into the buffer. + gzread returns the number of uncompressed bytes actually read (0 for + end of file, -1 for error). */ + +ZEXTERN int ZEXPORT gzwrite OF((gzFile file, + voidpc buf, unsigned len)); +/* + Writes the given number of uncompressed bytes into the compressed file. + gzwrite returns the number of uncompressed bytes actually written + (0 in case of error). +*/ + +ZEXTERN int ZEXPORTVA gzprintf OF((gzFile file, const char *format, ...)); +/* + Converts, formats, and writes the args to the compressed file under + control of the format string, as in fprintf. gzprintf returns the number of + uncompressed bytes actually written (0 in case of error). The number of + uncompressed bytes written is limited to 4095. The caller should assure that + this limit is not exceeded. If it is exceeded, then gzprintf() will return + return an error (0) with nothing written. In this case, there may also be a + buffer overflow with unpredictable consequences, which is possible only if + zlib was compiled with the insecure functions sprintf() or vsprintf() + because the secure snprintf() or vsnprintf() functions were not available. +*/ + +ZEXTERN int ZEXPORT gzputs OF((gzFile file, const char *s)); +/* + Writes the given null-terminated string to the compressed file, excluding + the terminating null character. + gzputs returns the number of characters written, or -1 in case of error. +*/ + +ZEXTERN char * ZEXPORT gzgets OF((gzFile file, char *buf, int len)); +/* + Reads bytes from the compressed file until len-1 characters are read, or + a newline character is read and transferred to buf, or an end-of-file + condition is encountered. The string is then terminated with a null + character. + gzgets returns buf, or Z_NULL in case of error. +*/ + +ZEXTERN int ZEXPORT gzputc OF((gzFile file, int c)); +/* + Writes c, converted to an unsigned char, into the compressed file. + gzputc returns the value that was written, or -1 in case of error. +*/ + +ZEXTERN int ZEXPORT gzgetc OF((gzFile file)); +/* + Reads one byte from the compressed file. gzgetc returns this byte + or -1 in case of end of file or error. +*/ + +ZEXTERN int ZEXPORT gzungetc OF((int c, gzFile file)); +/* + Push one character back onto the stream to be read again later. + Only one character of push-back is allowed. gzungetc() returns the + character pushed, or -1 on failure. gzungetc() will fail if a + character has been pushed but not read yet, or if c is -1. The pushed + character will be discarded if the stream is repositioned with gzseek() + or gzrewind(). +*/ + +ZEXTERN int ZEXPORT gzflush OF((gzFile file, int flush)); +/* + Flushes all pending output into the compressed file. The parameter + flush is as in the deflate() function. The return value is the zlib + error number (see function gzerror below). gzflush returns Z_OK if + the flush parameter is Z_FINISH and all output could be flushed. + gzflush should be called only when strictly necessary because it can + degrade compression. +*/ + +ZEXTERN z_off_t ZEXPORT gzseek OF((gzFile file, + z_off_t offset, int whence)); +/* + Sets the starting position for the next gzread or gzwrite on the + given compressed file. The offset represents a number of bytes in the + uncompressed data stream. The whence parameter is defined as in lseek(2); + the value SEEK_END is not supported. + If the file is opened for reading, this function is emulated but can be + extremely slow. If the file is opened for writing, only forward seeks are + supported; gzseek then compresses a sequence of zeroes up to the new + starting position. + + gzseek returns the resulting offset location as measured in bytes from + the beginning of the uncompressed stream, or -1 in case of error, in + particular if the file is opened for writing and the new starting position + would be before the current position. +*/ + +ZEXTERN int ZEXPORT gzrewind OF((gzFile file)); +/* + Rewinds the given file. This function is supported only for reading. + + gzrewind(file) is equivalent to (int)gzseek(file, 0L, SEEK_SET) +*/ + +ZEXTERN z_off_t ZEXPORT gztell OF((gzFile file)); +/* + Returns the starting position for the next gzread or gzwrite on the + given compressed file. This position represents a number of bytes in the + uncompressed data stream. + + gztell(file) is equivalent to gzseek(file, 0L, SEEK_CUR) +*/ + +ZEXTERN int ZEXPORT gzeof OF((gzFile file)); +/* + Returns 1 when EOF has previously been detected reading the given + input stream, otherwise zero. +*/ + +ZEXTERN int ZEXPORT gzdirect OF((gzFile file)); +/* + Returns 1 if file is being read directly without decompression, otherwise + zero. +*/ + +ZEXTERN int ZEXPORT gzclose OF((gzFile file)); +/* + Flushes all pending output if necessary, closes the compressed file + and deallocates all the (de)compression state. The return value is the zlib + error number (see function gzerror below). +*/ + +ZEXTERN const char * ZEXPORT gzerror OF((gzFile file, int *errnum)); +/* + Returns the error message for the last error which occurred on the + given compressed file. errnum is set to zlib error number. If an + error occurred in the file system and not in the compression library, + errnum is set to Z_ERRNO and the application may consult errno + to get the exact error code. +*/ + +ZEXTERN void ZEXPORT gzclearerr OF((gzFile file)); +/* + Clears the error and end-of-file flags for file. This is analogous to the + clearerr() function in stdio. This is useful for continuing to read a gzip + file that is being written concurrently. +*/ + + /* checksum functions */ + +/* + These functions are not related to compression but are exported + anyway because they might be useful in applications using the + compression library. +*/ + +ZEXTERN uLong ZEXPORT adler32 OF((uLong adler, const Bytef *buf, uInt len)); +/* + Update a running Adler-32 checksum with the bytes buf[0..len-1] and + return the updated checksum. If buf is NULL, this function returns + the required initial value for the checksum. + An Adler-32 checksum is almost as reliable as a CRC32 but can be computed + much faster. Usage example: + + uLong adler = adler32(0L, Z_NULL, 0); + + while (read_buffer(buffer, length) != EOF) { + adler = adler32(adler, buffer, length); + } + if (adler != original_adler) error(); +*/ + +ZEXTERN uLong ZEXPORT adler32_combine OF((uLong adler1, uLong adler2, + z_off_t len2)); +/* + Combine two Adler-32 checksums into one. For two sequences of bytes, seq1 + and seq2 with lengths len1 and len2, Adler-32 checksums were calculated for + each, adler1 and adler2. adler32_combine() returns the Adler-32 checksum of + seq1 and seq2 concatenated, requiring only adler1, adler2, and len2. +*/ + +ZEXTERN uLong ZEXPORT crc32 OF((uLong crc, const Bytef *buf, uInt len)); +/* + Update a running CRC-32 with the bytes buf[0..len-1] and return the + updated CRC-32. If buf is NULL, this function returns the required initial + value for the for the crc. Pre- and post-conditioning (one's complement) is + performed within this function so it shouldn't be done by the application. + Usage example: + + uLong crc = crc32(0L, Z_NULL, 0); + + while (read_buffer(buffer, length) != EOF) { + crc = crc32(crc, buffer, length); + } + if (crc != original_crc) error(); +*/ + +ZEXTERN uLong ZEXPORT crc32_combine OF((uLong crc1, uLong crc2, z_off_t len2)); + +/* + Combine two CRC-32 check values into one. For two sequences of bytes, + seq1 and seq2 with lengths len1 and len2, CRC-32 check values were + calculated for each, crc1 and crc2. crc32_combine() returns the CRC-32 + check value of seq1 and seq2 concatenated, requiring only crc1, crc2, and + len2. +*/ + + + /* various hacks, don't look :) */ + +/* deflateInit and inflateInit are macros to allow checking the zlib version + * and the compiler's view of z_stream: + */ +ZEXTERN int ZEXPORT deflateInit_ OF((z_streamp strm, int level, + const char *version, int stream_size)); +ZEXTERN int ZEXPORT inflateInit_ OF((z_streamp strm, + const char *version, int stream_size)); +ZEXTERN int ZEXPORT deflateInit2_ OF((z_streamp strm, int level, int method, + int windowBits, int memLevel, + int strategy, const char *version, + int stream_size)); +ZEXTERN int ZEXPORT inflateInit2_ OF((z_streamp strm, int windowBits, + const char *version, int stream_size)); +ZEXTERN int ZEXPORT inflateBackInit_ OF((z_streamp strm, int windowBits, + unsigned char FAR *window, + const char *version, + int stream_size)); +#define deflateInit(strm, level) \ + deflateInit_((strm), (level), ZLIB_VERSION, sizeof(z_stream)) +#define inflateInit(strm) \ + inflateInit_((strm), ZLIB_VERSION, sizeof(z_stream)) +#define deflateInit2(strm, level, method, windowBits, memLevel, strategy) \ + deflateInit2_((strm),(level),(method),(windowBits),(memLevel),\ + (strategy), ZLIB_VERSION, sizeof(z_stream)) +#define inflateInit2(strm, windowBits) \ + inflateInit2_((strm), (windowBits), ZLIB_VERSION, sizeof(z_stream)) +#define inflateBackInit(strm, windowBits, window) \ + inflateBackInit_((strm), (windowBits), (window), \ + ZLIB_VERSION, sizeof(z_stream)) + + +#if !defined(ZUTIL_H) && !defined(NO_DUMMY_DECL) + struct internal_state {int dummy;}; /* hack for buggy compilers */ +#endif + +ZEXTERN const char * ZEXPORT zError OF((int)); +ZEXTERN int ZEXPORT inflateSyncPoint OF((z_streamp z)); +ZEXTERN const uLongf * ZEXPORT get_crc_table OF((void)); + +#ifdef __cplusplus +} +#endif + +#endif /* ZLIB_H */ diff --git a/prebuilt/zlib1.dll b/prebuilt/zlib1.dll new file mode 100644 index 0000000000000000000000000000000000000000..1cf8a476e5a1c6df7632d973b02bb5199f7c4698 GIT binary patch literal 59904 zcmeFa3wRS%-akIggf_H+2^!q0QQ{J{YpUBd-Q7aUZc0ljw3L*#X%%QeAr`ENZ2~Az zDAQo4hq$<+>%KqNy+5z(`g?U(mlm&03#1ek3ahBNF6v$qA_%fVK_S1-_spbC0o}{H z@Bi{VzdlboXU?2C=X<{Adq0<%b?at96a+z!Upy`dUAWSpi~s*S|55~D?1bmX3eSyx z?V7HXve&MuYFN^2UEXx}9Zd`GvfjRM*|NKR*4q|aoBYeHOO{!SW>;G8x_i;$o5qYu zw`o<6TIqeM;4;I}#Bbv0D~}fP`_V^haJ^}?@#s{3)g3LuwI@CGs0-JkEABj6#;;JyWqutNp*EvgWZ=g2L?~!|&Ae5zy65hG$y{U<3hlH`$WTd2}3tNyg zN6jt%DXu2`p5gZ&qa4B?|H}bVaC85sf`G^P!%Otu&{e%Xc3cqVquionf{@Q2 zrwEVIK%D<)O%a|^D?0zlm)`@<+Ak?_lW*}VAD(~r`}jxjQD28Tfi(WCg0SGGrbP>V z3k6|TF)~o6P=ep1_$B?hKrT0_#RRHT5P$=M`4FXt|6GF5eN$6&)9uLOeKo1L5i=|PZ7l5A_cB#;>SSI0T-JlSGD5wrpFJ~P5Tp(L zsHN-aUoERU^4G?hq!mGO9-g&g~~@tz!K;a50j`W^dD zotnkYNP%J$m>)POwe_M$z>2itK_pmjD5C#=wF7d@l<0#GAf7i=Bf7*#iBXqMvNJ4P zml*X|NB+ql<$D(~h0cHeTNzl->jINPCK@P~~6sN1hoC93GRm8{~5-zN>>} zHbX_^-EmY-A2`|d89L;&qSC`Y37kB&*LNMK=aV7PXL0Q780XmA`T_VtV3voe%234M=q)Js|4}|?t*((a zTnYKg&V@c6&qeUgd)nJ_2o%}P^Z{PM4)zX4>AVVdsDL^25Azy&@ydS>1gc;p6vH5} zcN{w|06_y_2g~Ll;C;sqM=#rp1V=>z0n~XQ#$o1&VQ(cN;_|FX-`P|B5~&cW%>p`K zpZ5(Za48b<8k;Ro3K)>AVQXwg=NZ4DzUv$b*WO7rR;4D>pB|oW?S4=&DK%M>YHa?r z@O0ZD+=2~F^69wW2!UcsMa`5!!pY)%XKkwE$f<*|G4f|pi{T~IEkG;+4y!zvB5nFU z;E)GLOW#u{vBs8ZkOD1~{LCP2^7DiweXkJ-XTP**5oMf4X;M(A!{+RfHqEBYLA~_7 zVoH2w@Q-={l1pIIvpfymmkGks6nDJFN|MYNI4QMVhEmE(i?S?}?dDk6|7WEx2e8_> zS`E&ME9JSt(zN}DlybKm(k;~~#bqT*aaGr^pM3JkQ|}KPR=yK1wx9(;S(dGMYFSNI z^&GUI6z8$xY&JV-89!Cr{p<6WFH|8+>l&aRv7IW-w0?E>i>DzVEPsfx|(wJE$)aL%?;W^2jf!GKl;d zW#gkrDi5>~fRD7}|IT8YAu@-Mvt3^|w|;(axLE%mG|1uUnZ{#+DUxbTJnQu|_X%;z<#V9Bvf4ugVqqUkZGS}rtky!>_gzZ*Y?*}Ry_7D^luzrVw(UsEr&Ijt z@@oAJQs8&$qrfiTc;?Ef@wg*zo`ixg14V2%Dg`-mT$DB_RH!g})ESVUb8tl}i`dVK zr`YN|_8J=qMRg~B$@=B@#S_0^m3geRR=KlLxpSVhRi_k|>^~%JotfeYO)RX!onv3n zf0nH-K_OO{Cv7b<>DZmQ`~S*Hs~q8pAy!(1LSCjXU}q+tWp}!~O3_+CUcw5Cyo$MG zs`Jeiq58V{j_$d=xNvTmJIqm_L4A!Q?hO09%x!TV@YPRRW%b=UX_d`at*kODs|?Dj z%&;q)$eztKC^JoLW~OtGa-Vtas8eq%Gc!G`DU-c^>g_9c$?tEIL-!~%^^RTgIi2)y zNGZut?#qf?K9BS>MySrwUEj+As2>7=+6O({#7Zo{=k;s=Xf!AtFA_1hEYD-ih5a8O z&p319fLGbvg?y+=UJ0)Q6r*=^2lmydwiCP05{OG} zq~y=QfSZtDjTq}A(x&y4c~-w}HTxbBohO((H)x!}jKQDmg=YPW&iMc(e3t+L05W4~ z%A80Qr$)o1AJYrIZ!7#Nbw2VRD5(5^DwH}Oc$98KB!N~gA+bSjRRw)@-B^L3FvdWlJVPdsrw#q~q?Z9N%1ugn*7HQ{Qs5O_CV4WYfDMW8v>arjQZp+z?9>}3s416HY{ng9 zb(CMI;L?kG18Xs}V$1YEx6)#k0zcqy;Q zpkY>GW3}19=a$epaIgtHvZeRj?uqZ{cB?{>?&J>*beni6f2h~(mh%MZ;ci_YD>k$n z(|ETmtc9bXV}_pfwj1+!-z?O(jv1p^7*8n^oog{9`e^BxkxH3_R(hQ}+A$-Ive3JK zwySsrwRkijj0A*98wo7$m_^$78nTK$LL=r6H>4jo5K>;J)V_{RXA)GLjh6*+- z$6XPb^*6Hfjmor~aIqCQ6o5-ah=eGGNmzLf+erK@M-8F(jjbU8r0VQhYg0Rd!MQ>n zOkJDCHE7J_IEXvx;a>3_)*I6~xmNsLuT(6_e3T^ z#i_+EmpW(N%yveur<_?is&gK>9CsjB5}k63ewq|GNU#-y?@Soyww-hbt}nJ?C}1VR z+IRLeuL}Och6hA%&SIng+tm5{vIB9S#Ti_+M=PgIxEcnvBS8Z0g%zj8B*Z49C&mRdPHjb5#3?PHPFx>yj?-fSe#+CjWBQH z0NPkJ+{TVb#&CZIeKuAOD+pb(NLwxGAQa%t&~7mhQw0u!PyIiRje3DFJwzOTZU0U9tB0BZ{AmFG4i)%2{53;8FQ%a`H_F{Q3}YHdqx&=mg?Ws@5}mVuWp>A` z9AZ517=%EA@j7OG134s?R1vF@Iw}JrSWwNqRLwLKTzgx%I1A=nynZfBiTS-;*!v}z z5Sh**n|a+R7}5T53AxW^>HXu$Mnmb50w4bN)vVly%U7UPGKpS(iSBT1A+`Mnp&xd< z%Ct2>v&$1VKTHMyMG^*eJ~Sh89FTUQ0jnlkU=Vg<8Be76Z6UdCFAf@P!6am&!red5=uZTKX5zj{#Xz0`IN zrX+b7&}L*+vf|7@_qud+j|&u1WI8E8_BU|XQJq7iYBoS~BQaIMrKh9%85{|Iz-*`EGkI&Q^ zr}6Y@4r5&}a~VK@8DQZocidyc$dL>wD56Z*3+_2uCM256#WTyv6H-f(>d3pu>z=F% z?L9{~T7?F!*Ico>68X?s)v!&rh^nbWChE|GATAh3Mi{JZ0ZJEJU|zsomD*O|wz6j1 zSQ`4X++~D!F)fSy#%e2ji~L4-H{@NCccaX=Y#@lNXxM7kg%65YX&GBxbm0R7D>bs!hVv@A&%ofOG#l)D z#MhNllL|C0?6 zZ>l4YQ|M&7o>)%xGcTY(SPX1t*}x%XYCfA*yN2CY1Rm#B z^OI}?8FWFtD|ARFb+&T{qGLU>aKjyl4sum+!ySkY)qz;g9f+xGx)9iLXuGUlZ)ofnKBl88#giDU;nPQeY#}5bua7bhuMGZb@+!Oy2LWO-h#~rCmws z+@y3?QreP~HYKI?&T!LX_V6eI0n&C26o$_kQkE@Wkx@UNVJ?C9yM8{V5I!=n%IsZp9Zo7JHZ- z8i>gM0I)_)toE_eJn@`(8u{$F^O)kZ8CRr&gN^JMrbWnbzmrX~*k4!h+?9#wlK)aR z3regSSEk1_N?I~rB~ zLgp-kcuee5{DLwItm6~=*lDOErPSAC1oH4ITc6ozPxr(!Ug$1stV zP`3i#C#hUFA968S{#KCNZDfVUYPtKfDP&xFYAB{gsb)QiD^}=3*H)S$1*F3YeIyx{ zkZ3Ax1Ux99021K8vN2r9XA6V+6raJlSK9Ch5NY2V*rinZl)}a}X^5PN0~nEg_Rt(h zob8U>J)c*@(4%>((+Zmx{5s^i&V=i#I<6x{&q&9$A>p_lRvIpFT;aC54T{@Dc;NOl znGURS9xDXY+4i2+DB;N&k~UllOC_)mU}Drc;Z$%S`whh$QF=)^Dm{g*@~HjNMtqnsfIb0zRd!W#e=XihWJop+&zYe!?md z`_ebD!d#-|O4r0bsWW>ff4#^Hj8prFh!vpkAay=kI})R{@>a&U=XP4>`?h>q^nLrM zBq{^l)s?>VNa|8^H6hZ!NKK8$YyL}#YsXfz6Oo_84kxS? zE?|WV&=K~~PoQn24bzC!pC$d%(dPviUVGv4^-&R!dl*+p>{%*E(U=z~Fw}S)U>ZfN-tDkFvZ45oqeHbnzXewa#%2-v`qSq zJ;WonjOv9ra&*sh_Opl9gMeXfv10(#KIrUGAgK1NT!!v6Eh*vf16O&7K)S$nY%d~^ z={iUVEdt3_=OzasC!SG<;XYQ$hoP`!Vn1^DNR;wnpdmm@B{UqU-T#y51H?hZ{lxhg z)Vv^!$oh!wQn4e?fP`} zwm{mtSK$u`ZCu@}7bbIj@R4!sDuImI5@N^WrM`G!Bbi*Yq3*%jBbXS?R@i$uCkwZr z!7t-vij@76;s(rVIzLvHh?EU+b0;`Q4e5fL&*x&AAlda2XhU%B0I*5i?8br*4zI?| z){m08dE6w(g%Rw`Gq(K0{Cp32Di`qcFJPe%Kkxh>nWFWzH`q-7=MiS0>(eHIKlV>3ogp|Phu`0iSb9RoFKnI zq9w%#;UL04t!%VcE^j?Ut7=w=fRq)k!UJi;21tfUg$tzn@8#MMc1)uxG}sG7;6b0^ zY)9!TN4F+57C~y1Ky1`#VuQoJBQ8=L-ZkNsM^uD*rEm zx~cFqz#`$o1~e`}MDRfe6ZS}L%P+#N30|$L=EJWP&SN4*Ct@ReDT5Vo?f7lHIK)qd z4MeVIsc)#ZFt4LX7f*;4P0-9Fq9Nx&#Wh7)H3{84J=n$_3RPr#<0!~wj7&0j2F!v0 z<28SwQ2BM0+VB^e2mfxNf=-tDrYg8xIs=v<42EcgEHlXBLSI1%Fiqo((Nqe3(J@?S z14BM2R#FQcMTWRWrO9Gg)nr`eLJY@w?;(ZHlfT+1ULe9Ch*10a#>j(}8fG-{weYXW z;_DwVMlio1e^gtEl_17y$3d(#A**AN{94?(yG9|ibx>*>1A=n)N^PTX^(^(p#~Yu) z^f2aL_0AJg+eBi`B3mOOcuJj3smoI8ij=FBx)P<%sMML1I=xcoQtI-Qx_qT>ky6*7 zEJN8crOvI?)dt-un>!4y$-Dih?L98HS~XSNi^*B3t%nGs%GTJCKDC{fI@w+~?>9|( zl3#2upVyI=ztX4AU+CBGES|R#cR6c}`IFWeq?aS|k(}5!^SzkchV@K?6}vvaLA@jH zh+UpPN4 zIlk0T?TM^C6puFuE&w{10N>{POex0nveA!@_$Np1Lo0RlG~Xz1ukGc9)uM@IKF2+n zNLErK<0g>&M3m3Jds1DI4>4{N)f^w635+Y5Xn(o07h{!Kv33k#UOp#2-hwBwG+@q! z1UwJIoXfXqeapO+{%QYRX$s-YhmdR@MMgCok|&5(jC0(c$U6w}BLY-+VQh&SWX1VlX1&}O??vF0mQShp z4+B#`4JQA~i`-hf74h*_wNV6~iuL}>fw^#TK2co9Oe3l|8M68=!zuz)g#nJ2>sc!3 zT}hR;h7vxahaIb7EeOTwn!T7^7|l}AK!>S)-1*J!LEx?EdFtH$Gh%oS#j=K5$24cT z>tun^I@EFI(TjUo51eDgqsKY}<+A3~dZoA)alF*^c@v}n#iQ1*N?}r92Xzc19Xf|t zMx|1&2Q7X|5^pea6c;&eU~fqQG80es9#ZA?D~O<|>j@*fQDnA%@MN#vXN$b7zSfCL z=#W7i#C$_|Ht?Aq`32Q9825jCve)1n8+jPF$Vccn?*HUuugN!Fn{SD%9{EJ)8^i6G z$nD5SJFGmP=GN!R@1Lz#%Jb@a=OSR32datJ^&(YE$WM+JRyqzMmXO8+qjND1?f{}q zQw$h3U#dHP0OZZ%hQ6!8>ELpMC(?g79*?ExH~BL>kp~>al6v+Q1|HyAfb%>);-C!Q zSapcxysI!jwHt9;1UT2~s4T@qhiWfLzNs3%@$lB`S?|dBQ<05Fo-pTIL*n;A9foP` zye27dKT>Q~o_zYO)aFN8?vG;yP1{mRVlBp~dYS^j>Np?##*+vvWGd5)ir?gOv007I ze&3{a1DkJTYfWrcgR5}1verapV6IGCUI6l)Z&a!qm01nGaad3io|Z>=)WYa|u0d+c zB++mc$s#tb7A8TUfHG))*|EsWG$^xaW&R?>+P{x9%_oospN66Hf2%rj z)8)wX(Yh6Vcv=}yfDY2532Fy3Ra68%11-iffF+_kLOAB#*{m|mZ`D*pCZjz-4iV!# zj&?C`PSxC%C!am*8!exWOM(AF2CxhP!RKsH7!yP~77BE)_%4O2)*3O{I&w`6q&_{V zHUTk6E3?p^UOZ;sYp3WHfuK~IC{9N6LMyHUh9UAYE8JL`p<U`mB z+-ELL@l-~BhJnVQaM(Ihi}Ta{A3BfjOcmz(^?~@h)cQKM1O4OEDO~fY|1|w0wY`Qm zNe4-RUZnV98j28Oy$tM~WqPTt1=$cSq=r;2bvk&G>*&@J zNw}sY$X4G083>YsIa)EN9mZH>rTQuj9dsw2YJ;zqH?SU0aCwS4-q5dT`?P&0EuMtF zk2uwL6CH(sJVq)(YI|X5K!1Z2EY+IeA{#Wk=@r3+xay$qTCVL&+%-#6py@U9!Ws6j zW}XtoW7JFw%>Uks5#ob~VF7)cDm;;C?*iR*4XoT`f13s^PJ>q0;D@yO%r7b}c?cJ# z%Pn~+KE2~$V#=Hph}3o;_zgXk+U_C7Tam|hmYjO)%I^A!;6ds)w0Is;>P*WB-NEq( z>~NZO!n_MpTPoDd2^Tj43oryJ0wgF<+PR!)u~2Hwt4d3Q(z1w!Q5`F8a1O4~ z+XrJ~JP`_xaKx?o6^ybb*qZ3a_@sXDhcNSfirmr|_g%$2pn&C5Ve4q^DV_#yivdRh zM9|kq_HPjVqs0&<=wCX*MBoby{R5K#sC^L9=xsnlU8Eij4GmB5@u3BZr+m|a{3d?^ zu%x6;YN-WLDJ}Vl=3DeiOPPPPCo=aE-W689063|sn1lYJ*F#u9D=Ol&X23Q2r724V zcjOaQmY>%?JqLoXS-%QxU&%`HAj%a4Eg0A~FYQKojZzlx)dF zeMHv=@c1|qCt(4?B;G|+?XV>a!LHyCqXK2uCBmHndk$iG>Uvt2C*fsGZ*DtuM1W3aUt@)^OMU4uFUc|mak0WGc*mH(OUSbxwwLW7F_*~L z8LTpiXu+ty0JTu)##W;FU9PoUxE_&QuW7(+n~C!RQ*9DRh8~M zQR7=fQ!~z(&qsqJr@qLh3!#R+4xf0`u!i&DGj+wZH`i#JwNO+3+o{x+*u8LKp^Y%abth!kTD%Agx&Y-S??%U;|6LAkucKyZj>TzFeSE+8~FJx>aPh2%pi z15u?AIz>Jt&O+>VWnJ7B=mtEXnXriJ1oY7)AJIhzuY!V=Z6cqJa9RHDN5>ht98%4e~-H|cr`cd$}|i?75y7VmWCY*L}(621CA_Zbv9loQx!$vBNUzFv)NYY@v3bSws|0h z|ES#tXTXg1tl;K^4*>*PRlZ$=XXH!3VL*_25HMxpJu^VGA4A4oazaq01qCvJM5|Kj zk^-cbRI-hZL&Ngfv0q9*UqP8K!|T{2yh0j~NSP3djA{*~kd?6)Nu_iFTSpykl1f02B!{ z`G_dLpXOD{Dwm@G0>=$BTi@dhHMKc*A*bt7K;qon{8cWGlj=}kS=}(59dPbdRyQ<# z7?g9+i(qME@S$UH>*pvbN?}>Bu*zMr^wQw!070(uR32tPqJ1lCWL(ft_rAD z5H6-4ArTHGBm$@mI1ocTe2U7o9SVe)nG^T2!ZN1cJk-F-pK~C}Y+2wTuYB6ne2=vC zbLYp+(>a@7j5VaTJ}er(_#inyCdL^WqZhDQqOgHPo);r|1JGNz92xeHT_g^a!iMN? zKsr)jGcYJ^-5y+}Lj_=%<_ffaamJL|W~!~*P$RlNj)dVbnNZX8pkN3RbJMfDi+zy0 z5i|~NLfcZ?kE!L(qQYg=ASm{bD0hq4ZALQjL%;)Mi~}nCcLb-4&Pel2jt}e!g%f1P zO0;27fxR3Qx6s7a2xv&xOc{%R3F2q}orX}u z1r1?oYMi>aPVUYK9Zs|FK?vT4{t04~VP+3hxreQ85C@&7IbQCkb;+P&QbjhEKUOzj z;AqiN!}Jm`h5slkT*O~(5W~(sX@ir@QsKRg_ z#vLqNjxb21cVdENrlPUja6#tgVV0SUy0qM%;tr>em6qB-Ck8FsdY`!)z3zFl`{N^;a*FK3G#61vY;KLHEy4bTn zf+PtSY3n(x&c`ZA5^uj34>)NrwQ$U z!1h4afxT_l?LUl0p+@x&CN;YE2?`gvYvvI6N@21M5D{Ss>Pgdg6uR2e=q+#zZc)^tQdaaflzGDH%h0rUCEdE9 zGt}XpH$2>v@I*c&(ZZ6p<-iey!*o&xn}Mv0X-)^qqeX5bUMPf|ca{uEhL>rp9#QNb zjfh}t09#@|`bq-^l+aUb<^xuSRFgZQntrd2+zPK|@_xzS{a96Ufa#0rt@eEVqr0pNjgIfrM1OW^&O%$+aRsQ(zTx&H}bvw!>qSA|1HL(wn+R+Gm)TE8eu&w1KiV>g+ zvf$12kk@sJfkLsgaU0suOtrj3hb_e3Ar6NqcJ}dY%gz04z}|y(VidY8;SPdo@B4nE zmVfdk(&Mq8YnxW7!p)Bt3#Kj81(P^KFjbZdrdja1*5db7{Djn$R9$MSn5zEiN5Nmb z31tiMtH;lWpB68{*2!|(aB5-2@Z5akD_D6}MSL%Gg|-|YbGH|rFa;XP{~z$pVpFj- z%2%wESd^J&g|^0KhNtpfR|>W@>Xj0sQess~vXqi++6c?z1i^MPcoTkiNuyHYR!Yi} zH?#s3M!p|cYNIeMj~6_F1mXo|#0$uNpe`7lr=+%a+)Qe*DlJK|ftIXEEngNJ2>T7{ z+V%Qg4220hd5bM;)3D9=)Z4LfaG=iUr^=^u6t`h5tb9p6ZCLXa`Lwj|5|2`1Q�h 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 literal 0 HcmV?d00001 diff --git a/reformat/AWGS.cpp b/reformat/AWGS.cpp new file mode 100644 index 0000000..8bf434a --- /dev/null +++ b/reformat/AWGS.cpp @@ -0,0 +1,546 @@ +/* + * CiderPress + * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. + * See the file LICENSE for distribution terms. + */ +/* + * Reformat AWGS files. + */ +#include "StdAfx.h" +#include "AWGS.h" + +/* + * Decide whether or not we want to handle this file. + */ +void +ReformatAWGS_WP::Examine(ReformatHolder* pHolder) +{ + ReformatHolder::ReformatApplies applies = ReformatHolder::kApplicNot; + + if (pHolder->GetFileType() == kTypeGWP && pHolder->GetAuxType() == 0x8010) + applies = ReformatHolder::kApplicYes; + + pHolder->SetApplic(ReformatHolder::kReformatAWGS_WP, applies, + ReformatHolder::kApplicNot, ReformatHolder::kApplicNot); +} + +/* + * Convert AWGS into formatted text. + */ +int +ReformatAWGS_WP::Process(const ReformatHolder* pHolder, + ReformatHolder::ReformatID id, ReformatHolder::ReformatPart part, + ReformatOutput* pOutput) +{ + const unsigned char* srcBuf = pHolder->GetSourceBuf(part); + long srcLen = pHolder->GetSourceLen(part); + fUseRTF = true; + Chunk doc, header, footer; + unsigned short val; + + CheckGSCharConv(); + + /* must at least have the doc header and globals */ + if (srcLen < kMinExpectedLen) { + WMSG0("Too short to be AWGS\n"); + return -1; + } + + RTFBegin(kRTFFlagColorTable); + + /* + * Pull interesting values out of the document header. + */ + val = Get16LE(srcBuf + 0); + if (val != kExpectedVersion1 && val != kExpectedVersion2) { + WMSG2("AWGS_WP: unexpected version number (got 0x%04x, wanted 0x%04x)\n", + val, kExpectedVersion1); + DebugBreak(); + } + val = Get16LE(srcBuf + 2); + if (val != kDocHeaderLen) { + WMSG2("Unexpected doc header len (got 0x%04x, wanted 0x%04x)\n", + val, kDocHeaderLen); + return -1; + } + /* the color table is 32 bytes at +56, should we be interested */ + + srcBuf += kDocHeaderLen; + srcLen -= kDocHeaderLen; + + /* + * Pull interesting values out of the WP global variables section. + */ + val = Get16LE(srcBuf + 0); + if (val > kExpectedIntVersion) { + WMSG2("Unexpected internal version number (got %d, expected %d)\n", + val, kExpectedIntVersion); + return -1; + } + + /* date/time are pascal strings */ + WMSG2("File saved at '%.26s' '%.10s'\n", srcBuf + 6, srcBuf + 32); + + srcBuf += kWPGlobalsLen; + srcLen -= kWPGlobalsLen; + + /* + * Now come the three chunks, in order: main document, header, footer. + */ + WMSG0("AWGS_WP: scanning doc\n"); + if (!ReadChunk(&srcBuf, &srcLen, &doc)) + return -1; + WMSG0("AWGS_WP: scanning header\n"); + if (!ReadChunk(&srcBuf, &srcLen, &header)) + return -1; + WMSG0("AWGS_WP: scanning footer\n"); + if (!ReadChunk(&srcBuf, &srcLen, &footer)) + return -1; + + if (srcLen != 0) { + WMSG1("AWGS NOTE: %ld bytes left in file\n", srcLen); + } + + /* + * Dump the chunks, starting with header and footer. + */ + RTFSetColor(kColorMediumBlue); + RTFSetFont(kFontCourierNew); + RTFSetFontSize(10); + BufPrintf("
"); + RTFSetColor(kColorNone); + RTFNewPara(); + + PrintChunk(&header); + + RTFSetColor(kColorMediumBlue); + RTFSetFont(kFontCourierNew); + RTFSetFontSize(10); + BufPrintf("
"); + RTFSetColor(kColorNone); + RTFNewPara(); + + RTFSetColor(kColorMediumBlue); + RTFSetFont(kFontCourierNew); + RTFSetFontSize(10); + BufPrintf("
"); + RTFSetColor(kColorNone); + RTFNewPara(); + + PrintChunk(&footer); + + RTFSetColor(kColorMediumBlue); + RTFSetFont(kFontCourierNew); + RTFSetFontSize(10); + BufPrintf("
"); + RTFSetColor(kColorNone); + RTFNewPara(); + + WMSG0("AWGS_WP: rendering document\n"); + PrintChunk(&doc); + + RTFEnd(); + + SetResultBuffer(pOutput, true); + return 0; +} + +/* + * Read one of the chunks of the file. + */ +bool +ReformatAWGS_WP::ReadChunk(const unsigned char** pSrcBuf, long* pSrcLen, + Chunk* pChunk) +{ + /* starts with the saveArray count */ + pChunk->saveArrayCount = Get16LE(*pSrcBuf); + if (pChunk->saveArrayCount == 0) { + /* AWGS always has at least 1 paragraph */ + WMSG0("Save array is empty\n"); + return false; + } + + *pSrcBuf += 2; + *pSrcLen -= 2; + + /* locate and move past the SaveArray */ + pChunk->saveArray = *pSrcBuf; + + *pSrcBuf += pChunk->saveArrayCount * kSaveArrayEntryLen; + *pSrcLen -= pChunk->saveArrayCount * kSaveArrayEntryLen; + if (*pSrcLen <= 0) { + WMSG2("SaveArray exceeds file length (count=%d len now %ld)\n", + pChunk->saveArrayCount, *pSrcLen); + return false; + } + + /* + * Scan the "save array" to find the highest-numbered ruler. This tells + * us how many rulers there are. + */ + pChunk->numRulers = GetNumRulers(pChunk->saveArray, pChunk->saveArrayCount); + if (*pSrcLen < pChunk->numRulers * kRulerEntryLen) { + WMSG2("Not enough room for rulers (rem=%ld, needed=%ld)\n", + *pSrcLen, pChunk->numRulers * kRulerEntryLen); + return false; + } + WMSG1("+++ found %d rulers\n", pChunk->numRulers); + + pChunk->rulers = *pSrcBuf; + *pSrcBuf += pChunk->numRulers * kRulerEntryLen; + *pSrcLen -= pChunk->numRulers * kRulerEntryLen; + + /* + * Now we're at the docTextBlocks section. + */ + pChunk->textBlocks = *pSrcBuf; + pChunk->numTextBlocks = GetNumTextBlocks(pChunk->saveArray, + pChunk->saveArrayCount); + if (!SkipTextBlocks(pSrcBuf, pSrcLen, pChunk->numTextBlocks)) + return false; + + return true; +} + +/* + * Output a single chunk. We do this by walking down the saveArray. + */ +void +ReformatAWGS_WP::PrintChunk(const Chunk* pChunk) +{ + const int kDefaultStatusBits = kAWGSJustifyLeft | kAWGSSingleSpace; + SaveArrayEntry sae; + const unsigned char* saveArray; + int saCount; + const unsigned char* blockPtr; + long blockLen; + const unsigned char* pRuler; + unsigned short rulerStatusBits; + + saveArray = pChunk->saveArray; + saCount = pChunk->saveArrayCount; + for ( ; saCount > 0; saCount--, saveArray += kSaveArrayEntryLen) { + UnpackSaveArrayEntry(saveArray, &sae); + + /* + * Page-break paragraphs have no real data and an invalid value + * in the "rulerNum" field. So we just throw out a page break + * here and call it a day. + */ + if (sae.attributes == 0x0001) { + /* this is a page-break paragraph */ + RTFSetColor(kColorMediumBlue); + RTFSetFont(kFontCourierNew); + RTFSetFontSize(10); + BufPrintf(""); + RTFSetColor(kColorNone); + RTFNewPara(); + RTFPageBreak(); // only supported by Word + continue; + } + + if (sae.rulerNum < pChunk->numRulers) { + pRuler = pChunk->rulers + sae.rulerNum * kRulerEntryLen; + rulerStatusBits = Get16LE(pRuler + 2); + } else { + WMSG1("AWGS_WP GLITCH: invalid ruler index %d\n", sae.rulerNum); + rulerStatusBits = kDefaultStatusBits; + } + + if (rulerStatusBits & kAWGSJustifyFull) + RTFParaJustify(); + else if (rulerStatusBits & kAWGSJustifyRight) + RTFParaRight(); + else if (rulerStatusBits & kAWGSJustifyCenter) + RTFParaCenter(); + else if (rulerStatusBits & kAWGSJustifyLeft) + RTFParaLeft(); + RTFSetPara(); + + /* + * Find the text block that holds this paragraph. We could speed + * this up by creating an array of entries rather than walking the + * list every time. However, the block count tends to be fairly + * small (e.g. 7 for a 16K doc). + */ + blockPtr = FindTextBlock(pChunk, sae.textBlock); + if (blockPtr == nil) { + WMSG1("AWGS_WP bad textBlock %d\n", sae.textBlock); + return; + } + blockLen = (long) Get32LE(blockPtr); + if (blockLen <= 0 || blockLen > 65535) { + WMSG1("AWGS_WP invalid block len %d\n", blockLen); + return; + } + blockPtr += 4; + + if (sae.offset >= blockLen) { + WMSG2("AWGS_WP bad offset: %d, blockLen=%ld\n", + sae.offset, blockLen); + return; + } + PrintParagraph(blockPtr + sae.offset, blockLen - sae.offset); + } +} + +/* + * Print the contents of the text blocks. + * + * We're assured that the text block format is correct because we had to + * skip through them earlier. We don't really need to worry about running + * off the end due to a bad file. + */ +const unsigned char* +ReformatAWGS_WP::FindTextBlock(const Chunk* pChunk, int blockNum) +{ + const unsigned char* blockPtr = pChunk->textBlocks; + long count = pChunk->numTextBlocks; + unsigned long blockSize; + + while (blockNum--) { + blockSize = Get32LE(blockPtr); + blockPtr += 4 + blockSize; + } + + return blockPtr; +} + + +/* + * Print one paragraph. + * + * Stop when we hit '\r'. We watch "maxLen" just to be safe. + * + * Returns the #of bytes consumed. + */ +int +ReformatAWGS_WP::PrintParagraph(const unsigned char* ptr, long maxLen) +{ + const unsigned char* startPtr = ptr; + unsigned short firstFont; + unsigned char firstStyle, firstSize, firstColor; + unsigned char uch; + + if (maxLen < 7) { + WMSG1("AWGS_WP GLITCH: not enough storage for para header (%d)\n", + maxLen); + return 1; // don't return zero or we might loop forever + } + /* pull out the paragraph header */ + firstFont = Get16LE(ptr); + firstStyle = *(ptr + 2); + firstSize = *(ptr + 3); + firstColor = *(ptr + 4); + + ptr += 7; + maxLen -= 7; + + /* + * Set the font first; that defines the point size multiplier. Set + * the size second, because the point size determines whether we + * show underline. Set the style last. + */ + //WMSG3("+++ Para start: font=0x%04x size=%d style=0x%02x\n", + // firstFont, firstSize, firstStyle); + RTFSetGSFont(firstFont); + RTFSetGSFontSize(firstSize); + RTFSetGSFontStyle(firstStyle); + + while (maxLen > 0) { + uch = *ptr++; + maxLen--; + switch (uch) { + case 0x01: // font change - two bytes follow + if (maxLen >= 2) { + RTFSetGSFont(Get16LE(ptr)); + ptr += 2; + maxLen -= 2; + } + break; + case 0x02: // text style change + if (maxLen >= 1) { + RTFSetGSFontStyle(*ptr++); + maxLen--; + } + break; + case 0x03: // text size change + if (maxLen >= 1) { + RTFSetGSFontSize(*ptr++); + maxLen--; + } + break; + case 0x04: // color change (0-15) + if (maxLen >= 1) { + ptr++; + maxLen--; + } + break; + case 0x05: // page token (replace with page #) + case 0x06: // date token (replace with date) + case 0x07: // time token (replace with time) + RTFSetColor(kColorMediumBlue); + if (uch == 0x05) + BufPrintf(""); + else if (uch == 0x06) + BufPrintf(""); + else + BufPrintf("