/** * Copyright (C) 2009 - 2021 Peter Dell * * 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 . */ package com.wudsn.ide.hex; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.eclipse.core.runtime.IStatus; import org.eclipse.jface.preference.JFacePreferences; import org.eclipse.jface.viewers.StyledString; import org.eclipse.jface.viewers.StyledString.Styler; import org.eclipse.swt.graphics.TextStyle; import com.wudsn.ide.base.BasePlugin; import com.wudsn.ide.base.common.EnumUtility; import com.wudsn.ide.base.common.HexUtility; import com.wudsn.ide.base.common.NumberUtility; import com.wudsn.ide.base.common.Profiler; import com.wudsn.ide.base.common.TextUtility; import com.wudsn.ide.base.gui.MessageManager; import com.wudsn.ide.base.hardware.HardwareCharacterSet; import com.wudsn.ide.hex.HexEditor.MessageIds; import com.wudsn.ide.hex.parser.AtariDiskImageKFileParser; import com.wudsn.ide.hex.parser.AtariMADSParser; import com.wudsn.ide.hex.parser.AtariParser; import com.wudsn.ide.hex.parser.AtariSDXParser; import com.wudsn.ide.hex.parser.IFFParser; final class HexEditorParserComponent { public static final long UNDEFINED_OFFSET = -1; private final static int BYTES_PER_ROW = 16; // Callback API. private MessageManager messageManager; // Style components. private Styler offsetStyler; private Styler addressStyler; private Styler charStyler; private Styler errorStyler; // File content and state. private boolean fileContentParsed; private HexEditorFileContentMode fileContentMode; private byte[] fileContent; private int bytesPerRow; private HardwareCharacterSet characterSet; // Previous state with regards to parsing. private HexEditorFileContentMode oldFileContentMode; private byte[] oldFileContent; private int oldBytesPerRow; private HardwareCharacterSet oldCharacterSet; // Parsing state. private List possibleFileContentModes; private List outlineBlocks; private long[] byteTextOffsets; private int byteTextIndex; // Line buffers for binary to hex and char conversion. private char[] hexChars; private char[] hexBuffer; private char[] charBuffer; public HexEditorParserComponent(MessageManager messageManager) { if (messageManager == null) { throw new IllegalArgumentException("Parameter 'messageManager' must not be null."); } this.messageManager = messageManager; // Get static stylers for the styled string. offsetStyler = StyledString.createColorRegistryStyler(JFacePreferences.COUNTER_COLOR, null); addressStyler = StyledString.createColorRegistryStyler(JFacePreferences.QUALIFIER_COLOR, null); charStyler = StyledString.createColorRegistryStyler(JFacePreferences.HYPERLINK_COLOR, null); charStyler = new Styler() { @Override public void applyStyles(TextStyle textStyle) { textStyle.font = null; } }; errorStyler = StyledString.createColorRegistryStyler(JFacePreferences.ERROR_COLOR, null); // Initialize hex chars and normal character set type. hexChars = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; characterSet = HardwareCharacterSet.ASCII; clear(); } private void clear() { // Initialize with empty file. fileContentParsed = false; fileContentMode = HexEditorFileContentMode.BINARY; setFileContent(new byte[0]); characterSet = HardwareCharacterSet.ASCII; bytesPerRow = BYTES_PER_ROW; oldFileContentMode = null; oldFileContent = null; oldCharacterSet = null; oldBytesPerRow = 0; possibleFileContentModes = new ArrayList(); outlineBlocks = new ArrayList(); } public void setFileContent(byte[] fileContent) { if (fileContent == null) { throw new IllegalArgumentException("Parameter 'fileContent' must not be null."); } this.fileContent = fileContent; initByteTextOffsets(); } /** * Reserve enough space for the lookup table that maps text offsets to file * offsets. */ private void initByteTextOffsets() { // Twice the space, because some formats display the content twice, for // example ATARI_DISK_IMAGE_K_FILE. byteTextOffsets = new long[fileContent.length * 2]; Arrays.fill(byteTextOffsets, -1); byteTextIndex = 0; } /** * Determines the possible file content modes based on the file content. * * @return The suggested default file content mode, not null. */ public HexEditorFileContentMode determinePossibleFileContentModes() { HexEditorFileContentMode result = HexEditorFileContentMode.BINARY; possibleFileContentModes.clear(); possibleFileContentModes.add(fileContentMode); HexEditorFileContentMode defaultMode = result; FileContent fileContent = getFileContent(); // COM header present? if (fileContent.getLength() > 6) { // AtariDOS COM file? if (fileContent.getWord(0) == AtariParser.COM_HEADER) { int startAddress = fileContent.getWord(2); int endAddress = fileContent.getWord(4); if (startAddress >= 0 && endAddress >= startAddress) { defaultMode = HexEditorFileContentMode.ATARI_COM_FILE; possibleFileContentModes.add(defaultMode); if (fileContent.getLength() > 16) { if (fileContent.getWord(6) == AtariMADSParser.RELOC_HEADER) { defaultMode = HexEditorFileContentMode.ATARI_MADS_FILE; possibleFileContentModes.add(defaultMode); } } // New default? if (result.equals(HexEditorFileContentMode.BINARY)) { result = defaultMode; } } } // SpartaDOS X non relocatable file? else if (fileContent.getWord(0) == AtariSDXParser.NON_RELOC_HEADER) { int startAddress = fileContent.getWord(2); int endAddress = fileContent.getWord(4); if (startAddress > 0 && endAddress >= startAddress) { defaultMode = HexEditorFileContentMode.ATARI_SDX_FILE; possibleFileContentModes.add(defaultMode); // New default? if (result.equals(HexEditorFileContentMode.BINARY)) { result = defaultMode; } } } // SpartaDOS X relocatable file? else if (fileContent.getWord(0) == AtariSDXParser.RELOC_HEADER && fileContent.getLength() > 8) { int blockNumber = fileContent.getByte(2); if (blockNumber > 0) { defaultMode = HexEditorFileContentMode.ATARI_SDX_FILE; possibleFileContentModes.add(defaultMode); // New default? if (result.equals(HexEditorFileContentMode.BINARY)) { result = defaultMode; } } } } // ATR header present? if ((fileContent.getLength() > 16 && fileContent.getByte(0) == 0x96 && fileContent.getByte(1) == 0x02)) { defaultMode = HexEditorFileContentMode.ATARI_DISK_IMAGE; possibleFileContentModes.add(defaultMode); // Special case of k-file (converted COM file) int offset = AtariDiskImageKFileParser.ATARI_DISK_IMAGE_K_FILE_COM_FILE_OFFSET; if (fileContent.getLength() > offset + 2 && fileContent.getWord(offset) == 0xffff) { final int[] kFileBootHeader = new int[] { 0x00, 0x03, 0x00, 0x07, 0x14, 0x07, 0x4C, 0x14, 0x07 }; boolean kFileBootHeaderFound = true; for (int i = 0; i < kFileBootHeader.length; i++) { if (fileContent.getByte(16 + i) != kFileBootHeader[i]) { kFileBootHeaderFound = false; } } if (kFileBootHeaderFound) { defaultMode = HexEditorFileContentMode.ATARI_DISK_IMAGE_K_FILE; possibleFileContentModes.add(defaultMode); } } // New default? if (result.equals(HexEditorFileContentMode.BINARY)) { result = defaultMode; } } // SAP header present? if ((fileContent.getLength() > 11 && fileContent.getByte(0) == 0x53 && fileContent.getByte(1) == 0x41) && fileContent.getByte(2) == 0x50) { possibleFileContentModes.add(HexEditorFileContentMode.ATARI_SAP_FILE); // New default? if (result.equals(HexEditorFileContentMode.BINARY)) { result = HexEditorFileContentMode.ATARI_SAP_FILE; } } // PRG header present? if ((fileContent.getLength() > 2 && fileContent.getWord(0) + fileContent.getLength() - 2 < 0x10000)) { possibleFileContentModes.add(HexEditorFileContentMode.C64_PRG_FILE); int loadAddress = fileContent.getWord(0); if (result.equals(HexEditorFileContentMode.BINARY) && loadAddress >= 0x800 && loadAddress < 0x2000) { result = HexEditorFileContentMode.C64_PRG_FILE; } } // IFF files always have an even number of bytes if (fileContent.getLength() > 8 && (fileContent.getLength() & 0x1) == 0) { char[] id = new char[4]; int offset = 0; id[0] = (char) fileContent.getByte(0); id[1] = (char) fileContent.getByte(1); id[2] = (char) fileContent.getByte(2); id[3] = (char) fileContent.getByte(3); String chunkName = String.copyValueOf(id); offset += 4; var chunkLength = fileContent.getDoubleWordBigEndian(4); offset += 4; if (IFFParser.isValidChunkName(chunkName) && offset + chunkLength <= fileContent.getLength()) { possibleFileContentModes.add(HexEditorFileContentMode.IFF_FILE); boolean iff = chunkName.equals("FORM") || chunkName.equals("LIST") || chunkName.equals("CAT "); if (result.equals(HexEditorFileContentMode.BINARY) && iff) { result = HexEditorFileContentMode.IFF_FILE; } } } return result; } /** * Sets the file content for {@link #parseFileContent()}. * * @param fileContentMode The file content mode, not null. */ public void setFileContentMode(HexEditorFileContentMode fileContentMode) { if (fileContentMode == null) { throw new IllegalArgumentException("Parameter 'fileContentMode' must not be null."); } this.fileContentMode = fileContentMode; } /** * Gets the file content for {@link #parseFileContent()}. * * @return fileContentMode The file content mode, not null. */ public HexEditorFileContentMode getFileContentMode() { return fileContentMode; } /** * Sets the character set type. * * @param characterSet The character set type, not null. */ public void setCharacterSet(HardwareCharacterSet characterSet) { if (characterSet == null) { throw new IllegalArgumentException("Parameter 'characterSet' must not be null."); } this.characterSet = characterSet; } /** * Gets the character set type. * * @return characterSet The character set type, not null. */ public HardwareCharacterSet getCharacterSet() { return characterSet; } /** * Sets the number of bytes per row for {@link #parseFileContent()}. * * @param bytesPerRow The number of bytes per row, a positive integer. */ public void setBytesPerRow(int bytesPerRow) { if (bytesPerRow < 1) { throw new IllegalArgumentException( "Parameter 'bytesPerRow' must not be positive. Specified valie was " + bytesPerRow + "."); } this.bytesPerRow = bytesPerRow; } /** * Gets the number of bytes per row for {@link #parseFileContent()}. * * @return The number of bytes per row, a positive integer. */ public int getBytesPerRow() { return bytesPerRow; } /** * Determines if parsing is required. * * @return true if parsing is required, false * otherwise. */ public boolean isParsingFileContentRequired() { return !fileContentParsed || !Arrays.equals(fileContent, oldFileContent) || !fileContentMode.equals(oldFileContentMode) || !characterSet.equals(oldCharacterSet) || bytesPerRow != oldBytesPerRow; } /** * Parse the file content set with {@link #setFileContent(byte[])} according to * the parameters set with * {@link #setFileContentMode(HexEditorFileContentMode)}, * {@link #setBytesPerRow(int)} and * {@link #setCharacterSet(HardwareCharacterSet)}. * * @return The styles string representing the content. */ public StyledString parseFileContent() { Profiler profiler = new Profiler(this); profiler.begin("parseFileContent", fileContent.length + " bytes"); outlineBlocks.clear(); initByteTextOffsets(); StyledString contentBuilder = new StyledString(); HexEditorContentOutlineTreeObject treeObject; String text = TextUtility.format(Texts.HEX_EDITOR_FILE_SIZE, HexUtility.getLongValueHexString(fileContent.length), NumberUtility.getLongValueDecimalString(fileContent.length)); contentBuilder.append(text); treeObject = new HexEditorContentOutlineTreeObject(contentBuilder); treeObject.setFileStartOffset(0); treeObject.setTextStartOffset(contentBuilder.length()); outlineBlocks.add(treeObject); contentBuilder = new StyledString(); if (!possibleFileContentModes.contains(fileContentMode)) { messageManager.sendMessage(MessageIds.FILE_CONTENT_MODE, IStatus.ERROR, Texts.MESSAGE_E300, EnumUtility.getText(fileContentMode)); return contentBuilder; } if (fileContent.length > 0) { boolean error; HexEditorParser parser = fileContentMode.createParser(); // Initialize the buffers for the hex and char conversion. hexBuffer = new char[3 + bytesPerRow * 3 + 2]; for (int i = 0; i < hexBuffer.length; i++) { hexBuffer[i] = ' '; } hexBuffer[1] = ':'; hexBuffer[hexBuffer.length - 2] = '|'; charBuffer = new char[bytesPerRow + 1]; charBuffer[charBuffer.length - 1] = '\n'; parser.init(this, offsetStyler, addressStyler); error = parser.parse(contentBuilder); if (error) { messageManager.sendMessage(MessageIds.FILE_CONTENT_MODE, IStatus.ERROR, Texts.MESSAGE_E301, EnumUtility.getText(fileContentMode)); } } profiler.end("parseFileContent"); // Copy current state to state backup for change detection in {@link // #isParsingFileContentRequired}, fileContentParsed = true; oldFileContentMode = fileContentMode; oldFileContent = fileContent; oldCharacterSet = characterSet; oldBytesPerRow = bytesPerRow; return contentBuilder; } /** * Gets the file content. * * @return The file content, not null. */ final FileContent getFileContent() { return new FileContentImpl(fileContent); // TODO Do not create newly } /** * Prints a block header in the context area and adds a block to the outline. * * @param contentBuilder The content builder, not null. * @param headerStyledString The style string for the block header in the * outline, not null. * @param offset The start offset, a non-negative integer. * * @return The tree object representing the block. */ final HexEditorContentOutlineTreeObject printBlockHeader(StyledString contentBuilder, StyledString headerStyledString, long offset) { if (contentBuilder == null) { throw new IllegalArgumentException("Parameter 'contentBuilder' must not be null."); } if (headerStyledString == null) { throw new IllegalArgumentException("Parameter 'styledString' must not be null."); } HexEditorContentOutlineTreeObject treeObject; treeObject = new HexEditorContentOutlineTreeObject(headerStyledString); treeObject.setFileStartOffset(offset); treeObject.setTextStartOffset(contentBuilder.length()); outlineBlocks.add(treeObject); return treeObject; } /** * Prints the last block in case if contains an error like the wrong number of * bytes. * * @param contentBuilder The content builder, not null. * @param errorText The error text, not empty and not null. * @param length The length of the last block, a non-negative integer. * @param offset The offset of the last block, a non-negative integer. */ final void printBlockWithError(StyledString contentBuilder, String errorText, long length, long offset) { if (contentBuilder == null) { throw new IllegalArgumentException("Parameter 'contentBuilder' must not be null."); } if (errorText == null) { throw new IllegalArgumentException("Parameter 'errorText' must not be null."); } HexEditorContentOutlineTreeObject treeObject; StyledString styledString = new StyledString(errorText, errorStyler); treeObject = new HexEditorContentOutlineTreeObject(styledString); treeObject.setFileStartOffset(UNDEFINED_OFFSET); treeObject.setFileEndOffset(UNDEFINED_OFFSET); treeObject.setTextStartOffset(contentBuilder.length()); treeObject.setTextEndOffset(contentBuilder.length()); outlineBlocks.add(treeObject); contentBuilder.append(styledString); contentBuilder.append("\n"); offset = printBytes(treeObject, contentBuilder, offset, length - 1, true, 0); } final void skipByteTextIndex(long offset) { byteTextIndex += offset; } final long printBytes(HexEditorContentOutlineTreeObject treeObject, StyledString contentBuilder, long offset, long maxOffset, boolean withStartAddress, int startAddress) { if (offset < 0) { throw new IllegalArgumentException( "Parameter 'offset' must not be negative, specified value is " + offset + "."); } if (maxOffset < 0) { throw new IllegalArgumentException( "Parameter 'offset' must not be negative, specified value is " + maxOffset + "."); } int length = Math.max(4, HexUtility.getLongValueHexLength(fileContent.length)); char[] characterMapping = characterSet.getCharacterMapping(); while (offset <= maxOffset) { int contentBuilderLineStartOffset = contentBuilder.length(); contentBuilder.append(HexUtility.getLongValueHexString(offset, length), offsetStyler); if (withStartAddress) { contentBuilder.append(" : "); contentBuilder.append(HexUtility.getLongValueHexString(startAddress, length), addressStyler); } // Remember byte offset where the new line starts. int contentBuilderStartOffset = contentBuilder.length(); int h = 3; for (int b = 0; b < bytesPerRow; b++) { char highChar; char lowChar; char charValue; if (offset > maxOffset) { highChar = ' '; lowChar = ' '; charValue = ' '; } else { int byteValue = getFileContent().getByte(offset); highChar = hexChars[byteValue >> 4]; lowChar = hexChars[byteValue & 0xf]; charValue = characterMapping[byteValue]; byteTextOffsets[byteTextIndex++] = (b == 0 ? contentBuilderLineStartOffset : contentBuilderStartOffset + h); offset++; startAddress++; } hexBuffer[h++] = highChar; hexBuffer[h++] = lowChar; h++; charBuffer[b] = charValue; } contentBuilder.append(hexBuffer); contentBuilder.append(charBuffer, charStyler); } treeObject.setFileEndOffset(offset); treeObject.setTextEndOffset(contentBuilder.length()); return offset; } /** * Gets the list of outline blocks determined by {@link #parseFileContent()} . * * @return The list of outline blocks, may be empty, not null. */ public List getOutlineBlocks() { return outlineBlocks; } /** * Gets the selection represented by the start and end offset in the text field. * * @param x is the offset of the first selected character * @param y is the offset after the last selected character. * @return The selection or null. */ public HexEditorSelection getSelection(int x, int y) { if (x > y) { throw new IllegalArgumentException("x is greater than y"); } long startOffset = UNDEFINED_OFFSET; for (long i = 0; i < byteTextIndex; i++) { // BasePlugin.getInstance().log( // "HexEditor.getSelection(): i={0} textOffset={1} // nextTextOffset={2} startOffset={3} endOffset={4}", // new Object[] { String.valueOf(i), String.valueOf(textOffset), // String.valueOf(nextTextOffset), // String.valueOf(startOffset), String.valueOf(endOffset) }); if (getByteTextOffset(i) >= x) { startOffset = i; break; } } if (startOffset == UNDEFINED_OFFSET) { return null; } long endOffset = UNDEFINED_OFFSET; for (long i = startOffset; i < byteTextIndex; i++) { // BasePlugin.getInstance().log( // "HexEditor.getSelection(): i={0} textOffset={1} // nextTextOffset={2} startOffset={3} endOffset={4}", // new Object[] { String.valueOf(i), String.valueOf(textOffset), // String.valueOf(nextTextOffset), // String.valueOf(startOffset), String.valueOf(endOffset) }); if (getByteTextOffset(i) >= y) { endOffset = i; break; } } if (endOffset == UNDEFINED_OFFSET) { return null; } long length; byte[] bytes; length = endOffset - startOffset + 1; BasePlugin.getInstance().log("HexEditor.getSelection(): startOffset={2} endoffset={3} length={4}", new Object[] { String.valueOf(x), String.valueOf(y), String.valueOf(startOffset), String.valueOf(endOffset), String.valueOf(length) }); // Length not empty or negative? if (length > 0 && length < fileContent.length) { // Reposition into first occurrence of in the file. // This is relevant for the format that display the content more // than once. startOffset = startOffset % fileContent.length; endOffset = endOffset % fileContent.length; // Selection does not cross file end boundary? if (startOffset <= endOffset) { bytes = new byte[(int) length]; System.arraycopy(fileContent, (int) startOffset, bytes, 0, bytes.length); } else { endOffset = startOffset; bytes = new byte[0]; } } else { endOffset = startOffset; bytes = new byte[0]; } HexEditorSelection hexEditorSelection = new HexEditorSelection(startOffset, endOffset, bytes); return hexEditorSelection; } /** * Gets the text offset for a byte offset. * * @param byteOffset The byte offset in the original byte array. * @return The text offset where the byte is represented or * UNDEFINED_OFFSET if there is no such text offset. */ public long getByteTextOffset(long byteOffset) { if (byteOffset < byteTextOffsets.length) { return byteTextOffsets[(int) byteOffset]; } return UNDEFINED_OFFSET; } }