All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.adobe.internal.mac.resource.ResourceParser Maven / Gradle / Ivy

There is a newer version: 2024.11.18751.20241128T090041Z-241100
Show newest version
package com.adobe.internal.mac.resource;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;

import com.adobe.internal.io.CountingInputStream;
import com.adobe.internal.io.ExtendedDataInputStream;
import com.adobe.internal.io.RangedInputStream;

/**
 * A general framework for parsing Mac resource files.  The location of the resource (data fork,
 * resource fork, or other) doesn't matter as long the full location of the resource is given.
 * 
 * This resource parser will read the basic structure of the resource (see 
 * http://developer.apple.com/documentation/mac/MoreToolbox/MoreToolbox-99.html for Apple's Resource File Format})
 * and will call back to handlers for each type of resource (e.g. 'FOND', 'sfnt',  'DITL', etc.) that are 
 * provided to the parser.
 * 
 * Because Resource files are non-linear and the I/O interface is linear (a stream) the parsing is done in a
 * two pass process.  In the first pass the parser collects data about all of the individual resources in the 
 * Resource file.  In the second pass the handlers for each resource type are called as the resources
 * occur in the stream.
 *
 */
public class ResourceParser 
{
	private static final boolean DEBUG = false;

	/**
	 * Information about a resource type that is extracted from the resource type list. 
	 *
	 */
	private static class ResourceTypeEntry
	{
		private byte[] type;
		private int numberOfEntries;
		private List /**/ resources;
		private int offset;

		public ResourceTypeEntry(byte[] type, int number, int offset)
		{
			this.type = type;
			this.numberOfEntries = number;
			this.resources = new ArrayList(this.numberOfEntries + 1);
			this.offset = offset;
		}

		public int getNumberOfEntries() 
		{
			return this.numberOfEntries;
		}

		public byte[] getResourceType() 
		{
			return this.type;
		}

		public int getOffset()
		{
			return this.offset;
		}

		public void addResource(ResourceEntry resource) 
		{
			this.resources.add(resource);
		}

		public String toString()
		{
			StringBuilder sb = new StringBuilder("type = ");
				sb.append(new String(this.type)).append("\n").
					append("number of entries = ").append(this.numberOfEntries).append("\n").
					append("offset = ").append(this.offset).append("\n");
			return sb.toString();
		}
	}

	/**
	 * Information about a specific resource.  This information is used internally for parsing and is
	 * also given to the callback resource type handlers.
	 */
	public static class ResourceEntry
	{
		private byte[] type;
		private int id;
		private byte attributes;
		private int dataOffset;
		private int nameOffset;
		private String name;
		private byte[] nameBytes;
		private int script;

		protected ResourceEntry(byte[] type, int id, byte attributes, int dataOffset, int nameOffset) 
		{
			this.type = type;
			this.id = id;
			this.attributes = attributes;
			this.dataOffset = dataOffset;
			this.nameOffset = nameOffset;
		}

		/**
		 * Get the offset to the resource data for this resource.
		 * @return the offset of the data for this resource from the beginning of the resource table
		 */
		public int getDataOffset()
		{
			return this.dataOffset;
		}

		/**
		 * Get the offset to the name data for this resource.
		 * @return the offset to the name of this resource from the beginning of the name table; -1 if no name entry
		 */
		public int getNameOffset()
		{
			return this.nameOffset;
		}

		/**
		 * Put this ResourceEntry into a string format for debugging or logging purposes.
		 * @return the string representation
		 */
		public String toString()
		{
			StringBuilder sb = new StringBuilder("type = ");
			sb.append(new String(this.type)).append("\n").
				append("id = ").append(this.id).append("\n").
				append("attributes = ").append(this.attributes).append("\n").
				append("data offset = ").append(this.dataOffset).append("\n").
				append("name offset = ").append(this.nameOffset).append("\n").
				append("name = ").append(this.name);
			return sb.toString();
		}

