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.
This commit is contained in:
dev747368
2026-03-02 19:13:41 +00:00
parent 8e0fc9bc64
commit 6ea6748c15
13 changed files with 629 additions and 1059 deletions
@@ -168,17 +168,25 @@ public class GetInfoFSBFileHandler implements FSBFileHandler {
//---------------------------------------------------------------------------------------------
private static final Function<Object, String> PLAIN_TOSTRING = o -> o.toString();
private static final Function<Object, String> 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<Object, String> 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<Object, String> 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<Object, String> 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<FileAttributeType, Function<Object, String>> 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
}
@@ -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";
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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.
* <p>
* The image file is made up of concatenated 2112 byte pages (2048 bytes data and 64 bytes OOB)
* formatted such:
* <p>
* <pre>
* page {
* struct obj_hdr { [512 bytes] ... parentObjId, name, size, datetime, etc ... }
* ...1536 bytes...
* struct oob { [64 bytes] objId, misc etc }
* } (repeated)
* </pre>
* <p>
* If page was a File obj_hdr, the pages following will be the data of that file:
* <p>
* <pre>
* page {
* 2048 bytes filedata
* struct oob { [64 bytes] .... }
* }
* </pre>
* <p>
* NOTES:
* <ul>
* <li>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.
* <li>Changing the size of the page changes the default size of the OOB data, which can change the
* layout of the OOB data.
* <li>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.
* <li>This impl has only been tested with images that are freshly created with no
* usage / modifications.
* </ul>
*/
@FileSystemInfo(type = "yaffs2", description = "YAFFS2", factory = YAFFS2FileSystemFactory.class)
public class YAFFS2FileSystem extends AbstractFileSystem<YAFFS2FileSystem.Metadata> {
static class Metadata {
final long objId;
int pageNum;
int equivPageNum; // if hardlink
private Map<Long, GFileImpl> map = new HashMap<>();
private Map<GFile, YAFFS2Entry> 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<GFile> getListing(GFile directory) throws IOException {
if (directory == null || directory.equals(root)) {
List<GFile> 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<Long, Metadata> 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<GFile> 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;
}
}
@@ -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<YAFFS2FileSystem>, 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;
}
}
}
@@ -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 {
/**
* <pre>
* 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
* }
* </pre>
*/
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;
}
}
@@ -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();
}
}
@@ -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.
* <p>
* The layout of data in the OOB data can vary depending the version, size, and MTD-vs-yaffs
* option used in mkyaffs.
* <p>
* 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;
}
}
@@ -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;
}
}
@@ -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));
}
}