pax_global_header00006660000000000000000000000064142402024660014512gustar00rootroot0000000000000052 comment=a4eb95864c5c26b4ec67a1749c2f93a38944ab52 cbmconvert-cbmconvert-2.1.5/000077500000000000000000000000001424020246600160215ustar00rootroot00000000000000cbmconvert-cbmconvert-2.1.5/CMakeLists.txt000066400000000000000000000060321424020246600205620ustar00rootroot00000000000000CMAKE_MINIMUM_REQUIRED (VERSION 3.0.2) SET (VERSION 2.1.5) CONFIGURE_FILE (version.cmake version.h) INCLUDE_DIRECTORIES (${CMAKE_CURRENT_BINARY_DIR}) IF(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) # Setting build type to RelWithDebInfo as none was specified. # Must occur before PROJECT SET(CMAKE_BUILD_TYPE "RelWithDebInfo" CACHE STRING "Choose the type of build, options are: None Debug Release RelWithDebInfo MinSizeRel ..." FORCE) # Set the possible values of build type for cmake-gui SET_PROPERTY(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "None" "Debug" "Release" "MinSizeRel" "RelWithDebInfo") ENDIF() IF (CMAKE_VERSION VERSION_GREATER_EQUAL 3.12) PROJECT (cbmconvert VERSION ${VERSION} LANGUAGES C DESCRIPTION "Create and convert Commodore 8-bit binary file archives" HOMEPAGE_URL "https://github.com/dr-m/cbmconvert/") ELSEIF (CMAKE_VERSION VERSION_GREATER_EQUAL 3.9) PROJECT (cbmconvert VERSION ${VERSION} LANGUAGES C DESCRIPTION "Create and convert Commodore 8-bit binary file archives") ELSE() PROJECT (cbmconvert VERSION ${VERSION}) ENDIF() SET (CPACK_PACKAGE_DESCRIPTION "cbmconvert extracts files from most known archive file formats that are used on 8-bit Commodore computer platforms and writes them to several different formats, including some formats used by some emulators.") SET (CPACK_PACKAGE_CONTACT "marko.makela@iki.fi") SET (CPACK_DEBIAN_PACKAGE_SECTION "otherosfs") SET (CPACK_DEBIAN_PACKAGE_DEPENDS "libc6") SET (CPACK_WIX_PRODUCT_GUID "aa32b470-5a32-51e2-9bb3-f0827715bee0") SET (CPACK_WIX_UPGRADE_GUID "8f269924-924b-51fe-bc67-c87444c15aa6") SET (CPACK_PACKAGE_INSTALL_DIRECTORY "cbmconvert") INCLUDE (CPack) ADD_EXECUTABLE (cbmconvert main.c util.c read.c write.c lynx.c unark.c unarc.c t64.c c2n.c image.c archive.c util.h input.h output.h) ADD_EXECUTABLE (zip2disk zip2disk.c) ADD_EXECUTABLE (disk2zip disk2zip.c) IF (WIN32) INSTALL(FILES cbmconvert.html DESTINATION ".") ELSE() INCLUDE(GNUInstallDirs) INSTALL(FILES cbmconvert.html DESTINATION ${CMAKE_INSTALL_DOCDIR}) INSTALL(FILES cbmconvert.1 zip2disk.1 disk2zip.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1) ENDIF() INSTALL(TARGETS cbmconvert zip2disk disk2zip) ENABLE_TESTING() ADD_TEST (cbmconvert_usage ${CMAKE_BINARY_DIR}/cbmconvert) ADD_TEST (disk2zip_usage ${CMAKE_BINARY_DIR}/disk2zip) ADD_TEST (zip2disk_usage ${CMAKE_BINARY_DIR}/zip2disk) SET_TESTS_PROPERTIES(cbmconvert_usage disk2zip_usage zip2disk_usage PROPERTIES WILL_FAIL TRUE) ADD_TEST (NAME empty_files COMMAND ${CMAKE_COMMAND} -DCBMCONVERT=$ -DZIP2DISK=$ -DDISK2ZIP=$ -P ${CMAKE_CURRENT_SOURCE_DIR}/empty_files.cmake) ADD_TEST (NAME small_files COMMAND ${CMAKE_COMMAND} -DCBMCONVERT=$ -DZIP2DISK=$ -DDISK2ZIP=$ -P ${CMAKE_CURRENT_SOURCE_DIR}/small_files.cmake) ADD_TEST (NAME file_names COMMAND ${CMAKE_COMMAND} -DCBMCONVERT=$ -P ${CMAKE_CURRENT_SOURCE_DIR}/file_names.cmake) cbmconvert-cbmconvert-2.1.5/COPYING000066400000000000000000000433001424020246600170540ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 19yy 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) 19yy name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. cbmconvert-cbmconvert-2.1.5/Doxyfile000066400000000000000000000014121424020246600175250ustar00rootroot00000000000000# Doxygen configuration file for cbmconvert, much like a -*- makefile -*- PROJECT_NAME = cbmconvert-2.1 WARNINGS = YES HAVE_DOT = YES GRAPHICAL_HIERARCHY = YES CLASS_GRAPH = YES COLLABORATION_GRAPH = YES INCLUDE_GRAPH = YES INCLUDED_BY_GRAPH = YES EXTRACT_STATIC = YES WARNINGS = YES INPUT = . FILE_PATTERNS = *.h *.c ENABLE_PREPROCESSING = YES MACRO_EXPANSION = YES EXPAND_ONLY_PREDEF = YES SEARCH_INCLUDES = YES FULL_PATH_NAMES = NO GENERATE_LATEX = NO COMPACT_LATEX = YES PDF_HYPERLINKS = YES PAPER_TYPE = a4 GENERATE_RTF = NO GENERATE_MAN = NO GENERATE_HTML = YES SEARCHENGINE = YES CGI_NAME = cbmconvert CGI_URL = http://localhost/cgi-bin DOC_URL = http://localhost/cbmconvert DOC_ABSPATH = /var/www/cbmconvert BIN_ABSPATH = /usr/lib/cgi-bin cbmconvert-cbmconvert-2.1.5/PACKAGING.md000066400000000000000000000021611424020246600176270ustar00rootroot00000000000000# Creating installation packages ## Unix-like systems (`Unix Makefiles`) ```sh mkdir build cd build cmake .. cpack -G RPM -C Release ``` The command `cpack --help` will display the list of generators that are available on your system. ## Microsoft Windows In the Microsoft Visual Studio installer, be sure to install the components for CMake integration. You will also need the following: * [Git](https://git-scm.com/download/win) * [CMake](https://cmake.org/download/) * [WiX Toolset](https://wixtoolset.org/releases/) Execute the following in the Visual Studio Developer Command Prompt (a specially configured `cmd.exe` or Powershell). ```sh mkdir build cd build cmake .. cmake --build . --config Release cpack -G WIX ``` ## Debian GNU/Linux, Ubuntu, and similar systems The `cpack` generator will skip `ctest` or the creation of a separate package for debug information: ```sh cpack -G DEB -C Release sudo dpkg -i cbmconvert*.deb ``` You can also initiate a build, run tests and create and install more conventional packages using the following commands: ```sh dpkg-buildpackage --no-sign sudo dpkg -i ../cbmconvert*.deb ``` cbmconvert-cbmconvert-2.1.5/README.md000066400000000000000000000063771424020246600173150ustar00rootroot00000000000000# cbmconvert: create, extract and convert 8-bit Commodore binary archives `cbmconvert` extracts files from most known archive file formats that are used on 8-bit Commodore computer platforms and writes them to several different formats, including some formats used by some emulators. ## Examples To convert Lynx archive files (`*.lnx`) into 1541 disk image(s): ```sh cbmconvert -D4 image.d64 -l *.lnx ``` To extract the individual files of a number of 1541 disk images, you could execute the following commands in the Bourne Again shell: ```bash for i in *.d64 do mkdir "${i%.d64}" cd "${i%.d64}" cbmconvert -d ../"$i" cd .. done ``` ## Motivation There are many archiving programs for the Commodore 64 and other 8-bit Commodore computers, most of which are incompatible with archiving programs on other systems. It is faster and more convenient to convert files with native code running on a 32-bit or 64-bit processor than by 8-bit 6502 code running in an old computer or an emulator. ## Requirements * [CMake](https://cmake.org) 3.0.2 or later * At least 32-bit C compiler, compliant to to ISO/IEC 9899:1990 (C90) or later * A POSIX-like operating system (including Microsoft Windows) ## Installation You can build, test, and install the code as follows. See [separate instructions](PACKAGING.md) how to create installation packages for various operating systems. ```sh mkdir build cd build cmake .. cmake --build . ctest -C Debug cmake --install . ``` Note: The `-C Debug` option for `ctest` is only needed on multi-target generators, such as Microsoft Visual Studio or Ninja Multi-Config. On many Unix-like systems, a single-target 'Unix Makefiles' generator will be used by default, and the type of the target may be changed by executing one of the following in the build directory: ```sh cmake -DCMAKE_BUILD_TYPE=Debug . cmake --build . cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo . cmake --build . ``` ### Multi-target configuration Starting with CMake 3.17, the `Ninja Multi-Config` generator may be used to compile multiple types of executables in a single build directory: ```sh mkdir build cd build cmake .. -G 'Ninja Multi-Config' cmake --build . --config Debug cmake --build . --config RelWithDebInfo ctest -C Debug ctest -C RelWithDebInfo cmake --install . --config RelWithDebInfo ``` ## Instrumentation and Test Coverage It can be useful to run tests on instrumented builds. To do that, you may adapt one of the `cmake --build` and `ctest` invocations above and specify some `CMAKE_C_FLAGS`. Examples include: * `-fanalyzer` (GCC static analysis) * `-fsanitize=address` (GCC and Clang; environment variable `ASAN_OPTIONS`) * `-fsanitize=undefined` (GCC and Clang; environment variable `UBSAN_OPTIONS`) * `-fsanitize=memory` (Clang; environment variable `MSAN_OPTIONS`) * `--coverage` (GCC code coverage; invoke `gcov` on the `*.gcno` files) For the sanitizers, you may want to specify a file name prefix `log_path` for any error messages, instead of having them written via the standard error to a `ctest` log file: ```sh UBSAN_OPTIONS=print_stacktrace=1:log_path=ubsan ctest ASAN_OPTIONS=abort_on_error=1:log_path=asan ctest ``` ## Further information For more information, see [cbmconvert.html](cbmconvert.html) and the manual pages: ```sh man cbmconvert man zip2disk man disk2zip ``` cbmconvert-cbmconvert-2.1.5/TODO.md000066400000000000000000000016771424020246600171230ustar00rootroot00000000000000# Things to be done (feel free to contribute): ## image.c * `ReadImage()`: Check the BAM * 1581: changing of subdirectories * specifying the disk name and ID * `setupSideSectors()` and `checkSideSectors()`: 1581 support * `validate (Image *image, bool correct)` needs to be written (also for GEOS), with output like the following: ``` Unallocated but used blocks: 18,0 18,1 18,3 Allocated but unused blocks: 18,4 Improper free blocks count on tracks: 17 (10, should be 9) 18 (1, should be 3) ``` * use memory-mapped files or sector-level file access (with seeks) ## main.c: * interactive GNU Readline based interface ## integrate `cbmconvert` with `cbmlink` * writing files to Commodore memory * reading files from Commodore memory? * writing and reading files on Commodore mass storage devices * disk image access? maybe not; arbitrary seeks would be slow ## tests * implement `ctest_coverage()` * implement `ctest_memcheck()` (for ASAN, MSAN, UBSAN) cbmconvert-cbmconvert-2.1.5/archive.c000066400000000000000000000056201424020246600176110ustar00rootroot00000000000000/** * @file archive.c * A collection of files * @author Marko Mäkelä (marko.makela at iki.fi) */ /* ** Copyright © 1998, 2022, Marko Mäkelä ** ** 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. */ #include #include #include #include #include "output.h" /** Allocate an archive data structure. * @return a newly allocated empty archive structure */ struct Archive* newArchive (void) { return calloc (sizeof (struct Archive), 1); } /** Deallocate an archive data structure. * @param archive the archive to be deallocated */ void deleteArchive (struct Archive* archive) { while (archive->first) { struct ArchiveEntry* ae = archive->first->next; free (archive->first->data); free (archive->first); archive->first = ae; } free (archive); } /** Write a file to an archive. * @param name native (PETSCII) name of the file * @param data the contents of the file * @param length length of the file contents * @param archive the archive the file is written to * @param log Call-back function for diagnostic output * @return status of the operation */ enum WrStatus WriteArchive (const struct Filename* name, const byte_t* data, size_t length, struct Archive* archive, log_t log) { struct ArchiveEntry* ae; switch (name->type) { default: (*log) (Errors, name, "Unsupported file type."); return WrFail; case DEL: case SEQ: case PRG: case USR: case REL: break; } /* check for duplicate file names */ for (ae = archive->first; ae; ae = ae->next) if (!memcmp (&ae->name.name, name->name, sizeof name->name)) return WrFileExists; if (!(ae = malloc (sizeof (*ae)))) { (*log) (Errors, name, "Out of memory."); return WrNoSpace; } if (!(ae->data = malloc (length))) { free (ae); (*log) (Errors, name, "Out of memory."); return WrNoSpace; } memcpy (&ae->name, name, sizeof (*name)); if (length) memcpy (ae->data, data, length); ae->length = length; ae->next = 0; if (archive->last) archive->last->next = ae; else archive->first = ae; archive->last = ae; return WrOK; } cbmconvert-cbmconvert-2.1.5/c2n.c000066400000000000000000000232751424020246600166600ustar00rootroot00000000000000/** * @file c2n.c * Commodore C2N archive extractor and archiver * @author Marko Mäkelä (marko.makela at iki.fi) */ /* ** Copyright © 2001,2021,2022 Marko Mäkelä ** ** 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. */ #include #include #include #include #include "input.h" #include "output.h" /** Commodore C2N tape header */ struct c2n_header { /** Header identifier tag */ byte_t tag; /** Least significant byte of the start address of the following block */ byte_t startAddrLow; /** Most significant byte of the start address of the following block */ byte_t startAddrHigh; /** Least significant byte of the end address of the following block */ byte_t endAddrLow; /** Most significant byte of the end address of the following block */ byte_t endAddrHigh; /** Commodore file name, padded with spaces */ byte_t filename[16]; /** Unused, padded with spaces */ byte_t padding[171]; }; /** Commodore C2N tape header identifier tags */ enum c2n_tag { tBasic = 1, /**< relocatable (BASIC) program */ tDataBlock = 2, /**< actual data block of a data file */ tML = 3, /**< absolute program */ tDataHeader = 4, /**< data file header */ tEnd = 5 /**< end-of-tape header */ }; /** copy a file name from tape header * and convert trailing spaces to trailing shifted spaces * @param header the tape header * @param name the file name */ static void header2name (const struct c2n_header* header, struct Filename* name) { unsigned char* c; memcpy (name->name, header->filename, sizeof header->filename); for (c = name->name + sizeof name->name; c-- > name->name; *c = 0xa0) if (*c != 0x20) break; } /** copy a file name to tape header * and convert trailing shifted spaces to trailing spaces * @param name the file name * @param header the tape header */ static void name2header (const struct Filename* name, struct c2n_header* header) { register byte_t* c; /* fill the header with spaces */ memset (header, ' ', sizeof *header); /* copy the name */ memcpy (header->filename, name->name, sizeof header->filename); /* convert trailing shifted spaces to spaces */ for (c = header->filename + sizeof header->filename; c-- > header->filename; *c = 0x20) if (*c != 0xa0) break; } /** Read and convert a Commodore C2N tape archive * @param file the file input stream * @param filename host system name of the file * @param writeCallback function for writing the contained files * @param log Call-back function for diagnostic output * @return status of the operation */ enum RdStatus ReadC2N (FILE* file, const char* filename, write_file_t writeCallback, log_t log) { /** name of the file being processed */ struct Filename name; /* clear the file type code (to denote uninitialized file name) */ name.type = 0; /* clear the record length (no relative files on tapes) */ name.recordLength = 0; while (!feof (file)) { /** tape header */ struct c2n_header header; /** start address of the file being processed */ unsigned start; /** end address of the file being processed */ unsigned end; switch (fread (&header, 1, sizeof header, file)) { case 0: if (!ferror (file)) continue; (*log) (Errors, name.type ? &name : 0, "fread: %s", strerror (errno)); return RdFail; case 192: break; default: errEOF: (*log) (Errors, name.type ? &name : 0, "unexpected end of file"); return RdFail; } nextHeader: start = header.startAddrLow | (unsigned) header.startAddrHigh << 8; end = header.endAddrLow | (unsigned) header.endAddrHigh << 8; switch (header.tag) { case tBasic: case tML: header2name (&header, &name); name.type = PRG; if ((header.tag == tBasic && header.startAddrLow != 1) || start >= end) (*log) (Warnings, &name, "Suspicious addresses 0x%04x..0x%04x", start, end); break; case tDataHeader: header2name (&header, &name); name.type = SEQ; if (start != 0x33c || end != 0x3fc) (*log) (Warnings, &name, "Suspicious addresses 0x%04x..0x%04x (expected 0x33c..0x3fc)", start, end); if ((byte_t) (end - start) != 192) (*log) (Warnings, name.type ? &name : 0, "Block length differs from 192"); break; case tEnd: header2name (&header, &name); name.type = DEL; (*log) (Everything, &name, "Ignoring end-of-tape marker"); continue; default: (*log) (Errors, name.type ? &name : 0, "Unknown C2N header code 0x%02x", header.tag); return RdFail; } if (name.type == SEQ) { /** number of bytes read so far */ size_t length = 0; /** the data buffer */ byte_t* buf = 0; nextBlock: switch (fread (&header, 1, sizeof header, file)) { case 0: if (!ferror (file)) goto writeData; (*log) (Errors, &name, "fread: %s", strerror (errno)); errExit: free (buf); return RdFail; case 192: break; default: free (buf); goto errEOF; } if (header.tag == tDataBlock) { byte_t* b = realloc (buf, length + (sizeof header) - 1); if (!b) { (*log) (Errors, &name, "Out of memory."); goto errExit; } buf = b; memcpy (buf + length, ((byte_t*) &header) + 1, (sizeof header) - 1); length += (sizeof header) - 1; goto nextBlock; } else { enum WrStatus status; writeData: if (!length) (*log) (Warnings, &name, "no data"); status = (*writeCallback) (&name, buf, length); free (buf); switch (status) { case WrOK: break; case WrNoSpace: return RdNoSpace; case WrFail: default: return RdFail; } if (!feof (file)) goto nextHeader; } } else { byte_t* buf; enum WrStatus status; size_t readlength, length = (end - start) & 0xffff; if (!(buf = malloc (length + 2))) { (*log) (Errors, &name, "Out of memory."); return RdFail; } buf[0] = header.startAddrLow; buf[1] = header.startAddrHigh; if (length != (readlength = fread (&buf[2], 1, length, file))) { if (feof (file)) (*log) (Warnings, &name, "Truncated file, proceeding anyway"); if (ferror (file)) { (*log) (Errors, &name, "fread: %s", strerror (errno)); free (buf); return RdFail; } } status = (*writeCallback) (&name, buf, readlength + 2); free (buf); switch (status) { case WrOK: break; case WrNoSpace: return RdNoSpace; case WrFail: default: return RdFail; } } } return RdOK; } /** Write an archive in Commodore C2N tape format * @param archive the archive to be written * @param filename host file name of the archive file * @return status of the operation */ enum ArStatus ArchiveC2N (const struct Archive* archive, const char* filename) { FILE* f; struct ArchiveEntry* ae; if (!(f = fopen (filename, "wb"))) return errno == ENOSPC ? ArNoSpace : ArFail; for (ae = archive->first; ae; ae = ae->next) { /** tape header */ struct c2n_header header; name2header (&ae->name, &header); if (ae->name.type == PRG) { size_t end = ae->length; if (end < 2) continue; /* too short file */ header.startAddrLow = ae->data[0]; header.startAddrHigh = ae->data[1]; end += header.startAddrLow | (unsigned) header.startAddrHigh << 8; header.endAddrLow = (byte_t) (end -= 2); header.endAddrHigh = (byte_t) (end >> 8); header.tag = header.startAddrLow == 1 ? tBasic : tML; if (1 != fwrite (&header, sizeof header, 1, f) || ae->length - 2 != fwrite (ae->data + 2, 1, ae->length - 2, f)) { fail: fclose (f); return ArFail; } } else { /* convert anything else than programs to data files */ /** data file header */ static const byte_t dataheader[] = { tDataHeader, 0x3c, 3, 0xfc, 3 }; /** number of bytes written */ unsigned cnt = 0; memcpy (&header, dataheader, sizeof dataheader); if (1 != fwrite (&header, sizeof header, 1, f)) goto fail; do { unsigned next = cnt + (sizeof header) - 1; header.tag = tDataBlock; if (next > ae->length) { memcpy (&header.startAddrLow, ae->data + cnt, ae->length - cnt); (&header.startAddrLow)[ae->length - cnt] = 0; } else memcpy (&header.startAddrLow, ae->data + cnt, (sizeof header) - 1); if (1 != fwrite (&header, sizeof header, 1, f)) goto fail; cnt = next; } while (cnt < ae->length); } } fclose (f); return ArOK; } cbmconvert-cbmconvert-2.1.5/cbmconvert.1000066400000000000000000000147551424020246600202610ustar00rootroot00000000000000.\" Manual page in -*- nroff -*- format; see man(7) .TH CBMCONVERT 1 "September 18, 2001" .SH NAME cbmconvert \- create, extract and convert various Commodore binary archives .SH SYNOPSIS .B cbmconvert .RI [ options ] " \(file" ... .SH DESCRIPTION This manual page documents brie\(fly the .B cbmconvert command. .PP There are many archiving programs for the Commodore 64, all of which are incompatible with archiving programs on other systems. The \fBcbmconvert\fP utility tries to address this problem. It extracts \(files from most known formats and writes them to several different formats, including some formats used by some Commodore 64 emulators: .TP .B Native (raw) \(files Files with just the raw data. Written with the \fB-I\fP and \fB-N\fP options, read with the \fB-n\fP option. .TP .B PC64 \(files Also known as "P00" \(files. Written with the \fB-P\fP option, read with the \fB-p\fP option. .TP .B Lynx archives Lynx was originally developed for the Commodore 64. It modi\(fies the next-sector links in place and combines a number of \(files on a Commodore disk to a single \(file that can be transferred e.g. over a modem connection. Earlier versions of this format do not specify the length of the last contained \(file, not protecting it from padding that could be introduced e.g. by the X-modem transfer protocol. There are no checksums on the data either. Lynx archives are written with \fB-L\fP and read with \fB-l\fP. .TP .B Commodore C2N tape archives Written with \fB-C\fP, read with \fB-c\fP. These \(files are raw dumps of the data format the Commodore KERNAL routines maintain. Block checksums and countdown leaders (0x89..0x81 for the \(first copy and 9..1 for the second) are omitted, and the blocks are not stored twice, but only once. The data consists of 192-byte tape header blocks, 192-byte data \(file blocks, and arbitrary-length program \(file blocks. .TP .B Commodore 128 CP/M disk images Written with \fB-M\fP, read with \fB-m\fP. .TP .B CBM DOS disk images Written with \fB-D\fP, read with \fB-d\fP. Commodore 1571 and 1581 support have not been tested properly, and not all 1581 features have been implemented. .TP .B ARC/SDA (Self-Dissolving Archive) Read with \fB-a\fP. No write support. .TP .B Arkive Archives in this Lynx-like format are read with the \fB-k\fP option. There is no write support. .TP .B T64 This tape format was introduced by C64S emulator. Many variations of this format exist. These \(files are read with the \fB-t\fP option. .PP \fBcbmconvert\fP reads all \(files in all input \(files listed on the command line and writes them in the speci\(fied format. As there is no interactive user interface, the only way to copy only some \(files from a set of archive \(files to an archive \(file or a disk image is to extract all the \(files to a single-\(file format such as the PC64 format, and to copy the desited individual \(files to the output archive with another invocation of \fBcbmconvert\fP. .SH OPTIONS \fBcbmconvert\fP follows the usual Unix command line syntax, with options starting with a dash (`\fB-\fP'). .TP .B -- Stop processing options. This is useful if the \(first \(file name begins with a dash. .TP .B -I Output \(files in native (raw) format, with ISO 9660 compliant \(file names. .TP .B -P Output \(files in PC64 format. .TP .B -N Output \(files in native (raw) format. .TP .BI -L " archive.lnx" Output \(files in Lynx format. .TP .BI -C " archive.c2n" Output \(files in Commodore C2N tape format. .TP .BR -D4 [ o ] " \fIimage.d64\fP" Write to a Commodore 1541 CBM DOS disk image. The \fBo\fP option speci\(fies that \(file name collisions should be resolved by overwriting existing \(files. The default behaviour is keep the old \(files. .TP .BR -D7 [ o ] " \fIimage.d71\fP" Write to a Commodore 1571 CBM DOS disk image. .TP .BR -D8 [ o ] " \fIimage.d81\fP" Write to a Commodore 1581 CBM DOS disk image. .TP .BR -M4 [ o ] " \fIimage.d64\fP" Write to a Commodore 1541 disk image in the Commodore 128 CP/M format. .TP .BR -M7 [ o ] " \fIimage.d71\fP" Write to a Commodore 1571 disk image in the Commodore 128 CP/M format. .TP .BR -M8 [ o ] " \fIimage.d81\fP" Write to a Commodore 1581 disk image in the Commodore 128 CP/M format. .TP .B -i2 Switch disk images when running out of space or a duplicate \(file name is detected. .TP .B -i1 Switch disk images when running out of space. This is the default behaviour. .TP .B -i0 Never switch disk images. .TP .B -n Input \(files in native (raw) format. .TP .B -p Input \(files in PC64 format. .TP .B -a Input \(files in ARC/SDA format. .TP .B -k Input \(files in Arkive format. .TP .B -l Input \(files in Lynx format. .TP .B -t Input \(files in T64 format. .TP .B -c Input \(files in Commodore C2N format. .TP .B -d Input \(files in CBM DOS disk image format. .TP .B -m Input \(files in Commodore 128 CP/M disk image format. .TP .B -v2 Verbose mode. Display all messages. .TP .B -v1 Display warning and error messages. This is the default option. .TP .B -v0 Display error messages only. .SH BUGS Many of the \(file formats lack safety measures, such as storing the exact lengths of the contained \(files, or storing even rudimentary checksums. Most formats have been reverse-engineered, and there may be other implementations that accept \(files in a stricter format than \fBcbmconvert\fP produces or produce \(files that \fBcbmconvert\fP does not recognize. .PP On disk images, it is common to decorate directory listings with unnecessary entries that contain Commodore-speci\(fic graphic characters. Since subdirectories were not supported by Commodore until the 1581 disk drive was introduced, the slash character (`/') is valid in Commodore \(file names but not on the Unix system. For these reasons, it is advisable to avoid the raw \(file format and the host \(file system whenever possible, and to convert directly from one Commodore-speci\(fic format to another. .PP The program lacks an interactive user interface. A shell-like command line interface could be useful, and a graphical \(file manager like interface could be even better. Are there any volunteers? .PP More disk image formats should be supported, and the 1571 and 1581 support should be tested extensively. Unsupported formats include the 8050, the 8250, the 2040 and the Commodore 64 CP/M format for the 1541. .SH AUTHOR The \fBcbmconvert\fP utility was designed and implemented by Marko M\(:akel\(:a. .PP Support for Commodore 1581 disk images was programmed by Pasi Ojala. .PP The ARC/SDA dissolving code was originally written by Chris Smeets. .SH SEE ALSO .BR c2n (1), .BR disk2zip (1), .BR zip2disk (1). cbmconvert-cbmconvert-2.1.5/cbmconvert.html000066400000000000000000000417661424020246600210670ustar00rootroot00000000000000 cbmconvert release notes

cbmconvert release notes

cbmconvert
create, extract and convert different Commodore binary archives

Copyright © 1993‒2006,2021‒2022 Marko Mäkelä.

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.

Table of Contents

1 Introduction
2 Getting Started
3 Using cbmconvert
3.1 cbmconvert
3.1.1 CP/M disk images
3.1.2 GEOS disk images
3.1.3 Commodore C2N tape archives
3.1.4 Flaws of cbmconvert
3.2 disk2zip
3.3 zip2disk
4 Credits
5 Distribution
5.1 Revision History

1 Introduction

There are many archiving programs for the Commodore 64, all of which are incompatible with archiving programs on other systems. This cbmconvert utility tries to address this problem. It extracts files from most known file formats and writes them to several different formats, including some formats used by some Commodore 64 emulators.

This release of cbmconvert does not contain LHARC support any more, since 1541 disk images (either plain or ZipCoded) and Lynx archives are much easier to handle. If you need LHARC support, get the older release, cbmconvert 1.1.