		protected void setNameBytes(byte[] nameBytes) 
		{
			this.nameBytes = nameBytes;
			generateName();
		}

		private void generateName()
		{
			this.script = ScriptUtility.scriptCodeFromRsrcID(this.id);
			try {
				this.name = new String(this.nameBytes, ScriptUtility.scriptCodeToCharset(script));
			} catch (UnsupportedEncodingException e) {
			}
		}

		/**
		 * Get the type of this resource.  The type is a 4 byte identifier unique to a specific resource
		 * type.
		 * @return the type of this resource
		 */
		public byte[] getType() 
		{
			return this.type;
		}

		/**
		 * Get the attributes for this resource.
		 * @return the bit flags containing the attributes
		 */
		public byte getAttributes() 
		{
			return this.attributes;
		}

		/**
		 * Get the bytes of the resource name.  These are the raw bytes exactly as encoded in the name table.
		 * @return the raw bytes of the resource name
		 */
		public byte[] getNameBytes()
		{
			return this.nameBytes;
		}

		/**
		 * Get the name of this resource.  This name is interpreted from the name bytes by using heuristics 
		 * to guess the encoding.  If no guess was possible or it was not possible to decode those bytes this
		 * will return null.
		 * @return the resource name
		 */
		public String getName() 
		{
			return this.name;
		}	

		/**
		 * Get the script code used to turn the name bytes for this resource into a name string.
		 * @return the script code used for the name of this resource
		 */
		public int getScriptCode()
		{
			return this.script;
		}
		
		/**
		 * Get the id of this resource.
		 * @return the id of this resource
		 */
		public int getID()
		{
			return this.id;
		}
	}

	/**
	 * The interface that a callback resource handler must support.
	 *
	 */
	public interface ResourceHandler
	{
		/**
		 * The callback that is executed when a resource of the type supported by this handler
		 * is encountered in a resource file.
		 * @param entry the information about this resource
		 * @param length the length of the data stream
		 * @param stream the data stream
		 */
		void handleResource(ResourceEntry entry, long length, InputStream stream);

		/**
		 * Get the resource type supported by this resource handler.
		 * @return the resource type supported by this handler
		 */
		byte[] getResourceType();
	}

	/**
	 * The key for the resource handlers stored in the resource handler map.
	 *
	 */
	private static final class ResouceHandlerKey
	{
		private final byte[] type;

		public ResouceHandlerKey(byte[] type) 
		{
			this.type = type;
		}

		public int hashCode() {
			final int prime = 31;
			int result = 1;
			for (int index = 0; index < this.type.length; index++) 
			{
				result = prime * result + type[index];
			}
			return result;
		}

		public boolean equals(Object obj) {
			if (this == obj)
			{
				return true;
			}
			if (obj == null)
			{
				return false;
			}
			if (!(obj instanceof ResouceHandlerKey))
			{
				return false;
			}
			final ResouceHandlerKey other = (ResouceHandlerKey) obj;
			if (!Arrays.equals(type, other.type))
			{
				return false;
			}
			return true;
		}
	}

	// The url pointing to the Resource file.
	private URL url;

	// The resource data gathered during the first pass of the Resource file.
	private List /**/ resourceTypes;
	private List /**/ resources;
	private List /**/ resourceNames;

	// The handlers for the specific resource types.
	private Map /**/ resourceHandlers;

	/**
	 * Constructor.
	 * 
	 * This parser can be reused to parse multiple Resource files.
	 */
	public ResourceParser()
	{
		this.resourceHandlers = new HashMap();
	}

	/**
	 * Set the URL for the resource file to be parsed.
	 * @param url location of the resource data
	 */
	public void setURL(URL url)
	{
		this.url = url;
	}

	/**
	 * Adds a resource handler to the parser.  If a handler for that resource type has already been added
	 * then it is removed and the one being added replaces it.
	 * @param handler a handler for a specific resource type
	 */
	public void addHandler(ResourceHandler handler)
	{
		this.resourceHandlers.put(new ResouceHandlerKey(handler.getResourceType()), handler);
	}

