From 6ea6748c154ab9337becd1dcd1e9b4912d1600a2 Mon Sep 17 00:00:00 2001 From: dev747368 <48332326+dev747368@users.noreply.github.com> Date: Mon, 2 Mar 2026 19:13:41 +0000 Subject: [PATCH] GP-6503 Improve YAFFS2 impl so it doesn't trigger on windows sys32 files YAFFS2 did not have very robust probe /matching logic, and was matching a random windows system32 file. --- .../filehandlers/GetInfoFSBFileHandler.java | 24 +- .../file/formats/yaffs2/YAFFS2Analyzer.java | 155 -------- .../file/formats/yaffs2/YAFFS2Buffer.java | 80 ---- .../file/formats/yaffs2/YAFFS2Data.java | 38 -- .../file/formats/yaffs2/YAFFS2Entry.java | 171 -------- .../formats/yaffs2/YAFFS2ExtendedTags.java | 96 ----- .../file/formats/yaffs2/YAFFS2FileSystem.java | 365 +++++++++++++----- .../yaffs2/YAFFS2FileSystemFactory.java | 86 +++++ .../file/formats/yaffs2/YAFFS2Header.java | 307 ++++++++------- .../formats/yaffs2/YAFFS2InputStream.java | 143 ------- .../file/formats/yaffs2/YAFFS2OOBStruct.java | 65 ++++ ...S2Constants.java => YAFFS2ObjectType.java} | 36 +- .../file/formats/yaffs2/YAFFS2Utils.java | 122 ------ 13 files changed, 629 insertions(+), 1059 deletions(-) delete mode 100644 Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2Analyzer.java delete mode 100644 Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2Buffer.java delete mode 100644 Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2Data.java delete mode 100644 Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2Entry.java delete mode 100644 Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2ExtendedTags.java create mode 100644 Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2FileSystemFactory.java delete mode 100644 Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2InputStream.java create mode 100644 Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2OOBStruct.java rename Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/{YAFFS2Constants.java => YAFFS2ObjectType.java} (54%) delete mode 100644 Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2Utils.java diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/GetInfoFSBFileHandler.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/GetInfoFSBFileHandler.java index b1a47eb9d9..33dd8dcc9e 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/GetInfoFSBFileHandler.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/filehandlers/GetInfoFSBFileHandler.java @@ -168,17 +168,25 @@ public class GetInfoFSBFileHandler implements FSBFileHandler { //--------------------------------------------------------------------------------------------- private static final Function PLAIN_TOSTRING = o -> o.toString(); private static final Function SIZE_TOSTRING = - o -> (o instanceof Long) ? FSUtilities.formatSize((Long) o) : o.toString(); + o -> (o instanceof Long l) ? FSUtilities.formatSize(l) : o.toString(); private static final Function UNIX_ACL_TOSTRING = - o -> (o instanceof Number) ? String.format("%05o", (Number) o) : o.toString(); + o -> (o instanceof Number num) ? String.format("%05o", num) : o.toString(); private static final Function DATE_TOSTRING = - o -> (o instanceof Date) ? FSUtilities.formatFSTimestamp((Date) o) : o.toString(); + o -> (o instanceof Date date) ? FSUtilities.formatFSTimestamp(date) : o.toString(); private static final Function FSRL_TOSTRING = - o -> (o instanceof FSRL) ? ((FSRL) o).toPrettyString().replace("|", "|\n\t") : o.toString(); + o -> (o instanceof FSRL fsrl) ? fsrl.toPrettyString().replace("|", "|\n\t") : o.toString(); + //@formatter:off + // predefined type-to-string mappings private static final Map> FAT_TOSTRING_FUNCS = - Map.ofEntries(entry(FSRL_ATTR, FSRL_TOSTRING), entry(SIZE_ATTR, SIZE_TOSTRING), - entry(COMPRESSED_SIZE_ATTR, SIZE_TOSTRING), entry(CREATE_DATE_ATTR, DATE_TOSTRING), - entry(MODIFIED_DATE_ATTR, DATE_TOSTRING), entry(ACCESSED_DATE_ATTR, DATE_TOSTRING), - entry(UNIX_ACL_ATTR, UNIX_ACL_TOSTRING)); + Map.ofEntries( + entry(FSRL_ATTR, FSRL_TOSTRING), + entry(SIZE_ATTR, SIZE_TOSTRING), + entry(COMPRESSED_SIZE_ATTR, SIZE_TOSTRING), + entry(CREATE_DATE_ATTR, DATE_TOSTRING), + entry(MODIFIED_DATE_ATTR, DATE_TOSTRING), + entry(ACCESSED_DATE_ATTR, DATE_TOSTRING), + entry(UNIX_ACL_ATTR, UNIX_ACL_TOSTRING) + ); + //@formatter:on } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2Analyzer.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2Analyzer.java deleted file mode 100644 index c9e923c94c..0000000000 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2Analyzer.java +++ /dev/null @@ -1,155 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package ghidra.file.formats.yaffs2; - -import java.util.Arrays; - -import ghidra.app.plugin.core.analysis.AnalysisWorker; -import ghidra.app.plugin.core.analysis.AutoAnalysisManager; -import ghidra.app.util.bin.*; -import ghidra.app.util.importer.MessageLog; -import ghidra.file.analyzers.FileFormatAnalyzer; -import ghidra.program.model.address.Address; -import ghidra.program.model.address.AddressSetView; -import ghidra.program.model.data.DataType; -import ghidra.program.model.listing.Data; -import ghidra.program.model.listing.Program; -import ghidra.util.exception.CancelledException; -import ghidra.util.task.TaskMonitor; - -// analyzer for YAFFS2 image files -// places header and extended tags (footer) structures, and annotates data locations -public class YAFFS2Analyzer extends FileFormatAnalyzer implements AnalysisWorker { - - @Override - public String getName() { - return "YAFFS2 Image Annotation (used in Android System and Userdata image files)"; - } - - @Override - public boolean getDefaultEnablement(Program program) { - return false; - } - - @Override - public String getDescription() { - return "Annotates YAFFS2 Image files (used in Android System and UserData Images."; - } - - @Override - public boolean canAnalyze(Program program) { - try { - return YAFFS2Utils.isYAFFS2Image(program); - } - catch (Exception e) { - // not a yaffs2 image - } - return false; - } - - @Override - public boolean isPrototype() { - return true; - } - - private MessageLog log; - - @Override - public boolean analyze(Program program, AddressSetView set, TaskMonitor monitor, MessageLog log) - throws Exception { - this.log = log; - AutoAnalysisManager manager = AutoAnalysisManager.getAnalysisManager(program); - return manager.scheduleWorker(this, null, false, monitor); - } - - @Override - public boolean analysisWorkerCallback(Program program, Object workerContext, TaskMonitor monitor) - throws Exception, CancelledException { - - Address address = program.getMinAddress(); - - ByteProvider provider = MemoryByteProvider.createProgramHeaderByteProvider(program, false); - BinaryReader reader = new BinaryReader(provider, true); - - int index = 0; - byte[] block; - long lastObjectType = 3; // initialized to object type 3 (a directory) - YAFFS2Header header; - YAFFS2ExtendedTags tags; - YAFFS2Data dataBlock; - - // loop over reader by record length (2112 bytes) and lay down the appropriate structures - while (index < reader.length()) { - if (monitor.isCancelled()) { - break; - } - - // grab next block of data - block = reader.readByteArray(index, YAFFS2Constants.RECORD_SIZE); - - // header structure - if (lastObjectType != 1) { - header = - new YAFFS2Header(Arrays.copyOfRange(block, 0, YAFFS2Constants.DATA_BUFFER_SIZE)); - DataType headerDataType = header.toDataType(); - Data headerData = createData(program, address.add(index), headerDataType); - if (headerData == null) { - log.appendMsg("Unable to create header."); - } - lastObjectType = header.getObjectType(); - } - // data block structure - else { - dataBlock = - new YAFFS2Data(Arrays.copyOfRange(block, 0, YAFFS2Constants.DATA_BUFFER_SIZE)); - DataType dataBlockDataType = dataBlock.toDataType(); - Data dataBlockData = createData(program, address.add(index), dataBlockDataType); - if (dataBlockData == null) { - log.appendMsg("Unable to create data block."); - } - } - - // tags structure - tags = - new YAFFS2ExtendedTags(Arrays.copyOfRange(block, YAFFS2Constants.DATA_BUFFER_SIZE, - YAFFS2Constants.RECORD_SIZE - 1)); - DataType tagsDataType = tags.toDataType(); - - // create data for this block - Data tagsData = - createData(program, address.add(index + YAFFS2Constants.DATA_BUFFER_SIZE), - tagsDataType); - if (tagsData == null) { - log.appendMsg("Unable to create tags (footer)."); - } - - //increment to next record - index += YAFFS2Constants.RECORD_SIZE; - } - - changeDataSettings(program, monitor); - removeEmptyFragments(program); - - return true; - - } - - @Override - public String getWorkerName() { - return "YAFFS2Analyzer"; - } - -} diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2Buffer.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2Buffer.java deleted file mode 100644 index afa0b6035c..0000000000 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2Buffer.java +++ /dev/null @@ -1,80 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package ghidra.file.formats.yaffs2; - -import java.io.*; - -// a method to return a single YAFFS2 record -public class YAFFS2Buffer { - - private static int recordSize = YAFFS2Constants.RECORD_SIZE; - private InputStream inStream; - private OutputStream outStream; - private byte[] recordBuffer; - - public YAFFS2Buffer(InputStream inStream) { - this(inStream, recordSize); - } - - public YAFFS2Buffer(InputStream inStream, int recordSize) { - this.inStream = inStream; - this.outStream = null; - this.initialize(recordSize); - } - - private void initialize(int recordSize) { - this.recordBuffer = new byte[recordSize]; - } - - public byte[] readRecord() throws IOException { - if (inStream == null) { - if (outStream == null) { - throw new IOException("input buffer is closed"); - } - throw new IOException("reading from an output buffer"); - } - long numBytes = inStream.read(recordBuffer, 0, recordSize); - if (numBytes == -1) { - return null; - } - return recordBuffer; - } - - public long skip(long numToSkip) throws IOException { - return inStream.skip(numToSkip); - } - - public boolean isEOFRecord(byte[] record) { - for (int i = 0, sz = getRecordSize(); i < sz; ++i) { - if (record[i] != 0) { - return false; - } - } - return true; - } - - public int getRecordSize() { - return recordSize; - } - - public void close() throws IOException { - if (inStream != System.in) { - inStream.close(); - } - inStream = null; - } - -} diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2Data.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2Data.java deleted file mode 100644 index 180d4bc2b3..0000000000 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2Data.java +++ /dev/null @@ -1,38 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package ghidra.file.formats.yaffs2; - -import ghidra.app.util.bin.StructConverter; -import ghidra.program.model.data.*; -import ghidra.util.exception.DuplicateNameException; - -import java.io.IOException; - -public class YAFFS2Data implements StructConverter { - - public YAFFS2Data(byte[] buffer) { - } - - // an array to hold data bytes for one record - @Override - public DataType toDataType() throws DuplicateNameException, IOException { - Structure structure = new StructureDataType("yaffs2Data", 0); - structure.add(new ArrayDataType(BYTE, YAFFS2Constants.DATA_BUFFER_SIZE, BYTE.getLength()), - "data", null); - return structure; - } - -} diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2Entry.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2Entry.java deleted file mode 100644 index 4c02d78928..0000000000 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2Entry.java +++ /dev/null @@ -1,171 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package ghidra.file.formats.yaffs2; - -import java.util.Arrays; - -public class YAFFS2Entry { - - private long fileOffset; - private YAFFS2Header header; - private YAFFS2ExtendedTags extendedTags; - - /** - * in YAFFS2 speak, a record is called a "chunk" - * this class parses out a header chunk, reading the header and the footer in a record - */ - public YAFFS2Entry(byte[] buffer) { - // header - header = new YAFFS2Header(Arrays.copyOfRange(buffer, 0, YAFFS2Constants.HEADER_SIZE)); - - // extended tags - extendedTags = - new YAFFS2ExtendedTags(Arrays.copyOfRange(buffer, YAFFS2Constants.DATA_BUFFER_SIZE, - YAFFS2Constants.RECORD_SIZE)); - } - - public YAFFS2Entry() { - } - - public long getObjectId() { - return extendedTags.getObjectId(); - } - - public boolean isDirectory() { - return header.isDirectory(); - } - - public short getChecksum() { - return header.getChecksum(); - } - - public String getName() { - return header.getName(); - } - - public long getYstMode() { - return header.getYstMode(); - } - - public long getYstUId() { - return header.getYstUId(); - } - - public long getYstGId() { - return header.getYstGId(); - } - - public String getYstATime() { - return header.getYstATime(); - } - - public String getYstMTime() { - return header.getYstMTime(); - } - - public String getYstCTime() { - return header.getYstCTime(); - } - - public long getSize() { - return header.getSize(); - } - - public long getEquivId() { - return header.getEquivId(); - } - - public String getAliasFileName() { - return header.getAliasFileName(); - } - - public long getYstRDev() { - return header.getYstRDev(); - } - - public long getWinCTime() { - return header.getWinCTime(); - } - - public long getWinATime() { - return header.getWinATime(); - } - - public long getWinMTime() { - return header.getWinMTime(); - } - - public long getInbandObjId() { - return header.getInbandObjId(); - } - - public long getInbandIsShrink() { - return header.getInbandIsShrink(); - } - - public long getFileSizeHigh() { - return header.getFileSizeHigh(); - } - - public long getShadowsObject() { - return header.getShadowsObject(); - } - - public long getIsShrink() { - return header.getIsShrink(); - } - - public long getSequenceNumber() { - return extendedTags.getSequenceNumber(); - } - - public long getChunkId() { - return extendedTags.getChunkId(); - } - - public long getNumberBytes() { - return extendedTags.getNumberBytes(); - } - - public long getEccColParity() { - return extendedTags.getEccColParity(); - } - - public long getEccLineParity() { - return extendedTags.getEccLineParity(); - } - - public long getEccLineParityPrime() { - return extendedTags.getEccLineParityPrime(); - } - - public long getParentObjectId() { - return header.getParentObjectId(); - } - - public boolean isFile() { - return header.isFile(); - } - - public void setFileOffset(Long foffset) { - fileOffset = foffset; - } - - public long getFileOffset() { - return fileOffset; - } - -} diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2ExtendedTags.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2ExtendedTags.java deleted file mode 100644 index e2dbfae907..0000000000 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2ExtendedTags.java +++ /dev/null @@ -1,96 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package ghidra.file.formats.yaffs2; - -import ghidra.app.util.bin.StructConverter; -import ghidra.program.model.data.*; -import ghidra.util.exception.DuplicateNameException; - -import java.io.IOException; - -public class YAFFS2ExtendedTags implements StructConverter { - - // extended tags (a footer really) - private long sequenceNumber; - private long objectId; - private long chunkId; - private long numberBytes; - private long eccColParity; - private long eccLineParity; - private long eccLineParityPrime; - - public YAFFS2ExtendedTags(byte[] buffer) { - - // parse extended tags structure - sequenceNumber = YAFFS2Utils.parseInteger(buffer, 0, 4); - objectId = YAFFS2Utils.parseInteger(buffer, 4, 4); - chunkId = YAFFS2Utils.parseInteger(buffer, 8, 4); - numberBytes = YAFFS2Utils.parseInteger(buffer, 12, 4); - eccColParity = YAFFS2Utils.parseInteger(buffer, 16, 4); - eccLineParity = YAFFS2Utils.parseInteger(buffer, 20, 4); - eccLineParityPrime = YAFFS2Utils.parseInteger(buffer, 24, 4); - - } - - public YAFFS2ExtendedTags() { - } - - public long getObjectId() { - return objectId; - } - - public long getSequenceNumber() { - return sequenceNumber; - } - - public long getChunkId() { - return chunkId; - } - - public long getNumberBytes() { - return numberBytes; - } - - public long getEccColParity() { - return eccColParity; - } - - public long getEccLineParity() { - return eccLineParity; - } - - public long getEccLineParityPrime() { - return eccLineParityPrime; - } - - // extended tags structure for analyzer - @Override - public DataType toDataType() throws DuplicateNameException, IOException { - - Structure structure = new StructureDataType("yaffs2Tags", 0); - structure.add(DWORD, "sequenceNumber", null); - structure.add(DWORD, "objectId", null); - structure.add(DWORD, "chunkId", null); - structure.add(DWORD, "numberBytes", null); - structure.add(DWORD, "eccColParity", null); - structure.add(DWORD, "eccLineParity", null); - structure.add(DWORD, "eccLineParityPrime", null); - structure.add(new ArrayDataType(BYTE, 36, BYTE.getLength()), "unused", null); - return structure; - - } - -} diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2FileSystem.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2FileSystem.java index bd20a7f35d..3deba1e2fb 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2FileSystem.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2FileSystem.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,129 +15,308 @@ */ package ghidra.file.formats.yaffs2; +import static ghidra.formats.gfilesystem.fileinfo.FileAttributeType.*; + import java.io.IOException; import java.util.*; -import ghidra.app.util.bin.ByteArrayProvider; -import ghidra.app.util.bin.ByteProvider; +import org.apache.commons.io.FilenameUtils; + +import ghidra.app.util.bin.*; import ghidra.formats.gfilesystem.*; import ghidra.formats.gfilesystem.annotations.FileSystemInfo; -import ghidra.formats.gfilesystem.factory.GFileSystemBaseFactory; +import ghidra.formats.gfilesystem.fileinfo.*; +import ghidra.program.model.lang.Endian; +import ghidra.util.Msg; import ghidra.util.exception.CancelledException; import ghidra.util.task.TaskMonitor; -@FileSystemInfo(type = "yaffs2", description = "YAFFS2", factory = GFileSystemBaseFactory.class) -public class YAFFS2FileSystem extends GFileSystemBase { +/** + * File system implementation for YAFFS2 images with 2048 byte pages and 64 bytes of OOB data + * after each page. + *