All code has been cleaned up. The file type management in cbmconvert 2.0 is very flexible, and it is very easy to add file types later. Currently cbmconvert reads the following file types: Arkive, Lynx, ARC64/128, PC64, tape images (T64 and native Commodore C2N), disk images (1541, 1571, 1581, both native and C128 CP/M format), and raw files. Conversions between 1541 disk images and ZipCoded files can be made with the two supplied utilities disk2zip and zip2disk.

2 Getting Started

Compile cbmconvert as follows:

mkdir build
cd build
cmake ..
cmake --build .
cmake --install .

The compilation should leave you with three executables: cbmconvert, zip2disk and disk2zip. The installation step is optional; you may also invoke the executables from the build directory.

16-bit compilers will probably have trouble with cbmconvert. For instance, the standard file I/O library in Borland C uses signed 16-bit integers for size_t, which is not enough.

3 Using cbmconvert

Starting with version 2.0.0, cbmconvert consists of three programs:

cbmconvert
The main program that makes most conversions.
disk2zip
ZipCode creator. Encodes a 35-track 1541 disk image to four ZipCode files (1!filename..4!filename).
zip2disk
ZipCode dissolver. Decodes a 35-track 1541 disk image from four ZipCode files.

3.1 cbmconvert

Since version 2.0, cbmconvert has independent file reader and writer modules that are controlled through a main program. There is only a command-line interface, but adding an interactive user interface should be easy (any volunteers?).

cbmconvert reads all files in all input files you specify on the command line and writes them in the format you specify. If you want to copy only some files from a set of archive files to an archive file or disk image, you will have to extract all the files to a single-file format such as the PC64 format, and to copy the wanted individual files to the output archive with another invocation of cbmconvert.

You can get a list of cbmconvert options by invoking the program without any arguments. cbmconvert supports the following file formats:

Raw files
Files with just the raw data. Written with the -I and -N options, read with the -n option.
PC64 files
Also known as "P00" files. Written with the -P option, read with the -p option.
Lynx archives
Written with -L, read with -l.
Commodore C2N tape archives
Written with -C, read with -c. These files are raw dumps of the data format the Commodore KERNAL routines maintain. Block checksums and countdown leaders (0x89..0x81 for the first copy and 9..1 for the second) are omitted, and the blocks are not stored twice, but only once. The data consists of 192-byte tape header blocks, 192-byte data file blocks, and arbitrary-length program file blocks.
C128 CP/M disk images
Written with -M[478], read with -m.
CBM DOS disk images
Written with -D[478], read with -d. 1571 and 1581 support have not been tested properly, and not all 1581 features have been implemented.
ARC/SDA
Read with -a. No write support.
Arkive
Read with -k. No write support.
T64
Read with -t. No write support.

3.1.1 CP/M disk images

Since cbmconvert has been written exclusively with Commodore file names in mind, the support for CP/M disk images is a bit clumsy. You may want to rename the files after extracting them from the disks. For instance, if you have downloaded the CP/M disk images from http://www.funet.fi/pub/cbm/demodisks/c128/, you can use the following commands in bash to extract the files from the disk images:

gunzip *.d64.gz

for i in *.d64; do mkdir ${i%.d64}; cd ${i%.d64}; cbmconvert -m ../$i; cd ..; done

