/* ###
 * 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.
 */
//Annotates an HFS+ attributes b-Tree file.
//@category Apple.iOS

import ghidra.app.script.GhidraScript;
import ghidra.app.util.bin.*;
import ghidra.docking.settings.SettingsDefinition;
import ghidra.file.formats.ios.btree.*;
import ghidra.file.formats.ios.decmpfs.DecmpfsHeader;
import ghidra.file.formats.ios.xattr.XattrConstants;
import ghidra.program.model.address.Address;
import ghidra.program.model.data.DataType;
import ghidra.program.model.data.EndianSettingsDefinition;
import ghidra.program.model.listing.Data;
import ghidra.program.model.listing.Program;

public class BTreeAnnotationScript extends GhidraScript {

	@Override
	public void run() throws Exception {
		ByteProvider provider =
			MemoryByteProvider.createProgramHeaderByteProvider(currentProgram, false);

		BinaryReader reader = new BinaryReader(provider, false);

		BTreeRootNodeDescriptor root = new BTreeRootNodeDescriptor(reader);

		markupRecordOffsets(currentProgram, root, 0, root);

		Data headerNodeData = createBTreeNode(currentProgram, root, 0);

		Data headerRecordData =
			createBTreeHeaderRecord(currentProgram, root.getHeaderRecord(),
				headerNodeData.getLength());

		Data userDataRecordData =
			createUserDataRecord(currentProgram, root.getUserDataRecord(),
				headerNodeData.getLength() + headerRecordData.getLength());

		Data mapRecordData =
			createMapRecord(currentProgram, root.getMapRecord(), headerNodeData.getLength() +
				headerRecordData.getLength() + userDataRecordData.getLength());

		if (mapRecordData == null) {
			printerr("mapRecordData == null ????");
		}

		processNodes(currentProgram, root);
	}

	@Override
	public AnalysisMode getScriptAnalysisMode() {
		return AnalysisMode.DISABLED;
	}

	private int processNodes(Program program, BTreeRootNodeDescriptor root) throws Exception {

		int nodeIndex = 1;
		int min = (int) program.getMinAddress().getOffset();
		int max = (int) program.getMaxAddress().getOffset();
		monitor.setMaximum(max - min);
		monitor.setMessage("Applying node descriptors...");

		int nodeSize = root.getHeaderRecord().getNodeSize() & 0xffff;
		for (int i = nodeSize; i < program.getMemory().getSize(); i += nodeSize) {
			if (monitor.isCancelled()) {
				break;
			}
			monitor.setProgress(min + i);

			BTreeNodeDescriptor nodeI = root.getNode(nodeIndex);
			createBTreeNode(program, nodeI, i);

			StringBuffer buffer = new StringBuffer();
			buffer.append("Index:   0x" + Integer.toHexString(nodeIndex) + '\n');
			buffer.append("flink:   0x" + Integer.toHexString(nodeI.getFLink()) + '\n');
			buffer.append("blink:   0x" + Integer.toHexString(nodeI.getBLink()) + '\n');
			buffer.append("kind:      " + nodeI.getKind() + '\n');
			buffer.append("Records: 0x" + Integer.toHexString(nodeI.getNumRecords()) + '\n');

			setPlateComment(toAddr(i), buffer.toString());

			markupBTreeNodeData(program, nodeI);
			markupRecordOffsets(program, root, i, nodeI);

			++nodeIndex;
		}
		return nodeIndex;
	}

	private void markupRecordOffsets(Program program, BTreeRootNodeDescriptor root, int offset,
			BTreeNodeDescriptor nodeI) throws Exception {
		/* TODO
		int pos = 2;
		Address startAddress = toAddr( program, offset + root.getHeaderRecord().getNodeSize() - pos );
		Address currentAddress = startAddress;
		for ( int i = 0 ; i < nodeI.getRecordOffsets().size() ; ++i ) {
			createData( program , currentAddress, new WordDataType() );
			pos += 2;
			currentAddress = currentAddress.subtract( 2 );
		}
		if ( currentAddress.compareTo( startAddress ) < 0 ) {
			createFragment( program , "RecordOffset", currentAddress.add( 2 ), startAddress );
		}
		*/
	}

