wudsn-ide/com.wudsn.ide.asm/src/com/wudsn/ide/asm/compiler/writer/AppleFileWriter.java
2021-09-18 01:09:46 +02:00

344 lines
13 KiB
Java

/**
* Copyright (C) 2009 - 2021 <a href="https://www.wudsn.com" target="_top">Peter Dell</a>
*
* This file is part of WUDSN IDE.
*
* WUDSN IDE 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.
*
* WUDSN IDE 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 WUDSN IDE. If not, see <http://www.gnu.org/licenses/>.
*/
package com.wudsn.ide.asm.compiler.writer;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.runtime.CoreException;
import com.webcodepro.applecommander.storage.Disk;
import com.webcodepro.applecommander.storage.DiskFullException;
import com.webcodepro.applecommander.storage.FileEntry;
import com.webcodepro.applecommander.storage.FormattedDisk;
import com.wudsn.ide.asm.Texts;
import com.wudsn.ide.asm.compiler.CompilerFileWriter;
import com.wudsn.ide.asm.compiler.CompilerFiles;
import com.wudsn.ide.base.common.FileUtility;
import com.wudsn.ide.base.common.MarkerUtility;
import com.wudsn.ide.base.common.StringUtility;
/**
* Compiler file writer for Apple II
*
* @author Peter Dell
* @since 1.6.3
*/
public final class AppleFileWriter extends CompilerFileWriter {
private final static String HELLO_ENTRY_NAME = "HELLO";
private final static String OUTPUT_IMAGE_ENTRY_NAME = "WORLD";
private final class AppleSoftTokens {
public final static byte PRINT = (byte) 0xba;
public final static byte CHRS = (byte) 0xe7;
public final static byte CALL = (byte) 0x8c;
// public String[] tokenNames = { "END", "FOR ", "NEXT ", "DATA ",
// "INPUT ", "DEL", "DIM ", "READ ", "GR", "TEXT", "PR#", "IN#",
// "CALL ", "PLOT", "HLIN ", "VLIN ", "HGR2", "HGR", "HCOLOR=",
// "HPLOT ", "DRAW ", "XDRAW ", "HTAB ", "HOME", "ROT=", "SCALE=",
// "SHLOAD", "TRACE", "NOTRACE", "NORMAL", "INVERSE", "FLASH",
// "COLOR=", "POP", "VTAB ", "HIMEM:", "LOMEM:", "ONERR ",
// "RESUME", "RECALL", "STORE", "SPEED=", "LET ", "GOTO ", "RUN",
// "IF ", "RESTORE", "& ", "GOSUB ", "RETURN", "REM ", "STOP",
// "ON ", "WAIT", "LOAD", "SAVE", "DEF", "POKE ", "PRINT ",
// "CONT", "LIST", "CLEAR", "GET ", "NEW", "TAB(", "TO ", "FN",
// "SPC(", "THEN ", "AT ", "NOT ", "STEP ", "+ ", "- ", "* ",
// "/ ", "^ ", "AND ", "OR ", "> ", "= ", "< ", "SGN", "INT",
// "ABS", "USR", "FRE", "SCRN(", "PDL", "POS ", "SQR", "RND",
// "LOG", "EXP", "COS", "SIN", "TAN", "ATN", "PEEK", "LEN",
// "STR$", "VAL", "ASC", "CHR$", "LEFT$", "RIGHT$", "MID$" };
// public int[] tokenAddresses = { 0xD870, 0xD766, 0xDCF9, 0xD995,
// 0xDBB2,
// 0xF331, 0xDFD9, 0xDBE2, 0xF390, 0xF399, 0xF1E5, 0xF1DE, 0xF1D5,
// 0xF225, 0xF232, 0xF241, 0xF3D8, 0xF3E2, 0xF6E9, 0xF6FE, 0xF769,
// 0xF76F, 0xF7E7, 0xFC58, 0xF721, 0xF727, 0xF775, 0xF26D, 0xF26F,
// 0xF273, 0xF277, 0xF280, 0xF24F, 0xD96B, 0xF256, 0xF286, 0xF2A6,
// 0xF2CB, 0xF318, 0xF3BC, 0xF39F, 0xF262, 0xDA46, 0xD93E, 0xD912,
// 0xD9C9, 0xD849, 0x03F5, 0xD921, 0xD96B, 0xD9DC, 0xD86E, 0xD9EC,
// 0xE784, 0xD8C9, 0xD8B0, 0xE313, 0xE77B, 0xFDAD5, 0xD896,
// 0xD6A5, 0xD66A, 0xDBA0, 0xD649, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
// 0, 0, 0, 0, 0, 0, 0, 0, 0xEB90, 0xEC23, 0xEBAF, 0x000A, 0xE2DE,
// 0xD412, 0xDFCD, 0xE2FF, 0xEE8D, 0xEFAE, 0xE941, 0xEF09, 0xEFEA,
// 0xEFF1, 0xF03A, 0xF09E, 0xE764, 0xE6D6, 0xE3C5, 0xE707, 0xE6E5,
// 0xE646, 0xE65A, 0xE686, 0xE691 };
}
@Override
public boolean createOrUpdateDiskImage(CompilerFiles files) {
String imageFilePath = files.outputFilePathWithoutExtension + ".dsk";
String outputImageEntryName = OUTPUT_IMAGE_ENTRY_NAME;
String outputImageEntryTitle = "Loading " + files.outputFileNameWithoutExtension;
boolean autoCreate = true;
if (StringUtility.isSpecified(imageFilePath)) {
File imageFile = new File(imageFilePath);
if (!imageFile.exists()) {
// When auto creation is active, we copy a template file.
// This is the only way to get a disk image which is not only
// formatted but also has a boot loader which runs "HELLO".
if (autoCreate) {
String resource = "/lib/AppleDos.dsk";
InputStream inputStream = Disk.class.getClassLoader().getResourceAsStream(resource);
if (inputStream == null) {
throw new RuntimeException("Cannot get input stream for '" + resource + "'");
}
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buffer = new byte[8192];
int count;
do {
try {
count = inputStream.read(buffer);
} catch (IOException ex) {
throw new RuntimeException("Cannot read input stream for '" + resource + "'", ex);
}
if (count > 0) {
bos.write(buffer, 0, count);
}
} while (count > -1);
try {
bos.close();
} catch (IOException ex) {
throw new RuntimeException(ex);
}
try {
FileUtility.writeBytes(imageFile, bos.toByteArray());
} catch (CoreException ex) {
MarkerUtility.createMarker(files.mainSourceFile.iFile, 0, ex.getStatus().getSeverity(), "{0}",
ex.getStatus().getMessage());
return false;
}
} else { // no auto creation
// ERROR: Disk image file '{0}' does not exist. Create a
// bootable disk image where the output file '{1}' can be
// stored.
MarkerUtility.createMarker(files.mainSourceFile.iFile, 0, IMarker.SEVERITY_ERROR,
Texts.MESSAGE_E132, imageFilePath, outputImageEntryName);
return false;
}
}
if (!imageFile.canWrite()) {
// ERROR: Disk image file '{0}' is not writeable. Make the disk
// image file writeable, so the output file '{1}' can be stored.
MarkerUtility.createMarker(files.mainSourceFile.iFile, 0, IMarker.SEVERITY_ERROR, Texts.MESSAGE_E133,
imageFilePath, outputImageEntryName);
return false;
}
}
Disk disk;
try {
disk = new Disk(imageFilePath);
} catch (IOException ex) {
// ERROR: Disk image file '{0}' cannot be opened for reading. System
// error: {1}
MarkerUtility.createMarker(files.mainSourceFile.iFile, 0, IMarker.SEVERITY_ERROR, Texts.MESSAGE_E134,
imageFilePath, ex.getMessage());
return false;
}
FormattedDisk[] formattedDisks = disk.getFormattedDisks();
if (formattedDisks == null || formattedDisks.length == 0) {
// ERROR: Disk image file '{0}' does not contain a valid file
// system. Make sure the disk image is properly formatted, so
// the output file '{1}' can be stored.
MarkerUtility.createMarker(files.mainSourceFile.iFile, 0, IMarker.SEVERITY_ERROR, Texts.MESSAGE_E135,
imageFilePath, outputImageEntryName);
return false;
}
byte[] outputFileContent;
try {
outputFileContent = FileUtility.readBytes(files.outputFile, 65536, true);
} catch (CoreException ex) {
// ERROR: Cannot read output file.
MarkerUtility.createMarker(files.mainSourceFile.iFile, 0, ex.getStatus().getSeverity(), "{0}",
ex.getStatus().getMessage());
return false;
}
FormattedDisk formattedDisk;
try {
formattedDisk = formattedDisks[0];
// Create HELLO with dummy address.
createHello(formattedDisk, outputImageEntryName, outputImageEntryTitle, 1234);
FileEntry entry = formattedDisk.getFile(outputImageEntryName);
if (entry == null) {
entry = formattedDisk.createFile();
// TODO This is required due to a BUG in AppleCommander
entry.setFilename(outputImageEntryName);
}
entry.setFiletype("B");
String fileName = files.outputFileName.toLowerCase();
int length = outputFileContent.length;
byte[] content = outputFileContent;
if (entry.needsAddress()) {
int address;
if (fileName.endsWith(".b") && length > 4) {
// // AppleDos 3.3 binary file:
// start-lo,start-hi,length-lo,length-hi,data
address = getWord(outputFileContent, 0);
length = length - 4;
content = getData(outputFileContent, 4);
} else if (fileName.endsWith(".prg") && length > 2) {
// C64 program file
// start-lo,start-hi,data
address = getWord(outputFileContent, 0);
length = length - 2;
content = getData(outputFileContent, 2);
} else if (fileName.endsWith(".xex") && length > 6
&& ((getWord(outputFileContent, 0) & 0xffff) == 0xffff)) {
// AtariDOS 2.5 binary file:
// $ff,$ff,start-lo,start-hi,end-lo,end-hi,data
address = getWord(outputFileContent, 2);
length = length - 6;
content = getData(outputFileContent, 6);
} else {
// ERROR: Output file {0} has unknown executable file
// extension or content. File extensions ".b", ".prg" and
// ".xex" are allowed.
MarkerUtility.createMarker(files.mainSourceFile.iFile, 0, IMarker.SEVERITY_ERROR,
Texts.MESSAGE_E138, files.outputFilePath);
return false;
}
// Method setAddress must be called after method setFileData!
entry.setFileData(content);
entry.setAddress(address);
// Update HELLO with actual start address.
createHello(formattedDisk, outputImageEntryName, outputImageEntryTitle, address);
} else {
entry.setFileData(outputFileContent);
}
} catch (DiskFullException ex) {
// ERROR: Disk image file '{0}' is full. System
// error: {1}
MarkerUtility.createMarker(files.mainSourceFile.iFile, 0, IMarker.SEVERITY_ERROR, Texts.MESSAGE_E136,
imageFilePath, ex.getMessage());
return false;
}
try {
formattedDisk.save();
} catch (IOException ex) {
// ERROR: Disk image file '{0}' cannot be opened for writing. System
// error: {1}
MarkerUtility.createMarker(files.mainSourceFile.iFile, 0, IMarker.SEVERITY_ERROR, Texts.MESSAGE_E137,
imageFilePath, ex.getMessage());
return false;
}
return true;
}
private static FileEntry createHello(FormattedDisk formattedDisk, String outputImageEntryName,
String outputImageEntryTitle, int runAddress) throws DiskFullException {
if (formattedDisk == null) {
throw new IllegalArgumentException("Parameter 'formattedDisk' must not be null.");
}
if (outputImageEntryName == null) {
throw new IllegalArgumentException("Parameter 'OUTPUT_IMAGE_ENTRY_NAME' must not be null.");
}
if (StringUtility.isEmpty(outputImageEntryName)) {
throw new IllegalArgumentException("Parameter 'OUTPUT_IMAGE_ENTRY_NAME' must not be empty.");
}
if (outputImageEntryTitle == null) {
throw new IllegalArgumentException("Parameter 'outputImageEntryTitle' must not be null.");
}
FileEntry entry;
entry = formattedDisk.getFile(HELLO_ENTRY_NAME);
if (entry == null) {
entry = formattedDisk.createFile();
entry.setFilename(HELLO_ENTRY_NAME);
}
entry.setFiletype("A");
byte[] program;
try {
// See "Beneath Apple DOS.pdf", 4-11/12 for the binary and AppleSoft
// file format.
// See http://www.textfiles.com/apple/ANATOMY/cmd.brun.bload.txt for
// the bug in the BRUN routine. Instead of BRUN now BLOAD is sent to
// DOS via PRINT CHR$(4) and the program is started via CALL.
// 10 PRINT "LOADING <title>" : PRINT CHR$(4);"BRUN WORLD" : CALL
// <address>
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bos.write(new byte[] { 0x00, 0x08, 0x0A, 0x00 });
bos.write(new byte[] { AppleSoftTokens.PRINT, '"' });
bos.write(outputImageEntryTitle.getBytes("US-ASCII"));
bos.write(new byte[] { '"', ':' });
bos.write(new byte[] { AppleSoftTokens.PRINT, AppleSoftTokens.CHRS, '(', '4', ')', ';', '"', 'B', 'L', 'O',
'A', 'D', ' ' });
bos.write(outputImageEntryName.getBytes("US-ASCII"));
bos.write(new byte[] { '"', ':', AppleSoftTokens.CALL });
bos.write(Integer.toString(runAddress).getBytes("US-ASCII"));
bos.write(new byte[] { 0, 0, 0 });
program = bos.toByteArray();
program[0] = (byte) (program.length);
} catch (UnsupportedEncodingException ex) {
throw new RuntimeException(ex);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
// Byte is the line length in bytes
entry.setFileData(program);
return entry;
}
private static int getWord(byte[] bytes, int index) {
if (bytes == null) {
throw new IllegalArgumentException("Parameter 'bytes' must not be null.");
}
return (0xff & bytes[index]) + 256 * (0xff & bytes[index + 1]);
}
private static byte[] getData(byte[] bytes, int index) {
if (bytes == null) {
throw new IllegalArgumentException("Parameter 'bytes' must not be null.");
}
int length = bytes.length - index;
byte[] result = new byte[length];
System.arraycopy(bytes, index, result, 0, length);
return result;
}
}