	private void init()
	{
		// clean out the data from any previous parsing
		this.resourceTypes = new ArrayList();
		this.resources = new LinkedList();
		this.resourceNames = new LinkedList();
	}

	/**
	 * Parse the Resource file.
	 * 
	 * @throws IOException
	 */
	public void parse() 
	throws IOException
	{
		init();

		InputStream is = this.url.openStream();
		CountingInputStream cis = new CountingInputStream(is);
		ExtendedDataInputStream dis = new ExtendedDataInputStream(cis);

		// read the header
		long dataOffset = dis.readUnsignedInt();
		long mapOffset = dis.readUnsignedInt();
		long dataLength = dis.readUnsignedInt();
		long mapLength = dis.readUnsignedInt();

		if (DEBUG)
		{
			System.out.println("=== RSRC Header");
			System.out.println("dataOffset = " + dataOffset);
			System.out.println("mapOffset = " + mapOffset);
			System.out.println("dataLength = " + dataLength);
			System.out.println("mapLength = " + mapLength);
			System.out.println();
		}

		// skip to the map
		long bytesToSkip = mapOffset - 16 /*bytes already read*/ + 22 /*unused bytes at front of map*/;
		dis.skipFully(bytesToSkip);

		// read the map header
		int rsrcForkAttributes = dis.readUnsignedShort();
		int resourceTypeListOffset = dis.readUnsignedShort();
		int resourceNameListOffset = dis.readUnsignedShort();
		int numberOfTypes = dis.readUnsignedShort();

		if (DEBUG)
		{
			System.out.println("=== Map Header");
			System.out.println("rsrcForkAttributes = " + rsrcForkAttributes);
			System.out.println("resourceTypeListOffset = " + resourceTypeListOffset);
			System.out.println("resourceNameListOffset = " + resourceNameListOffset);
			System.out.println("numberOfTypes = " + numberOfTypes);
			System.out.println();
		}

		// read the resource type list
		for (int typeIndex = 0; typeIndex <= numberOfTypes; typeIndex++)
		{
			byte[] type = new byte[4];
			dis.readFully(type);
			int number = dis.readUnsignedShort();
			int offset = dis.readShort();
			ResourceTypeEntry typeEntry = new ResourceTypeEntry(type, number, offset);
			insertIntoList(this.resourceTypes, typeEntry, 
					new OrderInt() { public int getOrderInt(Object obj) 
					{ return ((ResourceTypeEntry) obj).getOffset();	}
			}
			);
		}

		if (DEBUG)
		{
			dumpResourceTypeList();
		}

		// read the reference lists
		for (int typeIndex = 0; typeIndex <= numberOfTypes; typeIndex++)
		{
			ResourceTypeEntry currentType = (ResourceTypeEntry) this.resourceTypes.get(typeIndex);
			for (int resourceEntry = 0; resourceEntry <= currentType.getNumberOfEntries(); resourceEntry++)
			{
				int id = dis.readUnsignedShort();
				int resourceNameOffset = dis.readShort();
				byte attributes = dis.readByte();
				int resourceDataOffset = dis.readUnsigned3ByteInt();
				dis.readInt();	// reserved for handle - throw away
				ResourceEntry resource = new ResourceEntry(
						currentType.getResourceType(), id, attributes, resourceDataOffset, resourceNameOffset);
				currentType.addResource(resource);

				// insert into the resource list in order of offset in the resource table
				insertIntoList(this.resources, resource,
						new OrderInt() { public int getOrderInt(Object obj) 
						{ return ((ResourceEntry) obj).getDataOffset();	}
				}
				);

				// insert into the name list in order of the offset in the name list
				insertIntoList(this.resourceNames, resource,
						new OrderInt() { public int getOrderInt(Object obj) 
						{ return ((ResourceEntry) obj).getNameOffset();	}
				}
				);
			}
		}

		if (DEBUG)
		{
			dumpResourceList();
		}

		// read the name list
		for (int resourceIndex = 0; resourceIndex < this.resourceNames.size(); resourceIndex++)
		{
			ResourceEntry currentResource = (ResourceEntry) this.resourceNames.get(resourceIndex);
			int length = dis.readUnsignedByte();
			byte[] nameBytes = new byte[length];
			dis.readFully(nameBytes);
			currentResource.setNameBytes(nameBytes);
		}

		if (DEBUG)
		{
			dumpResourceList();
		}

		// close the original streams
		dis.close();
		dis = null;
		cis.close();
		cis = null;
		is.close();
		is = null;

		// read the data
		InputStream dataIS = this.url.openStream();
		CountingInputStream dataCIS = new CountingInputStream(dataIS);
		ExtendedDataInputStream dataDIS = new ExtendedDataInputStream(dataCIS);

		if (DEBUG)
		{
			System.out.println("\n#### Processing Resource Data");
			System.out.println("#########################");
		}
		// TODO check to see if the data offset is after the current position and then no need for a second stream
		dataDIS.skipFully(dataOffset);

		if (DEBUG)
		{
			System.out.println("dataOffset = " + dataOffset);
			System.out.println("stream position = " + dataCIS.getOffset());
		}

		for (int resourceIndex = 0; resourceIndex < this.resources.size(); resourceIndex++)
		{
			ResourceEntry currentResource = (ResourceEntry) this.resources.get(resourceIndex);
			long dataToSkip = (currentResource.getDataOffset() + dataOffset) - dataCIS.getOffset();
			dataDIS.skipFully(dataToSkip);

			long length = dataDIS.readUnsignedInt();
			if (DEBUG)
			{
				System.out.println(currentResource);
				System.out.println("dataToSkip = " + dataToSkip);

				System.out.println("length = " + length);
			}
			//long length = dataDIS.readInt();

			// call the resource type plugin	
			RangedInputStream subStream = new RangedInputStream(dataDIS, length);
			ResourceHandler handler = 
				(ResourceHandler) this.resourceHandlers.get(new ResouceHandlerKey(currentResource.getType()));
			if (handler != null)
			{
				handler.handleResource(currentResource, length, subStream);
			}

			// skip to the end of the resource
			//dataDIS.skip(length - subStream.getOffset());
			if (DEBUG)
			{
				System.out.println("============");
			}
		}

	}