	private void markupBTreeNodeData(Program program, BTreeNodeDescriptor descriptor)
			throws Exception {

		for (BTreeNodeRecord record : descriptor.getRecords()) {

			Address address = toAddr(record.getRecordOffset());

			DataType recordDataType = record.toDataType();
			Data recordData = createData(address, recordDataType);
			createFragment(record.getType(), recordData.getMinAddress(), recordData.getLength());
			setPlateComment(address,
				record.getType() + " 0x" + Integer.toHexString(record.getFileID()));

			if (descriptor.getKind() == BTreeNodeKinds.kBTLeafNode) {
				if (record.getType().equals(XattrConstants.DECMPFS_XATTR_NAME)) {
					markupDecmpfs(program, descriptor, record, recordData.getMaxAddress().add(1));
				}
				else if (record.getType().equals(XattrConstants.KAUTH_FILESEC_XATTR_NAME)) {
					//TODO
				}
			}
		}
	}

	private void markupDecmpfs(Program program, BTreeNodeDescriptor descriptor,
			BTreeNodeRecord record, Address address) throws Exception {

		DecmpfsHeader header = record.getDecmpfsHeader();
		DataType headerDataType = header.toDataType();
		Data headerData = createData(address, headerDataType);
		changeEndianSettings(headerData);
		createFragment(header.getCompressionMagic(), headerData.getMinAddress(),
			headerData.getLength());
		StringBuffer buffer = new StringBuffer();
		buffer.append(header.getCompressionMagic());
		buffer.append('\n');
		buffer.append("CompressionType: 0x" + Integer.toHexString(header.getCompressionType()));
		buffer.append('\n');
		buffer.append("UncompressedSize: 0x" + Long.toHexString(header.getUncompressedSize()));
		buffer.append('\n');
		setPlateComment(address, buffer.toString());
	}

	private Data createMapRecord(Program program, BTreeMapRecord mapRecord, int offset)
			throws Exception {
		Address address = toAddr(offset);
		DataType dataType = mapRecord.toDataType();
		Data data = createData(address, dataType);
		createFragment(dataType.getName(), data.getMinAddress(), data.getLength());
		return data;
	}

	private Data createUserDataRecord(Program program, BTreeUserDataRecord userDataRecord,
			int offset) throws Exception {
		Address address = toAddr(offset);
		DataType dataType = userDataRecord.toDataType();
		Data data = createData(address, dataType);
		createFragment(dataType.getName(), data.getMinAddress(), data.getLength());
		return data;
	}

	private Data createBTreeHeaderRecord(Program program, BTreeHeaderRecord headerRecord,
			int offset)
			throws Exception {
		Address address = toAddr(offset);
		DataType dataType = headerRecord.toDataType();
		Data data = createData(address, dataType);
		createFragment(dataType.getName(), data.getMinAddress(), data.getLength());
		return data;
	}

	private Data createBTreeNode(Program program, BTreeNodeDescriptor node, int offset)
			throws Exception {
		Address address = toAddr(offset);
		DataType dataType = node.toDataType();
		Data data = createData(address, dataType);
		createFragment(dataType.getName(), data.getMinAddress(), data.getLength());
		return data;
	}

	private void changeEndianSettings(Data data) throws Exception {
		for (int i = 0; i < data.getNumComponents(); ++i) {
			Data component = data.getComponent(i);
			SettingsDefinition[] settings = component.getDataType().getSettingsDefinitions();
			for (int j = 0; j < settings.length; ++j) {
				if (settings[j] instanceof EndianSettingsDefinition) {
					EndianSettingsDefinition setting = (EndianSettingsDefinition) settings[j];
					setting.setBigEndian(component, false);
				}
			}
		}
	}
}