+ * The image file is made up of concatenated 2112 byte pages (2048 bytes data and 64 bytes OOB) + * formatted such: + *

+ *

+ *  page { 
+ * 	struct obj_hdr { [512 bytes] ... parentObjId, name, size, datetime, etc ... } 
+ * 	...1536 bytes... 
+ * 	struct oob { [64 bytes] objId, misc etc }
+ * } (repeated)
+ * 
+ *

+ * If page was a File obj_hdr, the pages following will be the data of that file: + *

+ *

+ * page { 
+ * 	2048 bytes filedata
+ * 	struct oob { [64 bytes] .... }
+ * }
+ * 
+ *

+ * NOTES: + *

    + *
  • There is no header / superblock at the beginning of the filesystem data, so parameters + * (like the page size or OOB data size, endianness, etc) are not discoverable without some + * guessing / try-and-see-if-it-produces-valid-looking-data. + *
  • Changing the size of the page changes the default size of the OOB data, which can change the + * layout of the OOB data. + *
  • The OOB data might be written in YAFFS-original format, or might be written in more recent + * Linux MTD format. (see mkyaff's --yaffs-ecclayout startup option). This impl only handles + * YAFFS-original format. + *
  • This impl has only been tested with images that are freshly created with no + * usage / modifications. + *
+ */ +@FileSystemInfo(type = "yaffs2", description = "YAFFS2", factory = YAFFS2FileSystemFactory.class) +public class YAFFS2FileSystem extends AbstractFileSystem { + static class Metadata { + final long objId; + int pageNum; + int equivPageNum; // if hardlink - private Map map = new HashMap<>(); - private Map map2 = new HashMap<>(); - - public YAFFS2FileSystem(String fileSystemName, ByteProvider provider) { - super(fileSystemName, provider); - } - - @Override - public boolean isValid(TaskMonitor monitor) throws IOException { - byte[] bytes = provider.readBytes(0, YAFFS2Constants.MAGIC_SIZE); - // check for initial byte equal to 0x03, 'directory' - // and check that the first byte of the file name is null - // ... this is the yaffs2 root level dir header - return ((bytes[0] == 0x03) && (bytes[10] == 0x00)); - } - - @Override - public void open(TaskMonitor monitor) throws IOException, CancelledException { - // TODO: should yaffsInput be closed? - YAFFS2InputStream yaffs2Input = new YAFFS2InputStream(provider.getInputStream(0)); - - // go through the image file, looking at each header entry, ignoring the data, storing the dir tree - while (!monitor.isCancelled()) { - YAFFS2Entry headerEntry = yaffs2Input.getNextHeaderEntry(); - if (headerEntry == null) { - break; - } - storeEntry(headerEntry, monitor); + Metadata(long objId) { + this.objId = objId; } } + private ByteProvider provider; + private int oobSize; + private int pageSize; + private int stride; + private Endian endian; + + public YAFFS2FileSystem(ByteProvider provider, int pageSize, int oobSize, Endian endian, + FSRLRoot fsFSRL, FileSystemService fsService) { + super(fsFSRL, fsService); + + this.pageSize = pageSize; + this.oobSize = oobSize; + this.stride = pageSize + oobSize; + this.endian = endian; + this.provider = provider; + } + @Override public void close() throws IOException { - map.clear(); - map2.clear(); - super.close(); + refManager.onClose(); + if (provider != null) { + FSUtilities.uncheckedClose(provider, null); + provider = null; + } + fsIndex.clear(); } - @Override - public List getListing(GFile directory) throws IOException { - if (directory == null || directory.equals(root)) { - List roots = new ArrayList<>(); - for (Long objId : map.keySet()) { - GFile parentFile = map.get(objId).getParentFile(); - if (parentFile != null) { - if (parentFile == root || parentFile.equals(root)) { - GFile file = map.get(objId); - roots.add(file); + public void mount(TaskMonitor monitor) throws IOException, CancelledException { + int pageCount = (int) (provider.length() / stride); + if (provider.length() % stride != 0) { + Msg.warn(this, "Non-integral yaffs2 file system image file length: %d, %d, %d" + .formatted(provider.length(), pageSize, oobSize)); + } + + // accumulate objId -> metadata(pagenum) mappings, allowing an earlier version of an object + // to be overwritten by a 'newer' definition in a later page. + Map objIdToMetadata = new HashMap<>(); + for (int pageNum = 0; pageNum < pageCount; pageNum++) { + monitor.checkCancelled(); + HeaderWithOOB page = readPage(pageNum); + if (page == null) { + break; + } + if (!page.hdr.isValid(provider)) { + break; + } + Metadata metadata = + objIdToMetadata.computeIfAbsent(page.oob.getObjectId(), Metadata::new); + metadata.pageNum = pageNum; + pageNum += page.hdr.getDataPageCount(pageSize); + } + + objIdToMetadata.entrySet() + .stream() + .sorted((o1, o2) -> Long.compare(o1.getKey(), o2.getKey())) + .forEach(entry -> { + try { + long objId = entry.getKey(); + Metadata metadata = entry.getValue(); + HeaderWithOOB page = readPage(metadata.pageNum); + if (metadata.pageNum == 0 && objId == 1 && + page.hdr.getParentObjectId() == 1) { + // skip entry that is just the 'root' directory + return; + } + + long parentObjId = page.hdr.getParentObjectId(); + GFile parent = parentObjId == 1 + ? fsIndex.getRootDir() + : fsIndex.getFileByIndex(parentObjId); + if (parent == null) { + parent = fsIndex.getRootDir(); + Msg.warn(this, + "Unable to find parent %x of %x".formatted(parentObjId, objId)); + } + String name = page.hdr.getName(); + switch (page.hdr.getObjectTypeEnum()) { + case File: + fsIndex.storeFileWithParent(name, parent, objId, false, + page.hdr.calcFileSize(), metadata); + break; + case Directory: + fsIndex.storeFileWithParent(name, parent, objId, true, -1, + metadata); + break; + case Symlink: + fsIndex.storeSymlinkWithParent(name, parent, objId, + page.hdr.getAliasFileName(), 0, metadata); + break; + case Hardlink: + Metadata equivTarget = objIdToMetadata.get(page.hdr.getEquivId()); + if (equivTarget == null) { + Msg.warn(this, + "Unable to find hardlink equiv: %x, %x, skipping: %s" + .formatted(metadata.pageNum, page.hdr.getEquivId(), + name)); + break; + } + + metadata.equivPageNum = equivTarget.pageNum; + HeaderWithOOB equivTargetObj = readPage(metadata.equivPageNum); + fsIndex.storeFileWithParent(name, parent, objId, false, + equivTargetObj.hdr.calcFileSize(), metadata); + break; + default: + break; + } } - } - } - return roots; + catch (IOException e) { + // shouldn't happen + } + }); + } + + private record HeaderWithOOB(YAFFS2Header hdr, YAFFS2OOBStruct oob) {} + + private HeaderWithOOB readPage(int pageNum) throws IOException { + byte[] pageBytes = provider.readBytes(pageToOffset(pageNum), stride); + if (isDefaultPage(pageBytes)) { + return null; } - List fileList = new ArrayList<>(); - for (Long objId : map.keySet()) { - GFile parentFile = map.get(objId).getParentFile(); - if (parentFile == null) { - continue; - } - if (parentFile.equals(directory)) { - GFile file = map.get(objId); - fileList.add(file); + BinaryReader br = + new BinaryReader(new ByteArrayProvider(pageBytes), endian == Endian.LITTLE); + YAFFS2Header hdr = YAFFS2Header.read(br); + br.setPointerIndex(pageSize); + YAFFS2OOBStruct tag = YAFFS2OOBStruct.read(br, oobSize); + return new HeaderWithOOB(hdr, tag); + } + + private boolean isDefaultPage(byte[] pageBytes) { + // return true if page is entirely 00's or FF's + byte b = pageBytes[0]; + if (b != 0 && b != -1) { + return false; + } + for (int i = 0; i < pageBytes.length; i++) { + if (pageBytes[i] != b) { + return false; } } - return fileList; + return true; + } + + private long pageToOffset(int pageNum) { + return pageNum * stride; } @Override public ByteProvider getByteProvider(GFile file, TaskMonitor monitor) throws IOException, CancelledException { - // get saved entry from selected file - YAFFS2Entry entry = map2.get(file); - - // exit if selection is a directory - if (entry.isDirectory()) { - throw new IOException(file.getName() + " is a directory"); + GFile resolvedFile = fsIndex.resolveSymlinks(file); + Metadata metadata = fsIndex.getMetadata(resolvedFile); + if (metadata == null) { + return null; + } + int pageNum = metadata.pageNum; + HeaderWithOOB page = readPage(pageNum); + if (page.hdr.getObjectTypeEnum() == YAFFS2ObjectType.Hardlink) { + pageNum = metadata.equivPageNum; + page = readPage(pageNum); } - // recall size of file and offset into the file system image - long fileOffset = entry.getFileOffset(); - long size = entry.getSize(); - - // return bytes for the selected file - try (YAFFS2InputStream YAFFS2Input = new YAFFS2InputStream(provider.getInputStream(0))) { - byte[] entryData = YAFFS2Input.getEntryData(fileOffset, size); - return new ByteArrayProvider(entryData, file.getFSRL()); + RangeMappedByteProvider result = + new RangeMappedByteProvider(provider, resolvedFile.getFSRL()); + long byteCount = page.hdr.calcFileSize(); + pageNum++; + for (long offset = pageToOffset(pageNum); byteCount > 0; pageNum++) { + int bytesInPage = (int) Math.min(byteCount, pageSize); + result.addRange(offset, bytesInPage); + byteCount -= bytesInPage; } + + return result; } - private void storeEntry(YAFFS2Entry entry, TaskMonitor monitor) { - if (entry == null) { - return; + @Override + public FileAttributes getFileAttributes(GFile file, TaskMonitor monitor) { + if (fsIndex.getRootDir().equals(file)) { + return FileAttributes.of( // file, attrs + FileAttribute.create("Page Size", pageSize), + FileAttribute.create("OOB Size", oobSize)); } - monitor.setMessage(entry.getName()); + Metadata metadata = fsIndex.getMetadata(file); + if (metadata != null) { + try { + HeaderWithOOB origpage = readPage(metadata.pageNum); + HeaderWithOOB page = origpage.hdr.getObjectTypeEnum() == YAFFS2ObjectType.Hardlink + ? readPage(metadata.equivPageNum) + : origpage; + YAFFS2ObjectType objType = page.hdr.getObjectTypeEnum(); - // search the file listing for the parent object ID (need this since yaffs2 file names are not full paths) - long parentObjectId = entry.getParentObjectId(); - long objectId = entry.getObjectId(); - GFile parentFile = (parentObjectId == 1) ? root : map.get(parentObjectId); - - // skip the first header (always a meaningless, "root" header) - if ((objectId == 1) & (parentObjectId == 1)) { - return; + return FileAttributes.of( // file attrs + FileAttribute.create(NAME_ATTR, origpage.hdr.getName()), + FileAttribute.create(PATH_ATTR, + FilenameUtils.getFullPathNoEndSeparator(file.getPath())), + objType == YAFFS2ObjectType.File + ? FileAttribute.create(SIZE_ATTR, page.hdr.calcFileSize()) + : null, + FileAttribute.create(MODIFIED_DATE_ATTR, + new Date(page.hdr.getYstMTime() * 1000)), + FileAttribute.create(CREATE_DATE_ATTR, new Date(page.hdr.getYstCTime() * 1000)), + FileAttribute.create(ACCESSED_DATE_ATTR, + new Date(page.hdr.getYstATime() * 1000)), + FileAttribute.create(FILE_TYPE_ATTR, convertFileType(objType)), + FileAttribute.create(USER_ID_ATTR, page.hdr.getYstUId()), + FileAttribute.create(GROUP_ID_ATTR, page.hdr.getYstGId()), + FileAttribute.create(UNIX_ACL_ATTR, page.hdr.getYstMode()), + origpage.hdr.getObjectTypeEnum() == YAFFS2ObjectType.Symlink + ? FileAttribute.create(SYMLINK_DEST_ATTR, + origpage.hdr.getAliasFileName()) + : null, + FileAttribute.create("Object Id", "%08x".formatted(origpage.oob.getObjectId())), + FileAttribute.create("Parent Id", + "%08x".formatted(origpage.hdr.getParentObjectId())), + FileAttribute.create("Page Number", "%08x".formatted(metadata.pageNum))); + } + catch (IOException e) { + // fall thru + } } - - // process the other headers - GFileImpl file = GFileImpl.fromFilename(this, parentFile, entry.getName(), - entry.isDirectory(), entry.getSize(), null); - map.put(entry.getObjectId(), file); - map2.put(file, entry); + return null; } + + private FileType convertFileType(YAFFS2ObjectType objType) { + return switch (objType) { + case File -> FileType.FILE; + case Directory -> FileType.DIRECTORY; + case Hardlink -> FileType.FILE; + case Symlink -> FileType.SYMBOLIC_LINK; + default -> FileType.OTHER; + }; + } + + @Override + public boolean isClosed() { + return provider == null; + } + } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2FileSystemFactory.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2FileSystemFactory.java new file mode 100644 index 0000000000..7b43c4365b --- /dev/null +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2FileSystemFactory.java @@ -0,0 +1,86 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.file.formats.yaffs2; + +import java.io.IOException; + +import ghidra.app.util.bin.BinaryReader; +import ghidra.app.util.bin.ByteProvider; +import ghidra.formats.gfilesystem.*; +import ghidra.formats.gfilesystem.factory.GFileSystemFactoryByteProvider; +import ghidra.formats.gfilesystem.factory.GFileSystemProbeByteProvider; +import ghidra.program.model.lang.Endian; +import ghidra.util.exception.CancelledException; +import ghidra.util.task.TaskMonitor; + +public class YAFFS2FileSystemFactory + implements GFileSystemFactoryByteProvider, GFileSystemProbeByteProvider { + + private static final int MIN_REQUIRED_OBJHDRS = 2; + private static final int MAX_OBJHDRS_TO_CHECK = 5; + + @Override + public boolean probe(ByteProvider byteProvider, FileSystemService fsService, + TaskMonitor monitor) throws IOException, CancelledException { + return hasObjHeaders(byteProvider, 2048, 64, true); + } + + /** + * Returns true if the stream appears to have a few valid yaffs2 obj_hdr structs at the start. + * + * @param bp {@link ByteProvider} stream + * @param pageSize typically 2048 + * @param oobSize only tested with 64 byte + * @param isLE only tested with LE + * @return boolean true if it appears to be a valid YAFFS2 image + * @throws IOException if error reading + */ + boolean hasObjHeaders(ByteProvider bp, int pageSize, int oobSize, boolean isLE) + throws IOException { + BinaryReader br = new BinaryReader(bp, isLE); + long stride = pageSize + oobSize; + int pageCount = (int) (bp.length() / stride); + int foundCount = 0; + for (int pageNum = 0; pageNum < pageCount && foundCount < MAX_OBJHDRS_TO_CHECK; pageNum++) { + br.setPointerIndex(pageNum * stride); + YAFFS2Header hdr = YAFFS2Header.read(br); + if (!hdr.isValid(bp)) { + return false; + } + pageNum += hdr.getDataPageCount(pageSize); + foundCount++; + } + return foundCount > MIN_REQUIRED_OBJHDRS; + } + + @Override + public GFileSystem create(FSRLRoot targetFSRL, ByteProvider byteProvider, + FileSystemService fsService, TaskMonitor monitor) + throws IOException, CancelledException { + try { + YAFFS2FileSystem fs = + new YAFFS2FileSystem(byteProvider, 2048, 64, Endian.LITTLE, targetFSRL, fsService); + fs.mount(monitor); + + return fs; + } + catch (IOException | CancelledException e) { + FSUtilities.uncheckedClose(byteProvider, null); + return null; + } + } + +} diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2Header.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2Header.java index a765d3adbb..e4988385f9 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2Header.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2Header.java @@ -16,82 +16,143 @@ package ghidra.file.formats.yaffs2; import java.io.IOException; +import java.nio.charset.StandardCharsets; -import ghidra.app.util.bin.StructConverter; -import ghidra.program.model.data.*; -import ghidra.util.exception.DuplicateNameException; +import ghidra.app.util.bin.BinaryReader; +import ghidra.app.util.bin.ByteProvider; +import ghidra.util.NumericUtilities; -public class YAFFS2Header implements StructConverter { +/** + *
+ * struct yaffs_obj_hdr  // Length: 512 (0x200)
+ * {
+ *   u32                type
+ *   u32                parent_obj_id
+ *   u16                sum_no_longer_used
+ *   YCHAR[256]         name
+ *   u32                yst_mode
+ *   u32                yst_uid
+ *   u32                yst_gid
+ *   u32                yst_atime
+ *   u32                yst_mtime
+ *   u32                yst_ctime
+ *   u32                file_size_low
+ *   int                equiv_id
+ *   YCHAR[160]         alias
+ *   u32                yst_rdev
+ *   u32[2]             win_ctime
+ *   u32[2]             win_atime
+ *   u32[2]             win_mtime
+ *   u32                inband_shadowed_obj_id
+ *   u32                inband_is_shrink
+ *   u32                file_size_high
+ *   u32[1]             reserved
+ *   int                shadows_obj
+ *   u32                is_shrink
+ * }
+ * 
+ */ +public class YAFFS2Header { + + /** + * Reads a YAFFS2 objhdr struct. + * + * @param br stream to read from + * @return new YAFFS2Header, never null + * @throws IOException if error reading + */ + public static YAFFS2Header read(BinaryReader br) throws IOException { + YAFFS2Header result = new YAFFS2Header(); + result.objectType = br.readNextInt(); + result.parentObjectId = br.readNextUnsignedInt(); + result.checksum = br.readNextShort(); + result.fileName = readNextYaffs2String(br, 256); + br.align(4 /*sizeof(int) */); + result.ystMode = br.readNextUnsignedInt(); + result.ystUId = br.readNextUnsignedInt(); + result.ystGId = br.readNextUnsignedInt(); + result.ystATime = br.readNextUnsignedInt(); + result.ystMTime = br.readNextUnsignedInt(); + result.ystCTime = br.readNextUnsignedInt(); + result.fileSizeLow = br.readNextUnsignedInt(); + result.equivId = br.readNextUnsignedInt(); + result.aliasFileName = readNextYaffs2String(br, 160); + result.ystRDev = br.readNextUnsignedInt(); + result.winCTime = br.readNextIntArray(2); + result.winATime = br.readNextIntArray(2); + result.winMTime = br.readNextIntArray(2); + result.inbandObjId = br.readNextUnsignedInt(); + result.inbandIsShrink = br.readNextUnsignedInt(); + result.fileSizeHigh = br.readNextUnsignedInt(); + br.readNextInt(); // reserved + result.shadowsObject = br.readNextUnsignedInt(); + result.isShrink = br.readNextUnsignedInt(); + // assert(br.index == +512) + return result; + } // header objects private long objectType; private long parentObjectId; private short checksum; - private String fileName; - private long ystMode; - private long ystUId; - private long ystGId; - private String ystATime; - private String ystMTime; - private String ystCTime; - private long fileSizeLow; - private long equivId; - private String aliasFileName; - private long ystRDev; - private long winCTime; - private long winATime; - private long winMTime; - private long inbandObjId; - private long inbandIsShrink; - private long fileSizeHigh; - private long shadowsObject; - private long isShrink; - - /** - * Construct an entry from an archive's header bytes. - */ - public YAFFS2Header(byte[] buffer) { - - // parse header structure - objectType = YAFFS2Utils.parseInteger(buffer, 0, 4); - parentObjectId = YAFFS2Utils.parseInteger(buffer, 4, 4); - checksum = (short) YAFFS2Utils.parseInteger(buffer, 8, 2); - fileName = YAFFS2Utils.parseName(buffer, 10, 256); - // skip 2 bytes for the "unknown1" short field - ystMode = YAFFS2Utils.parseInteger(buffer, 268, 4); - ystUId = YAFFS2Utils.parseInteger(buffer, 272, 4); - ystGId = YAFFS2Utils.parseInteger(buffer, 276, 4); - ystATime = YAFFS2Utils.parseDateTime(buffer, 280, 4); - ystMTime = YAFFS2Utils.parseDateTime(buffer, 284, 4); - ystCTime = YAFFS2Utils.parseDateTime(buffer, 288, 4); - fileSizeLow = YAFFS2Utils.parseFileSize(buffer, 292, 4); - equivId = YAFFS2Utils.parseInteger(buffer, 296, 4); - aliasFileName = YAFFS2Utils.parseName(buffer, 300, 160); - ystRDev = YAFFS2Utils.parseInteger(buffer, 460, 4); - winCTime = buffer[464]; - winATime = buffer[472]; - winMTime = buffer[480]; - inbandObjId = YAFFS2Utils.parseInteger(buffer, 488, 4); - inbandIsShrink = YAFFS2Utils.parseInteger(buffer, 492, 4); - fileSizeHigh = YAFFS2Utils.parseInteger(buffer, 496, 4); - // skip 4 bytes for the "reserved" int field - shadowsObject = YAFFS2Utils.parseInteger(buffer, 504, 4); - isShrink = YAFFS2Utils.parseInteger(buffer, 508, 4); - - } + private String fileName; + private long ystMode; + private long ystUId; + private long ystGId; + private long ystATime; + private long ystMTime; + private long ystCTime; + private long fileSizeLow; + private long equivId; + private String aliasFileName; + private long ystRDev; + private int[] winCTime; + private int[] winATime; + private int[] winMTime; + private long inbandObjId; + private long inbandIsShrink; + private long fileSizeHigh; + private long shadowsObject; + private long isShrink; public YAFFS2Header() { } + /** + * Returns the number of data pages that will follow this page. + * + * @param pageSize size of pages in this fs + * @return the number of data pages that will follow this page + */ + public long getDataPageCount(int pageSize) { + return getObjectTypeEnum() == YAFFS2ObjectType.File + ? NumericUtilities.getUnsignedAlignedValue(calcFileSize(), pageSize) / pageSize + : 0; + } + + /** + * Returns true if the data in this object appears valid + * + * @param bp stream that contains the entire yaffs2 image + * @return boolean true if the data in this object appears valid + */ + public boolean isValid(ByteProvider bp) { + YAFFS2ObjectType ote = getObjectTypeEnum(); + if (ote == YAFFS2ObjectType.File) { + long filesize = calcFileSize(); + if (filesize < 0 || filesize > bp.length()) { + return false; + } + } + return ote != YAFFS2ObjectType.INVALID; + } + public long getObjectType() { return objectType; } - - public boolean isDirectory() { - if (objectType == 3) { - return true; - } - return false; + + public YAFFS2ObjectType getObjectTypeEnum() { + return YAFFS2ObjectType.parse(objectType); } public short getChecksum() { @@ -114,15 +175,15 @@ public class YAFFS2Header implements StructConverter { return ystGId; } - public String getYstATime() { + public long getYstATime() { return ystATime; } - public String getYstMTime() { + public long getYstMTime() { return ystMTime; } - public String getYstCTime() { + public long getYstCTime() { return ystCTime; } @@ -130,93 +191,71 @@ public class YAFFS2Header implements StructConverter { return fileSizeLow; } + public long getTotalSize() { + return fileSizeLow | (fileSizeHigh << 32L); + } + public long getEquivId() { return equivId; } - public String getAliasFileName() { - return aliasFileName; - } + public String getAliasFileName() { + return aliasFileName; + } - public long getYstRDev() { - return ystRDev; - } + public long getYstRDev() { + return ystRDev; + } - public long getWinCTime() { - return winCTime; - } + public int[] getWinCTime() { + return winCTime; + } - public long getWinATime() { - return winATime; - } + public int[] getWinATime() { + return winATime; + } - public long getWinMTime() { - return winMTime; - } + public int[] getWinMTime() { + return winMTime; + } - public long getInbandObjId() { - return inbandObjId; - } + public long getInbandObjId() { + return inbandObjId; + } - public long getInbandIsShrink() { - return inbandIsShrink; - } + public long getInbandIsShrink() { + return inbandIsShrink; + } - public long getFileSizeHigh() { - return fileSizeHigh; - } + public long getFileSizeHigh() { + return fileSizeHigh; + } - public long getShadowsObject() { - return shadowsObject; - } + public long calcFileSize() { + return fileSizeLow | + (fileSizeHigh != NumericUtilities.MAX_UNSIGNED_INT32_AS_LONG ? fileSizeHigh : 0); + } - public long getIsShrink() { - return isShrink; - } + public long getShadowsObject() { + return shadowsObject; + } + + public long getIsShrink() { + return isShrink; + } public long getParentObjectId() { return parentObjectId; } - public boolean isFile() { - if (objectType == 1) { - return true; + static String readNextYaffs2String(BinaryReader br, int len) throws IOException { + // truncate both trailing 0's and FF's. + byte[] bytes = br.readNextByteArray(len); + int i = bytes.length - 1; + while (i >= 0 && (bytes[i] == 0 || bytes[i] == -1)) { + i--; } - return false; + return new String(bytes, 0, i + 1, StandardCharsets.UTF_8); } - - // header structure for analyzer - @Override - public DataType toDataType() throws DuplicateNameException, IOException { - - Structure structure = new StructureDataType( "yaffs2Hdr", 0 ); - structure.add( DWORD, "objectType", null ); - structure.add( DWORD, "parentObjectId", null ); - structure.add( WORD, "checksum", null ); - structure.add( STRING, YAFFS2Constants.FILE_NAME_SIZE, "fileName", null ); - structure.add( WORD, "unknown1", null ); - structure.add( DWORD, "ystMode", null ); - structure.add( DWORD, "ystUId", null ); - structure.add( DWORD, "ystGId", null ); - structure.add( DWORD, "ystATime", null ); - structure.add( DWORD, "ystMTime", null ); - structure.add( DWORD, "ystCTime", null ); - structure.add( DWORD, "fileSizeLow", null ); - structure.add( DWORD, "equivId", null ); - structure.add( STRING, YAFFS2Constants.ALIAS_FILE_NAME_SIZE, "aliasFileName", null ); - structure.add( DWORD, "ystRDev", null ); - structure.add( QWORD, "winCTime", null ); - structure.add( QWORD, "winATime", null ); - structure.add( QWORD, "winMTime", null ); - structure.add( DWORD, "inbandObjId", null ); - structure.add( DWORD, "inbandIsShrink", null ); - structure.add( DWORD, "fileSizeHigh", null ); - structure.add( DWORD, "reserved", null ); - structure.add( DWORD, "shadowsObject", null ); - structure.add( DWORD, "isShrink", null ); - structure.add(new ArrayDataType(BYTE, YAFFS2Constants.EMPTY_DATA_SIZE, BYTE.getLength()), "emptyData", null); - return structure; - - } - + } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2InputStream.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2InputStream.java deleted file mode 100644 index e1be14e962..0000000000 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2InputStream.java +++ /dev/null @@ -1,143 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package ghidra.file.formats.yaffs2; - -import java.io.*; - -public class YAFFS2InputStream implements Closeable { - - private int dataBufferSize = YAFFS2Constants.DATA_BUFFER_SIZE; - private static int recordSize = YAFFS2Constants.RECORD_SIZE; - private boolean hasHitEOF; - private long entrySize; // TODO: why is this a member var instead of local var? - long fileEntryOffset; - protected final YAFFS2Buffer buffer; - private YAFFS2Entry currEntry; - - public YAFFS2InputStream(InputStream is) { - this(is, recordSize); - } - - public YAFFS2InputStream(InputStream is, int recordSize) { - this.buffer = new YAFFS2Buffer(is, recordSize); - this.hasHitEOF = false; - this.fileEntryOffset = 0; - } - - // get the next header, skipping the data block records - public YAFFS2Entry getNextHeaderEntry() throws IOException { - - if (hasHitEOF) { - return null; - } - - // entry will be null on first try - if (currEntry != null) { - // if header is of type file, skip the data section to get to the next header object - if (currEntry.isFile()) { - long numToSkip = (recordSize) * (currEntry.getSize() / dataBufferSize) + recordSize; - long skipped = buffer.skip(numToSkip); - if (skipped < 0) { - throw new RuntimeException("failed to skip current entry's data section"); - } - } - } - - // get a new YAFFS2 record - byte[] headerBuf = getRecord(); - - // check if we hit the EOF - if (hasHitEOF) { - currEntry = null; - return null; - } - - // parse the new buffer into a YAFFS2 entry - currEntry = new YAFFS2Entry(headerBuf); - - // save where this entry is located in the image file - currEntry.setFileOffset(fileEntryOffset); - - // compute the size of this entry - entrySize = recordSize * (currEntry.getSize() / dataBufferSize) + recordSize; - if (currEntry.isFile()) { - entrySize = entrySize + recordSize; - } - - // compute where the next entry starts in the image file - fileEntryOffset = fileEntryOffset + entrySize; - - return currEntry; - } - - // get data for the selected file - skip to offset and return length bytes - public byte[] getEntryData(long offset, long length) throws IOException { - - long numberOfBuffers = length / dataBufferSize; - long remainder = length % dataBufferSize; - long numberToRead = recordSize * numberOfBuffers + remainder; - int indx = 0; - byte[] contents = new byte[(int) numberToRead]; - entrySize = offset + recordSize; - - // skip to correct file location (offset gets us to the header, recordSize gets past that header to the start of data) - long skipped = buffer.skip(offset + recordSize); - if (skipped < 0) { - throw new RuntimeException("failed to skip to the data section"); - } - - // read fully populated buffers of data - while (numberOfBuffers > 0) { - byte[] dataBuf = getRecord(); - System.arraycopy(dataBuf, 0, contents, indx, dataBufferSize); - numberOfBuffers -= 1; - indx += dataBufferSize; - } - - // read another partial record? - if (remainder > 0) { - byte[] dataBuf = getRecord(); - System.arraycopy(dataBuf, 0, contents, indx, (int) remainder); - } - - return contents; - } - - private byte[] getRecord() throws IOException { - if (hasHitEOF) { - return null; - } - - byte[] headerBuf = buffer.readRecord(); - - // check if we hit the end of file - if (headerBuf == null) { - hasHitEOF = true; - } - else if (buffer.isEOFRecord(headerBuf)) { - hasHitEOF = true; - } - - // return null for EOF, the record if not - return hasHitEOF ? null : headerBuf; - } - - @Override - public void close() throws IOException { - buffer.close(); - } - -} diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2OOBStruct.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2OOBStruct.java new file mode 100644 index 0000000000..1182367a99 --- /dev/null +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2OOBStruct.java @@ -0,0 +1,65 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.file.formats.yaffs2; + +import java.io.IOException; + +import ghidra.app.util.bin.BinaryReader; + +/** + * Represents the data YAFFS2 puts in the OOB area of a page. See the yaffs_guts.h for + * yaffs_tags or yaffs_ext_tags, etc. + *

+ * The layout of data in the OOB data can vary depending the version, size, and MTD-vs-yaffs + * option used in mkyaffs. + *

+ * We currently only care about the objectId, but a more fully-featured implementation might need + * more information from this area. + */ +public class YAFFS2OOBStruct { + + /** + * Reads a yaffs2_ext_tag-formatted OOB data area. + * + * @param br stream to read from + * @param oobSize size of the OOB data + * @return new {@link YAFFS2OOBStruct}, never null + * @throws IOException if error reading + */ + public static YAFFS2OOBStruct read(BinaryReader br, int oobSize) throws IOException { + long start = br.getPointerIndex(); + YAFFS2OOBStruct result = new YAFFS2OOBStruct(); + result.sequenceNumber = br.readNextUnsignedInt(); + result.objectId = br.readNextUnsignedInt(); + br.setPointerIndex(start + oobSize); + return result; + } + + private long sequenceNumber; + private long objectId; + + public YAFFS2OOBStruct() { + // empty + } + + public long getObjectId() { + return objectId; + } + + public long getSequenceNumber() { + return sequenceNumber; + } +} diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2Constants.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2ObjectType.java similarity index 54% rename from Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2Constants.java rename to Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2ObjectType.java index ec169804dd..3aaf3eaf84 100644 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2Constants.java +++ b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2ObjectType.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,22 +15,20 @@ */ package ghidra.file.formats.yaffs2; -public class YAFFS2Constants { - - public final static int MAGIC_SIZE = 11; - - public final static int FILE_NAME_SIZE = 256; - - public final static int ALIAS_FILE_NAME_SIZE = 160; - - public final static int RECORD_SIZE = 2112; - - public final static int HEADER_SIZE = 512; - - public final static int EXTENDED_TAGS_SIZE = 64; - - public final static int DATA_BUFFER_SIZE = 2048; - - public final static int EMPTY_DATA_SIZE = 1536; +/** + * Yaffs2 object type enum. The java enum ordinal must match the yaffs enum values. + */ +public enum YAFFS2ObjectType { + Unknown, // 0 + File, // 1 + Symlink, // 2 + Directory, // 3 + Hardlink, // 4 + Special, // 5 + INVALID; // represents value that doesn't match, including Unknown + public static YAFFS2ObjectType parse(long i) { + YAFFS2ObjectType[] values = values(); + return i > Unknown.ordinal() && i < INVALID.ordinal() ? values[(int) i] : INVALID; + } } diff --git a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2Utils.java b/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2Utils.java deleted file mode 100644 index b42d5eb77c..0000000000 --- a/Ghidra/Features/FileFormats/src/main/java/ghidra/file/formats/yaffs2/YAFFS2Utils.java +++ /dev/null @@ -1,122 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package ghidra.file.formats.yaffs2; - -import java.text.DateFormat; - -import ghidra.program.model.address.Address; -import ghidra.program.model.listing.Program; - -public class YAFFS2Utils { - - /** - parse file names - */ - public static String parseName(byte[] buffer, final int offset, final int length) { - StringBuffer result = new StringBuffer(length); - int end = offset + length; - - for (int i = offset; i < end; ++i) { - byte b = buffer[i]; - if (b == 0) { // Trailing null - break; - } - result.append((char) (b & 0xFF)); // Allow for sign-extension - } - - return result.toString(); - } - - /** - read values are unsigned int, return as a long (because of Java) - */ - public static long parseInteger(final byte[] buffer, final int offset, final int length) { - long result = 0; - int end = offset + length; - int start = offset; - int j = 0; - - for (int i = start; i < end; i++) { - result += ((long) buffer[i] & 0xFF) << (8 * j); - j++; - } - return result; - } - - /** - compute the file size, set file size to 0 if object type is dir - */ - public static long parseFileSize(final byte[] buffer, final int offset, final int length) { - long result = 0; - int end = offset + length; - int start = offset; - int j = 0; - int k = 0; - - for (int i = start; i < end; i++) { - // check for 0xffffffff buffer value, a special case - if (buffer[i] == -1) - k++; - - // compute integer result - result += ((long) buffer[i] & 0xFF) << (8 * j); - j++; - } - - // if special case was found (ex, for a dir header), return 0 as size - if (k < 4) { - return result; - } - return 0; - } - - /** - return the date/time string for the parsed file - */ - public static String parseDateTime(final byte[] buffer, final int offset, final int length) { - long result = 0; - int end = offset + length; - int start = offset; - int j = 0; - - for (int i = start; i < end; i++) { - result += ((long) buffer[i] & 0xFF) << (8 * j); - j++; - } - - // note that input here needs to be ms, result above is in sec - return DateFormat.getDateTimeInstance().format(result * 1000); - } - - /** - no Magic Bytes, so check for an empty directory in the first header - */ - public final static boolean isYAFFS2Image(Program program) { - byte[] bytes = new byte[YAFFS2Constants.MAGIC_SIZE]; - try { - Address address = program.getMinAddress(); - program.getMemory().getBytes(address, bytes); - } - catch (Exception e) { - } - // check for initial byte equal to 0x03, 'directory' - // and check that the first byte of the file name is null - // ... this is the yaffs2 root level dir header - return ((bytes[0] == 0x03) && (bytes[10] == 0x00)); - - } - -}