	private void dumpResourceList() 
	{
		Iterator iter = this.resources.listIterator();
		System.out.println("Resources");
		System.out.println("===============");
		while (iter.hasNext())
		{
			ResourceEntry entry = (ResourceEntry) iter.next();
			System.out.println(entry);
			System.out.println("---------------");
		}
	}

	private void dumpResourceTypeList() 
	{
		Iterator iter = this.resourceTypes.listIterator();
		System.out.println("Resource Types");
		System.out.println("===============");
		while (iter.hasNext())
		{
			ResourceTypeEntry entry = (ResourceTypeEntry) iter.next();
			System.out.println(entry);
			System.out.println("---------------");
		}
	}

	/**
	 * Used for inserting into a list in an ordering given by the ordering of the integers.
	 *
	 */
	interface OrderInt
	{
		int getOrderInt(Object obj);
	}

	void insertIntoList(List/**/ list, Object entry, OrderInt order)
	{
		if (order.getOrderInt(entry) == -1)
		{
			return;
		}
		if (list.isEmpty())
		{
			list.add(entry);
		} else {
			boolean added = false;
			ListIterator iter = list.listIterator();
			while (iter.hasNext())
			{
				Object current = iter.next();
				if (order.getOrderInt(entry) < order.getOrderInt(current))
				{
					iter.previous();
					iter.add(entry);
					added = true;
					break;
				}
			}
			if (!added)
			{
				list.add(entry);
			}
		}
	}
}