for i in */*.prg; do mv $i "`echo ${i%.prg}|tr A-Z a-z`"; done

CP/M does not store the exact length of the files anywhere. That is why many CP/M programs create files whose length is a multiple of 128 bytes. Text files in CP/M commonly use ^Z as an end-of-file delimiter, and many programs fill the extra bytes in the last 128-byte block of the file with this character. The CP/M routines in cbmconvert does so as well, and it removes the ^Z padding when reading the files. So, you could use cbmconvert to remove the padding. But you may prefer the following Perl 5 command:

perl -i -e 'undef $/; while (<>) { s/\032+\z//gs; print }' *

3.1.2 GEOS disk images

GEOS files on disk images will be handled transparently by converting them to and from the Convert format on the fly. All unused areas in the Convert file will be zeroed out, and all possible integrity checks will be made in both conversions. If the file is interpreted to be in GEOS or Convert format, and it fails in any integrity check, it will be treated as a native Commodore file.

You should note that Convert does not provide much safety for the file. For sequential files, the original file length is not stored anywhere. For VLIR files, it is, but even then there are no checksums.

As a speciality, all GEOS files have a file information block. The contents of this block can be viewed with the simple Perl script cvtinfo.pl that reads files in Convert format. It was just a quick hack; feel free to make it more robust.

The GEOS routines have been tested with twelve 1541 disk images and with several megabytes of *.cvt files, and they are believed to be error free now. There were problems with the VLIR sector format; Mr. Boyce's reverse engineered information about it was incorrect, or at least inadequate.

3.1.3 Commodore C2N tape archives

The Commodore C2N tape format uses 187-byte file names that are padded with spaces. On disks, file names are 16 bytes and padded with shifted spaces. Since cbmconvert was designed primarily for disk-based archives, it truncates tape file names to 16 bytes and changes the padding from regular spaces to shifted spaces and vice versa.

End-of-tape markers on C2N tapes are not converted.

There are two kinds of program files on tapes: relocatable program files (type code 1, typically written in BASIC) and absolute program files (type code 3, typically written in machine language). There is no such distinction among disk files. When writing program files to C2N archives, cbmconvert marks the files relocatable if the least significant byte of their start address is 1. This typically indicates a BASIC program.

On output, all file types expect PRG are converted to tape data files consisting of 191-byte fixed-length data blocks. The output to tape data blocks tries to mimic the Commodore KERNAL behaviour: a zero byte is written to the last data block after end-of-file, and the bytes following the zero byte are identical with the bytes in the preceding data or header block.

The data file padding in the last block is not detected or removed on input. Also, the padding is not added on output if the file length is an exact multiple of 191 bytes, even though the KERNAL routines would add an extra block whose first data byte is zero. This prevents data files from growing when they are imported from tape and exported again, but this may cause incompatibilities when writing a data file with exactly n·191 bytes of payload.

3.1.4 Flaws of cbmconvert

The conversion may lose some file name or attribute information. In some cases, the filenames will be converted from PETSCII to ASCII or vice versa, and some PETSCII or ASCII specific characters will be lost in the conversions. Non-standard directory information available in some formats will be lost as well.

Lynx, Arkive and ARC allow duplicate filenames. But cbmconvert detects them and refuses to write multiple files with identical names to archives or to disk images. These duplicates are often used as directory separators, to separate groups of filenames. If you really need such separators, use 1-block-long PRG or SEQ files with different names. For example, the first separator can be a string of 16 dashes, the second one can be 15 dashes, and so on.

Arkive also works on illegal file types that cannot be created without directly modifying the directory blocks. Such files will be converted to a supported file type. Files with unknown types on disk images will be skipped.

Handling the errors on disk images could be more robust. Not all (typically BAM-related) errors on CBM DOS disk images are detected. For erroneous 1541 disk images that do not work with the new cbmconvert, you may want to try the disk2files program of the previous release.

3.2 disk2zip

This program converts a standard 35-track, 683-block 1541 disk image to ZipCode format. Because the ZipCode format stores the disk identifier, this conversion adds information. You can use the -i option to specify the disk identifier.

3.3 zip2disk

ZipCoded archives consist of four files prefixed with a number from 1 to 4, and an exclamation point. To extract an archive having the files 1!disk, 2!disk, 3!disk and 4!disk, type zip2disk disk. You may then delete the original files with rm [1-4]\!disk or similar. But note that some software (demos and copy-protected software) may rely on the disk identifier, which will not be stored in the 1541 disk image file.

Support for ZipCode SixPacks (1!!disk, 2!!disk, ..., 6!!disk) could be added easily, but this format is used mainly for really custom 1541 disks with abnormal low-level format, which cbmconvert does not support.

4 Credits

Most of the cbmconvert package was written by Marko Mäkelä. The original version appeared in 1993, and a complete rewrite was started in 1997 and finished in 1998.

The zip2disk module was originally programmed by Paul David Doherty. The ISO 9660 compliant filename truncation algorithm was taken from the T64TOP00 utility created by Wolfgang Lorenz as a part of his now discontinued PC64 emulator.

Support for 1581 CBM DOS disk images was programmed by Pasi Ojala.

The ARC/SDA dissolving code was originally written by Chris Smeets.

The necessary information for supporting GEOS was provided by Joe Forster/STA, Doug Cotton, Alexander Boyce (author of the unofficial GEOS Programmer's Reference Guide) and by the anonymous author of the file "convertfrmt.txt".

5 Distribution

The source code repository of cbmconvert is available at https://github.com/dr-m/cbmconvert/.

5.1 Revision History

Version 2.1.5:
Bug fixes, and regression tests for everything except the read-only formats -a, -k, -t
Version 2.1.4:
Bug fixes; public source code repository
Version 2.1.3:
Bug fixes
Version 2.1.2:
Bug fixes
Version 2.1.1:
Bug fixes
Version 2.1:
Added support for raw Commodore C2N tape archives. Renamed the CP/M-related options to -m and -M, so that the C2N-related options can be -c and -C.
Version 2.0.5:
Mikko Suonio provided patches for tolerating truncated T64 and Lynx archives. His patches also handle single-file T64 archives that have number of entries set to zero, and Lynx archives that have file names starting with a space.
Version 2.0.5.1:
It was brought to my attention that Lynx 17 on the Commodore 64 and some other utilities do not recognize the Lynx archives created by cbmconvert. I slightly modified the magic cookie to fix the problem.
cbmconvert-cbmconvert-2.1.5/cvtinfo.pl000077500000000000000000000023311424020246600200300ustar00rootroot00000000000000#!/usr/local/bin/perl #Display additional information of GEOS files. while ($#ARGV >= 0) { $FILE = shift(@ARGV); open FILE or die "Can't open $FILE!\n"; read FILE,$header,254; read FILE,$info,254; close FILE; ($name, $id) = (unpack ("A3A16A11A*", $header)) [1, 3]; chop $name while ($name =~ /\240$/); $name .= ".cvt"; ($type, $class, $author, $needsclass, $desc) = unpack ("x67Cx7A20A20A20x23a*", $info); if ($type == 8) { $author = ""; $needsclass = ""; } $class =~ s/\0.*$//; $author =~ s/\0.*$//; $needsclass =~ s/\0.*$//; $class =~ tr/\240/ /; $author =~ tr/\240/ /; $needsclass =~ tr/\240/ /; $desc =~ tr/\240/ /; $class =~ tr/ //s; $author =~ tr/ //s; $needsclass =~ tr/ //s; $desc =~ s/\0.*$//; $desc =~ s/\r/ \n /gm; $desc =~ tr/\0-\11\14-\37//d; if ($id =~ /^(PRG|SEQ) formatted GEOS file/) { print $name, "\n"; if ($needsclass ne "") { print " $needsclass document."; } elsif ($class ne "") { print " ", $class, $author ne "" ? " by $author." : "."; } print "\n $desc" unless $desc eq ""; print "\n\n"; } } cbmconvert-cbmconvert-2.1.5/debian/000077500000000000000000000000001424020246600172435ustar00rootroot00000000000000cbmconvert-cbmconvert-2.1.5/debian/changelog000066400000000000000000000005241424020246600211160ustar00rootroot00000000000000cbmconvert (2.1.5-1) unstable; urgency=medium * Maintenance release, with regression tests and bug fixes. -- Marko Mäkelä Sun, 15 May 2022 16:48:40 +0300 cbmconvert (2.1.4-1) unstable; urgency=medium * Initial CMake based release. -- Marko Mäkelä Fri, 26 Nov 2021 22:18:40 +0200 cbmconvert-cbmconvert-2.1.5/debian/control000066400000000000000000000013121424020246600206430ustar00rootroot00000000000000Source: cbmconvert Section: otherosfs Priority: optional Maintainer: Marko Mäkelä Build-Depends: debhelper-compat (= 13), cmake Rules-Requires-Root: no Standards-Version: 4.6.0.1 Homepage: https://github.com/dr-m/cbmconvert/wiki Vcs-Browser: https://github.com/dr-m/cbmconvert/ Vcs-Git: https://github.com/dr-m/cbmconvert.git Package: cbmconvert Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends} Description: Create and convert Commodore 8-bit binary file archives cbmconvert extracts files from most known archive file formats that are used on 8-bit Commodore computer platforms and writes them to several different formats, including some formats used by some emulators. cbmconvert-cbmconvert-2.1.5/debian/copyright000066400000000000000000000032701424020246600212000ustar00rootroot00000000000000Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: cbmconvert Upstream-Contact: https://github.com/dr-m/cbmconvert/issues Source: https://github.com/dr-m/cbmconvert License: GPL-2+ Files: * Copyright: (C) 1993-2021 Marko Mäkelä License: GPL-2+ Files: image.c Copyright: (C) Pasi Ojala , (C) 1993-2021 Marko Mäkelä License: GPL-2+ Files: t64.c Copyright: (C) 1995 Daniel Fandrich, (C) 1997-2021 Marko Mäkelä License: GPL-2+ Files: unarc.c Copyright: (C) 1990 Chris Smeets, (C) 1993-2021 Marko Mäkelä License: GPL-2+ Files: zip2disk.c Copyright: (C) Paul David Doherty , (C) 1993-2021 Marko Mäkelä License: GPL-2+ Files: debian/* Copyright: (C) 2021 Marko Mäkelä License: GPL-2+ License: GPL-2+ This package 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 package 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, see . On Debian systems, the complete text of the GNU General Public License version 2 can be found in `/usr/share/common-licenses/GPL-2'. cbmconvert-cbmconvert-2.1.5/debian/rules000077500000000000000000000004301424020246600203200ustar00rootroot00000000000000#!/usr/bin/make -f # -*- makefile -*- # Uncomment this to turn on verbose mode. #export DH_VERBOSE=1 # Enable Debian Hardening # https://wiki.debian.org/Hardening export DEB_BUILD_MAINT_OPTIONS = hardening=+all include /usr/share/dpkg/default.mk %: dh $@ --buildsystem=cmake cbmconvert-cbmconvert-2.1.5/debian/source/000077500000000000000000000000001424020246600205435ustar00rootroot00000000000000cbmconvert-cbmconvert-2.1.5/debian/source/format000066400000000000000000000000141424020246600217510ustar00rootroot000000000000003.0 (quilt) cbmconvert-cbmconvert-2.1.5/disk2zip.1000066400000000000000000000026041424020246600176440ustar00rootroot00000000000000.\" Manual page in -*- nroff -*- format; see man(7) .TH DISK2ZIP 1 "September 18, 2001" .SH NAME disk2zip \- Commodore 1541 ZipCode compressor .SH SYNOPSIS .B disk2zip .RI [ options ] " image\(file.d64 " [ " zipcode\(file " ] .SH DESCRIPTION This manual page documents brie\(fly the .B disk2zip command. .PP The Commodore 1541 disk archiver ZipCode was developed in order to make it possible to transfer disk images over modem lines. Nowadays it is better to use .BR gzip (1) on the disk images. The \fBdisk2zip\fP command is provided mainly for historical reasons, as a replacement for the ZipCode compressor. .SH OPTIONS \fBdisk2zip\fP follows the usual Unix command line syntax, with options starting with a dash (`\fB-\fP'). .TP .B -- Stop processing options. This is useful if the \(first \(file name argument begins with a dash. .TP .BI -i iidd Use 0xii 0xdd (hexadecimal) as the disk identi\(fier. The default is 0x36 0x34, or `64'. .SH SECURITY The names of the created ZipCode \(files are pre\(fixed with the strings \fB1!\fP, \fB2!\fP, \fB3!\fP, and \fB4!\fP, just like ZipCode works on the Commodore 64. Some Unix shells treat the character \fB!\fP specially. To avoid unpleasant situations, please refer to the documentation of the shell you are using. .SH AUTHOR The \fBdisk2zip\fP utility was designed and implemented by Marko M\(:akel\(:a. .SH SEE ALSO .BR cbmconvert (1), .BR zip2disk (1). cbmconvert-cbmconvert-2.1.5/disk2zip.c000066400000000000000000000211341424020246600177250ustar00rootroot00000000000000/** * @file disk2zip.c * Convert a 1541 disk image to four Zip-Code files * @author Marko Mäkelä (marko.makela at iki.fi) */ /* ** Copyright © 1993-1998,2001,2006,2021,2022 Marko Mäkelä ** ** 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. */ #include #include #include #ifdef MSDOS /** Directory path separator character */ #define PATH_SEPARATOR '\\' #else /** Directory path separator character */ #define PATH_SEPARATOR '/' #endif /* MSDOS */ /** determine whether a character is hexadecimal * @param a the ASCII character * @return nonzero if it is hexadecimal */ #define ISHEX(a) \ ((a >= '0' && a <= '9') || ((a & ~32) >= 'A' && (a & ~32) <= 'F')) /** convert a hexadecimal ASCII character to a number * @param a the ASCII character * @return the numeric interpretation of the character */ #define ASC2HEX(a) \ ((a < '0' || a > '9') ? (((a & ~32) < 'A' || (a & ~32) > 'F') ? -1 : \ ((a & ~32) - 'A' + 10)) : (a - '0')) /** basic start address of ZipCoded files */ #define ZCADDR 0x400 /** the disk image */ static FILE* infile; /** current output file */ static FILE* outfile; /** current track number */ static int track; /** maximum number of sectors in the current track */ static unsigned max_sect; /** interleave factors */ static int eveninc = -10, oddinc = 11; /** disk identifier, two bytes */ static unsigned char id[2] = { '6', '4' }; /** decoding buffer for current track */ unsigned char trackbuf[21][256]; /** input file name */ static char* inname; /** output file name */ static char* outname; /** first character of the actual file name */ static char* fname; /** Initialize the files * @param filename base name for the output file * @retval 0 on success * @retval 1 on out of memory * @retval 2 if the input file could not be opened */ static int init_files (const char* filename) { size_t i = strlen (filename); /* allocate memory for the output filenames */ if (!(outname = (char*) malloc (i + 3))) return 1; /* copy the base filename */ memcpy (outname, filename, i + 1); /* modify the filename */ for (fname = outname + i; fname > outname && *fname != PATH_SEPARATOR; fname--); if (fname > outname) fname++; memmove (fname + 2, fname, i + 1 - (size_t) (fname - outname)); fname[1] = '!'; /* try to open the input file */ return (!infile && !(infile = fopen (inname, "rb"))) ? 2 : 0; } /** Open an output file * @param number number of the output file ('1' to '4') * @return 0 on success; 1 on error */ static int open_file (char number) { *fname = number; if (number > '1') fclose (outfile); if (!(outfile = fopen (outname, "wb"))) return 1; if (number == '1') return EOF == fputc ((ZCADDR - 2), outfile) || EOF == fputc ((ZCADDR - 2) >> 8, outfile) || 2 != fwrite (id, 1, 2, outfile); else return EOF == fputc (ZCADDR, outfile) || EOF == fputc (ZCADDR >> 8, outfile); } /** Encode a sector * @param sect the sector number * @return 0 on success; 1 on failure */ static int write_sector (int sect) { /* a histogram: number of occurrences of different bytes */ static int histogram[256]; int i; unsigned char* sectbuf = trackbuf[sect]; memset (histogram, 0, sizeof histogram); /* see if the all bytes in the sector are identical */ for (i = 0; i < 256; i++) if (256 == ++histogram[sectbuf[i]]) return EOF == fputc (track | 0x40, outfile) || EOF == fputc (sect, outfile) || EOF == fputc (sectbuf[i], outfile); /* see whether there is a byte that does not occur in the sector */ for (i = 0; i < 256 && histogram[i]; i++); /* if not, store the sector without compression */ if (i > 255) Uncompressed: return EOF == fputc (track, outfile) || EOF == fputc (sect, outfile) || 1 != fwrite (sectbuf, 256, 1, outfile); /* otherwise, see if run-length encoding would bring savings */ else { int rep = i, j, count; for (i = 1, j = count = 0; i < 256; i++) { if (sectbuf[i] == sectbuf[j]) continue; count += (i < j + 3) ? i - j : 3; j = i; } count += (i < j + 3) ? i - j : 3; if (count > 253) goto Uncompressed; /* apply run-length encoding */ if (EOF == fputc (track | 0x80, outfile) || EOF == fputc (sect, outfile) || EOF == fputc (count, outfile) || EOF == fputc (rep, outfile)) return 1; for (i = 1, j = 0;; i++) { if (i < 256 && sectbuf[i] == sectbuf[j]) continue; if (i > j + 3) { if (EOF == fputc (rep, outfile) || EOF == fputc (i - j, outfile) || EOF == fputc (sectbuf[j], outfile)) return 1; } else if (1 != fwrite (§buf[j], (size_t) (i - j), 1, outfile)) return 1; if (i > 255) break; j = i; } return 0; } } /** Encode a track * @return 0 on success; 1 on failure */ static int write_track (void) { unsigned i = 0; int sect = 0; for (; i++ < max_sect; sect += i & 1 ? oddinc : eveninc) if (write_sector (sect)) return 1; return 0; } /** Main program * @param argc number of command-line arguments * @param argv contents of command-line arguments * @retval 0 on success * @retval 1 on usage error * @retval 2 on out of memory * @retval 3 on input or output error * @retval 4 on error in the disk image */ int main (int argc, char** argv) { int status; optloop: argv++; if (argc > 1 && **argv == '-') { switch ((*argv)[1]) { case 0: infile = stdin; break; case '-': if (!(*argv)[2]) { /* "--" disables processing further options */ argv++; argc--; break; } case 'i': if (!(*argv)[2] && argv[1]) { /* "-i" specifies disk identifier */ argv++; argc-=2; if (ISHEX ((*argv)[0]) && ISHEX ((*argv)[1]) && ISHEX ((*argv)[2]) && ISHEX ((*argv)[3]) && !(*argv)[4]) { id[0] = (unsigned char) (ASC2HEX((*argv)[1]) | ASC2HEX((*argv)[0]) << 4); id[1] = (unsigned char) (ASC2HEX((*argv)[3]) | ASC2HEX((*argv)[2]) << 4); goto optloop; } } /* fall through */ default: /* unknown option */ goto Usage; } } if (argc != 2 && argc != 3) { Usage: fputs ("ZipCode disk image compressor v1.0.2\n" "Usage: disk2zip [options] disk_image_name [zip_image_name]\n" "Options: -i nnmm: Use $nn $mm (hexadecimal) as disk identifier.\n", stderr); return 1; } inname = *argv++; switch (init_files (*argv ? *argv : inname)) { case 2: fprintf (stderr, "disk2zip: File %s not found.\n", inname); status = 3; goto FuncExit; case 1: fputs ("disk2zip: Out of memory.\n", stderr); status = 2; goto FuncExit; } for (track = 1; track <= 35; track++) { max_sect = 17U + (track < 31) + (track < 25) + (track < 18) * 2U; if (track == 18 || track == 25) eveninc++, oddinc--; switch (track) { case 1: if (open_file ('1')) { OpenError: fprintf (stderr, "disk2zip: Error in opening file %s.\n", outname); status = 3; goto FuncExit; } break; case 9: if (open_file ('2')) goto OpenError; break; case 17: if (open_file ('3')) goto OpenError; break; case 26: if (open_file ('4')) goto OpenError; break; } if (max_sect != fread (trackbuf, 256, max_sect, infile)) { fputs ("disk2zip: Error in reading the input file.\n", stderr); status = 4; goto FuncExit; } if (write_track()) { status = 3; goto FuncExit; } } status = 0; FuncExit: if (infile && infile != stdin) fclose (infile); if (outfile) fclose (outfile); free (outname); return status; } cbmconvert-cbmconvert-2.1.5/empty_files.cmake000066400000000000000000000112261424020246600213450ustar00rootroot00000000000000MACRO(MD5SUM expected filename) EXECUTE_PROCESS(COMMAND ${CMAKE_COMMAND} -E md5sum ${filename} OUTPUT_VARIABLE md5) IF (NOT md5 MATCHES ${expected}) MESSAGE(FATAL_ERROR "unexpected md5sum: " ${md5}) ENDIF() ENDMACRO() MACRO(EXECUTE_PROGRAM) EXECUTE_PROCESS(COMMAND ${ARGV} RESULT_VARIABLE res) IF (res) MESSAGE(FATAL_ERROR "${ARGV} failed: " ${res}) ENDIF() ENDMACRO() MACRO(EXECUTE_PROGRAM_EXPECT expect_res) EXECUTE_PROCESS(COMMAND ${ARGN} RESULT_VARIABLE res) IF (NOT res EQUAL ${expect_res}) MESSAGE(FATAL_ERROR "${ARGN} failed: " ${res}) ENDIF() ENDMACRO() MACRO(CBMCONVERT) EXECUTE_PROGRAM(${CBMCONVERT} ${ARGV}) ENDMACRO() FILE(REMOVE empty.prg empty.d64 empty.c2n e empty.p00 empty.lnx emptz.d64 1!empty 2!empty 3!empty 4!empty) EXECUTE_PROGRAM_EXPECT(1 ${CBMCONVERT} -C) EXECUTE_PROGRAM_EXPECT(1 ${CBMCONVERT} -L) EXECUTE_PROGRAM_EXPECT(1 ${CBMCONVERT} -M) EXECUTE_PROGRAM_EXPECT(1 ${CBMCONVERT} -m) EXECUTE_PROGRAM_EXPECT(1 ${CBMCONVERT} -a -k -t -z) EXECUTE_PROGRAM_EXPECT(1 ${CBMCONVERT} -L empty.lnx -M) EXECUTE_PROGRAM_EXPECT(2 ${CBMCONVERT} -vw -D4 empty.d64 empty.prg) MD5SUM(274f94a63ada0913cf717677e536cdf9 empty.d64) FILE(WRITE empty.prg "") EXECUTE_PROGRAM_EXPECT(2 ${CBMCONVERT} -D4 empty.prg) EXECUTE_PROGRAM_EXPECT(2 ${CBMCONVERT} -D7 empty.prg) EXECUTE_PROGRAM_EXPECT(2 ${CBMCONVERT} -D8 empty.prg) FILE(WRITE empty.p00 "") EXECUTE_PROGRAM_EXPECT(4 ${CBMCONVERT} -p empty.p00) FILE(WRITE empty.p00 "26 octets invalid contents") EXECUTE_PROGRAM_EXPECT(4 ${CBMCONVERT} -p empty.p00) FILE(REMOVE empty.p00) FILE(WRITE e "") EXECUTE_PROGRAM_EXPECT(4 ${CBMCONVERT} -p e) FILE(REMOVE e) CBMCONVERT(-v1 -D4 empty.d64 empty.prg) MD5SUM(b92a237dc6356940542593037d2184cf empty.d64) CBMCONVERT(-v2 -L empty.lnx -d -- empty.d64) MD5SUM(67715c3da9c4c73168d2e7177ea25545 empty.lnx) CBMCONVERT(-vv -P -l empty.lnx) MD5SUM(34dca27bbc851ac44ca0817dabeb9593 empty.p00) EXECUTE_PROGRAM_EXPECT(4 ${CBMCONVERT} -v0 -P -l empty.prg) EXECUTE_PROGRAM_EXPECT(1 ${CBMCONVERT} -vi -P -l empty.prg) CBMCONVERT(-vv -C empty.c2n empty.prg) EXECUTE_PROGRAM(${CMAKE_COMMAND} -E compare_files empty.c2n empty.prg) CBMCONVERT(-N -c empty.c2n) EXECUTE_PROGRAM_EXPECT(1 ${DISK2ZIP} -i 0 empty.d64 empty) EXECUTE_PROGRAM_EXPECT(1 ${DISK2ZIP} -i 00000 empty.d64 empty) EXECUTE_PROGRAM_EXPECT(1 ${DISK2ZIP} -i0000 empty.d64 empty) EXECUTE_PROGRAM(${DISK2ZIP} -i 0000 -- empty.d64 empty) IF (CMAKE_VERSION VERSION_GREATER_EQUAL 3.19) FILE(CHMOD 4!empty PERMISSIONS OWNER_READ) EXECUTE_PROGRAM_EXPECT(3 ${DISK2ZIP} empty.d64 empty) FILE(CHMOD 3!empty PERMISSIONS OWNER_READ) EXECUTE_PROGRAM_EXPECT(3 ${DISK2ZIP} empty.d64 empty) FILE(CHMOD 2!empty PERMISSIONS OWNER_READ) EXECUTE_PROGRAM_EXPECT(3 ${DISK2ZIP} empty.d64 empty) FILE(CHMOD 1!empty PERMISSIONS OWNER_READ) EXECUTE_PROGRAM_EXPECT(3 ${DISK2ZIP} empty.d64 empty) FILE(CHMOD empty.d64 PERMISSIONS OWNER_READ) EXECUTE_PROGRAM_EXPECT(3 ${ZIP2DISK} empty) FILE(REMOVE 1!empty 2!empty 3!empty 4!empty) FILE(CHMOD empty.d64 PERMISSIONS OWNER_WRITE OWNER_READ) ENDIF() EXECUTE_PROGRAM_EXPECT(3 ${ZIP2DISK} empty e.d64) EXECUTE_PROGRAM_EXPECT(3 ${DISK2ZIP} -- - empty) EXECUTE_PROGRAM_EXPECT(4 ${DISK2ZIP} - empty INPUT_FILE empty.prg) EXECUTE_PROGRAM_EXPECT(4 ${DISK2ZIP} empty.prg empty) EXECUTE_PROGRAM(${DISK2ZIP} empty.d64 ././././././empty) MD5SUM(0c66b6561fb968faeb751eb94a68098b 1!empty) MD5SUM(ef4bae7508940492d5452b4f13965cab 2!empty) MD5SUM(026a1e36d2b20820920922d907394cf5 3!empty) MD5SUM(0cae9f355cf09bc153272c4b8d2b97cd 4!empty) EXECUTE_PROGRAM(${ZIP2DISK} ././empty e.d64) EXECUTE_PROGRAM(${CMAKE_COMMAND} -E compare_files empty.d64 e.d64) EXECUTE_PROGRAM(${ZIP2DISK} ./empty) EXECUTE_PROGRAM(${CMAKE_COMMAND} -E compare_files empty.d64 e.d64) FILE(RENAME 4!empty 4!e) EXECUTE_PROGRAM_EXPECT(3 ${ZIP2DISK} empty fail.d64) FILE(WRITE 4!empty "") EXECUTE_PROGRAM_EXPECT(4 ${ZIP2DISK} empty fail.d64) FILE(RENAME 3!empty 3!e) EXECUTE_PROGRAM_EXPECT(3 ${ZIP2DISK} empty fail.d64) FILE(WRITE 3!empty "") EXECUTE_PROGRAM_EXPECT(4 ${ZIP2DISK} empty fail.d64) FILE(RENAME 2!empty 2!e) EXECUTE_PROGRAM_EXPECT(3 ${ZIP2DISK} empty fail.d64) FILE(WRITE 2!empty "") EXECUTE_PROGRAM_EXPECT(4 ${ZIP2DISK} empty fail.d64) FILE(RENAME 1!empty 1!e) EXECUTE_PROGRAM_EXPECT(3 ${ZIP2DISK} empty fail.d64) EXECUTE_PROGRAM(${CMAKE_COMMAND} -E compare_files fail.d64 empty.prg) FILE(WRITE 1!empty "") EXECUTE_PROGRAM_EXPECT(4 ${ZIP2DISK} empty fail.d64) EXECUTE_PROGRAM(${CMAKE_COMMAND} -E compare_files fail.d64 empty.prg) EXECUTE_PROGRAM(${ZIP2DISK} e empty.d64) EXECUTE_PROGRAM(${CMAKE_COMMAND} -E compare_files empty.d64 e.d64) FILE(REMOVE empty.prg empty.d64 empty.c2n empty.p00 empty.lnx e.d64 fail.d64 1!empty 2!empty 3!empty 4!empty 1!e 2!e 3!e 4!e) cbmconvert-cbmconvert-2.1.5/file_names.cmake000066400000000000000000000046561424020246600211400ustar00rootroot00000000000000MACRO(EXECUTE_PROGRAM) EXECUTE_PROCESS(COMMAND ${ARGV} RESULT_VARIABLE res) IF (res) MESSAGE(FATAL_ERROR "${ARGV} failed: " ${res}) ENDIF() ENDMACRO() MACRO(EXECUTE_PROGRAM_EXPECT expect_res) EXECUTE_PROCESS(COMMAND ${ARGN} RESULT_VARIABLE res) IF (NOT res EQUAL ${expect_res}) MESSAGE(FATAL_ERROR "${ARGN} failed: " ${res}) ENDIF() ENDMACRO() MACRO(CBMCONVERT) EXECUTE_PROGRAM(${CBMCONVERT} ${ARGV}) ENDMACRO() FILE(GLOB junk junk*.prg _.[p0-9][0-9][0-9] wyzxxxcz.[p0-9][0-9][0-9]) FILE(REMOVE f.c2n _.prg wyzxxxcz.prg ${junk} -P aaaaaaaa.prg aaaaaaaa.p00 zzzzzzzz.prg zzzzzzzz.p00 00000000.prg 00000000.p00) STRING(ASCII 3 3 4 24 4 t21) STRING(ASCII 3 3 4 8 4 t5) STRING(ASCII 119 65 47 31 217 218 219 255 120 120 120 99 218 99 193 255 f) SET(s64 " ") FILE(WRITE f.c2n "${t21}${f}${s64}${s64}${s64}${t5}${s64}${s64}${s64}") EXECUTE_PROGRAM_EXPECT(2 ${CBMCONVERT} -I -c f.c2n f.c2n -P f.c2n) FOREACH(i RANGE 1 10) CBMCONVERT(-Pc f.c2n f.c2n f.c2n f.c2n f.c2n f.c2n f.c2n f.c2n f.c2n f.c2n) ENDFOREACH() EXECUTE_PROGRAM_EXPECT(4 ${CBMCONVERT} -Pc f.c2n) FILE(GLOB junk _.[p0-9][0-9][0-9] wyzxxxcz.[p0-9][0-9][0-9]) FILE(REMOVE _.prg wyzxxxcz.prg ${junk}) FOREACH(i RANGE 1 100) CBMCONVERT(-Ic f.c2n f.c2n f.c2n f.c2n f.c2n f.c2n f.c2n f.c2n f.c2n f.c2n) ENDFOREACH() CBMCONVERT(-Ic f.c2n) EXECUTE_PROGRAM_EXPECT(4 ${CBMCONVERT} -Ic f.c2n) FILE(GLOB junk _.[p0-9][0-9][0-9] wyzxxxcz.[p0-9][0-9][0-9]) FILE(REMOVE f.c2n _.prg wyzxxxcz.prg ${junk}) STRING(ASCII 160 160 160 160 160 160 160 160 160 160 160 160 160 160 160 160 s) FILE(WRITE f.c2n "${t21}${s}${s64}${s64}${s64}") FILE(APPEND f.c2n "${t21}AAAAAAAAAAAAAAAA${s64}${s64}${s64}") FILE(APPEND f.c2n "${t21}ZZZZZZZZZZZZZZZZ${s64}${s64}${s64}") FILE(APPEND f.c2n "${t21}0000000000000000${s64}${s64}${s64}") CBMCONVERT(-Ic f.c2n) CBMCONVERT(-Pc f.c2n) FILE(REMOVE _.prg _.p00 aaaaaaaa.prg aaaaaaaa.p00 zzzzzzzz.prg zzzzzzzz.p00 00000000.prg 00000000.p00) FILE(WRITE f.c2n "${t21}JUNK ${s64}${s64}${s64}") FOREACH(i RANGE 1 10) CBMCONVERT(-c f.c2n f.c2n f.c2n f.c2n f.c2n f.c2n f.c2n f.c2n f.c2n f.c2n) ENDFOREACH() IF (FALSE) # This is very slow FOREACH(i RANGE 11 1000) CBMCONVERT(-c f.c2n f.c2n f.c2n f.c2n f.c2n f.c2n f.c2n f.c2n f.c2n f.c2n) ENDFOREACH() CBMCONVERT(-c f.c2n) EXECUTE_PROGRAM_EXPECT(4 ${CBMCONVERT} -c f.c2n) ENDIF() FILE(GLOB junk junk~*.prg) FILE(REMOVE f.c2n junk.prg ${junk}) cbmconvert-cbmconvert-2.1.5/image.c000066400000000000000000002504571424020246600172640ustar00rootroot00000000000000/** * @file image.c * Disk image management * @author Marko Mäkelä (marko.makela at iki.fi) * @author Pasi Ojala (albert at cs.tut.fi) */ /* ** Copyright © 1993-1998,2001,2006,2021,2022 Marko Mäkelä ** 1581 disk image management by Pasi Ojala ** ** 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. */ #include #include #include #include #include #include "output.h" #include "input.h" /* Disk geometry information structure */ struct DiskGeometry { /** disk image type identifier */ enum ImageType type; /** disk image size */ size_t blocks; /** format specifier */ byte_t formatID; /** number of BAM blocks */ byte_t BAMblocks; /** directory track number */ byte_t dirtrack; /** number of disk tracks */ byte_t tracks; /** number of sectors per track */ byte_t* sectors; /** sector interleaves (number of sectors to advance) */ byte_t* interleave; }; /* Disk directory entry */ struct DirEnt { /** track of next directory block (only the first dirent of the block) */ byte_t nextTrack; /** sector of next directory block (only the first dirent of the block) */ byte_t nextSector; /** Commodore file type */ byte_t type; /** track of the first file data block */ byte_t firstTrack; /** sector of the first file data block */ byte_t firstSector; /** Commodore file name */ byte_t name[16]; /** track of the first side sector (relative files only) */ byte_t ssTrack; /** sector of the first side sector (relative files only) */ byte_t ssSector; /** relative file record length */ byte_t recordLength; /** info block track of GEOS file */ #define infoTrack ssTrack /** info block sector of GEOS file */ #define infoSector ssSector /** GEOS file format: 1=VLIR, 0=sequential */ #define isVLIR recordLength /** GEOS time stamp (otherwise unused bytes) */ struct { byte_t type; /**< GEOS file type */ byte_t year; /**< GEOS file time stamp: year */ byte_t month; /**< GEOS file time stamp: month */ byte_t day; /**< GEOS file time stamp: day */ byte_t hour; /**< GEOS file time stamp: hour */ byte_t minute; /**< GEOS file time stamp: minute */ } geos; /** file's total block count, least significant byte */ byte_t blocksLow; /** file's total block count, most significant byte */ byte_t blocksHigh; }; /** CP/M disk directory entry */ struct CpmDirEnt { /** user area 0-0xF or 0xE5 (unused entry) */ byte_t area; /** file base name (bits 0..6);
* basename[0]..basename[3]: bit7=1 for user defined attributes 1 to 4;
* basename[4]..basename[7]: bit7=1 for system interface attributes (BDOS) */ byte_t basename[8]; /** file suffix (bits 0..6);
* suffix[0]: bit7=1 for read-only;
* suffix[1]: bit7=1 for system file;
* suffix[2]: bit7=1 for archive */ byte_t suffix[3]; /** number of directory extent */ byte_t extent; /** unused bytes */ byte_t unused[2]; /** number of 128-byte blocks in this extent (max $80) */ byte_t blocks; /** file block pointers, in this case 8-bit */ byte_t block[16]; }; /** Calculate the allocation blocks of a CP/M file * @param block the file block pointers * @param i index to the file block pointers * @return the corresponding allocation block */ #define CPMBLOCK(block,i) \ (au == 8 ? block[i] : block[2 * (i)] + \ ((unsigned) block[2 * (i) + 1] << 8)) /** table of sectors per track on the 1541 */ static byte_t sect1541[] = { 21, 21, 21, 21, 21, 21, 21, 21, 21, /* tracks 1 .. 9 */ 21, 21, 21, 21, 21, 21, 21, 21, /* tracks 10 .. 17 */ 19, 19, 19, 19, 19, 19, 19, /* tracks 18 .. 24 */ 18, 18, 18, 18, 18, 18, /* tracks 25 .. 30 */ 17, 17, 17, 17, 17 /* tracks 31 .. 35 */ }; /** table of sectors per track on the 1571 */ static byte_t sect1571[] = { 21, 21, 21, 21, 21, 21, 21, 21, 21, /* tracks 1 .. 9 */ 21, 21, 21, 21, 21, 21, 21, 21, /* tracks 10 .. 17 */ 19, 19, 19, 19, 19, 19, 19, /* tracks 18 .. 24 */ 18, 18, 18, 18, 18, 18, /* tracks 25 .. 30 */ 17, 17, 17, 17, 17, /* tracks 31 .. 35 */ 21, 21, 21, 21, 21, 21, 21, 21, 21, /* tracks 36 .. 44 */ 21, 21, 21, 21, 21, 21, 21, 21, /* tracks 45 .. 52 */ 19, 19, 19, 19, 19, 19, 19, /* tracks 53 .. 59 */ 18, 18, 18, 18, 18, 18, /* tracks 60 .. 65 */ 17, 17, 17, 17, 17 /* tracks 66 .. 70 */ }; /** table of sectors per track on the 1581 */ static byte_t sect1581[] = { 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40 }; /** table of interleave per track on the 1541 */ static byte_t int1541[] = { 10, 10, 10, 10, 10, 10, 10, 10, 10, /* tracks 1 .. 9 */ 10, 10, 10, 10, 10, 10, 10, 10, /* tracks 10 .. 17 */ 3, 10, 10, 10, 10, 10, 10, /* tracks 18 .. 24 */ 10, 10, 10, 10, 10, 10, /* tracks 25 .. 30 */ 10, 10, 10, 10, 10 /* tracks 31 .. 35 */ }; /** table of interleave per track on the 1571 */ static byte_t int1571[] = { 10, 10, 10, 10, 10, 10, 10, 10, 10, /* tracks 1 .. 9 */ 10, 10, 10, 10, 10, 10, 10, 10, /* tracks 10 .. 17 */ 3, 10, 10, 10, 10, 10, 10, /* tracks 18 .. 24 */ 10, 10, 10, 10, 10, 10, /* tracks 25 .. 30 */ 10, 10, 10, 10, 10, /* tracks 31 .. 35 */ 10, 10, 10, 10, 10, 10, 10, 10, 10, /* tracks 36 .. 44 */ 10, 10, 10, 10, 10, 10, 10, 10, /* tracks 45 .. 52 */ 3, 10, 10, 10, 10, 10, 10, /* tracks 53 .. 59 */ 10, 10, 10, 10, 10, 10, /* tracks 60 .. 65 */ 10, 10, 10, 10, 10 /* tracks 66 .. 70 */ }; /** table of interleave per track on the 1581 */ static byte_t int1581[] = { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }; /** The disk geometry database. If you add more drives, you will need * to update the ImageType definition in output.h and all related * switch statements in this file. */ static struct DiskGeometry diskGeometry[] = { { Im1541, 683, 'A', 1, 18, elementsof(sect1541), sect1541 - 1, int1541 - 1 }, { Im1571, 1366, 'A', 1, 18, elementsof(sect1571), sect1571 - 1, int1571 - 1 }, { Im1581, 3200, 'D', 1, /* BAM blocks are in a separate chain */ 40, elementsof(sect1581), sect1581 - 1, int1581 - 1 }, }; /** Determine disk image geometry. * @param type the disk image type * @return the corresponding geometry, or NULL if none found */ static const struct DiskGeometry* getGeometry (enum ImageType type) { int i; for (i = elementsof(diskGeometry); i--; ) if (diskGeometry[i].type == type) return &diskGeometry[i]; return 0; } /** Get a pointer to the block in the specified track and sector. * @param image the disk image * @param track the track number * @param sector the sector number * @return pointer to the first byte in the sector, or NULL */ static byte_t* getBlock (struct Image* image, byte_t track, byte_t sector) { const struct DiskGeometry* geom; int t, b; if (!image || !image->buf || !(geom = getGeometry (image->type))) return 0; if (track < 1 || track > geom->tracks || sector >= geom->sectors[track]) return 0; /* illegal track or sector */ for (t = 1, b = 0; t < track; t++) b += geom->sectors[t]; b += sector; return &image->buf[b << 8]; } /** Determine if the block at the specified track and sector is free. * @param image the disk image * @param track the track number * @param sector the sector number * @return true if the block is available */ static bool isFreeBlock (const struct Image* image, byte_t track, byte_t sector) { const struct DiskGeometry* geom; if (!image || !image->buf || !(geom = getGeometry (image->type))) return false; if (track < 1 || track > geom->tracks || sector >= geom->sectors[track]) return false; /* illegal track or sector */ switch (image->type) { const byte_t* BAM; case ImUnknown: return false; case Im1571: if (track > 35) { track -= 36; if (!(BAM = getBlock ((struct Image*) image, image->dirtrack + 35, 0))) return false; return true && BAM[(track * 3) + (sector >> 3)] & (1 << (sector & 7)); } /* fall through: track <= 35 */ case Im1541: if (!(BAM = getBlock ((struct Image*) image, image->dirtrack, 0))) return false; return true && BAM[(track << 2) + 1 + (sector >> 3)] & (1 << (sector & 7)); case Im1581: if(track > image->partTops[image->dirtrack - 1] || track < image->partBots[image->dirtrack - 1]) return false; if (!(BAM = getBlock ((struct Image*) image, image->dirtrack, 1))) return false; if (track > 40) { if (!(BAM = getBlock ((struct Image*) image, BAM[0], BAM[1]))) return false; track -= 40; } return true && (BAM[16 + (track - 1) * 6 + (sector >> 3) + 1] & (1 << (sector & 7))); } return false; } /** Find the next free block that is closest to the specified track and sector. * @param image the disk image * @param track (input/output) the track number * @param sector (input/output) the sector number * @return true if a block was found */ static bool findNextFree (const struct Image* image, byte_t* track, byte_t* sector) { const struct DiskGeometry* geom; byte_t t = *track, s = *sector; size_t visited[64 / (sizeof(size_t) * CHAR_BIT)]; unsigned i; if (!image || !image->buf || !(geom = getGeometry (image->type))) return false; if (t < 1 || t > geom->tracks || s >= geom->sectors[t]) return false; if (t >= image->dirtrack) { /* search from the current track upwards */ for (; t <= image->partTops[image->dirtrack - 1]; t++) { memset(visited, 0, sizeof visited); for (i = geom->sectors[t]; i; i--) { if (isFreeBlock (image, t, s)) { *track = t; *sector = s; return true; } visited[s / ((sizeof *visited) * CHAR_BIT)] |= ((size_t) 1) << (s % ((sizeof *visited) * CHAR_BIT)); s += geom->interleave[t]; s %= geom->sectors[t]; while (visited[s / ((sizeof *visited) * CHAR_BIT)] & ((size_t) 1) << (s % ((sizeof *visited) * CHAR_BIT))) if (++s == geom->sectors[t]) { s = 0; if (i == 1) break; i--; } } } /* search from lower tracks (from the directory track downwards) */ for (t = image->dirtrack - 1; t >= image->partBots[image->dirtrack - 1]; t--) { memset(visited, 0, sizeof visited); for (i = geom->sectors[t]; i; i--) { if (isFreeBlock (image, t, s)) { *track = t; *sector = s; return true; } visited[s / ((sizeof *visited) * CHAR_BIT)] |= ((size_t) 1) << (s % ((sizeof *visited) * CHAR_BIT)); s += geom->interleave[t]; s %= geom->sectors[t]; while (visited[s / ((sizeof *visited) * CHAR_BIT)] & ((size_t) 1) << (s % ((sizeof *visited) * CHAR_BIT))) if (++s == geom->sectors[t]) { s = 0; if (i == 1) break; i--; } } } } else { /* search from the current track downwards */ for (; t >= image->partBots[image->dirtrack - 1]; t--) { memset(visited, 0, sizeof visited); for (i = geom->sectors[t]; i; i--) { if (isFreeBlock (image, t, s)) { *track = t; *sector = s; return true; } visited[s / ((sizeof *visited) * CHAR_BIT)] |= ((size_t) 1) << (s % ((sizeof *visited) * CHAR_BIT)); s += geom->interleave[t]; s %= geom->sectors[t]; while (visited[s / ((sizeof *visited) * CHAR_BIT)] & ((size_t) 1) << (s % ((sizeof *visited) * CHAR_BIT))) if (++s == geom->sectors[t]) { s = 0; if (i == 1) break; i--; } } } /* search from upper tracks (from the directory track upwards) */ for (t = image->dirtrack + 1; t <= image->partTops[image->dirtrack - 1]; t++) { memset(visited, 0, sizeof visited); for (i = geom->sectors[t]; i; i--) { if (isFreeBlock (image, t, s)) { *track = t; *sector = s; return true; } visited[s / ((sizeof *visited) * CHAR_BIT)] |= ((size_t) 1) << (s % ((sizeof *visited) * CHAR_BIT)); s += geom->interleave[t]; s %= geom->sectors[t]; while (visited[s / ((sizeof *visited) * CHAR_BIT)] & ((size_t) 1) << (s % ((sizeof *visited) * CHAR_BIT))) if (++s == geom->sectors[t]) { s = 0; if (i == 1) break; i--; } } } /* last resort: search from the directory track */ t = image->dirtrack; memset(visited, 0, sizeof visited); for (i = geom->sectors[t]; i; i--) { if (isFreeBlock (image, t, s)) { *track = t; *sector = s; return true; } visited[s / ((sizeof *visited) * CHAR_BIT)] |= ((size_t) 1) << (s % ((sizeof *visited) * CHAR_BIT)); s += geom->interleave[t]; s %= geom->sectors[t]; while (visited[s / ((sizeof *visited) * CHAR_BIT)] & ((size_t) 1) << (s % ((sizeof *visited) * CHAR_BIT))) if (++s == geom->sectors[t]) { s = 0; if (i == 1) break; i--; } } } return false; } /** Get a block pointer table to all blocks in the file * starting at the specified track and sector. * @param buf the block pointer table * @param image the disk image * @param track track number of the file's first block * @param sector sector number of the file's first block * @param log Call-back function for diagnostic output * @param dirent directory entry for diagnostic output (optional) * @return the number of blocks mapped (0 on failure) */ static size_t mapInode (byte_t*** buf, struct Image* image, byte_t track, byte_t sector, log_t log, const struct DirEnt* dirent) { const struct DiskGeometry* geom; byte_t t, s; size_t size; byte_t* block; if (!buf || *buf || !image || !image->buf || !(geom = getGeometry (image->type))) return 0; /* Determine the number of blocks. */ for (t = track, s = sector, size = 0; t; size++) { if (size > geom->blocks) return 0; /* endless file */ if (!(block = getBlock (image, t, s))) return 0; if (isFreeBlock (image, t, s)) { if (log) { struct Filename name; if (dirent) { memcpy (name.name, dirent->name, sizeof name.name); name.type = dirent->type; name.recordLength = dirent->recordLength; } (*log) (Warnings, dirent ? &name : 0, "Unallocated block %u,%u reachable from %u,%u", t, s, track, sector); } else return 0; } t = block[0]; s = block[1]; } /* Set up the block pointer table. */ if (!(*buf = malloc (size * sizeof **buf))) return 0; for (t = track, s = sector, size = 0; t; size++) { if (!((*buf)[size] = getBlock (image, t, s))) { free (*buf); *buf = 0; return 0; } t = (*buf)[size][0]; s = (*buf)[size][1]; } return size; } /** Allocate the block at the specified track and sector. * Set the track and sector to the next block candidate. * @param image the disk image * @param track (input/output) the track number * @param sector (input/output) the sector number * @return true if a block was allocated */ static bool allocBlock (struct Image* image, byte_t* track, byte_t* sector) { const struct DiskGeometry* geom; if (!track || !sector || !image || !image->buf || !(geom = getGeometry (image->type))) return false; if (*track < 1 || *track > geom->tracks || *sector >= geom->sectors[*track]) return false; /* illegal track or sector */ switch (image->type) { byte_t* BAM; case ImUnknown: return false; case Im1571: if (*track > 35) { byte_t tr = *track - 35; byte_t* BAM2; if (!(BAM = getBlock ((struct Image*) image, image->dirtrack, 0)) || !(BAM2 = getBlock ((struct Image*) image, 35 + image->dirtrack, 0))) return false; if (!(BAM2[((tr - 1) * 3) + (*sector >> 3)] & (1 << (*sector & 7)))) return false; /* already allocated */ /* decrement the count of free sectors per track */ BAM[0xDC + tr]--; /* allocate the block */ BAM2[((tr - 1) * 3) + (*sector >> 3)] &= (byte_t) ~(1 << (*sector & 7)); /* find next free block */ findNextFree (image, track, sector); return true; } /* fall through: track <= 35 */ case Im1541: if (!(BAM = getBlock ((struct Image*) image, image->dirtrack, 0))) return false; if (!(BAM[(*track << 2) + 1 + (*sector >> 3)] & (1 << (*sector & 7)))) return false; /* already allocated */ /* decrement the count of free sectors per track */ BAM[*track << 2]--; /* allocate the block */ BAM[(*track << 2) + 1 + (*sector >> 3)] &= (byte_t) ~(1 << (*sector & 7)); /* find next free block */ findNextFree (image, track, sector); return true; case Im1581: { byte_t** BAMblocks = 0; byte_t offset; if(*track > image->partTops[image->dirtrack - 1] || *track < image->partBots[image->dirtrack - 1]) return false; if (2 != mapInode (&BAMblocks, image, image->dirtrack, 1, 0, 0)) { if (BAMblocks) free (BAMblocks); return false; } offset = *track; if(offset > 40) { BAM = BAMblocks[1]; offset -= 40; } else BAM = BAMblocks[0]; free (BAMblocks); if (!(BAM[16 + (offset - 1) * 6 + (*sector >> 3) + 1] & (1 << (*sector & 7)))) return false; /* already allocated */ BAM[16 + (offset - 1) * 6] -= 1; BAM[16 + (offset - 1) * 6 + (*sector >> 3) + 1] &= (byte_t) ~(1 << (*sector & 7)); /* find next free block */ findNextFree (image, track, sector); return true; } } return false; } /** Format disk image. * @param image the image to be formatted */ static void FormatImage (struct Image* image) { const struct DiskGeometry* geom; const char id1 = '9'; /* disk ID characters */ const char id2 = '8'; const char title[] = "CBMCONVERT 2.0"; /* disk title */ if (!image || !image->buf || !(geom = getGeometry (image->type))) return; /* Clear all sectors */ memset (image->buf, 0, geom->blocks * 256); switch (image->type) { byte_t track, sector; byte_t* BAM; case ImUnknown: return; case Im1541: /* Initialize the BAM */ if (!(BAM = getBlock (image, image->dirtrack, 0))) return; /* set the track/sector links */ BAM[0] = image->dirtrack; BAM[1] = 1; BAM[0x100] = 0; /* first directory block */ BAM[0x101] = 0xFF; /* set the format identifier */ BAM[2] = geom->formatID; BAM[3] = 0x00; /* set the disk title */ memcpy (&BAM[0x90], title, 16); /* pad the disk header */ memset (&BAM[0xA0], 0xA0, 11); /* set the format specifier */ BAM[0xA5] = '2'; BAM[0xA6] = geom->formatID; /* set the disk ID */ BAM[0xA2] = id1; BAM[0xA3] = id2; /* free all blocks */ memset (&BAM[4], 0xFF, (size_t) geom->tracks << 2); for (track = 1; track <= geom->tracks; track++) { /* set amount of free blocks on each track */ BAM[track << 2] = sector = geom->sectors[track]; /* allocate non-existent blocks */ for (; sector < 24; sector++) BAM[(track << 2) + 1 + (sector >> 3)] &= (byte_t) ~(1 << (sector & 7)); } /* Allocate the BAM and directory entries. */ track = image->dirtrack; sector = 0; allocBlock (image, &track, §or); track = BAM[0]; sector = BAM[1]; allocBlock (image, &track, §or); break; case Im1571: /* Initialize the BAM */ if (!(BAM = getBlock (image, image->dirtrack, 0))) return; /* set the track/sector links */ BAM[0] = image->dirtrack; BAM[1] = 1; BAM[0x100] = 0; /* first directory block */ BAM[0x101] = 0xFF; /* set the format identifier */ BAM[2] = geom->formatID; BAM[3] = 0x80; /* set the disk title */ memcpy (&BAM[0x90], title, 16); /* pad the disk header */ memset (&BAM[0xA0], 0xA0, 11); /* set the format specifier */ BAM[0xA5] = '2'; BAM[0xA6] = geom->formatID; /* set the disk ID */ BAM[0xA2] = id1; BAM[0xA3] = id2; /* free all blocks */ memset (&BAM[4], 0xFF, 35 << 2); memset (&BAM[683 << 8], 0xFF, 35 * 3); for (track = 1; track <= 35; track++) { /* set amount of free blocks on each track */ BAM[track << 2] = sector = geom->sectors[track]; /* allocate non-existent blocks */ for (; sector < 24; sector++) BAM[(track << 2) + 1 + (sector >> 3)] &= (byte_t) ~(1 << (sector & 7)); } /* second side */ for (track = 0; track < 35; track++) { /* set amount of free blocks on each track */ BAM[0xDC + track + 1] = sector = geom->sectors[track + 1]; /* allocate non-existent blocks */ for (; sector < 24; sector++) BAM[(683 << 8) + (track * 3) + (sector >> 3)] &= (byte_t) ~(1 << (sector & 7)); } /* Allocate the BAM and directory entries. */ track = image->dirtrack; sector = 0; allocBlock (image, &track, §or); track = BAM[0]; sector = BAM[1]; allocBlock (image, &track, §or); track = image->dirtrack + 35; sector = 0; allocBlock (image, &track, §or); break; case Im1581: /* Initialize the Header block */ if (!(BAM = getBlock (image, image->dirtrack, 0))) return; image->partTops[image->dirtrack - 1] = geom->tracks; image->partBots[image->dirtrack - 1] = 1; image->partUpper[image->dirtrack - 1] = 0; /* set the track/sector link to the first directory block */ BAM[0] = image->dirtrack; BAM[1] = 3; BAM[0x100] = image->dirtrack; /* BAM block 1 */ BAM[0x101] = 2; BAM[0x200] = 0; /* BAM block 2 */ BAM[0x201] = 0xFF; BAM[0x300] = 0; BAM[0x301] = 0xFF; /* first directory block */ /* set the format identifier */ BAM[2] = geom->formatID; BAM[3] = 0; /* set the disk title */ memcpy (&BAM[0x4], title, 16); /* pad the disk header */ memset (&BAM[0x14], 0xA0, 11); /* set the format specifier */ BAM[0x19] = '3'; BAM[0x1a] = geom->formatID; /* set the disk ID */ BAM[0x16] = id1; BAM[0x17] = id2; /* BAM block 1 */ BAM = getBlock (image, image->dirtrack, 1); BAM[2] = geom->formatID; BAM[3] = ~geom->formatID; BAM[4] = id1; /* Disk ID */ BAM[5] = id2; BAM[6] = 192; /* I/O byte */ BAM[7] = 0; /* Auto loader flag */ for (track = image->partBots[image->dirtrack - 1]; track <= image->partTops[image->dirtrack - 1] && track <= 40; track++) { byte_t* tmp = BAM + 16 + (track - 1) * 6; /* set amount of free blocks on each track */ tmp[0] = (track == image->dirtrack) ? 36 : 40; /* 4 reserved blocks on dirtrack */ tmp[1] = (track == image->dirtrack) ? 0xf0 : 0xff; /* ditto */ tmp[2] = tmp[3] = tmp[4] = tmp[5] = 0xff; } /* BAM block 2 */ BAM = getBlock (image, BAM[0], BAM[1]); BAM[2] = geom->formatID; BAM[3] = ~geom->formatID; BAM[4] = id1; /* Disk ID */ BAM[5] = id2; BAM[6] = 192; /* I/O byte (copy) */ BAM[7] = 0; /* Auto loader flag (copy) */ for (track = image->partTops[image->dirtrack - 1]; track >= image->partBots[image->dirtrack - 1] && track > 40; track--) { byte_t* tmp = BAM + 16 + (track - 41) * 6; /* set amount of free blocks on each track */ tmp[0] = 40; tmp[1] = tmp[2] = tmp[3] = tmp[4] = tmp[5] = 0xff; } break; } } /** Read a file starting at the specified track and sector to a buffer * @param buf the buffer * @param image the disk image * @param track track number of the file's first block * @param sector sector number of the file's first block * @return the file length, or 0 on error */ static size_t readInode (byte_t** buf, const struct Image* image, byte_t track, byte_t sector) { const struct DiskGeometry* geom; byte_t t, s; size_t size; if (!buf || *buf || !image || !image->buf || !(geom = getGeometry (image->type))) return 0; /* Determine the file size. */ for (t = track, s = sector, size = 0; t; size += 254) { const byte_t* block; if (size > 254 * geom->blocks) return 0; /* endless file */ if (!(block = getBlock ((struct Image*) image, t, s))) return 0; if (isFreeBlock (image, t, s)) return 0; t = block[0]; s = block[1]; } if (s < 2) return 0; /* The last byte pointer must be at least 2. */ size += s - 255; if (!(*buf = malloc (size))) return 0; /* Read the file. */ for (t = track, s = sector, size = 0; t; size += 254) { const byte_t* block = getBlock ((struct Image*) image, t, s); t = block[0]; s = block[1]; memcpy (&(*buf)[size], &block[2], t ? 254 : s - 1); } return size += s - 255; } /** Make a back-up copy of the disk image's Block Availability Map. * @param image the disk image * @param BAM (output) the BAM backup * @return true on success */ static bool backupBAM (const struct Image* image, byte_t** BAM) { const struct DiskGeometry* geom; if (!BAM || *BAM || !image || !image->buf || !(geom = getGeometry (image->type))) return false; switch (image->type) { const byte_t* bamblock; case ImUnknown: case Im1541: if (!(bamblock = getBlock ((struct Image*) image, image->dirtrack, 0))) return false; if (!(*BAM = malloc ((size_t) geom->tracks << 2))) return false; memcpy (*BAM, &bamblock[4], (size_t) geom->tracks << 2); return true; case Im1571: if (!(bamblock = getBlock ((struct Image*) image, image->dirtrack, 0))) return false; if (!(*BAM = malloc ((size_t) geom->tracks << 2))) return false; memcpy (*BAM, &bamblock[4], 35 << 2); memcpy (*BAM + (35 << 2), &bamblock[0xDD], 35); memcpy (*BAM + 35 * 5, bamblock + (683 << 8), 35 * 3); return true; case Im1581: { byte_t** bamblocks = 0; if (2 != mapInode (&bamblocks, (struct Image*) image, image->dirtrack, 1, 0, 0)) { if (bamblocks) free (bamblocks); return false; } if (!(*BAM = malloc (2 << 8))) return false; memcpy (*BAM, bamblocks[0], 256); memcpy (*BAM + 256, bamblocks[1], 256); free (bamblocks); return true; } } return false; } /** Restore a back-up copy of the disk image's Block Availability Map. * @param image the disk image * @param BAM the BAM backup * @return true on success */ static bool restoreBAM (struct Image* image, byte_t** BAM) { const struct DiskGeometry* geom; if (!BAM || !*BAM || !image || !image->buf || !(geom = getGeometry (image->type))) return false; switch (image->type) { case ImUnknown: break; case Im1541: { byte_t* bamblock; if (!(bamblock = getBlock (image, image->dirtrack, 0))) return false; memcpy (&bamblock[4], *BAM, (size_t) geom->tracks << 2); } done: free (*BAM); *BAM = 0; return true; case Im1571: { byte_t* bamblock; if (!(bamblock = getBlock (image, image->dirtrack, 0))) return false; memcpy (&bamblock[4], *BAM, 35 << 2); memcpy (&bamblock[0xDD], *BAM + (35 << 2), 35); memcpy (&bamblock[683 << 8], *BAM + 35 * 5, 35 * 3); goto done; } case Im1581: { byte_t** bamblocks = 0; if (2 != mapInode (&bamblocks, image, image->dirtrack, 1, 0, 0)) { if (bamblocks) free (bamblocks); return false; } memcpy (bamblocks[0], *BAM, 256); memcpy (bamblocks[1], *BAM + 256, 256); free (bamblocks); goto done; } } return false; } /** Write a file to the disk, starting from the specified track and sector. * @param image the disk image * @param track track number of the first file block * @param sector sector number of the first file block * @param buf the file contents * @param size length of the file contnets * @return status of the operation */ static enum WrStatus writeInode (struct Image* image, byte_t track, byte_t sector, const byte_t* buf, size_t size) { byte_t t, s; size_t count; byte_t* oldBAM = 0; if (!buf || !image || !image->buf) return WrFail; /* Make a copy of the BAM. */ if (!backupBAM (image, &oldBAM)) return WrFail; /* Write the file. */ for (t = track, s = sector, count = 0; count < size; count += 254) { byte_t* block; if (!(block = getBlock (image, t, s))) { restoreBAM (image, &oldBAM); return WrFail; } if (!allocBlock (image, &t, &s)) { restoreBAM (image, &oldBAM); return WrNoSpace; } if (count + 254 < size) { /* not yet last block */ block[0] = t; block[1] = s; memcpy (&block[2], &buf[count], 254); } else { block[0] = 0; block[1] = (byte_t) (size - count + 1); memcpy (&block[2], &buf[count], size - count); } } free (oldBAM); return WrOK; } /** Free the block at the specified track and sector. * @param image the disk image * @param track track number of the block * @param sector sector number of the block * @return true on success */ static bool freeBlock (struct Image* image, byte_t track, byte_t sector) { const struct DiskGeometry* geom; if (!image || !image->buf || !(geom = getGeometry (image->type))) return false; if (track < 1 || track > geom->tracks || sector >= geom->sectors[track]) return false; /* illegal track or sector */ if (isFreeBlock (image, track, sector)) return false; /* already freed */ switch (image->type) { byte_t* BAM; case ImUnknown: return false; case Im1571: if (track > 35) { byte_t tr = track - 35; byte_t* BAM2; if (!(BAM = getBlock ((struct Image*) image, image->dirtrack, 0)) || !(BAM2 = getBlock ((struct Image*) image, 35 + image->dirtrack, 0))) return false; /* increment the count of free sectors per track */ BAM[0xDC + tr]++; /* free the block */ BAM2[((tr - 1) * 3) + (sector >> 3)] |= (byte_t) (1 << (sector & 7)); return true; } /* fall through: track <= 35 */ case Im1541: if (!(BAM = getBlock ((struct Image*) image, image->dirtrack, 0))) return false; /* increment the count of free sectors per track */ BAM[track << 2]++; /* free the block */ BAM[(track << 2) + 1 + (sector >> 3)] |= (byte_t) (1 << (sector & 7)); return true; case Im1581: { byte_t** BAMblocks = 0; if(track > image->partTops[image->dirtrack - 1] || track < image->partBots[image->dirtrack - 1]) return false; if (2 != mapInode (&BAMblocks, image, image->dirtrack, 1, 0, 0)) { if (BAMblocks) free (BAMblocks); return false; } if(track > 40) { BAM = BAMblocks[1]; track -= 40; } else BAM = BAMblocks[0]; free (BAMblocks); BAM[16 + (track - 1) * 6] += 1; BAM[16 + (track - 1) * 6 + (sector >> 3) + 1] |= (byte_t) (1 << (sector & 7)); return true; } } return false; } /** Wipe out and delete the file starting at the specified track and sector. * @param image the disk image * @param track track number of the first file block * @param sector sector number of the first file block * @param do_it flag: really remove the file * @return status of the operation */ static enum ImStatus deleteInode (struct Image* image, byte_t track, byte_t sector, bool do_it) { byte_t t, s; if (!image || !image->buf) return ImFail; /* Make sure that the whole file has been allocated. */ for (t = track, s = sector; t; ) { byte_t* block; if (!(block = getBlock (image, t, s))) return ImFail; if (isFreeBlock (image, t, s)) return ImFail; t = block[0]; s = block[1]; } if (do_it) { /* Free the space allocated by the file. */ for (t = track, s = sector; t; ) { byte_t* block = getBlock (image, t, s); freeBlock (image, t, s); t = block[0]; s = block[1]; /* clear the block */ memset (block, 0, 256); } } return ImOK; } /** Find the directory corresponding to a file * @param image the disk image * @param name the Commodore file name * @return the corresponding directory entry, or NULL */ static struct DirEnt* getDirEnt (struct Image* image, const struct Filename* name) { const struct DiskGeometry* geom; byte_t** directory = 0; struct DirEnt* dirent = 0; size_t block, i; size_t freeslotBlock = (size_t) -1, freeslotEntry = 0; if (!name || !image || !image->buf || !(geom = getGeometry (image->type))) return 0; /* Read the current directory. */ if (!(block = mapInode (&directory, image, image->dirtrack, 0, 0, 0))) return 0; /* Check that the directory is long enough to hold the BAM blocks and at least one directory sector */ if (block < geom->BAMblocks) { free (directory); return 0; } /* Search for the name in the directory. */ for (block = geom->BAMblocks; ; block++) { dirent = (struct DirEnt*) directory[block]; for (i = 0; i * sizeof (struct DirEnt) < (dirent->nextTrack ? 256U : dirent->nextSector); i++) { if (freeslotBlock == (size_t) -1 && !dirent[i].type) { /* null file type => unused slot */ freeslotBlock = block; freeslotEntry = i; } if (!memcmp (dirent[i].name, name->name, 16)) { free (directory); return &dirent[i]; } } if (!dirent->nextTrack) break; } /* The name was not found in the directory. */ if (image->direntOpts == DirEntDontCreate) { free (directory); return 0; } if (freeslotBlock == (size_t) -1) { /* Append a directory entry */ dirent = (struct DirEnt*) directory[block]; if (i < 256 / sizeof *dirent) { /* grow the directory by growing its last sector */ dirent->nextSector = (byte_t) ((sizeof *dirent) * (1 + dirent->nextSector / sizeof *dirent)); freeslotBlock = block; freeslotEntry = i; } else { /* allocate a new directory block */ byte_t track, sector; byte_t t, s; track = image->dirtrack; sector = geom->BAMblocks; if (!findNextFree (image, &track, §or)) { free (directory); return 0; } t = dirent->nextTrack = track; s = dirent->nextSector = sector; if (!allocBlock (image, &t, &s)) { dirent->nextTrack = 0; dirent->nextSector = 0xFF; free (directory); return 0; } /* Remap the directory from the disk image */ free (directory); directory = 0; if (!mapInode ((byte_t***)&directory, image, image->dirtrack, 0, 0, 0)) return 0; block++; /* initialize the new directory block */ memset (directory[block], 0, 256); ((struct DirEnt*) directory[block])->nextSector = 0xFF; freeslotBlock = block; freeslotEntry = 0; } } /* Clear the directory entry. */ dirent = &((struct DirEnt*) directory[freeslotBlock])[freeslotEntry]; if (freeslotEntry) memset (dirent, 0, sizeof *dirent); else memset (&dirent->type, 0, (sizeof *dirent) - 2); free (directory); return dirent; } #ifdef DEBUG /** Determine if a directory entry is valid * @param image the disk image * @param dirent the directory entry * @return true if the directory entry is valid */ static bool isValidDirEnt (const struct Image* image, const struct DirEnt* dirent) { const struct DiskGeometry* geom; if (!image || !image->buf || !(geom = getGeometry (image->type))) return false; if ((byte_t*) dirent < image->buf || (byte_t*) dirent > &image->buf[geom->blocks << 8] || ((byte_t*) dirent - image->buf) % sizeof *dirent) return false; return true; } #endif /** Determine whether a directory entry represents a GEOS file * @param dirent the directory entry * @return true if it is a GEOS directory entry */ static bool isGeosDirEnt (const struct DirEnt* dirent) { enum Filetype type = dirent->type & 0x8F; return type >= DEL && type < REL && dirent->geos.type != 0 && (dirent->isVLIR == 0 || dirent->isVLIR == 1); } /** Determine the file type of a directory entry * @param image the disk image * @param dirent the directory entry * @return the file type of the directory entry, or 0 */ static enum Filetype getFiletype (const struct Image* image, const struct DirEnt* dirent) { enum Filetype type; #ifdef DEBUG if (!isValidDirEnt (image, dirent)) return 0; #endif type = dirent->type & 0x8F; if (type < DEL || type > (image->type == Im1581 ? CBM : REL)) return 0; /* illegal file type */ return type; } /** Remove a directory entry and the files it is pointing to * @param image the disk image * @param dirent the directory entry * @return status of the operation */ static enum ImStatus deleteDirEnt (struct Image* image, struct DirEnt* dirent) { enum ImStatus status; #ifdef DEBUG if (!isValidDirEnt (image, dirent)) return ImFail; #endif if (isGeosDirEnt (dirent)) { /* Check if the inodes can be deleted. */ if (ImOK != deleteInode (image, dirent->firstTrack, dirent->firstSector, false) || ImOK != deleteInode (image, dirent->infoTrack, dirent->infoSector, false)) return ImFail; if (dirent->isVLIR) { unsigned vlirblock; const byte_t* vlir = getBlock (image, dirent->firstTrack, dirent->firstSector); for (vlirblock = 1; vlirblock < 128; vlirblock++) if (vlir[2 * vlirblock] && ImOK != deleteInode (image, vlir[2 * vlirblock], vlir[2 * vlirblock + 1], false)) return ImFail; /* Delete the VLIR nodes. */ for (vlirblock = 1; vlirblock < 128; vlirblock++) if (vlir[2 * vlirblock]) deleteInode (image, vlir[2 * vlirblock], vlir[2 * vlirblock + 1], true); } /* Delete the info block and the file. */ deleteInode (image, dirent->infoTrack, dirent->infoSector, true); deleteInode (image, dirent->firstTrack, dirent->firstSector, true); dirent->type = 0; return ImOK; } else if (getFiletype (image, dirent) == REL && (ImOK != deleteInode (image, dirent->firstTrack, dirent->firstSector, false) || ImOK != deleteInode (image, dirent->ssTrack, dirent->ssSector, true))) return ImFail; status = deleteInode (image, dirent->firstTrack, dirent->firstSector, true); if (status == ImOK) /* nuke the directory entry */ dirent->type = 0; return status; } /** Determine the number of free blocks on the disk image. * @param image the disk image * @return the total number of available blocks */ static unsigned blocksFree (const struct Image* image) { unsigned sum = 0; const struct DiskGeometry* geom; if (!image || !image->buf || !(geom = getGeometry (image->type))) return 0; switch (image->type) { const byte_t* BAM; byte_t track; case ImUnknown: return 0; case Im1541: if (!(BAM = getBlock ((struct Image*) image, image->dirtrack, 0))) return 0; for (track = 1; track <= geom->tracks; track++) sum += BAM[track << 2]; return sum; case Im1571: if (!(BAM = getBlock ((struct Image*) image, image->dirtrack, 0))) return 0; for (track = 1; track <= 35; track++) sum += BAM[track << 2] + BAM[0xDC + track]; return sum; case Im1581: { byte_t** BAMblocks = 0; if (2 != mapInode (&BAMblocks, (struct Image*) image, image->dirtrack, 1, 0, 0)) { if (BAMblocks) free (BAMblocks); return 0; } for (track = image->partBots[image->dirtrack - 1]; track <= image->partTops[image->dirtrack - 1] && track <= 40; track++) sum += BAMblocks[0][16 + (track - 1)*6]; for (track = image->partTops[image->dirtrack - 1]; track >= image->partBots[image->dirtrack - 1] && track > 40; track--) sum += BAMblocks[1][16 + (track - 41) * 6]; free (BAMblocks); return sum; } } return 0; } /** Set up the side sector file for relative files. * @param image the disk image * @param dirent the directory entry * @param blocks number of data blocks in the file * @param log Call-back function for diagnostic output * @return status of the operation */ static enum WrStatus setupSideSectors (struct Image* image, struct DirEnt* dirent, size_t blocks, log_t log) { size_t sscount; #ifdef DEBUG if (!isValidDirEnt (image, dirent) || getFiletype (image, dirent) != REL) return WrFail; #endif if(image->type == Im1581) return WrFail; /* TODO: super side sector setup etc. */ sscount = rounddiv (blocks, 120); if (sscount < 1) return WrFail; if (sscount > 6 || blocksFree (image) < sscount) /* too many side sector blocks */ return WrNoSpace; if (!findNextFree (image, &dirent->ssTrack, &dirent->ssSector)) return WrNoSpace; { byte_t* buf; enum WrStatus status; size_t sslength = 14 + 254 * (sscount - 1) + 2 * (blocks % 120); if (!(buf = calloc (sslength, 1))) return WrFail; status = writeInode (image, dirent->ssTrack, dirent->ssSector, buf, sslength); free (buf); if (status != WrOK) return status; } { byte_t** sidesect = 0; byte_t** datafile = 0; size_t ssentry; byte_t ss, i, track, sector; if (blocks != mapInode (&datafile, image, dirent->firstTrack, dirent->firstSector, log, dirent)) return WrFail; if (sscount != mapInode (&sidesect, image, dirent->ssTrack, dirent->ssSector, log, dirent)) { free (datafile); return WrFail; } for (ss = 0; ss < sscount; ss++) { sidesect[ss][2] = ss; sidesect[ss][3] = dirent->recordLength; sidesect[ss][4] = dirent->ssTrack; sidesect[ss][5] = dirent->ssSector; for (i = 1; i < sscount; i++) { sidesect[ss][4 + i * 2] = sidesect[i - 1][0]; sidesect[ss][5 + i * 2] = sidesect[i - 1][1]; } } track = dirent->firstTrack; sector = dirent->firstSector; for (ssentry = 0; track; ssentry++) { if (ssentry / 120 >= sscount) return WrFail; ss = (byte_t) (ssentry / 120); sidesect[ss][16 + (ssentry % 120) * 2] = track; sidesect[ss][17 + (ssentry % 120) * 2] = sector; track = datafile[ssentry][0]; sector = datafile[ssentry][1]; } free (datafile); free (sidesect); } return WrOK; } /** Check if the side sectors of a relative file are OK * @param image the disk image * @param dirent the directory entry * @param log Call-back function for diagnostic output * @return true if the side sectors pass the integrity checks */ static bool checkSideSectors (const struct Image* image, const struct DirEnt* dirent, log_t log) { size_t ss, ssentry, sscount, i; byte_t** sidesect = 0; byte_t** datafile = 0; byte_t track, sector; #ifdef DEBUG if (!isValidDirEnt (image, dirent)) return false; #endif if (getFiletype (image, dirent) != REL) return false; /* Map the data file and the side sectors. */ if (!(i = mapInode (&datafile, (struct Image*) image, dirent->firstTrack, dirent->firstSector, log, dirent))) return false; if (!(sscount = mapInode (&sidesect, (struct Image*) image, dirent->ssTrack, dirent->ssSector, log, dirent))) { free (datafile); return false; } /* Check the block counts */ if (sscount != rounddiv(i, 120) || i + sscount != dirent->blocksLow + ((unsigned) dirent->blocksHigh << 8) || i != 120U * (sscount - 1U) + (sidesect[sscount - 1][1] - 15) / 2U) { Failed: free (datafile); free (sidesect); return false; } /* Check the side sector links */ for (ss = 0; ss < sscount; ss++) { if (sidesect[ss][2] != ss || sidesect[ss][3] != dirent->recordLength || sidesect[ss][4] != dirent->ssTrack || sidesect[ss][5] != dirent->ssSector) goto Failed; for (i = 1; i < sscount; i++) if (sidesect[ss][4 + i * 2] != sidesect[i - 1][0] || sidesect[ss][5 + i * 2] != sidesect[i - 1][1]) goto Failed; } /* Check the links to the data file */ track = dirent->firstTrack; sector = dirent->firstSector; for (ss = ssentry = 0; track; ssentry++) { ss = ssentry / 120; if (ss >= sscount || sidesect[ss][16 + (ssentry % 120) * 2] != track || sidesect[ss][17 + (ssentry % 120) * 2] != sector) goto Failed; track = datafile[ssentry][0]; sector = datafile[ssentry][1]; } free (datafile); free (sidesect); return true; } /** Generate a CP/M translation table. * @param image the disk image * @param au (output) the size of the allocation unit * @param sectors (output) number of useable sectors * @return the translation table, or NULL on error */ static byte_t** CpmTransTable (struct Image* image, unsigned* au, unsigned* sectors) { byte_t** table = 0; byte_t* trackbuf; const struct DiskGeometry* geom; unsigned track = 1, sector = 10, sectorcount = 2; unsigned block; if (!image || !au || !image->buf || !(geom = getGeometry (image->type))) return 0; /* Set the allocation unit size and the number of sectors. */ switch (geom->blocks) { case 683: /* 1541 */ *au = 8; if ((table = calloc (*sectors = 680, sizeof (*table)))) for (block = 0, trackbuf = image->buf; block < *sectors; block++) { table[block] = &trackbuf[sector << 8]; sector = (sector + 5) % geom->sectors[track]; if (++sectorcount == geom->sectors[track]) { trackbuf += geom->sectors[track++] << 8; if (track == geom->dirtrack) { sectorcount = 1; sector = 5; } else { sectorcount = 0; sector = 0; } } } break; case 1366: /* 1571 */ *au = 8; if ((table = calloc (*sectors = 1360, sizeof (*table)))) for (block = 0, trackbuf = image->buf; block < *sectors; block++) { table[block] = &trackbuf[sector << 8]; sector = (sector + 5) % geom->sectors[track]; if (++sectorcount == geom->sectors[track]) { trackbuf += geom->sectors[track++] << 8; if (track == 36) { sectorcount = 2; sector = 10; } else if (track % 36 == geom->dirtrack) { sectorcount = 1; sector = 5; } else { sectorcount = 0; sector = 0; } } } break; case 3200: /* 1581 */ *au = 16; sector = sectorcount = 0; if ((table = calloc (*sectors = 3180, sizeof (*table)))) for(block = 0, trackbuf = image->buf; block < *sectors; block++) { table[block] = &trackbuf[sector << 8]; sector = (sector + 1) % geom->sectors[track]; if (++sectorcount == geom->sectors[track]) { trackbuf += geom->sectors[track++] << 8; sectorcount = sector = (track == geom->dirtrack) ? 20 : 0; } } break; } return table; } /** Convert a CP/M directory entry to a PETSCII file name. * @param dirent the CP/M directory entry * @param name (output) the Commodore file name */ static void CpmConvertName (const struct CpmDirEnt* dirent, struct Filename* name) { char cpmname[13]; unsigned i, j; for (i = 0; i < sizeof(dirent->basename); i++) cpmname[i] = dirent->basename[i] & 0x7f; while (cpmname[i - 1] == ' ') i--; for (cpmname[i++] = '.', j = 0; j < sizeof(dirent->suffix); j++) cpmname[i++] = dirent->suffix[j] & 0x7f; while (cpmname[i - 1] == ' ') i--; if (cpmname[i - 1] == '.') i--; cpmname[i] = 0; /* Convert the ASCII name to PETSCII name */ for (i = 0; cpmname[i] && i < sizeof(name->name); i++) { if (cpmname[i] >= 'A' && cpmname[i] <= 'Z') name->name[i] = (unsigned char) (cpmname[i] - 'A' + 0xC1); else if (cpmname[i] >= 'a' && cpmname[i] <= 'z') name->name[i] = (unsigned char) (cpmname[i] - 'a' + 0x41); else name->name[i] = (unsigned char) cpmname[i]; } memset (&name->name[i], 0xA0/* shifted space*/, (sizeof name->name) - i); name->type = PRG; name->recordLength = 0; } /** Write a file into a CP/M disk image. * @param name native (PETSCII) name of the file * @param data the contents of the file * @param length length of the file contents * @param image the disk image * @param log Call-back function for diagnostic output * @return status of the operation */ enum WrStatus WriteCpmImage (const struct Filename* name, const byte_t* data, size_t length, struct Image* image, log_t log) { byte_t** trans; struct CpmDirEnt** allocated; struct CpmDirEnt* dirent; struct CpmDirEnt cpmname; unsigned au; /* allocation unit size */ unsigned sectors; /* number of disk sectors */ unsigned slot; /* next directory slot */ unsigned blocksfree; /* number of free blocks */ if (!name || !data || !image || !image->buf || !(trans = CpmTransTable (image, &au, §ors))) return WrFail; if (!(allocated = calloc (2 * sectors / au, sizeof(*allocated)))) { free (trans); return WrFail; } if (!(dirent = malloc (au * 8 * sizeof (*dirent)))) { free (allocated); free (trans); return WrFail; } blocksfree = 2 * (sectors / au - 1); /* Convert the file name */ { unsigned i; memset (&cpmname, 0, sizeof cpmname); memset (cpmname.basename, ' ', sizeof cpmname.basename + sizeof cpmname.suffix); /* Convert the file name base */ for (i = 0; i < sizeof(name->name) && i < sizeof(cpmname.basename) && !memchr (".\240", name->name[i], 3); i++) if (i && name->name[i] == ' ') /* stop at the first space */ break; else if (name->name[i] >= 0x41 && name->name[i] <= 0x5A) cpmname.basename[i] = name->name[i] + 'A' - 0x41; /* upper case only */ else if (name->name[i] >= 0xC1 && name->name[i] <= 0xDA) cpmname.basename[i] = name->name[i] + 'A' - 0xC1; else if ((name->name[i] & 0x7F) < 32 || name->name[i] == ' ') cpmname.basename[i] = '-'; /* control chars and space */ else if (name->name[i] < 127) cpmname.basename[i] = name->name[i]; else cpmname.basename[i] = '+'; /* graphics characters */ /* Convert the file name suffix */ if (name->name[i] != ' ' && ++i < sizeof(name->name)) { unsigned j; for (j = 0; j < sizeof(cpmname.suffix) && i < sizeof(name->name); i++, j++) if ((name->name[i] & 0x7F) == ' ') /* stop at the first space */ break; else if (name->name[i] >= 0x41 && name->name[i] <= 0x5A) cpmname.suffix[j] = name->name[i] + 'A' - 0x41; /* upper case only */ else if (name->name[i] >= 0xC1 && name->name[i] <= 0xDA) cpmname.suffix[j] = name->name[i] + 'A' - 0xC1; else if ((name->name[i] & 0x7F) < 32) cpmname.suffix[j] = '-'; /* control chars */ else if (name->name[i] < 127) cpmname.suffix[j] = name->name[i]; else cpmname.suffix[j] = '+'; /* graphics characters */ } } /* Traverse through the directory and determine the amount and location of free blocks */ { unsigned d, i; bool found = false; /* Read the directory entries */ for (d = au; d--; ) memcpy (&dirent[d * 8], trans[d], 8 * sizeof (*dirent)); for (d = slot = 0; d < au * 8; d++) { if (dirent[d].area == 0xE5 || !memcmp (&dirent[d], "\0\0\0\0\0\0\0\0\0\0\0", 12)) continue; if (!memcmp (dirent[d].basename, cpmname.basename, sizeof cpmname.basename + sizeof cpmname.suffix)) { if (image->direntOpts == DirEntOnlyCreate) { free (dirent); free (allocated); free (trans); return WrFileExists; } found = true; continue; /* overwrite the file */ } if (d != slot) memmove (&dirent[slot], &dirent[d], (au * 8 - d) * sizeof(*dirent)); d = slot++; for (i = 0; i < rounddiv(dirent[d].blocks, au); i++) if (CPMBLOCK (dirent[d].block, i) < 2 || CPMBLOCK (dirent[d].block, i) >= 2 * sectors / au) { struct Filename fn; CpmConvertName (&dirent[d], &fn); (*log) (Warnings, &fn, "Illegal block address in block %u of extent 0x%02x", i, dirent[d].extent); } else if (allocated[CPMBLOCK (dirent[d].block, i)]) { struct Filename fn; CpmConvertName (&dirent[d], &fn); (*log) (Warnings, &fn, "Sector 0x%02x allocated multiple times", CPMBLOCK (dirent[d].block, i)); } else { allocated[CPMBLOCK (dirent[d].block, i)] = &dirent[d]; blocksfree--; } } /* See if the file was found */ if (!found && image->direntOpts == DirEntDontCreate) { free (dirent); free (allocated); free (trans); return WrFail; } /* Clear the empty directory entries */ memset (&dirent[slot], 0xE5, (au * 8 - slot) * sizeof(*dirent)); /* Ensure that enough free space is available */ if (slot >= 8 * au || length > (8 * au - slot) * au / 2 * 16 * 128 || length > blocksfree * au * 128) { free (dirent); free (allocated); free (trans); return WrNoSpace; } } /* Write the file */ { struct CpmDirEnt* de = 0; size_t block, blocks; size_t freeblock = 2; for (block = 0, blocks = rounddiv(length, 128); blocks;) { if (!(block % 128)) { /* advance to next directory slot */ de = &dirent[slot++]; memcpy (de, &cpmname, sizeof cpmname); de->extent = (byte_t) (block / 128); } /* Copy the blocks */ { unsigned j; de->blocks = (byte_t) (blocks < 128 ? blocks : 128); blocks -= de->blocks; for (j = 0; j < de->blocks; j++, block++) { if (!(j % au)) { unsigned k; /* Get next free block */ while (allocated[freeblock]) freeblock++; allocated[freeblock] = de; if (au == 8) de->block[j / au] = (byte_t) freeblock; else { de->block[(j / au) * 2] = (byte_t) freeblock; de->block[(j / au) * 2 + 1] = (byte_t) (freeblock >> 8); } /* Pad it with ^Z */ for (k = 0; k < au / 2; k++) memset (trans[(au / 2) * freeblock + k], 0x1A, 256); } /* Copy the block */ memcpy (trans[(au / 2) * freeblock + ((j / 2) % (au / 2))] + 128 * (j % 2), data + 128 * block, length >= 128 * (block + 1) ? 128 : length - 128 * block); } } } } /* Write the directory entries */ { unsigned d = au; while (d--) memcpy (trans[d], &dirent[d * 8], 8 * sizeof (*dirent)); } free (dirent); free (allocated); free (trans); return WrOK; } /** Read and convert a disk image in C128 CP/M format * @param file the file input stream * @param filename host system name of the file * @param writeCallback function for writing the contained files * @param log Call-back function for diagnostic output * @return status of the operation */ enum RdStatus ReadCpmImage (FILE* file, const char* filename, write_file_t writeCallback, log_t log) { struct Image image; byte_t** trans; unsigned au; /* allocation unit size */ unsigned sectors; /* number of disk sectors */ /* determine disk image type from its length */ { const struct DiskGeometry* geom = 0; size_t length, blocks; unsigned i; if (fseek (file, 0, SEEK_END)) { seekError: (*log) (Errors, 0, "fseek: %s", strerror(errno)); return RdFail; } else { long l = ftell (file); if (l < 0) goto seekError; length = (size_t) l; } if (fseek (file, 0, SEEK_SET)) goto seekError; if (length % 256) { unknownImage: (*log) (Errors, 0, "Unknown CP/M disk image type"); return RdFail; } for (i = 0, blocks = length / 256; i < elementsof(diskGeometry); i++) if (diskGeometry[i].blocks == blocks) { geom = &diskGeometry[i]; break; } if (!geom) goto unknownImage; /* Initialize the disk image structure. */ if (!(image.buf = malloc (length))) { (*log) (Errors, 0, "Out of memory"); return RdFail; } image.type = geom->type; image.dirtrack = geom->dirtrack; image.name = 0; if (1 != fread (image.buf, length, 1, file)) { (*log) (Errors, 0, "fread: %s", strerror(errno)); free (image.buf); return RdFail; } /* Get the CP/M sector translations. */ if (!(trans = CpmTransTable (&image, &au, §ors))) { free (image.buf); goto unknownImage; } } /* Traverse through the directory and extract the files */ { struct CpmDirEnt* directory; unsigned d; struct Filename name; for (d = 0; d < au * 8; d++) { unsigned i, j, length; directory = ((struct CpmDirEnt*) trans[d / 8]) + (d % 8); if (directory->area == 0xE5) continue; /* unused entry */ CpmConvertName (directory, &name); if (directory->extent) { (*log) (Warnings, &name, "starting with non-zero extent 0x%02x, file ignored", directory->extent); continue; } /* search for following extents */ for (i = d, j = length = 0; i < au * 8; i++) { struct CpmDirEnt* dir = ((struct CpmDirEnt*) trans[i / 8]) + (i % 8); if (memcmp (dir, directory, 12) || dir->extent != j || dir->blocks > 128) break; j++; length += dir->blocks; if (dir->blocks < 128) break; } /* j holds the number of directory extents */ if (!j) { (*log) (Warnings, &name, "error in directory entry, file skipped"); continue; } if (directory->area) (*log) (Warnings, &name, "user area code 0x%02x ignored", directory->area); length *= 128; /* Read the file */ { byte_t* curr, *buf = malloc (length); if (!buf) { (*log) (Warnings, &name, "out of memory"); d += j - 1; continue; } for (curr = buf, j += d; d < j; d++) { directory = ((struct CpmDirEnt*) trans[d / 8]) + (d % 8); for (i = 0; i < directory->blocks; i++) { unsigned sect = (au / 2) * CPMBLOCK (directory->block, i / au) + ((i / 2) % (au / 2)); if (sect >= sectors) { (*log) (Errors, &name, "Illegal block address in block %u of extent 0x%02x", i, directory->extent); free (buf); goto FileDone; } memcpy (curr, trans[sect] + 128 * (i % 2), 128); curr += 128; } } /* Remove trailing EOF characters (only when they are at end of the last block). */ while (length-- && buf[length] == 0x1A); length++; switch ((*writeCallback) (&name, buf, length)) { case WrOK: break; case WrNoSpace: free (buf); free (image.buf); free (trans); return RdNoSpace; case WrFail: default: free (buf); free (image.buf); free (trans); return RdFail; } free (buf); } FileDone: d--; } } free (image.buf); free (trans); return RdOK; } /** Write to an image in CBM DOS format * @param name native (PETSCII) name of the file * @param data the contents of the file * @param length length of the file contents * @param image the disk image * @param log Call-back function for diagnostic output * @return status of the operation */ enum WrStatus WriteImage (const struct Filename* name, const byte_t* data, size_t length, struct Image* image, log_t log) { struct DirEnt* dirent; if (!name || !data || !image || !image->buf || !getGeometry (image->type)) return WrFail; /* See if it is a GEOS file. */ if (name->type >= DEL && name->type < REL && length > 2 * 254 && !strncmp ((char*)&data[sizeof (struct DirEnt) + 1], " formatted GEOS file ", 21)) { size_t len; struct Filename geosname; const byte_t* info = &data[254]; dirent = (struct DirEnt*) &data[-2]; /* Read the name from the directory entry. */ memcpy (geosname.name, dirent->name, sizeof geosname.name); geosname.type = getFiletype (image, dirent); geosname.recordLength = 0; if (!isGeosDirEnt (dirent) || memcmp (info, "\3\25\277", 3)) goto notGEOS; if (dirent->isVLIR) { const byte_t* vlir = &data[2 * 254]; unsigned vlirblock; len = 3 * 254; for (vlirblock = 0; vlirblock < 127; vlirblock++) { unsigned blocks = vlir[2 * vlirblock]; unsigned lastblocklen = vlir[2 * vlirblock + 1]; if (!blocks) { if (lastblocklen != 0 && lastblocklen != 0xFF) goto notGEOS; } else if (lastblocklen < 2) goto notGEOS; else len = 254 * (rounddiv(len, 254) + blocks - 1) + lastblocklen - 1; } if (len > length) { (*log) (Warnings, &geosname, "%d bytes too short file", len - length); goto notGEOS; } } else len = length; if ((info[0x42] ^ dirent->type) & 0x8F) (*log) (Warnings, &geosname, "file types differ: $%02x $%02x", info[0x42], dirent->type); if (info[0x43] != dirent->geos.type) (*log) (Warnings, &geosname, "GEOS file types differ: $%02x $%02x", info[0x43], dirent->geos.type); if (info[0x44] != dirent->isVLIR) (*log) (Warnings, &geosname, "VLIR flags differ: $%02x $%02x", info[0x44], dirent->isVLIR); if (len != length) (*log) (Warnings, &geosname, "File size mismatch: %d extraneous bytes", length - len); if (rounddiv(len, 254) - 1 != dirent->blocksLow + ((unsigned) dirent->blocksHigh << 8)) { size_t blks = rounddiv(len, 254) - 1; dirent->blocksLow = (byte_t) blks; dirent->blocksHigh = (byte_t) (blks >> 8); (*log) (Warnings, &geosname, "invalid block count"); } dirent = getDirEnt (image, &geosname); if (!dirent) return WrNoSpace; if (dirent->type) { if (image->direntOpts == DirEntOnlyCreate) return WrFileExists; /* delete the old file */ if (ImOK != deleteDirEnt (image, dirent)) { (*log) (Errors, &geosname, "Could not delete existing file."); return WrFail; } } if (blocksFree (image) < rounddiv(len, 254) - 1) return WrNoSpace; /* set the directory entry parameters */ memcpy ((byte_t*)dirent + 2, data, sizeof (struct DirEnt) - 2); dirent->type = 0; dirent->firstTrack = 0; dirent->firstSector = 0; dirent->infoTrack = image->dirtrack + 1; dirent->infoSector = 0; { byte_t* oldBAM = 0; enum WrStatus status; /* back up the old BAM */ if (!backupBAM (image, &oldBAM)) { (*log) (Errors, name, "Backing up the BAM failed."); return WrFail; } if (!findNextFree (image, &dirent->infoTrack, &dirent->infoSector)) return WrNoSpace; status = writeInode (image, dirent->infoTrack, dirent->infoSector, &data[254], 254); if (status != WrOK) { restoreBAM (image, &oldBAM); (*log) (Errors, &geosname, "Writing the info sector failed."); return status; } if (dirent->isVLIR) { byte_t vlir[254]; const byte_t* vlirsrc = &data[254 * 2]; unsigned vlirblock; const byte_t* buf = &data[254 * 3]; byte_t track = dirent->infoTrack; byte_t sector = dirent->infoSector; memcpy (vlir, vlirsrc, 254); for (vlirblock = 0; vlirblock < 127; vlirblock++) { unsigned blocks = vlirsrc[2 * vlirblock]; unsigned lastblocklen = vlirsrc[2 * vlirblock + 1]; if (blocks) { if (!findNextFree (image, &track, §or)) { restoreBAM (image, &oldBAM); return WrNoSpace; } vlir[vlirblock * 2] = track; vlir[vlirblock * 2 + 1] = sector; len = 254 * (blocks - 1) + lastblocklen - 1; status = writeInode (image, track, sector, buf, len); if (status != WrOK) { restoreBAM (image, &oldBAM); (*log) (Errors, &geosname, "Writing a VLIR node failed."); return status; } buf += 254 * blocks; } } dirent->firstTrack = dirent->infoTrack; dirent->firstSector = dirent->infoSector; if (!findNextFree (image, &dirent->firstTrack, &dirent->firstSector)) { restoreBAM (image, &oldBAM); return WrNoSpace; } status = writeInode (image, dirent->firstTrack, dirent->firstSector, vlir, 254); if (status != WrOK) { restoreBAM (image, &oldBAM); (*log) (Errors, &geosname, "Writing the VLIR block failed."); return status; } } else { dirent->firstTrack = dirent->infoTrack; dirent->firstSector = dirent->infoSector; if (!findNextFree (image, &dirent->firstTrack, &dirent->firstSector)) { restoreBAM (image, &oldBAM); return WrNoSpace; } status = writeInode (image, dirent->firstTrack, dirent->firstSector, &data[254 * 2], length - 254 * 2); if (status != WrOK) { restoreBAM (image, &oldBAM); (*log) (Errors, &geosname, "Writing the data sectors failed."); return status; } } free (oldBAM); } dirent->type = *data; return WrOK; notGEOS: (*log) (Warnings, name, "not a valid GEOS (Convert) file"); } dirent = getDirEnt (image, name); if (!dirent) return WrNoSpace; if (dirent->type) { if (image->direntOpts == DirEntOnlyCreate) return WrFileExists; /* delete the old file */ if (ImOK != deleteDirEnt (image, dirent)) { (*log) (Errors, name, "Could not delete existing file."); return WrFail; } } /* Check that there is enough space for the file. */ if (blocksFree (image) < rounddiv(length, 254) + (name->type == REL ? rounddiv(rounddiv(length, 254), 120) : 0)) return WrNoSpace; /* set the file name */ memcpy (dirent->name, name->name, 16); /* set the track and sector of the file */ dirent->firstTrack = image->dirtrack + 1; dirent->firstSector = 0; if (!findNextFree (image, &dirent->firstTrack, &dirent->firstSector)) return WrNoSpace; { size_t blocks; byte_t* oldBAM = 0; enum WrStatus status; /* set the block count */ blocks = rounddiv(length, 254); if (name->type == REL) { /* set the record length for relative files */ dirent->recordLength = name->recordLength; /* adjust the block count */ blocks += rounddiv(blocks, 120); } dirent->blocksLow = (byte_t) blocks; dirent->blocksHigh = (byte_t) (blocks >> 8); /* back up the old BAM */ if (!backupBAM (image, &oldBAM)) { (*log) (Errors, name, "Backing up the BAM failed."); return WrFail; } status = writeInode (image, dirent->firstTrack, dirent->firstSector, data, length); if (status != WrOK) { restoreBAM (image, &oldBAM); (*log) (Errors, name, "Writing the data bytes failed."); return status; } switch (name->type) { case REL: /* set the initial track and sector for the side sectors */ dirent->ssTrack = image->dirtrack + 1; dirent->ssSector = 0; status = setupSideSectors (image, dirent, rounddiv(length, 254), log); if (status != WrOK) { restoreBAM (image, &oldBAM); (*log) (Errors, name, "Could not set up the side sectors."); return status; } /* fall through */ case DEL: case SEQ: case PRG: case USR: free (oldBAM); dirent->type = (byte_t) (name->type | 0x80); return WrOK; default: restoreBAM (image, &oldBAM); (*log) (Errors, name, "Unsupported file type."); return WrFail; } } } /** Read and convert a disk image in CBM DOS format * @param file the file input stream * @param filename host system name of the file * @param writeCallback function for writing the contained files * @param log Call-back function for diagnostic output * @return status of the operation */ enum RdStatus ReadImage (FILE* file, const char* filename, write_file_t writeCallback, log_t log) { const struct DiskGeometry* geom = 0; struct Image image; /* determine disk image type from its length */ { size_t length, blocks; unsigned i; if (fseek (file, 0, SEEK_END)) { seekError: (*log) (Errors, 0, "fseek: %s", strerror(errno)); return RdFail; } else { long l = ftell (file); if (l < 0) goto seekError; length = (size_t) l; } if (fseek (file, 0, SEEK_SET)) goto seekError; if (length % 256) { unknownImage: (*log) (Errors, 0, "Unknown disk image type"); return RdFail; } for (i = 0, blocks = length / 256; i < elementsof(diskGeometry); i++) if (diskGeometry[i].blocks == blocks) { geom = &diskGeometry[i]; break; } if (!geom) goto unknownImage; /* Initialize the disk image structure. */ if (!(image.buf = malloc (length))) { (*log) (Errors, 0, "Out of memory"); return RdFail; } image.type = geom->type; image.dirtrack = geom->dirtrack; image.name = 0; if (1 != fread (image.buf, length, 1, file)) { (*log) (Errors, 0, "fread: %s", strerror(errno)); free (image.buf); return RdFail; } } /* Traverse through the root directory and extract the files */ { byte_t** directory = 0; size_t block; if (!(block = mapInode (&directory, &image, image.dirtrack, 0, log, 0))) { (*log) (Errors, 0, "Could not read the directory on track %u.", image.dirtrack); free (image.buf); return RdFail; } if (block < geom->BAMblocks) { (*log) (Errors, 0, "Directory too short."); free (image.buf); free (directory); return RdFail; } /* Traverse through the directory */ for (block = geom->BAMblocks;; block++) { unsigned i; struct Filename name; for (i = 0; i < 256 / sizeof (struct DirEnt); i++) { struct DirEnt* dirent = &((struct DirEnt*) directory[block])[i]; memcpy (name.name, dirent->name, 16); name.type = getFiletype (&image, dirent); name.recordLength = dirent->recordLength; if (isGeosDirEnt (dirent)) { static const char cvt[] = "PRG formatted GEOS file V1.0"; size_t length = 0; byte_t* buf; const byte_t *vlir = 0, *info = getBlock (&image, dirent->infoTrack, dirent->infoSector); if (!info || memcmp (info, "\0\377\3\25\277", 5)) goto notGEOS; /* invalid info block */ if (dirent->isVLIR) { unsigned vlirblock; vlir = getBlock (&image, dirent->firstTrack, dirent->firstSector); if (!vlir || vlir[0] != 0 || vlir[1] != 0xFF) goto notGEOS; /* see if the VLIR block is valid and determine file length */ for (length = 0, vlirblock = 1; vlirblock < 128; vlirblock++) if (!vlir[2 * vlirblock]) continue; else if (vlir[2 * vlirblock] > geom->tracks || vlir[2 * vlirblock + 1] >= geom->sectors[vlir[2 * vlirblock]]) goto notGEOS; else { byte_t* b = 0; size_t chainlen = readInode (&b, &image, vlir[2 * vlirblock], vlir[2 * vlirblock + 1]); if (!chainlen) goto notGEOS; free (b); length = 254 * rounddiv(length, 254) + chainlen; } } else { byte_t* b = 0; length = readInode (&b, &image, dirent->firstTrack, dirent->firstSector); if (!length) goto notGEOS; free (b); } /* convert the GEOS file name and type */ { unsigned j; for (j = 0; j < sizeof name.name; j++) if (name.name[j] >= 'A' && name.name[j] <= 'Z') name.name[j] -= (unsigned char) ('A' + 0xC1); else if (name.name[j] >= 'a' && name.name[j] <= 'z') name.name[j] -= (unsigned char) ('a' + 0x41); name.type = PRG; } if ((info[0x44] ^ dirent->type) & 0x8F) (*log) (Warnings, &name, "file types differ: $%02x $%02x", info[0x44], dirent->type); if (info[0x45] != dirent->geos.type) (*log) (Warnings, &name, "GEOS file types differ: $%02x $%02x", info[0x45], dirent->geos.type); if (info[0x46] != dirent->isVLIR) (*log) (Warnings, &name, "VLIR flags differ: $%02x $%02x", info[0x46], dirent->isVLIR); if (rounddiv(length, 254) + 1 + dirent->isVLIR != dirent->blocksLow + ((unsigned) dirent->blocksHigh << 8)) { size_t blks = rounddiv(length, 254) + 1 + dirent->isVLIR; dirent->blocksLow = (byte_t) blks; dirent->blocksHigh = (byte_t) (blks >> 8); (*log) (Warnings, &name, "invalid block count"); } if (!(buf = calloc ((2U + dirent->isVLIR) * 254U + length, 1))) { (*log) (Errors, &name, "Out of memory"); free (image.buf); free (directory); return RdFail; } /* set the Convert header data */ memcpy (&buf[0], &dirent->type, length = sizeof (struct DirEnt) - 2); memcpy (&buf[length], cvt, sizeof cvt); length += sizeof cvt; /* clear the track/sector information from the header */ buf[1] = buf[2] = buf[0x13] = buf[0x14] = 0; /* copy the info block */ memcpy (&buf[254], &info[2], 254); if (dirent->isVLIR) { unsigned vlirblock; bool ended = false, wasended = false; memcpy (&buf[2 * 254], &vlir[2], 254); for (length = 3 * 254, vlirblock = 1; vlirblock < 128; vlirblock++) if (vlir[2 * vlirblock]) { byte_t* b = 0; size_t chainlen = readInode (&b, &image, vlir[2 * vlirblock], vlir[2 * vlirblock + 1]); if (!chainlen || !b) { (*log) (Errors, &name, "unable to read VLIR chain!"); break; } length = 254 * rounddiv(length, 254); memcpy (&buf[length], b, chainlen); length += chainlen; free (b); if (ended && !wasended) { (*log) (Warnings, &name, "false EOF in VLIR sector"); wasended = true; } /* Set the VLIR block information: amount of blocks */ buf[(253 + vlirblock) * 2] = (byte_t) rounddiv(chainlen, 254); /* amount of used bytes in the last block of this chain */ buf[(253 + vlirblock) * 2 + 1] = (byte_t) (chainlen % 254 ? chainlen % 254 + 1 : 0xFF); } else { switch (vlir[2 * vlirblock + 1]) { case 0: ended = true; break; case 0xFF: if (ended && !wasended) { (*log) (Warnings, &name, "false EOF in VLIR sector"); wasended = true; } break; default: buf[(253 + vlirblock) * 2] = 0; buf[(253 + vlirblock) * 2 + 1] = ended ? 0 : 0xFF; (*log) (Warnings, &name, "invalid VLIR pointer $00%02x, " "corrected to $00%02x", vlir[2 * vlirblock + 1], buf[(253 + vlirblock) * 2 + 1]); break; } } } else { byte_t* b = 0; size_t len = readInode (&b, &image, dirent->firstTrack, dirent->firstSector); memcpy (&buf[2 * 254], b, len); length = 2 * 254 + len; free (b); } switch ((*writeCallback) (&name, buf, length)) { case WrOK: continue; case WrNoSpace: free (buf); free (image.buf); free (directory); return RdNoSpace; case WrFail: default: free (buf); free (image.buf); free (directory); return RdFail; notGEOS: (*log) (Warnings, &name, "not a valid GEOS file"); } } switch (name.type) { byte_t* buf; size_t length; case REL: if (!checkSideSectors (&image, dirent, log)) (*log) (Warnings, &name, "error in side sector data"); /* fall through */ case DEL: case SEQ: case PRG: case USR: buf = 0; length = readInode (&buf, &image, dirent->firstTrack, dirent->firstSector); if (name.type != REL && rounddiv(length, 254) != dirent->blocksLow + ((unsigned) dirent->blocksHigh << 8)) (*log) (Warnings, &name, "invalid block count"); switch ((*writeCallback) (&name, buf, length)) { case WrOK: break; case WrNoSpace: free (buf); free (image.buf); free (directory); return RdNoSpace; case WrFail: default: free (buf); free (image.buf); free (directory); return RdFail; } free (buf); break; case CBM: if(image.type == Im1581) { (*log) (Errors, &name, "skipping partition"); /* TODO: Recurse... */ break; } /* Fall through (CBM type only supported by the 1581) */ default: if (dirent->type) (*log) (Errors, &name, "unknown file type $%02x, skipping", dirent->type); } } if (!((struct DirEnt*) directory[block])->nextTrack) break; } free (directory); } free (image.buf); return RdOK; } /** Open an existing disk image or create a new one. * @param filename name of 1541 disk image on the host system * @param image address of the disk image buffer pointer * (will be allocated by this function) * @param type type of the disk image * @param direntOpts directory entry handling options * @return Status of the operation */ enum ImStatus OpenImage (const char* filename, struct Image** image, enum ImageType type, enum DirEntOpts direntOpts) { FILE* f; const struct DiskGeometry* geom; /* The image buffer may not have been allocated. */ if (!image || *image) return ImFail; if (!(geom = getGeometry (type))) return ImFail; /* Allocate and initialize the image structure. */ if (!(*image = calloc (1, sizeof (**image)))) return ImFail; if (!((*image)->buf = malloc (geom->blocks * 256))) { Failed: free ((*image)->name); free ((*image)->buf); free (*image); *image = 0; return ImFail; } if (!((*image)->name = malloc (strlen (filename) + 1))) goto Failed; strcpy ((char*)(*image)->name, filename); (*image)->type = type; (*image)->direntOpts = direntOpts; (*image)->dirtrack = geom->dirtrack; if (!(f = fopen (filename, "rb"))) { if (errno != ENOENT) /* It is OK if the file was not found. */ goto Failed; /* Initialize the image */ FormatImage (*image); } else { /* Read in the disk image */ if (1 != fread ((*image)->buf, geom->blocks * 256, 1, f) || EOF != fgetc (f)) { fclose (f); goto Failed; } fclose (f); } (*image)->partTops[(*image)->dirtrack - 1] = geom->tracks; (*image)->partBots[(*image)->dirtrack - 1] = 1; (*image)->partUpper[(*image)->dirtrack - 1] = 0; return ImOK; } /** Write back a disk image. * @param image address of the disk image buffer * (will be deallocated by this function) * @return Status of the operation */ enum ImStatus CloseImage (struct Image* image) { FILE* f; const struct DiskGeometry* geom; if (!image || !image->buf || !(geom = getGeometry (image->type))) return ImFail; if (!(f = fopen ((char*)image->name, "wb"))) return errno == ENOSPC ? ImNoSpace : ImFail; if (1 != fwrite (image->buf, geom->blocks * 256, 1, f)) { fclose (f); return errno == ENOSPC ? ImNoSpace : ImFail; } fclose (f); free (image->buf); image->buf = 0; return ImOK; } cbmconvert-cbmconvert-2.1.5/input.h000066400000000000000000000054311424020246600173340ustar00rootroot00000000000000/** * @file input.h * Definitions for file reading functions * @author Marko Mäkelä (marko.makela at iki.fi) */ /* ** Copyright © 1993-1997,2001,2022 Marko Mäkelä ** ** 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. */ #ifndef INPUT_H # define INPUT_H # include "util.h" # include "output.h" #ifndef __GNUC__ # define __attribute__(x) /* empty */ #endif /* File management */ /** Call-back function for writing files * @param name native (PETSCII) name of the file * @param data the contents of the file * @param length length of the file contents * @return status of the operation */ typedef __attribute__((nonnull)) enum WrStatus write_file_t (const struct Filename* name, const byte_t* data, size_t length); /** Status of a conversion operation */ enum RdStatus { RdOK, /**< Success */ RdFail, /**< Generic input or output failure */ RdNoSpace /**< Not enough space for the converted output */ }; /** Read and convert a file * @param file the file input stream * @param filename host system name of the file * @param writeCallback function for writing the contained files * @param log Call-back function for diagnostic output * @return status of the operation */ typedef __attribute__((nonnull)) enum RdStatus read_file_t (FILE* file, const char* filename, write_file_t writeCallback, log_t log); /** Read and convert a raw file */ read_file_t ReadNative; /** Read and convert a PC64 file (.P00, .S00 etc.) */ read_file_t ReadPC64; /** Read and convert a Lynx archive */ read_file_t ReadLynx; /** Read and convert an Arkive archive */ read_file_t ReadArkive; /** Read and convert an ARC/SDA archive */ read_file_t ReadARC; /** Read and convert a tape archive of the C64S emulator */ read_file_t ReadT64; /** Read and convert a Commodore C2N tape archive */ read_file_t ReadC2N; /** Read and convert a disk image in CBM DOS format */ read_file_t ReadImage; /** Read and convert a disk image in C128 CP/M format */ read_file_t ReadCpmImage; #endif /* INPUT_H */ cbmconvert-cbmconvert-2.1.5/lynx.c000066400000000000000000000246511424020246600171670ustar00rootroot00000000000000/** * @file lynx.c * Lynx archive extractor and archiver * @author Marko Mäkelä (marko.makela at iki.fi) */ /* ** Copyright © 1993-1997,2001,2006,2021,2022 Marko Mäkelä ** ** 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. */ #include #include #include #include #include "input.h" /** maximal length of the BASIC header, if any */ #define MAXBASICLENGTH 1024 /** Read and convert a Lynx archive * @param file the file input stream * @param filename host system name of the file * @param writeCallback function for writing the contained files * @param log Call-back function for diagnostic output * @return status of the operation */ enum RdStatus ReadLynx (FILE* file, const char* filename, write_file_t writeCallback, log_t log) { struct Filename name; unsigned f, fcount; /* File positions */ long headerPos; /* current header position */ long headerEnd; /* end of header (start of archive) */ long archivePos; /* current archive position */ bool errNoLength = false; /* set if the file length is unknown */ { byte_t* buf; size_t i, length; if (!(buf = malloc (MAXBASICLENGTH))) { memError: (*log) (Errors, 0, "Out of memory."); return RdFail; } length = fread (buf, 1, MAXBASICLENGTH, file); if (fseek (file, 0, SEEK_SET)) { seekError: free (buf); (*log) (Errors, 0, "fseek: %s", strerror(errno)); return RdFail; } /* skip the BASIC header, if any */ for (i = 4; i < MAXBASICLENGTH && i < length; i++) if (!(memcmp (&buf[i - 4], "\0\0\0\15", 4))) { /* skip the BASIC header */ if (fseek (file, (long) i, SEEK_SET)) goto seekError; break; } free (buf); } /* Determine number of blocks and files */ { char lynxhdr[25]; unsigned blkcount; if (3 != fscanf (file, " %u %24c\15 %u%*2[ \15]", &blkcount, lynxhdr, &fcount) || !blkcount || !strstr (lynxhdr, "LYNX") || !fcount) { (*log) (Errors, 0, "Not a Lynx archive."); return RdFail; } /* Set the file pointers. */ headerPos = ftell (file); headerEnd = archivePos = 254 * blkcount; } /* start extracting files */ for (f = 0; f++ < fcount;) { unsigned length, blocks; if (headerPos >= headerEnd) { hdrError: (*log) (Errors, 0, "Lynx header error."); return RdFail; } if (fseek (file, headerPos, 0)) { (*log) (Errors, 0, "fseek: %s", strerror(errno)); return RdFail; } /* read the file header information */ { unsigned i; int j; /* read the file name */ for (i = 0; i < 17; i++) { j = fgetc(file); switch (j) { case EOF: goto hdrError; case 13: /* file name terminator */ break; default: /* file name character */ if (i > 15) { (*log) (Errors, 0, "Too long file name"); return RdFail; } name.name[i] = (unsigned char) j; } if (j == 13) break; } if (!i) { (*log) (Warnings, 0, "blank file name"); } /* pad the rest of the file name with shifted spaces */ for (; i < 16; i++) name.name[i] = 0xA0; } { char filetype; unsigned len; bool notLastFile = f < fcount; /* set the file type */ if (2 != fscanf (file, " %u \015%c\015", &blocks, &filetype)) goto hdrError; if (!fscanf (file, " %u%*2[ \015]", &len)) { /* Unspecified file length */ if (filetype == 'R' || !notLastFile) /* The length must be known for relative files */ /* and for all but the last file. */ goto hdrError; errNoLength = true; len = 255; } length = len; name.recordLength = 0; switch (filetype) { unsigned sidesectors; default: name.type = 0; (*log) (Errors, &name, "Unknown type, defaulting to DEL"); /* fall through */ case 'D': name.type = DEL; break; case 'S': name.type = SEQ; break; case 'P': name.type = PRG; break; case 'U': name.type = USR; break; case 'R': name.type = REL; name.recordLength = (byte_t) length; /* Thanks to Peter Schepers for pointing out the error in the original formula. */ sidesectors = (blocks + 119) / 121; if (!sidesectors || blocks < 121 * sidesectors - 119 || blocks > 121 * sidesectors) goto hdrError; /* negative length file */ blocks -= sidesectors; /* Lynx is stupid enough to store the side sectors in the file. */ archivePos += 254 * sidesectors; if (!(fscanf (file, " %u \015", &length))) { if (notLastFile) goto hdrError; errNoLength = true; length = 255; } if (!name.recordLength) (*log) (Warnings, &name, "zero record length"); break; } if ((blocks && length < 2) || (length == 1) || (!blocks && !errNoLength && length)) { (*log) (Errors, &name, "illegal length, skipping file"); (*log) (Errors, &name, "FATAL: the archive may be corrupted from this point on!"); continue; } if (blocks) length += (unsigned)blocks * 254 - 255; else length = 0; if (name.type == REL && name.recordLength && length % name.recordLength) (*log) (Warnings, &name, "non-integer record count"); } headerPos = ftell (file); /* Extract the file */ { byte_t* buf; size_t readlength; if (fseek (file, archivePos, SEEK_SET)) { (*log) (Errors, &name, "fseek: %s", strerror(errno)); return RdFail; } if (!(buf = malloc (length))) goto memError; if (length != (readlength = fread (buf, 1, length, file))) { if (feof (file)) { (*log) (Warnings, &name, "Truncated file, proceeding anyway"); } if (ferror (file)) { free (buf); (*log) (Errors, &name, "fread: %s", strerror(errno)); return RdFail; } } archivePos += 254 * blocks; switch ((*writeCallback) (&name, buf, readlength)) { case WrOK: break; case WrNoSpace: free (buf); return RdNoSpace; case WrFail: default: free (buf); return RdFail; } free (buf); } } if (errNoLength) (*log) (Warnings, 0, "The last file may be too long."); return RdOK; } /** Write an archive in Lynx format * @param archive the archive to be written * @param filename host file name of the archive file * @return status of the operation */ enum ArStatus ArchiveLynx (const struct Archive* archive, const char* filename) { FILE* f; struct ArchiveEntry* ae; unsigned blockcounter; static const byte_t basichdr[] = { 0x01, 0x08, 0x5b, 0x08, 0x0a, 0x00, 0x97, 0x35, 0x33, 0x32, 0x38, 0x30, 0x2c, 0x30, 0x3a, 0x97, 0x35, 0x33, 0x32, 0x38, 0x31, 0x2c, 0x30, 0x3a, 0x97, 0x36, 0x34, 0x36, 0x2c, 0xc2, 0x28, 0x31, 0x36, 0x32, 0x29, 0x3a, 0x99, 0x22, 0x93, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x22, 0x3a, 0x99, 0x22, 0x20, 0x20, 0x20, 0x20, 0x20, 0x55, 0x53, 0x45, 0x20, 0x4c, 0x59, 0x4e, 0x58, 0x20, 0x54, 0x4f, 0x20, 0x44, 0x49, 0x53, 0x53, 0x4f, 0x4c, 0x56, 0x45, 0x20, 0x54, 0x48, 0x49, 0x53, 0x20, 0x46, 0x49, 0x4c, 0x45, 0x22, 0x3a, 0x89, 0x31, 0x30, 0x00, 0x00, 0x00, 0x0d }; static const byte_t lynxhdr[] = "*LYNX BY CBMCONVERT 2.0*"; { unsigned filecnt; /* Count the files. */ for (filecnt = 0, ae = archive->first; ae; filecnt++) ae = ae->next; if (!filecnt) return ArFail; /* Write the Lynx header. */ if (!(f = fopen (filename, "wb"))) return errno == ENOSPC ? ArNoSpace : ArFail; if (1 != fwrite (basichdr, sizeof basichdr, 1, f)) { fclose (f); return errno == ENOSPC ? ArNoSpace : ArFail; } /* This is a bit overestimating the header size. */ blockcounter = (unsigned) rounddiv(sizeof basichdr + 20U + sizeof lynxhdr + 36U * filecnt, 254U); fprintf (f, " %u %s\15 %u \15", blockcounter, lynxhdr, filecnt); } if (ferror (f)) { f_error: fclose (f); return errno == ENOSPC ? ArNoSpace : ArFail; } /* Write the Lynx directory. */ for (ae = archive->first; ae; ae = ae->next) { unsigned i; /* Write the file name. Replace CRs with periods. */ for (i = 0; i < sizeof ae->name.name && i < 16; i++) putc (ae->name.name[i] == 13 ? '.' : ae->name.name[i], f); i = (unsigned) rounddiv(ae->length, 254); fprintf (f, "\15 %u\15%c\15", ae->name.type == REL ? i + rounddiv(i, 120) : i, "DSPUR"[ae->name.type & 7]); if (ae->name.type == REL) fprintf (f, " %u \15", ae->name.recordLength); fprintf (f, " %u \15", ae->length ? (unsigned)(ae->length % 254 ? (ae->length - 254 * (i - 1) + 1) : 255) : 0U); } if (ferror (f)) goto f_error; /* Write the files. */ for (ae = archive->first; ae; ae = ae->next) { unsigned blocks = (unsigned) rounddiv(ae->length, 254); /* Reserve space for the side sectors. */ if (ae->name.type == REL) blockcounter += rounddiv (blocks, 120); /* Write the file. */ if (fseek (f, blockcounter * 254, SEEK_SET) || ae->length != fwrite (ae->data, 1, ae->length, f)) { fclose (f); return ArFail; } blockcounter += blocks; } fclose (f); return ArOK; } cbmconvert-cbmconvert-2.1.5/main.c000066400000000000000000000402401424020246600171110ustar00rootroot00000000000000/** * @file main.c * Commodore file format converter * @author Marko Mäkelä (marko.makela at iki.fi) */ /* ** Copyright © 1993‒1998,2001,2003,2006,2021‒2022 Marko Mäkelä ** ** 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. */ #include #include #include #include #include #include "version.h" #include "input.h" #include "output.h" /** The default file output function */ static write_t* writeFunc = #ifdef WRITE_PC64_DEFAULT WritePC64; #else WriteNative; #endif /** The default disk image output function */ static write_img_t* writeImageFunc = WriteImage; /** The disk image being managed */ static struct Image* image = 0; /** The default archive output function */ static write_ar_t* writeArchiveFunc = ArchiveLynx; /** The file archive being managed */ static struct Archive* archive = 0; /** Name of the archive file */ static const char* archiveFilename = 0; /** Default verbosity level */ static enum Verbosity verbosityLevel = Warnings; /** Current input file name */ static const char* currentFilename = 0; /** Disk image changing policy */ enum ChangeDisks { Never, /**< Never change disk images */ Sometimes, /**< Change images when out of space */ Always /**< Change images when out of space or duplicate file name */ }; /** The default disk image changing policy */ enum ChangeDisks changeDisks = Sometimes; /** Call-back function for diagnostic output * @param verbosity the verbosity level * @param name the file name associated with the message (or NULL) * @param format printf-like format string followed by arguments */ static void writeLog (enum Verbosity verbosity, const struct Filename* name, const char* format, ...) { static struct Filename oldname; if (verbosityLevel >= verbosity) { va_list ap; if (currentFilename) { fprintf (stderr, "`%s':\n", currentFilename); currentFilename = 0; } fputs (" ", stderr); if (name) { if (memcmp(name->name, oldname.name, sizeof name->name) || name->type != oldname.type || name->recordLength != oldname.recordLength) fprintf (stderr, "`%s':\n ", getFilename (name)); else fputs (" ", stderr); memcpy(&oldname, name, sizeof oldname); } va_start (ap, format); vfprintf (stderr, format, ap); va_end (ap); fputc ('\n', stderr); } } /** Write a file * @param name native (PETSCII) name of the file * @param data the contents of the file * @param length length of the file contents * @return status of the operation */ static enum WrStatus writeFile (const struct Filename* name, const byte_t* data, size_t length) { enum WrStatus status = WrFail; if (!image && !writeFunc) return status; if (!length) writeLog (Warnings, name, "Zero length file"); if (image) { status = (*writeImageFunc) (name, data, length, image, writeLog); switch (status) { case WrOK: writeLog (Everything, name, "Wrote %u bytes to image \"%s\"", length, image->name); return WrOK; case WrFail: writeLog (Errors, name, "Write failed!"); return WrFail; case WrFileExists: if (changeDisks < Always) { writeLog (Errors, name, "non-unique file name!"); return WrFileExists; } /* non-unique file name, fall through */ case WrNoSpace: if (changeDisks < Sometimes) { writeLog (Errors, name, "out of space!"); return WrNoSpace; } /* try to open a new disk image */ writeLog (Warnings, name, status == WrFileExists ? "non-unique file name, changing disk images..." : "out of space, changing disk images..."); switch (CloseImage (image)) { unsigned char* c; case ImNoSpace: writeLog (Errors, name, "out of space"); free (image->name); free (image); image = 0; return WrNoSpace; case ImFail: writeLog (Errors, name, "failed"); free (image->name); free (image); image = 0; return WrFail; case ImOK: writeLog (Everything, name, "wrote old image \"%s\"", image->name); /* ** Update the file name. If there is a number in the first ** component of the file name (excluding any directory component), ** increment it. */ for (c = image->name; *c; c++); for (; c >= image->name && *c != PATH_SEPARATOR; c--); for (c++; *c && *c != '.'; c++); while (--c >= image->name) if (*c >= '0' && *c < '9') { (*c)++; break; } else if (*c == '9') *c = '0'; else goto notUnique; if (c < image->name) { notUnique: writeLog (Errors, name, "Could not generate unique image file name"); free (image->name); free (image); image = 0; return WrFail; } writeLog (Everything, name, "Continuing to image \"%s\"...", image->name); { char* filename = (char*) image->name; enum ImageType type = image->type; enum DirEntOpts direntOpts = image->direntOpts; enum ImStatus imstatus; free (image); image = 0; imstatus = OpenImage (filename, &image, type, direntOpts); free (filename); switch (imstatus) { case ImOK: status = (*writeImageFunc) (name, data, length, image, writeLog); if (status == WrOK) writeLog (Everything, name, "OK, wrote %u bytes to image \"%s\"", length, image->name); else writeLog (Errors, name, "%s while writing to \"%s\", giving up.", status == WrNoSpace ? "out of space" : status == WrFileExists ? "duplicate file name" : "failed", image->name); return status; case ImNoSpace: writeLog (Errors, name, "out of space while creating image \"%s\"", image->name); return WrNoSpace; default: writeLog (Errors, name, "failed while creating image \"%s\"", image->name); return WrFail; } } } } } else if (archive) { status = WriteArchive (name, data, length, archive, writeLog); switch (status) { case WrOK: writeLog (Everything, name, "Wrote %u bytes to archive \"%s\"", length, archiveFilename); return WrOK; case WrFail: writeLog (Errors, name, "Write failed!"); return WrFail; case WrFileExists: writeLog (Errors, name, "non-unique file name!"); return WrFileExists; case WrNoSpace: writeLog (Errors, name, "out of space!"); return WrNoSpace; } } else { char* newname = 0; status = (*writeFunc) (name, data, length, &newname, writeLog); if (status == WrOK) writeLog (Everything, name, "Writing %u bytes to \"%s\"", length, newname); else writeLog (Errors, name, "%s while writing to \"%s\"", status == WrNoSpace ? "out of space" : "failed", newname); free (newname); return status; } return status; } /** Convert a disk image type code to a printable string * @param im the disk image type code * @return a corresponding printable character string */ static const char* imageType (enum ImageType im) { switch (im) { case ImUnknown: break; case Im1541: return "1541"; case Im1571: return "1571"; case Im1581: return "1581"; } return "(unknown)"; } /** The main program * @param argc number of command-line arguments * @param argv contents of the command-line arguments * @return 0 on success, nonzero on error */ int main (int argc, char** argv) { read_file_t* readFunc = ReadNative; FILE* file; char* prog = *argv; /* name of the program */ int retval = 0; /* return status */ /* process the option flags */ for (argv++; argc > 1 && **argv == '-'; argv++, argc--) { char* opts = *argv; if (!strcmp (opts, "--")) { /* disable processing further options */ argv++; argc--; break; } while (*++opts) /* process all flags */ switch (*opts) { case 'v': switch (opts[1]) { case 'v': case '2': verbosityLevel = Everything; break; case 'w': case '1': verbosityLevel = Warnings; break; case '0': verbosityLevel = Errors; break; default: goto Usage; } opts++; break; case 'i': switch (opts[1]) { case '0': changeDisks = Never; break; case '1': changeDisks = Sometimes; break; case '2': changeDisks = Always; break; default: goto Usage; } opts++; break; case 'n': readFunc = ReadNative; break; case 'p': readFunc = ReadPC64; break; case 'a': readFunc = ReadARC; break; case 'k': readFunc = ReadArkive; break; case 'l': readFunc = ReadLynx; break; case 't': readFunc = ReadT64; break; case 'c': readFunc = ReadC2N; break; case 'd': readFunc = ReadImage; break; case 'm': readFunc = ReadCpmImage; break; case 'I': writeFunc = Write9660; break; case 'P': writeFunc = WritePC64; break; case 'N': writeFunc = WriteNative; break; case 'L': if (image || archive || argc <= 2) goto Usage; archive = newArchive(); writeArchiveFunc = ArchiveLynx; archiveFilename = *++argv;argc--; break; case 'C': if (image || archive || argc <= 2) goto Usage; archive = newArchive(); writeArchiveFunc = ArchiveC2N; archiveFilename = *++argv;argc--; break; case 'M': case 'D': if (archive) goto Usage; if (argc > 2) { enum ImageType im = Im1541; switch (opts[1]) { case '4': im = Im1541; break; case '7': im = Im1571; break; case '8': im = Im1581; break; default: goto Usage; } writeFunc = 0; writeImageFunc = *opts == 'M' ? WriteCpmImage : WriteImage; opts++; argc--; argv++; { enum DirEntOpts dopts = DirEntOnlyCreate; if (opts[1] == 'o') { dopts = DirEntFindOrCreate; opts++; } if (OpenImage (*argv, &image, im, dopts) != ImOK) { fprintf (stderr, "Could not open the %s%s image '%s'.\n", writeImageFunc == WriteCpmImage ? "CP/M " : "", imageType (im), *argv); return 2; } } } else goto Usage; break; default: goto Usage; } } if (argc < 2) { Usage: fprintf (stderr, "cbmconvert " VERSION " - Commodore archive converter\n" "Usage: %s [options] file(s)\n", prog); fputs ("Options: -I: Create ISO 9660 compliant file names.\n" " -P: Output files in PC64 format.\n" " -N: Output files in native format.\n" " -L archive.lnx: Output files in Lynx format.\n" " -C archive.c2n: Output files in Commodore C2N format.\n" " -D4 imagefile: Write to a 1541 disk image.\n" " -D4o imagefile: Ditto, overwriting existing files.\n" " -D7[o] imagefile: Write to a 1571 disk image.\n" " -D8[o] imagefile: Write to a 1581 disk image.\n" " -M4[o] imagefile: Write to a 1541 CP/M disk image.\n" " -M7[o] imagefile: Write to a 1571 CP/M disk image.\n" " -M8[o] imagefile: Write to a 1581 CP/M disk image.\n" "\n" " -i2: Switch disk images on out of space or duplicate file name.\n" " -i1: Switch disk images on out of space.\n" " -i0: Never switch disk images.\n" "\n" " -n: input files in native format.\n" " -p: input files in PC64 format.\n" " -a: input files in ARC/SDA format.\n" " -k: input files in Arkive format.\n" " -l: input files in Lynx format.\n" " -t: input files in T64 format.\n" " -c: input files in Commodore C2N format.\n" " -d: input files in disk image format.\n" " -m: input files in C128 CP/M disk image format.\n" "\n" " -v2: Verbose mode. Display all messages.\n" " -v1: Display warnings in addition to errors.\n" " -v0: Display error messages only.\n" " --: Stop processing any further options.\n", stderr); if (image) { CloseImage (image); free (image->name); free (image); image = 0; } if (archive) { deleteArchive (archive); } return 1; } /* Process the files. */ for (; --argc; argv++) { enum RdStatus status; currentFilename = *argv; if (!(file = fopen (currentFilename, "rb"))) { fprintf (stderr, "fopen '%s': %s\n", currentFilename, strerror(errno)); retval = 2; continue; } status = (*readFunc) (file, *argv, writeFile, writeLog); fclose (file); switch (status) { case RdOK: writeLog (Everything, 0, "Archive extracted."); break; case RdNoSpace: writeLog (Errors, 0, "out of space."); retval = 3; goto read_error; default: writeLog (Errors, 0, "unexpected error."); retval = 4; read_error: if (image || archive) goto write; else return retval; } } write: if (image) { switch (CloseImage (image)) { case ImOK: writeLog (Everything, 0, "Wrote image file \"%s\"", image->name); break; case ImNoSpace: writeLog (Errors, 0, "Out of space while writing image file \"%s\"!", image->name); return 3; default: writeLog (Errors, 0, "Unexpected error while writing image \"%s\"!", image->name); return 4; } free (image->name); free (image); image = 0; } if (archive) { switch ((*writeArchiveFunc) (archive, archiveFilename)) { case ArOK: writeLog (Everything, 0, "Wrote archive file \"%s\"", archiveFilename); break; case ArNoSpace: writeLog (Everything, 0, "Out of space while writing archive file \"%s\"!", archiveFilename); return 3; default: writeLog (Everything, 0, "Unexpected error while writing image \"%s\"!", archiveFilename); return 4; } deleteArchive (archive); archive = 0; } if (verbosityLevel == Everything) fprintf (stderr, "%s: all done\n", prog); return retval; } cbmconvert-cbmconvert-2.1.5/output.h000066400000000000000000000123121424020246600175310ustar00rootroot00000000000000/** * @file output.h * Definitions for file writing functions * @author Marko Mäkelä (marko.makela at iki.fi) */ /* ** Copyright © 1993-1997,2001 Marko Mäkelä ** ** 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. */ #ifndef OUTPUT_H # define OUTPUT_H # include "util.h" /* File management */ /** Writing status */ enum WrStatus { WrOK, /** #include #include #include #include "input.h" /** Convert an ASCII character to PETSCII * @param c the ASCII character to be converted * @return a corresponding PETSCII character, or '+' if no conversion */ static unsigned char ascii2petscii (unsigned char c) { if (c >= 'A' && c <= 'Z') /* convert upper case letters */ c -= (unsigned char) ('A' - 0xC1); else if (c >= 'a' && c <= 'z') /* convert lower case letters */ c -= (unsigned char) ('a' - 0x41); else if ((c & 127) < 32) /* convert control characters */ c = '-'; else if (c == 0xa0); /* do not touch shifted spaces */ else if (c > 'z') /* convert graphics characters */ c = '+'; return c; } /** Read a file in the native format of the host system * @param file the file input stream * @param filename host system name of the file * @param writeCallback function for writing the contained files * @param log Call-back function for diagnostic output * @return status of the operation */ enum RdStatus ReadNative (FILE* file, const char* filename, write_file_t writeCallback, log_t log) { struct Filename name; const char* suffix = 0; size_t i; byte_t* buf; static byte_t dummy; enum WrStatus status; /* Get the file base name */ for (i = strlen (filename); i && filename[i] != PATH_SEPARATOR; i--); if (filename[i] == PATH_SEPARATOR) filename += i + 1; if (!*filename) { filename = "null.prg"; (*log) (Warnings, 0, "Null file name, changed to %s", filename); } i = strlen (filename); /* Determine the file type. */ name.recordLength = 0; if (i >= 3 && filename[i - 2] == ',') { switch (filename[i - 1]) { case 'd': case 'D': name.type = DEL; break; case 's': case 'S': name.type = SEQ; break; case 'p': case 'P': name.type = PRG; break; case 'u': case 'U': name.type = USR; break; default: goto UnknownType; } suffix = &filename[i - 2]; } else if (i >= 5 && (filename[i - 4] == '.' || filename[i - 4] == ',')) { suffix = &filename[i - 3]; if (!strcmp (suffix, "del") || !strcmp (suffix, "DEL")) name.type = DEL; else if (!strcmp (suffix, "seq") || !strcmp (suffix, "SEQ")) name.type = SEQ; else if (!strcmp (suffix, "prg") || !strcmp (suffix, "PRG") || !strcmp (suffix, "cvt") || !strcmp (suffix, "CVT")) name.type = PRG; /* CVT for GEOS Convert files */ else if (!strcmp (suffix, "usr")) name.type = USR; else if (!strcmp (suffix, "rel") || !strcmp (suffix, "REL")) { name.type = REL; (*log) (Warnings, 0, "unknown record length"); } else if (*suffix == 'l') { char* endptr = 0; unsigned long recordLength = strtoul (suffix + 1, &endptr, 16); if (*endptr || recordLength > 254) goto UnknownType; name.type = REL; name.recordLength = (unsigned char) recordLength; } else goto UnknownType; suffix--; } else { UnknownType: (*log) (Warnings, 0, "Unknown type, defaulting to PRG"); name.type = PRG; suffix = 0; } /* Copy the file name */ if (suffix) { for (i = 0; i < (unsigned) (suffix - filename) && i < sizeof(name.name); i++) name.name[i] = ascii2petscii((unsigned char) filename[i]); } else { for (i = 0; filename[i] && i < sizeof(name.name); i++) name.name[i] = ascii2petscii((unsigned char) filename[i]); } memset (&name.name[i], 0xA0/* shifted space */, (sizeof name.name) - i); /* Determine file length. */ if (fseek (file, 0, SEEK_END)) { seekError: (*log) (Errors, 0, "fseek: %s", strerror(errno)); return RdFail; } else { long l = ftell (file); if (l < 0) goto seekError; i = (size_t) l; } if (fseek (file, 0, SEEK_SET)) goto seekError; if (i == 0) buf = &dummy; else if (!(buf = malloc (i))) { (*log) (Errors, 0, "Out of memory."); return RdFail; } else if (1 != fread (buf, i, 1, file)) { (*log) (Errors, 0, "fread: %s", strerror(errno)); free (buf); return RdFail; } status = (*writeCallback) (&name, buf, i); if (i) free (buf); switch (status) { case WrOK: return RdOK; case WrNoSpace: return RdNoSpace; case WrFail: default: return RdFail; } } /** Read a PC64 file (.P00, .S00 etc.) * @param file the file input stream * @param filename host system name of the file * @param writeCallback function for writing the contained files * @param log Call-back function for diagnostic output * @return status of the operation */ enum RdStatus ReadPC64 (FILE* file, const char* filename, write_file_t writeCallback, log_t log) { struct Filename name; const char* suffix = 0; unsigned i; byte_t* buf; enum WrStatus status; /* Determine file type. */ { size_t s = strlen (filename); if (s < 5) { (*log) (Errors, 0, "No PC64 file name suffix"); return RdFail; /* no suffix */ } suffix = &filename[s - 4]; } if (1 == sscanf (suffix, ".d%02u", &i)) name.type = DEL; else if (1 == sscanf (suffix, ".s%02u", &i)) name.type = SEQ; else if (1 == sscanf (suffix, ".p%02u", &i)) name.type = PRG; else if (1 == sscanf (suffix, ".u%02u", &i)) name.type = USR; else if (1 == sscanf (suffix, ".r%02u", &i)) name.type = REL; else { (*log) (Errors, 0, "Unknown PC64 file type suffix"); return RdFail; } /* Determine file length. */ if (fseek (file, 0, SEEK_END)) { seekError: (*log) (Errors, 0, "fseek: %s", strerror(errno)); return RdFail; } else { long l = ftell (file); if (l < 0) goto seekError; i = (unsigned) l; } if (fseek (file, 0, SEEK_SET)) goto seekError; if (i < 26) { (*log) (Errors, 0, "short file"); return RdFail; } if (!(buf = malloc (i))) { (*log) (Errors, 0, "Out of memory."); return RdFail; } /* Read the file. */ if (1 != fread (buf, i, 1, file)) { (*log) (Errors, 0, "fread: %s", strerror(errno)); free (buf); return RdFail; } /* Check the file header. */ if (memcmp (buf, "C64File", 8)) { (*log) (Errors, 0, "Invalid PC64 header"); free (buf); return RdFail; } memcpy (name.name, &buf[8], 16); name.recordLength = buf[25]; status = (*writeCallback) (&name, &buf[26], i - 26); free (buf); switch (status) { case WrOK: return RdOK; case WrNoSpace: return RdNoSpace; case WrFail: default: return RdFail; } } cbmconvert-cbmconvert-2.1.5/small_files.cmake000066400000000000000000000147511424020246600213250ustar00rootroot00000000000000MACRO(MD5SUM expected filename) EXECUTE_PROCESS(COMMAND ${CMAKE_COMMAND} -E md5sum ${filename} OUTPUT_VARIABLE md5) IF (NOT md5 MATCHES ${expected}) MESSAGE(FATAL_ERROR "unexpected md5sum: " ${md5}) ENDIF() ENDMACRO() MACRO(EXECUTE_PROGRAM) EXECUTE_PROCESS(COMMAND ${ARGV} RESULT_VARIABLE res) IF (res) MESSAGE(FATAL_ERROR "${ARGV} failed: " ${res}) ENDIF() ENDMACRO() MACRO(EXECUTE_PROGRAM_EXPECT expect_res) EXECUTE_PROCESS(COMMAND ${ARGN} RESULT_VARIABLE res) IF (NOT res EQUAL ${expect_res}) MESSAGE(FATAL_ERROR "${ARGN} failed: " ${res}) ENDIF() ENDMACRO() MACRO(CBMCONVERT) EXECUTE_PROGRAM(${CBMCONVERT} ${ARGV}) ENDMACRO() FILE(REMOVE 123.d64 123.d71 123.d81 123.lnx 123.c2n 5.d64 5.lnx 5.l7f 5.rel) FILE(WRITE 1,S "1") FILE(WRITE 2,U "12") FILE(WRITE 3,D "123") FOREACH(i RANGE 1 254) LIST(APPEND b ${i}) ENDFOREACH() LIST(REMOVE_ITEM b 59) STRING(ASCII ${b} b) FILE(WRITE 4,P "${b};") FILE(WRITE 5.l7F "${b}:") CBMCONVERT(-D4 123.d64 1,S 2,U 3,D 4,P 5.l7F) MD5SUM(f068fc93ce11c33ba3dbc273b151a83e 123.d64) EXECUTE_PROGRAM_EXPECT(1 ${CBMCONVERT} -D 123.d64 5.l7F) CBMCONVERT(-D4o 123.d64 5.l7F) MD5SUM(f068fc93ce11c33ba3dbc273b151a83e 123.d64) CBMCONVERT(-L 123.lnx -d 123.d64) MD5SUM(0b8f4594e8dd8677934882d2456422b0 123.lnx) CBMCONVERT(-vv -C 123.c2n -d 123.d64) MD5SUM(b0170b67e12cbc190049e1da61718521 123.c2n) EXECUTE_PROGRAM_EXPECT(4 ${CBMCONVERT} -D4 123.d64 -c 123.c2n) MD5SUM(f068fc93ce11c33ba3dbc273b151a83e 123.d64) FILE(REMOVE 123.lnx 123.d64) CBMCONVERT(-L 123.lnx -c 123.c2n) MD5SUM(a8bc8ccd4608261c72bd56d455ad7235 123.lnx) EXECUTE_PROGRAM_EXPECT(4 ${CBMCONVERT} -C 123.c2n -p 123.lnx) FILE(REMOVE 123.c2n 123.lnx) FILE(RENAME 1,S 1,s) FILE(RENAME 2,U 2,u) FILE(RENAME 3,D 3,d) FILE(RENAME 4,P 4,p) FILE(RENAME 5.l7F 5.l7f) CBMCONVERT(-L 123.lnx -n 1,s 2,u 3,d 4,p 5.l7f) MD5SUM(0b8f4594e8dd8677934882d2456422b0 123.lnx) CBMCONVERT(-D4o 123.d64 -l 123.lnx) EXECUTE_PROGRAM_EXPECT(4 ${CBMCONVERT} -D4 123.d64 -l 123.lnx) MD5SUM(f068fc93ce11c33ba3dbc273b151a83e 123.d64) CBMCONVERT(-D4o 123.d64 5.l7f) MD5SUM(f068fc93ce11c33ba3dbc273b151a83e 123.d64) CBMCONVERT(-P -l 123.lnx) MD5SUM(ca80b5d5492283789cb53c1868783b83 1.s00) MD5SUM(2abfea98086627c448530dae6f19970f 2.u00) MD5SUM(ac1246d517a9d6db969185fe60746900 3.d00) MD5SUM(95b1642dcb7ff9761d0e8924c5a52c37 4.p00) MD5SUM(ed5d3175a2f116893991f8d25604d2b0 5.r00) EXECUTE_PROGRAM(${DISK2ZIP} 123.d64 123) MD5SUM(0c66b6561fb968faeb751eb94a68098b 1!123) MD5SUM(ef4bae7508940492d5452b4f13965cab 2!123) MD5SUM(c23f22ebdf4b51136abb86818f180333 3!123) MD5SUM(0cae9f355cf09bc153272c4b8d2b97cd 4!123) EXECUTE_PROGRAM(${ZIP2DISK} 123 4.d64) EXECUTE_PROGRAM(${CMAKE_COMMAND} -E compare_files 123.d64 4.d64) EXECUTE_PROGRAM_EXPECT(3 ${CBMCONVERT} -i0 -D4 123.d64 4.d64) EXECUTE_PROGRAM_EXPECT(3 ${CBMCONVERT} -i1 -D4 123.d64 4.d64) EXECUTE_PROGRAM_EXPECT(3 ${CBMCONVERT} -i2 -D4 123.d64 4.d64) MD5SUM(274f94a63ada0913cf717677e536cdf9 124.d64) FILE(REMOVE 124.d64) EXECUTE_PROGRAM(${CMAKE_COMMAND} -E compare_files 123.d64 4.d64) EXECUTE_PROGRAM_EXPECT(1 ${CBMCONVERT} -D4 123.d64 -i-) EXECUTE_PROGRAM_EXPECT(1 ${CBMCONVERT} -L 4.lnx -i) FILE(REMOVE 123.d64) CBMCONVERT(-p -D4o 123.d64 1.s00 2.u00 3.d00 4.p00 5.r00) EXECUTE_PROGRAM_EXPECT(4 ${CBMCONVERT} -p -D4 123.d64 1.s00 2.u00 3.d00 4.p00 5.r00) CBMCONVERT(-D4o 123.d64 -n 5.l7f) EXECUTE_PROGRAM(${CMAKE_COMMAND} -E compare_files 123.d64 4.d64) CBMCONVERT(-D7 123.d71 -d 123.d64) MD5SUM(04c5320430d7d4747a1b95c824cb4943 123.d71) EXECUTE_PROGRAM_EXPECT(4 ${CBMCONVERT} -D8 123.d81 -d 123.d71) MD5SUM(6ebe65648231d6d170faf073de4dd90c 123.d81) FILE(REMOVE 123.d81) EXECUTE_PROGRAM_EXPECT(4 ${CBMCONVERT} -D8 123.d81 -d 123.d64) MD5SUM(6ebe65648231d6d170faf073de4dd90c 123.d81) FILE(REMOVE 123.d81 123.d64) CBMCONVERT(-D4 123.d64 -d 123.d71) FILE(REMOVE 123.d71 123.d81) EXECUTE_PROGRAM(${CMAKE_COMMAND} -E compare_files 123.d64 4.d64) FILE(RENAME 5.l7f 5.lFF) CBMCONVERT(-P 5.lFF) MD5SUM(5efaffddcbebb247092bd9b4b11dde78 5_lff.p00) FILE(RENAME 5.lFF 5,f) CBMCONVERT(-P 5,f) MD5SUM(8a757d89cb216af47a66a6a8632fa5ed 5_f.p00) FILE(RENAME 5,f 5.rel) EXECUTE_PROGRAM_EXPECT(4 ${CBMCONVERT} -D4 123.d64 5.rel) FILE(REMOVE 1,s 2,u 3,d 4,p 5_lff.p00 5_f.p00 1.s00 2.u00 3.d00 4.p00 5.r00 123.lnx 123.d64 4.d64 1!123 2!123 3!123 4!123) FILE(RENAME 5.rel 5.l7f) FILE(APPEND 5.l7f "${b}:${b}:${b}:${b}:${b}:${b}:${b}") FILE(READ 5.l7f b8) FILE(APPEND 5.l7f "${b8}:${b8}:${b8}:${b8}:${b8}:${b8}:${b8}") FILE(READ 5.l7f b8) FILE(APPEND 5.l7f "${b8}:${b8}:${b8}:${b8}:${b8}:${b8}:${b8}:${b8}:${b8}") CBMCONVERT(-L 5.lnx 5.l7f) MD5SUM(3a72dcc312183124d05987ddd8b28205 5.lnx) CBMCONVERT(-D4 5.d64 5.l7f) MD5SUM(4e8315ba00cecef6883a1ed58cc66911 5.d64) FILE(RENAME 5.l7f 5.rel) CBMCONVERT(-I -l 5.lnx) EXECUTE_PROGRAM(${CMAKE_COMMAND} -E compare_files 5.l7F 5.rel) FILE(REMOVE 5.l7F) CBMCONVERT(-d 5.d64) EXECUTE_PROGRAM(${CMAKE_COMMAND} -E compare_files 5.l7F 5.rel) FILE(REMOVE 5.l7F 5.rel 5.lnx 5.d64) STRING(ASCII 1 1 1 1 129 1 t) FILE(WRITE 1!foo "${t}") EXECUTE_PROGRAM_EXPECT(4 ${ZIP2DISK} foo foo.d64) FILE(APPEND 1!foo "1") EXECUTE_PROGRAM_EXPECT(4 ${ZIP2DISK} foo foo.d64) FILE(APPEND 1!foo "2") EXECUTE_PROGRAM_EXPECT(4 ${ZIP2DISK} foo foo.d64) FILE(APPEND 1!foo "2") EXECUTE_PROGRAM_EXPECT(4 ${ZIP2DISK} foo foo.d64) STRING(ASCII 1 1 1 1 129 1 1 1 1 t) FILE(WRITE 1!foo "${t}") EXECUTE_PROGRAM_EXPECT(4 ${ZIP2DISK} foo foo.d64) STRING(ASCII 1 1 1 1 129 1 100 1 1 255 2 3 4 t) FILE(WRITE 1!foo "${t}") EXECUTE_PROGRAM_EXPECT(4 ${ZIP2DISK} foo foo.d64) STRING(ASCII 1 1 1 1 129 1 2 3 4 5 t) FILE(WRITE 1!foo "${t}") EXECUTE_PROGRAM_EXPECT(4 ${ZIP2DISK} foo foo.d64) STRING(ASCII 1 1 1 1 65 1 t) FILE(WRITE 1!foo "${t}") EXECUTE_PROGRAM_EXPECT(4 ${ZIP2DISK} foo foo.d64) STRING(ASCII 1 1 1 1 1 1 t) FILE(WRITE 1!foo "${t}") EXECUTE_PROGRAM_EXPECT(4 ${ZIP2DISK} foo foo.d64) FILE(REMOVE 1!foo foo.d64) STRING(ASCII 1 2 1 2 1 r) STRING(RANDOM LENGTH 187 r2) FILE(WRITE foo.c2n "${r}${r2}") STRING(ASCII 4 2 1 2 1 s) FILE(APPEND foo.c2n "${s}${r2}") STRING(ASCII 2 t) STRING(ASCII 5 v) STRING(ASCII 6 w) STRING(RANDOM LENGTH 191 u) FILE(APPEND foo.c2n "${t}${u}${v}${u}${w}${u}") EXECUTE_PROGRAM_EXPECT(4 ${CBMCONVERT} -D4 foo.d64 -c foo.c2n) FILE(WRITE foo.c2n "${u}") EXECUTE_PROGRAM_EXPECT(4 ${CBMCONVERT} -D4 foo.d64 -c foo.c2n) FILE(WRITE foo.c2n "${s}${s2}") EXECUTE_PROGRAM_EXPECT(4 ${CBMCONVERT} -D4 foo.d64 -c foo.c2n) FILE(APPEND foo.c2n "${t}truncated") EXECUTE_PROGRAM_EXPECT(4 ${CBMCONVERT} -D4 foo.d64 -c foo.c2n) STRING(ASCII 1 1 4 2 4 x) FILE(WRITE foo.c2n "${x}${r2}") EXECUTE_PROGRAM_EXPECT(4 ${CBMCONVERT} -D4 foo.d64 -c foo.c2n) FILE(REMOVE foo.c2n foo.d64) cbmconvert-cbmconvert-2.1.5/t64.c000066400000000000000000000160641424020246600166110ustar00rootroot00000000000000/** * @file t64.c * T64 archive extractor * @author Marko Mäkelä (marko.makela at iki.fi) */ /* ** Copyright © 1997,2001,2003,2021 Marko Mäkelä ** Based on the cbmarcs.c file of fvcbm 2.0 by Daniel Fandrich ** ** 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. */ #include #include #include #include #include "input.h" /** A C64S T64 file header */ struct t64header { /** Header identifier block (magic cookie) */ byte_t headerblock[32]; /** Minor version number */ byte_t minorVersion; /** Major version number */ byte_t majorVersion; /** Least significant byte of the maximum number of entries */ byte_t maxEntriesLow; /** Most significant byte of the maximum number of entries */ byte_t maxEntriesHigh; /** Least significant byte of the actual number of entries */ byte_t numEntriesLow; /** Most significant byte of the actual number of entries */ byte_t numEntriesHigh; /** Unused */ byte_t padding[26]; }; /** A T64 directory entry */ struct t64entry { /** 1 == normal file */ byte_t entryType; /** Commodore file type (1 or $82 for PRG) */ byte_t fileType; /** Least significant byte of the file's start address */ byte_t startAddrLow; /** Most significant byte of the file's start address */ byte_t startAddrHigh; /** Least significant byte of the file's end address */ byte_t endAddrLow; /** Most significant byte of the file's end address */ byte_t endAddrHigh; /** Unused */ byte_t padding1[2]; /** Least significant byte of the file offset */ byte_t fileOffsetLowest; /** Less significant byte of the file offset */ byte_t fileOffsetLower; /** More significant byte of the file offset */ byte_t fileOffsetHigher; /** Most significant byte of the file offset */ byte_t fileOffsetHighest; /** Unused */ byte_t padding2[4]; /** Commodore file name */ byte_t name[16]; }; /** Read and convert a tape archive of the C64S emulator * @param file the file input stream * @param filename host system name of the file * @param writeCallback function for writing the contained files * @param log Call-back function for diagnostic output * @return status of the operation */ enum RdStatus ReadT64 (FILE* file, const char* filename, write_file_t writeCallback, log_t log) { unsigned numEntries, entry; /* Check the header. */ { struct t64header t64header; unsigned maxEntries; static const char T64Header1[] = "C64 tape image file"; static const char T64Header2[] = "C64S tape file"; static const char T64Header3[] = "C64S tape image file"; if (1 != fread (&t64header, sizeof t64header, 1, file)) { freadFail: (*log) (Errors, 0, "fread: %s", strerror (errno)); return RdFail; } if (memcmp (t64header.headerblock, T64Header1, sizeof T64Header1 - 1) && memcmp (t64header.headerblock, T64Header2, sizeof T64Header2 - 1) && memcmp (t64header.headerblock, T64Header3, sizeof T64Header3 - 1)) { (*log) (Errors, 0, "Unknown T64 header"); return RdFail; } if (t64header.majorVersion != 1 || t64header.minorVersion > 1) (*log) (Errors, 0, "Unknown T64 version, trying anyway"); maxEntries = ((unsigned) t64header.maxEntriesLow | ((unsigned) t64header.maxEntriesHigh << 8)); numEntries = ((unsigned) t64header.numEntriesLow | ((unsigned) t64header.numEntriesHigh << 8)); if (!numEntries) { (*log) (Warnings, 0, "Number of entries set to zero; trying to read the first entry"); numEntries = 1; } else if (numEntries > maxEntries) { (*log) (Errors, 0, "Error in the number of entries"); return RdFail; } (*log) (Everything, 0, "T64 version %u.%u, %u/%u files", t64header.majorVersion, t64header.minorVersion, numEntries, maxEntries); } /* Process the files */ for (entry = 0; entry < numEntries; entry++) { struct t64entry t64entry; struct Filename name; long fileoffset; size_t length; name.type = PRG; name.recordLength = 0; if (fseek (file, (long) (entry * sizeof t64entry + sizeof (struct t64header)), SEEK_SET)) { (*log) (Errors, 0, "fseek: %s", strerror (errno)); return RdFail; } if (1 != fread (&t64entry, sizeof t64entry, 1, file)) goto freadFail; /* Convert the header. */ memcpy (name.name, t64entry.name, 16); { int i; /* Convert trailing spaces to shifted spaces. */ for (i = 16; --i && name.name[i] == ' '; name.name[i] = 0xA0); } fileoffset = (long) t64entry.fileOffsetLowest | t64entry.fileOffsetLower << 8 | t64entry.fileOffsetHigher << 16 | t64entry.fileOffsetHighest << 24; length = ((t64entry.endAddrLow | (size_t) t64entry.endAddrHigh << 8) - (t64entry.startAddrLow | (size_t) t64entry.startAddrHigh << 8)) & 0xFFFF; if (t64entry.entryType != 1) { unknown: (*log) (Errors, &name, "Unknown entry type 0x%02x 0x%02x, assuming PRG", t64entry.entryType, t64entry.fileType); } else if (t64entry.fileType != 1) { unsigned filetype = t64entry.fileType & 0x8F; if (filetype >= DEL && filetype <= USR) name.type = filetype; else goto unknown; } /* Read the file */ { byte_t* buf; enum WrStatus status; size_t readlength; if (!(buf = malloc (length + 2))) { (*log) (Errors, &name, "Out of memory."); return RdFail; } buf[0] = t64entry.startAddrLow; buf[1] = t64entry.startAddrHigh; if (fseek (file, fileoffset, SEEK_SET)) { (*log) (Errors, &name, "fseek: %s", strerror(errno)); free (buf); return RdFail; } if (length != (readlength = fread (&buf[2], 1, length, file))) { if (feof (file)) (*log) (Warnings, &name, "Truncated file, proceeding anyway"); if (ferror (file)) { (*log) (Errors, &name, "fread: %s", strerror(errno)); free (buf); return RdFail; } } status = (*writeCallback) (&name, buf, readlength + 2); free (buf); switch (status) { case WrOK: break; case WrNoSpace: return RdNoSpace; case WrFail: default: return RdFail; } } } return RdOK; } cbmconvert-cbmconvert-2.1.5/unarc.c000066400000000000000000000403401424020246600172760ustar00rootroot00000000000000/** * @file unarc.c * ARC/SDA (C64/C128) archive extractor * @author Chris Smeets * @author Marko Mäkelä (marko.makela at iki.fi) */ /* ** Derived from Chris Smeets' MS-DOS utility a2l that converts Commodore ** ARK, ARC and SDA files to LZH using LHARC 1.12. Optimized for speed, ** converted to standard C and adapted to the cbmconvert package by ** Marko Mäkelä. ** ** Original version: a2l.c March 1st, 1990 Chris Smeets ** Unix port: unarc.c August 28th, 1993 Marko Mäkelä ** Restructured for cbmconvert 2.0 and 2.1 by Marko Mäkelä */ #include #include #include #include #include #include "input.h" /** I/O status. 0=ok, or EOF */ static int Status; /** Current offset from ARC or SDA file's beginning */ static long FilePos; /** Current archive */ static FILE* fp; /** Bit Buffer */ static unsigned int BitBuf; /** checksum */ static unsigned int crc; /** used in checksum calculation */ static unsigned char crc2; /** Huffman codes */ static unsigned long hc[256]; /** Lengths of huffman codes */ static unsigned char hl[256]; /** Character associated with Huffman code */ static unsigned char hv[256]; /** Number of Huffman codes */ static int hcount; /** Run-Length control character */ static unsigned int ctrl; /** C64 Archive entry header */ struct entry { /** Version number, must be 1 or 2 */ unsigned char version; /** 0=store, 1=pack, 2=squeeze, 3=crunch, 4=squeeze+pack, 5=crunch in one pass */ unsigned char mode; /** Checksum */ unsigned int check; /** Original size. Only three bytes are stored */ size_t size; /** Compressed size in CBM disk blocks */ unsigned int blocks; /** File type. P,S,U or R */ unsigned char type; /** Filename length */ unsigned char fnlen; /** Filename. Only fnlen bytes are stored */ unsigned char name[17]; /** Record length if relative file */ unsigned char rl; /** Date archive was created. Same format as in MS-DOS directories */ unsigned int date; }; /** Current C64 Archive entry header */ struct entry entry; /** Lempel Zev compression string table entry */ struct lz { unsigned int prefix; /**< Prefix code */ byte_t ext; /**< Extension character */ }; /** Lempel Zev compression string table */ struct lz lztab[4096]; /** Lempel Zev stack handling error codes */ enum LZStackErrorType { PushError = 1, /**< Stack overflow */ PopError /**< Stack underflow (out of data) */ }; /** Lempel Zev exception handling structure */ static jmp_buf LZStackError; /** Lempel Zev stack */ static byte_t stack[512]; /** Set to 0 to reset un-crunch */ static int State = 0; /** Lempel Zev stack pointer */ static unsigned lzstack = 0; /** Current code size */ static int cdlen; /** Last received code */ static unsigned code; /** Bump cdlen when code reaches this value */ static int wtcl; /** Copy of wtcl */ static int wttcl; /** Shell Sort algorithm * from "C Programmer's Library" by Purdum, Leslie and Stegemoller */ static void ssort (void) { size_t m; size_t h,i,j,k; m = sizeof hl; while (m >>= 1) { k = (sizeof hl) - m; j = 1; do { i = j; do { h = i + m; if (hl[h - 1] > hl[i - 1]) { unsigned long t; unsigned char u; t = hc[i - 1], hc[i - 1] = hc[h - 1], hc[h - 1] = t; u = hv[i - 1], hv[i - 1] = hv[h - 1], hv[h - 1] = u; u = hl[i - 1], hl[i - 1] = hl[h - 1], hl[h - 1] = u; i -= m; } else break; } while (i >= 1); j += 1; } while(j <= k); } } /** Receive a byte (eight bits) from the input * @return the received byte */ static byte_t GetByte (void) { if (Status == EOF) return 0; if (feof (fp) || ferror (fp)) { Status = EOF; return 0; } else Status = 0; return (unsigned char) (fgetc (fp) & 0xff); } /** Receive a word (sixteen bits) from the input * @return the received word */ static word_t GetWord (void) { word_t u = 0; if (Status == EOF) return 0; if (feof (fp) || ferror (fp)) { Status = EOF; return 0; } else { Status = 0; } u = (word_t) fgetc (fp) & 0xff; u |= ((word_t) fgetc (fp) & 0xff) << 8; return u; } /** Receive a three-byte integer (twenty-four bits) from the input * @return the received integer */ static tbyte_t GetThree (void) { tbyte_t u = 0; if (Status == EOF || feof (fp) || ferror (fp)) { Status = EOF; return 0; } else Status = 0; u = (tbyte_t) (fgetc (fp) & 0xff); u |= ((tbyte_t) fgetc (fp) & 0xff) << 8; u |= ((tbyte_t) fgetc (fp) & 0xff) << 16; return u; } /** Receive a bit from the input * @return the received bit */ static unsigned GetBit (void) { unsigned result = BitBuf >>= 1; if (result == 1) return 1 & (BitBuf = GetByte() | 0x0100U); else return 1 & result; } /** Fetch a Huffman code and convert it to what it represents * @return the converted code */ static byte_t Huffin (void) { unsigned long hcode = 0; unsigned long mask = 1; int size = 1; int now; now = hcount; /* First non-zero Huffman code */ do { if (GetBit ()) hcode |= mask; while( hl[now] == size) { if (hc[now] == hcode) return hv[now]; if (--now < 0) { /* Error in decode table */ Status = EOF; return 0; } } size++; mask = mask << 1; } while (size < 24); Status = EOF; /* Error. Huffman code too big */ return 0; } /** Fetch ARC64 header. * @return true if header is ok. */ static bool GetHeader (void) { unsigned int w, i; const char LegalTypes[] = "SPUR"; unsigned long mask; if (feof(fp) || ferror(fp)) return false; else Status = 0; BitBuf = 2; /* Clear Bit buffer */ crc = 0; /* checksum */ crc2 = 0; /* Used in checksum calculation */ State = 0; /* LZW state */ ctrl = 254; /* Run-Length control character */ entry.version = GetByte(); entry.mode = GetByte(); entry.check = GetWord(); entry.size = GetThree(); entry.blocks = GetWord(); entry.type = GetByte(); entry.fnlen = GetByte(); /* Check for invalid header, If invalid, then we've input past the end */ /* Possibly due to XMODEM padding or whatever */ if (entry.fnlen > 16) return 0; for (w=0; w < entry.fnlen; w++) entry.name[w] = GetByte(); entry.name[entry.fnlen] = 0; if (entry.version > 1) { entry.rl = GetByte(); entry.date= GetWord(); } if (Status == EOF) return false; if (entry.version == 0 || entry.version > 2) return false; if (entry.version == 1) { /* If ARC64 version 1.xx */ if (entry.mode > 2) /* Only store, pack, squeeze */ return false; } if (entry.mode == 1) /* If packed get control char */ ctrl = GetByte(); /* V2 always uses 0xfe V1 varies */ if (entry.mode > 5) return false; if ((entry.mode == 2) || (entry.mode == 4)) { /* if squeezed or squashed */ hcount = 255; /* Will be first code */ for (w=0; w<256; w++) { /* Fetch Huffman codes */ hv[w] = (unsigned char) w; hl[w]=0; mask = 1; for (i=1; i<6; i++) { if (GetBit()) hl[w] |= (unsigned char) mask; mask <<= 1; } if (hl[w] > 24) return false; /* Code too big */ hc[w] = 0; if (hl[w]) { i = 0; mask = 1; while (i= sizeof stack) longjmp (LZStackError, PushError); else stack[lzstack++] = c; } /** Pop a byte from the Lempel Zev stack * @return the popped byte */ static byte_t pop (void) { if (!lzstack) longjmp (LZStackError, PopError); else return stack[--lzstack]; } /** Fetch LZ code * @return the fetched code */ static unsigned int getcode (void) { register int i; long blocks; code = 0; i = cdlen; while(i--) code = (code << 1) | GetBit (); /* Special case of 1 pass crunch. Checksum and size are at the end */ if ((code == 256) && (entry.mode == 5)) { i = 16; entry.check = 0; while (i--) entry.check = (entry.check << 1) | GetBit (); i = 24; entry.size = 0; while (i--) entry.size = (entry.size << 1) | GetBit (); i = 16; while (i--) /* This was never implemented */ GetBit (); blocks = ftell(fp)-FilePos; entry.blocks = (unsigned) (blocks / 254); if (blocks % 254) entry.blocks++; } /* Get ready for next time */ if ((cdlen < 12)) { if (!(--wttcl)) { wtcl = wtcl << 1; cdlen++; wttcl = wtcl; } } return code; } /** Un-crunch a byte * @return the uncrunched byte */ static byte_t unc (void) { static unsigned oldcode, incode; static byte_t kay; static unsigned omega; static unsigned char finchar; static unsigned ncodes; /* Current # of codes in table */ switch (State) { case 0: /* First time. Reset. */ lzstack = 0; ncodes = 258; /* 2 reserved codes */ wtcl = 256; /* 256 Bump code size when we get here */ wttcl = 254; /* 1st time only 254 due to resvd codes */ cdlen = 9; /* Start with 9 bit codes */ oldcode = getcode(); if (oldcode == 256) { /* Code 256 is EOF for this entry */ Status = EOF; /* (ie. a zero length file) */ return 0; } kay = (byte_t) oldcode; finchar = kay; State = 1; return kay; case 1: incode = getcode(); if (incode == 256) { State = 0; Status = EOF; return 0; } if (incode >= ncodes) { /* Undefined code, special case */ kay = finchar; push(kay); code = oldcode; omega = oldcode; incode = ncodes; } while ( code > 255 ) { /* Decompose string */ push(lztab[code].ext); code = lztab[code].prefix; } finchar = kay = (byte_t) code; State = 2; return kay; case 2: if (!lzstack) { /* Empty stack */ omega = oldcode; if (ncodes < sizeof lztab / sizeof *lztab) { lztab[ncodes].prefix = omega; lztab[ncodes].ext = kay; ncodes++; } oldcode = incode; State = 1; return unc(); } else return pop(); } Status = EOF; return 0; } /** Update the checksum * @param c the data to be added to the checksum */ static void UpdateChecksum (byte_t c) { c &= 0xff; if (entry.version == 1) /* Simple checksum for version 1 */ crc += c; else crc += (c ^ (++crc2)); /* A slightly better checksum for version 2 */ } /** Unpack a byte * @return the unpacked byte */ static byte_t UnPack (void) { switch (entry.mode) { case 0: /* Stored */ case 1: /* Packed (Run-Length) */ return GetByte(); case 2: /* Squeezed (Huffman only) */ case 4: /* Squashed (Huffman + Run-Length) */ return Huffin(); case 3: /* Crunched */ case 5: /* Crunched in one pass */ return unc(); default: /* Otherwise ERROR */ Status = EOF; return 0; } } /** Read and convert an ARC/SDA archive * @param file the file input stream * @param filename host system name of the file * @param writeCallback function for writing the contained files * @param log Call-back function for diagnostic output * @return status of the operation */ enum RdStatus ReadARC (FILE* file, const char* filename, write_file_t writeCallback, log_t log) { fp = file; switch (setjmp (LZStackError)) { case PopError: (*log) (Errors, 0, "Lempel Zev stack underflow"); return RdFail; case PushError: (*log) (Errors, 0, "Lempel Zev stack overflow"); return RdFail; } { long temp; if ((temp = GetStartPos()) < 0) { (*log) (Errors, 0, "Not a Commodore ARC or SDA."); return RdFail; } else if (fseek (fp, temp, SEEK_SET)) { (*log) (Errors, 0, "fseek: %s", strerror(errno)); return RdFail; } } FilePos = ftell (fp); while (GetHeader()) { byte_t* buffer; byte_t* buf; struct Filename name; size_t length = entry.size; if (entry.mode == 5) /* If 1 pass crunch size is unknown */ length = 65536; /* 64kB should be enough for everyone */ if (!(buf = buffer = malloc (length))) { (*log) (Errors, 0, "Out of memory."); return RdFail; } while (buf < &buffer[length]) { byte_t c = UnPack (); if (Status == EOF) break; /* If Run Length is needed */ if (entry.mode != 0 && entry.mode != 2 && c == ctrl) { int count = UnPack (); c = UnPack (); if (Status == EOF) break; if (count == 0) count = entry.version == 1 ? 255 : 256; while (--count) UpdateChecksum (*buf++ = c); } UpdateChecksum (*buf++ = c); } /* Set up the file name information */ { unsigned i; for (i = 0; i < entry.fnlen && i < sizeof name.name; i++) name.name[i] = entry.name[i]; /* pad the file name with shifted spaces */ while (i < sizeof name.name) name.name[i++] = 0xA0; switch (entry.type) { case 'S': name.type = SEQ; break; case 'P': name.type = PRG; break; case 'U': name.type = USR; break; case 'R': name.type = REL; name.recordLength = entry.rl; break; default: name.type = 0; (*log) (Errors, &name, "Unknown type, defaulting to DEL"); name.type = DEL; break; } } if ((crc ^ entry.check) & 0xffff) (*log) (Errors, &name, "Checksum error!"); switch ((*writeCallback) (&name, buffer, (size_t) (buf - buffer))) { case WrOK: break; case WrNoSpace: free (buffer); return RdNoSpace; case WrFail: default: free (buffer); return RdFail; } free (buffer); FilePos += (long)entry.blocks * 254; if (fseek (fp, FilePos, SEEK_SET)) { (*log) (Errors, 0, "fseek: %s", strerror (errno)); return RdFail; } } return RdOK; } cbmconvert-cbmconvert-2.1.5/unark.c000066400000000000000000000121711424020246600173070ustar00rootroot00000000000000/** * @file unark.c * Arkive archive extractor * @author Marko Mäkelä (marko.makela at iki.fi) */ /* ** Copyright © 1993-1997,2001,2021 Marko Mäkelä ** ** 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. */ #include #include #include #include #include "input.h" /** Arkive directory entry */ struct ArkiveEntry { /** Commodore file type */ byte_t filetype; /** Number of bytes in the last sector */ byte_t lastSectorLength; /** Commodore file name */ byte_t name[16]; /** Record length for random-access (relative) files */ byte_t recordLength; /** Unused bytes */ byte_t unknown[6]; /** Number of side sectors for random-access (relative) files */ byte_t sidesectCount; /** Length of the last side sector */ byte_t sidesectLastLength; /** Least significant byte of the file's block count */ byte_t blocksLow; /** Most significant byte of the file's block count */ byte_t blocksHigh; }; /** Read and convert an Arkive archive * @param file the file input stream * @param filename host system name of the file * @param writeCallback function for writing the contained files * @param log Call-back function for diagnostic output * @return status of the operation */ enum RdStatus ReadArkive (FILE* file, const char* filename, write_file_t writeCallback, log_t log) { struct Filename name; struct ArkiveEntry entry; int f, fcount; /* File positions */ size_t headerPos; /* current header position */ size_t archivePos; /* current archive position */ if (EOF == (fcount = fgetc (file))) { hdrError: (*log) (Errors, 0, "File header read failed: %s", strerror(errno)); return RdFail; } else { long l = ftell (file); if (l < 0) goto hdrError; headerPos = (size_t) l; } archivePos = 254 * rounddiv (headerPos + (size_t) fcount * sizeof entry, 254); /* start extracting files */ for (f = 0; f++ < fcount;) { size_t length; unsigned blocks; if (fseek (file, (long) headerPos, SEEK_SET) || 1 != fread (&entry, sizeof entry, 1, file)) goto hdrError; headerPos += sizeof entry; /* copy file name */ memcpy (name.name, entry.name, 16); /* copy the record length */ name.recordLength = entry.recordLength; /* determine file length */ blocks = (unsigned) (entry.blocksLow | ((unsigned) entry.blocksHigh << 8)); length = 254 * blocks + entry.lastSectorLength - 255; /* determine file type */ switch (entry.filetype & ~0x38) { case DEL: case SEQ: case PRG: name.type = entry.filetype & ~0x38; break; case REL: name.type = REL; if (!name.recordLength) (*log) (Warnings, &name, "zero record length"); { unsigned sidesectCount, sidesectLastLength; sidesectCount = (blocks + 119) / 121; sidesectLastLength = 15 + 2 * ((blocks - sidesectCount) % 120); if (entry.sidesectCount != sidesectCount || entry.sidesectLastLength != sidesectLastLength) { (*log) (Errors, &name, "improper side sector length"); (*log) (Errors, &name, "Following files may be totally wrong!"); } length = (blocks - sidesectCount) * 254 - 255 + entry.lastSectorLength; } break; default: name.type = 0; (*log) (Errors, &name, "Unknown type, defaulting to DEL"); name.type = DEL; break; } /* read the file */ { byte_t* buf; if (fseek (file, (long) archivePos, SEEK_SET)) { (*log) (Errors, &name, "fseek: %s", strerror(errno)); return RdFail; } if (!(buf = malloc (length))) { (*log) (Errors, &name, "Out of memory."); return RdFail; } if (length != fread (buf, 1, length, file)) { free (buf); (*log) (Errors, &name, "fread: %s", strerror(errno)); return RdFail; } archivePos += 254 * blocks; if (name.type == REL) /* Arkive stores the last side sector, */ /* wasting 254 bytes */ /* for each relative file. */ archivePos -= 254U * (entry.sidesectCount - 1); switch ((*writeCallback) (&name, buf, length)) { case WrOK: break; case WrNoSpace: free (buf); return RdNoSpace; case WrFail: default: free (buf); return RdFail; } free (buf); } } return RdOK; } cbmconvert-cbmconvert-2.1.5/util.c000066400000000000000000000043301424020246600171420ustar00rootroot00000000000000/** * @file util.c * Utility functions * @author Marko Mäkelä (marko.makela at iki.fi) */ /* ** Copyright © 1993-1997,2001,2021 Marko Mäkelä ** ** 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. */ #include #include #include "util.h" /** Convert a file name to a printable null-terminated string. * @param name the PETSCII file name to be converted * @return the corresponding ASCII file name */ const char* getFilename (const struct Filename* name) { static char buf[sizeof(name->name) + 5]; int i; if (!name) return 0; /* remove trailing shifted spaces */ for (i = sizeof(name->name); i-- && name->name[i] == 0xA0; ); buf [++i] = 0; while (i--) if (name->name[i] >= 0x41 && name->name[i] <= 0x5A) buf[i] = (char) (name->name[i] - 0x41 + 'a'); else if (name->name[i] >= 0xC1 && name->name[i] <= 0xDA) buf[i] = (char) (name->name[i] - 0xC1 + 'A'); else if (name->name[i] >= 0x61 && name->name[i] <= 0x7A) buf[i] = (char) (name->name[i] - 0x61 + 'A'); else if (name->name[i] >= 0x20 && name->name[i] <= 0x5F) buf[i] = (char) name->name[i]; else buf[i] = '_'; /* non-ASCII character */ switch (name->type) { case DEL: strcat (buf, ",del"); break; case SEQ: strcat (buf, ",seq"); break; case PRG: strcat (buf, ",prg"); break; case USR: strcat (buf, ",usr"); break; case REL: sprintf (buf + strlen(buf), ",l%02X", name->recordLength); break; case CBM: strcat (buf, ",cbm"); break; } return buf; } cbmconvert-cbmconvert-2.1.5/util.h000066400000000000000000000124431424020246600171530ustar00rootroot00000000000000/** * @file util.h * Definitions of data types and utility functions * @author Marko Mäkelä (marko.makela at iki.fi) */ /* ** Copyright © 1993-1997,2001,2021 Marko Mäkelä ** ** 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. */ #ifndef UTIL_H # define UTIL_H /** * Rounded integer division * @param a the numerator * @param b the denominator * @return a divided by b, rounded up to the next integer value */ # define rounddiv(a,b) ((a + b - 1U) / b) /** * Determine the number of elements in an array * @param a an array * @return number of elements in the array */ # define elementsof(a) ((sizeof a) / (sizeof *a)) # if defined(__MSDOS__) || defined(MSDOS) /** Directory path separator character */ # define PATH_SEPARATOR '\\' # else /** Directory path separator character */ # define PATH_SEPARATOR '/' # endif # include /* Common data types */ # if UCHAR_MAX != 255 # error "Wrong unsigned char range!" # endif /** A data type of exactly one byte */ typedef unsigned char byte_t; # if UINT_MAX < 65535 # error "Insufficient unsigned int range!" # endif /** An unsigned data type with at least 16 bits of precision */ typedef unsigned int word_t; # if UINT_MAX >= 16777216 /** An unsigned data type with at least 24 bits of precision */ typedef unsigned int tbyte_t; # else # if ULONG_MAX < 16777216 # error "Insufficient unsigned long range!" # endif /** An unsigned data type with at least 24 bits of precision */ typedef unsigned long int tbyte_t; # endif /** Truth value */ typedef enum { false = 0, /**< false, binary digit '0' */ true /**< true, binary digit '1' */ } bool; /** Commodore file types */ enum Filetype { DEL = 0x80, /**< Deleted (sequential) file */ SEQ, /**< Sequential data file */ PRG, /**< Sequential program file */ USR, /**< Sequential data file with user-defined structure */ REL, /**< Random-access data file */ CBM /**< 1581 partition */ }; /** Commodore file name */ struct Filename { /** The file name, padded with shifted spaces */ unsigned char name[16]; /** The file type */ enum Filetype type; /** Record length for random-access (relative) files */ byte_t recordLength; }; /** Disk image types */ enum ImageType { ImUnknown, /**< Unknown or unrecognized image */ Im1541, /**< 35-track 1541, 3040 or 4040 disk image */ Im1571, /**< 70-track 1571 disk image */ Im1581 /**< 80-track 1581 disk image */ }; /** Options for getDirEnt () */ enum DirEntOpts { DirEntDontCreate, /**< only try to find the file name */ DirEntOnlyCreate, /**< only create a new slot */ DirEntFindOrCreate/**< create the directory entry if it doesn't exist */ }; /** Disk image */ struct Image { /** type of disk image */ enum ImageType type; /** getDirEnt() behaviour */ enum DirEntOpts direntOpts; /** (active) directory track number */ byte_t dirtrack; /** disk image file name on the host system */ unsigned char* name; /** disk image data */ byte_t* buf; /** lower limits of partitions (for the 1581) */ byte_t partBots[80]; /** upper limits of partitions (for the 1581) */ byte_t partTops[80]; /** parent partitions (for the 1581) */ byte_t partUpper[80]; }; /** An entry in a file archive */ struct ArchiveEntry { /** Pointer to next archive entry */ struct ArchiveEntry* next; /** The file name of the entry */ struct Filename name; /** Length of the entry in bytes */ size_t length; /** The contents of the entry */ byte_t* data; }; /** A file archive */ struct Archive { /** The first archive entry */ struct ArchiveEntry* first; /** The last archive entry */ struct ArchiveEntry* last; }; /* Utility functions */ /** Convert a file name to a printable null-terminated string. * @param name the PETSCII file name to be converted * @return the corresponding ASCII file name */ const char* getFilename (const struct Filename* name); /** Verbosity level of diagnostic output */ enum Verbosity { Errors, /**< Display only errors; report an error */ Warnings, /**< Display errors and warnings; report a warning */ Everything /**< Display everything; report an informational message */ }; /** Call-back function for diagnostic output * @param verbosity the verbosity level * @param name the file name associated with the message (or NULL) * @param format printf-like format string followed by arguments */ typedef void log_t (enum Verbosity verbosity, const struct Filename* name, const char* format, ...); #endif /* UTIL_H */ cbmconvert-cbmconvert-2.1.5/version.cmake000066400000000000000000000000411424020246600205030ustar00rootroot00000000000000#cmakedefine VERSION "@VERSION@" cbmconvert-cbmconvert-2.1.5/write.c000066400000000000000000000245011424020246600173210ustar00rootroot00000000000000/** * @file write.c * Writes files with converted names * @author Marko Mäkelä (marko.makela at iki.fi) */ /* ** Copyright © 1993-1997,2001,2021,2022 Marko Mäkelä ** ** 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. */ #include #include #include #include #include #include "output.h" /** Convert a PETSCII file name to a printable null-terminated ASCII string. * @param name the PETSCII file name * @param newname (output) the converted file name * @return false on out of memory */ static bool filename2char (const struct Filename* name, char** newname) { size_t i = sizeof(name->name); char* c; if (!newname) return false; /* free the old string */ if (*newname) free (*newname); *newname = 0; /* search for shifted spaces at the end */ while (--i && name->name[i] == 0xA0); /* copy the PETSCII filename */ if (!(*newname = malloc (i + 2))) return false; memcpy (*newname, name->name, i + 1); (*newname)[i + 1] = 0; for (c = *newname; c <= *newname + i; c++) { if (*c == '/') /* map slash */ *c = '.'; else if (*c >= 0x41 && *c <= 0x5A) /* convert lower case letters */ *c += 'a' - 0x41; else if ((*c & 0x7f) < 32) /* convert control chars */ *c = '-'; else if ((*c & 0x7f) >= 0x41 && ((*c & 0x7f)) <= 0x5A) /* convert upper case letters */ *c += 'A' - 0xC1; else if (*c & 0x80) /* convert graphics characters */ *c = '+'; } return true; } /** Determine whether a character is a wovel in English * @param c the character * @return true if c is a wovel */ static bool isWovel (unsigned char c) { return !!memchr ("aeiou", c, 5); } /** A character that was removed to make a file name ISO 9660 compliant */ #define REMOVED '-' /** Truncate the file name in-place to ISO9660 compliant format. * @param name a null-terminated string that is at least 1 character long * (excluding the terminating NUL character) * @return the length of the truncated string, * excluding the terminating NUL character */ static size_t TruncateName (unsigned char* name) { unsigned char* c; size_t len, efflen; for (c = name; *c; c++) { if (*c >= 'a' && *c <= 'z') /* Lower case chars are OK */ continue; else if (*c >= '0' && *c <= '9')/* So are numbers */ continue; else if (*c >= 'A' && *c <= 'Z')/* Convert characters to lower case */ *c -= (unsigned char) ('A' - 'a'); else *c = '_'; /* Convert anything else to underscore */ } efflen = len = (size_t) (c - name); if (efflen > 8) { /* Remove underscores from the end */ for (c = &name[len]; c > name; c--) { if (*c == '_') { *c = REMOVED; if (--efflen <= 8) break; } } } if (efflen > 8) { /* remove wovels from the end */ size_t i; /* Search for the first non-wovel */ for (i = 0; i < len && isWovel (name[i]); i++); if (i < len) { for (c = &name[len]; c > &name[i]; c--) if (isWovel (*c)) { *c = REMOVED; if (--efflen <= 8) break; } } } if (efflen > 8) { /* remove letters from the end */ for (c = &name[len]; c > name; c--) if (*c >= 'a' && *c <= 'z') { *c = REMOVED; if (--efflen <= 8) break; } } if (efflen > 8) { /* remove anything from the end */ for (c = &name[len]; c > name; c--) if (*c && *c != REMOVED) { *c = REMOVED; if (--efflen <= 8) break; } } { /* remove removed characters */ unsigned char* t; for (c = t = name; *c; c++) if (*c != REMOVED) *t++ = *c; *t = 0; } return efflen; } /** Return a file name suffix corresponding to a Commodore file type * @param filename the Commodore file name * @return a corresponding file name suffix */ static const char* filesuffix (const struct Filename* filename) { /** Suffix for relative files */ static char relsuffix[5]; switch (filename->type) { case DEL: return ".del"; case SEQ: return ".seq"; case PRG: return ".prg"; case USR: return ".usr"; case REL: sprintf (relsuffix, ".l%02X", filename->recordLength & 0xFF); return relsuffix; case CBM: return ".cbm"; } return ""; } /** Write data to a file * @param data the contents of the file * @param length length of the file contents * @param newname (output) the converted file name * @param name native (PETSCII) name of the file * @param log Call-back function for diagnostic output * @return status of the operation */ static enum WrStatus do_it (const byte_t* data, size_t length, char** newname, const struct Filename* name, log_t log) { FILE* f; if (!(f = fopen (*newname, "wb"))) { (*log) (Errors, name, "fopen: %s", strerror (errno)); return errno == ENOSPC ? WrNoSpace : WrFail; } if (length != fwrite (data, 1, length, f)) { (*log) (Errors, name, "fwrite: %s", strerror (errno)); fclose (f); return errno == ENOSPC ? WrNoSpace : WrFail; } fclose (f); return WrOK; } /** Write a file in raw format * @param name native (PETSCII) name of the file * @param data the contents of the file * @param length length of the file contents * @param newname (output) the converted file name * @param log Call-back function for diagnostic output * @return status of the operation */ enum WrStatus WriteNative (const struct Filename* name, const byte_t* data, size_t length, char** newname, log_t log) { struct stat statbuf; char* filename; int i; if (!filename2char (name, newname)) return WrFail; if (!(filename = malloc (strlen (*newname) + (4 + 5 + 1)))) { free (filename); return WrFail; } /* try the plain filename */ sprintf (filename, "%s%.4s", *newname, filesuffix (name)); if (stat (filename, &statbuf)) { free (*newname); *newname = filename; return do_it (data, length, newname, name, log); } for (i = 0; i < 10000; i++) { sprintf (filename, "%s~%d%.4s", *newname, i, filesuffix (name)); if (stat (filename, &statbuf)) { /* found an available file name */ free (*newname); *newname = filename; return do_it (data, length, newname, name, log); } } free (filename); (*log) (Errors, name, "out of file name space"); return WrFail; } /** Write a file in PC64 format (.P00, .S00 etc.) * @param name native (PETSCII) name of the file * @param data the contents of the file * @param length length of the file contents * @param newname (output) the converted file name * @param log Call-back function for diagnostic output * @return status of the operation */ enum WrStatus WritePC64 (const struct Filename* name, const byte_t* data, size_t length, char** newname, log_t log) { char* filename,* c; int i; struct stat statbuf; if (!filename2char (name, newname)) return WrFail; if (!(filename = malloc (TruncateName ((unsigned char*)*newname) + 4 + 1))) return WrFail; i = sprintf (filename, "%s%.4s", *newname, filesuffix (name)); if (name->type == REL) /* Fix the suffix for relative files */ filename[i - 3] = 'r'; c = &filename[i - 2]; for (i = 0; i < 100; i++) { sprintf (c, "%02d", i); if (stat (filename, &statbuf)) { /* found an available file name */ FILE* f; free (*newname); *newname = filename; if (!(f = fopen (*newname, "wb"))) { (*log) (Errors, name, "fopen: %s", strerror (errno)); return errno == ENOSPC ? WrNoSpace : WrFail; } if (1 != fwrite ("C64File", 8, 1, f) || 1 != fwrite (name->name, 16, 1, f) || EOF == fputc (0, f) || EOF == fputc (name->recordLength, f) || length != fwrite (data, 1, length, f)) { (*log) (Errors, name, "fwrite: %s", strerror (errno)); fclose (f); return errno == ENOSPC ? WrNoSpace : WrFail; } fclose (f); return WrOK; } } free (filename); (*log) (Errors, name, "out of file name space"); return WrFail; } /** Write a file in raw format, using ISO 9660 compliant filenames * @param name native (PETSCII) name of the file * @param data the contents of the file * @param length length of the file contents * @param newname (output) the converted file name * @param log Call-back function for diagnostic output * @return status of the operation */ enum WrStatus Write9660 (const struct Filename* name, const byte_t* data, size_t length, char** newname, log_t log) { char* filename; int i; struct stat statbuf; if (!filename2char (name, newname)) return WrFail; if (!(filename = malloc (TruncateName ((unsigned char*)*newname) + 4 + 1))) return WrFail; /* try the basic file name */ sprintf (filename, "%s%.4s", *newname, filesuffix (name)); if (stat (filename, &statbuf)) { free (*newname); *newname = filename; return do_it (data, length, newname, name, log); } /* try with .000-style file names */ for (i = 0; i < 1000; i++) { sprintf (filename, "%s.%03u", *newname, i); if (stat (filename, &statbuf)) { /* found an available file name */ free (*newname); *newname = filename; return do_it (data, length, newname, name, log); } } free (filename); (*log) (Errors, name, "out of file name space"); return WrFail; } cbmconvert-cbmconvert-2.1.5/zip2disk.1000066400000000000000000000022401424020246600176400ustar00rootroot00000000000000.\" Manual page in -*- nroff -*- format; see man(7) .TH ZIP2DISK 1 "September 18, 2001" .SH NAME zip2disk \- Commodore 1541 ZipCode extractor .SH SYNOPSIS .B zip2disk .IR " zipcode\(file " [ " image\(file.d64 " ] .SH DESCRIPTION This manual page documents brie\(fly the .B zip2disk command. .PP The Commodore 1541 disk archiver ZipCode was developed in order to make it possible to transfer disk images over modem lines. Nowadays it is better to use .BR gzip (1) on the disk images. The \fBzip2disk\fP command can be used to convert ZipCode archives to 1541 disk images. .SH SECURITY The ZipCode \(file names must be pre\(fixed with the strings \fB1!\fP, \fB2!\fP, \fB3!\fP, and \fB4!\fP, just like ZipCode works on the Commodore 64. Some Unix shells treat the character \fB!\fP specially. To avoid unpleasant situations, please refer to the documentation of the shell you are using. .SH BUGS The sector-level disk identi\(fier stored in the ZipCode \(files is not included in the disk image. .SH AUTHOR The \fBzip2disk\fP utility was originally written by Paul David Doherty. It was improved and packaged by Marko M\(:akel\(:a. .SH SEE ALSO .BR cbmconvert (1), .BR disk2zip (1). cbmconvert-cbmconvert-2.1.5/zip2disk.c000066400000000000000000000156051424020246600177330ustar00rootroot00000000000000/** * @file zip2disk.c * Convert four Zip-Code files to a 1541 disk image * @author Marko Mäkelä (marko.makela at iki.fi) * @author Paul David Doherty (h0142kdd@rz.hu-berlin.de) */ /* ** Copyright © 1993-1997,2001,2006,2021,2022 Marko Mäkelä ** Original version © 1993 Paul David Doherty (h0142kdd@rz.hu-berlin.de) ** ** 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. */ #include #include #include #ifdef MSDOS /** Directory path separator character */ #define PATH_SEPARATOR '\\' #else /** Directory path separator character */ #define PATH_SEPARATOR '/' #endif /* MSDOS */ /** Suffix for output file names */ static const char out_suffix[] = ".d64"; /** Input file */ static FILE* infile; /** Output file */ static FILE* outfile; /** number of sectors on the current track */ static unsigned max_sect; /** current track number */ static unsigned track; /** decoding buffer for current track */ static unsigned char trackbuf[21][256]; /** flag: which sectors on the current track have been decoded? */ static int trackbuf_decoded[21]; /** input file name */ static char* inname; /** first character of the actual file name */ static char* fname; /** output file name */ static char* outname; /** Initialize the files * @param filename the base file name * @retval 0 on success * @retval 1 on out of memory * @retval 3 if no output could be created */ static int init_files (const char* filename) { /* flag: was an output file name already specified? */ int outflag = !!outname; size_t i = strlen (filename); if (!(inname = malloc (i + 3)) || (!outname && !(outname = malloc (i + sizeof out_suffix)))) return 1; /* copy the base filename */ memcpy (inname, filename, i + 1); if (!outflag) { memcpy (outname, filename, i); memcpy (outname + i, out_suffix, sizeof out_suffix); } /* modify input filename */ for (fname = inname + i; fname > inname && *fname != PATH_SEPARATOR; fname--); if (fname > inname) fname++; memmove (fname + 2, fname, i + 1 - (size_t) (fname - inname)); fname[1] = '!'; /* try to create output file */ if (!(outfile = fopen (outname, "wb"))) { return 3; } return 0; } /** Open an input file * @param number ASCII number of the input file * @return 0 on success; 1 on error */ static int open_file (char number) { *fname = number; if (infile) fclose (infile); if (!(infile = fopen (inname, "rb")) || -1 == fseek (infile, (number == '1') ? 4 : 2, 0)) return 1; return 0; } /** Decode a sector * @return 0 on success; 1 on failure */ static int read_sector (void) { int trk, sec, ch; trk = fgetc (infile); sec = fgetc (infile); if ((unsigned) (trk & 0x3f) != track || sec < 0 || (unsigned) sec >= max_sect || trackbuf_decoded[sec]) { Error: fprintf (stderr, "zip2disk: Input file %s is corrupted.\n", inname); return 1; } trackbuf_decoded[sec] = 1; /* RLE compressed sector */ if (trk & 0x80) { /* number of decompressed bytes */ int count = 0; /* length of the compressed stream, and the escape character */ int len = fgetc (infile), esc = fgetc (infile); if (len == EOF || esc == EOF) goto Error; while (len--) { ch = fgetc (infile); if (ch == EOF) goto Error; if (ch != esc) { trackbuf[sec][count++] = (unsigned char) ch; if (count > 256) goto Error; } else if (len >= 2) { /* escape character: get the number of repetitions */ int repnum = fgetc (infile); /* get the repeated character */ ch = fgetc (infile); if (repnum < 0 || repnum + count > 256 || ch == EOF) goto Error; memset (trackbuf[sec] + count, ch, (unsigned) repnum); count += repnum; len -= 2; } else goto Error; } if (count != 256) goto Error; } /* whole sector filled with a single character */ else if (trk & 0x40) { if ((ch = fgetc (infile)) == EOF) goto Error; memset (trackbuf[sec], ch, 256); } /* no compression */ else if (256 != fread (trackbuf[sec], 1, 256, infile)) goto Error; return 0; } /** Decode a track * @return 0 on success; 1 on failure */ static int read_track (void) { unsigned s; memset (trackbuf_decoded, 0, sizeof trackbuf_decoded); for (s = max_sect; s--; ) if (read_sector ()) return 1; return 0; } /** Main program * @param argc number of command-line arguments * @param argv contents of command-line arguments * @retval 0 on success * @retval 1 on usage error * @retval 2 on out of memory * @retval 3 on input or output error * @retval 4 on ZipCode format error */ int main (int argc, char** argv) { int status; if (argc != 2 && argc != 3) { fputs ("ZipCode disk image extractor v1.2.2\n" "Usage: zip2disk zip_image_name [disk_image_name]\n", stderr); return 1; } outname = (argc == 3) ? argv[2] : 0; switch (init_files (argv[1])) { case 3: fprintf (stderr, "zip2disk: Could not create %s.\n", outname); status = 3; goto ErrExit; case 1: fputs ("zip2disk: Out of memory.\n", stderr); status = 2; goto ErrExit; } for (track = 1; track <= 35; track++) { max_sect = 17U + ((track < 31) ? 1U : 0U) + ((track < 25) ? 1U : 0U) + ((track < 18) ? 2U : 0U); switch (track) { case 1: if (open_file ('1')) { OpenError: fprintf (stderr, "zip2disk: Error in opening file %s.\n", inname); status = 3; goto ErrExit; } break; case 9: if (open_file ('2')) goto OpenError; break; case 17: if (open_file ('3')) goto OpenError; break; case 26: if (open_file ('4')) goto OpenError; break; } if (read_track ()) { fclose (infile); fclose (outfile); status = 4; goto ErrExit; } if (max_sect != fwrite (trackbuf, 256, max_sect, outfile)) { fclose (infile); fclose (outfile); fputs ("zip2disk: Error in writing the output file.\n", stderr); status = 3; goto ErrExit; } } fclose (infile); fclose (outfile); status = 0; ErrExit: free (inname); return status; }