Exploring the ZIP File Format in JavaScript

The JavaScript program below, when run, prompts the user to upload a file in .ZIP format. Then the file is parsed and its internal structure is displayed as text in the JSON format.

Note that this code does not actually decompress the data, though it does display the compressed data for each internal file in hexadecimal format.

To see the code in action, copy it into an .html file and open that file in a web browser that runs JavaScript.


<html>
<body>

<div id="divUI">
	<label>ZIP File to Load:</label><br />
	<input type="file" onchange="inputFile_Changed(this);"></input><br />
	<label>File Contents as JSON:</label><br />
	<textarea id="textareaFileAsJSON" cols="80" rows="20"></textarea>
</div>

<script type="text/javascript">

// ui event handlers

function inputFile_Changed(inputFile)
{
	var file = inputFile.files[0];
	if (file != null)
	{
		var fileReader = new FileReader();
		fileReader.onload = function(event)
		{
			var fileAsBinaryString = event.target.result;
			var fileAsBytes = ByteHelper.binaryStringToBytes(fileAsBinaryString);
			var fileAsZipFile = ZipFile.fromBytes(fileAsBytes);
			var fileAsJSON = fileAsZipFile.toStringJSON();
			var textareaFileAsJSON = document.getElementById("textareaFileAsJSON");
			textareaFileAsJSON.value = fileAsJSON;
		}
		fileReader.readAsBinaryString(file);
	}
}

// classes

function ByteHelper()
{
	// static class
}
{
	ByteHelper.binaryStringToBytes = function(binaryString)
	{
		var bytesSoFar = [];
		for (var i = 0; i < binaryString.length; i++)
		{
			var byte = binaryString.charCodeAt(i);
			bytesSoFar.push(byte);
		}
		return bytesSoFar;
	}

	// "LE" = "Little Endian"

	ByteHelper.bytesToInteger16LE = function(bytes) 
	{
		return (bytes[1] << 8) | bytes[0];
	}

	ByteHelper.bytesToInteger32LE = function(bytes)
	{
		return (bytes[3] << 24) | (bytes[2] << 16) | (bytes[1] << 8) | bytes[0];
	}

	ByteHelper.bytesToStringHexadecimal = function(bytes)
	{
		var returnValue = "";
		for (var i = 0; i < bytes.length; i++)
		{
			var byte = bytes[i];
			var byteAsString = byte.toString(16);
			returnValue += byteAsString;
		}
		return returnValue;
	}

	ByteHelper.bytesToStringUTF8 = function(bytes)
	{
		var returnValue = "";
		for (var i = 0; i < bytes.length; i++)
		{
			var byte = bytes[i];
			var byteAsString = String.fromCharCode(byte);
			returnValue += byteAsString;
		}
		return returnValue;
	}
}

function ByteStream(bytes)
{
	this.bytes = bytes;
	this.byteOffset = 0;

	this.byteBuffer = [];
}
{
	ByteStream.prototype.hasMore = function()
	{
		return this.byteOffset < this.bytes.length;
	}

	ByteStream.prototype.readByte = function()
	{
		var byteRead = this.bytes[this.byteOffset];
		this.byteOffset++;

		return byteRead;
	}

	ByteStream.prototype.readBytes = function(byteCount)
	{
		return this.readBytesIntoBuffer(byteCount).slice();
	}

	ByteStream.prototype.readBytesIntoBuffer = function(byteCount)
	{
		this.byteBuffer.length = 0;
		for (var i = 0; i < byteCount; i++)
		{
			var byteRead = this.readByte();
			this.byteBuffer.push(byteRead);
		}
		return this.byteBuffer;
	}

	// "LE" = "Little Endian"

	ByteStream.prototype.readInteger16LE = function()
	{
		return ByteHelper.bytesToInteger16LE(this.readBytesIntoBuffer(2));
	}

	ByteStream.prototype.readInteger32LE = function()
	{
		return ByteHelper.bytesToInteger32LE(this.readBytesIntoBuffer(4));
	}

	ByteStream.prototype.readStringHexadecimal = function(length)
	{
		return ByteHelper.bytesToStringHexadecimal(this.readBytesIntoBuffer(length));
	}

	ByteStream.prototype.readStringUTF8 = function(length)
	{
		return ByteHelper.bytesToStringUTF8(this.readBytesIntoBuffer(length));
	}
}

function ZipFile(entriesLocal, entriesCentralDirectory, centralDirectoryEndRecord)
{
	this.entriesLocal = entriesLocal;
	this.entriesCentralDirectory = entriesCentralDirectory;
	this.centralDirectoryEndRecord = centralDirectoryEndRecord;
}
{
	ZipFile.fromBytes = function(bytes)
	{
		var entriesLocal = [];
		var entriesCentralDirectory = [];
		var centralDirectoryEndRecord = null;
	
		var reader = new ByteStream(bytes);

		while (reader.hasMore() == true)
		{
			var signature = reader.readInteger32LE();
			if (signature == ZipFileLocalFileHeader.Signature)
			{
				// It's a local file header.
				var versionNeededToExtract = reader.readInteger16LE();
				var flags = reader.readInteger16LE();
				var compressionMethod = reader.readStringUTF8(2);

				var timeLastModified = ZipFile.fromBytes_ReadTimeFromByteStream(reader);

				var crc32 = reader.readInteger32LE();
				var sizeCompressedInBytes = reader.readInteger32LE();
				var sizeUncompressedInBytes = reader.readInteger32LE();
				var filenameLength = reader.readInteger16LE();
				var extraFieldLength = reader.readInteger16LE();
				var filename = reader.readStringUTF8(filenameLength);
				var extraFieldAsHexadecimal = reader.readStringHexadecimal(extraFieldLength);

				var entryHeader = new ZipFileLocalFileHeader
				(
					signature,
					versionNeededToExtract,
					flags,
					compressionMethod,
					timeLastModified,
					crc32,
					sizeCompressedInBytes,
					sizeUncompressedInBytes,
					filename,
					extraFieldAsHexadecimal
				);

				var entryDataCompressedAsStringHexadecimal = 
					reader.readStringHexadecimal(sizeCompressedInBytes);

				var entry = new ZipFileLocalFileEntry(entryHeader, entryDataCompressedAsStringHexadecimal);

				entriesLocal.push(entry);
			}
			else if (signature == ZipFileCentralDirectoryEntry.Signature)
			{
				// It's a central directory file header.
				var versionMadeBy = reader.readInteger16LE();				
				var versionNeededToExtract = reader.readInteger16LE();
				var flags = reader.readInteger16LE();
				var compressionMethod = reader.readStringUTF8(2);
				var timeLastModified = ZipFile.fromBytes_ReadTimeFromByteStream(reader);
				var crc32 = reader.readInteger32LE();
				var sizeCompressedInBytes = reader.readInteger32LE();
				var sizeUncompressedInBytes = reader.readInteger32LE();
				var filenameLength = reader.readInteger16LE();
				var extraFieldLength = reader.readInteger16LE();
				var fileCommentLength = reader.readInteger16LE();
				var diskNumber = reader.readInteger16LE();
				var fileAttributesInternal = reader.readInteger16LE();
				var fileAttributesExternal = reader.readInteger32LE();
				var offsetOfLocalHeader = reader.readInteger32LE();
				var filename = reader.readStringUTF8(filenameLength);
				var extraFieldAsHexadecimal = reader.readStringHexadecimal(extraFieldLength);
				var fileComment = reader.readStringUTF8(fileCommentLength);

				var entry = new ZipFileCentralDirectoryEntry
				(
					signature,
					versionMadeBy,
					versionNeededToExtract,
					flags,
					compressionMethod,
					timeLastModified,
					crc32,
					sizeCompressedInBytes,
					sizeUncompressedInBytes,
					filename,
					extraFieldAsHexadecimal,
					fileComment,
					diskNumber,
					fileAttributesInternal,
					fileAttributesExternal,
					offsetOfLocalHeader
				);

				entriesCentralDirectory.push(entry);
			}
			else if (signature == ZipFileCentralDirectoryEndRecord.Signature)
			{
				// It's an end of central directory record.
				var diskNumber = reader.readInteger16LE();
				var diskStart = reader.readInteger16LE();
				var recordsOnDisk = reader.readInteger16LE();
				var recordsTotal = reader.readInteger16LE();
				var sizeInBytes = reader.readInteger32LE();
				var offset = reader.readInteger32LE();
				var commentLength = reader.readInteger16LE();
				var comment = reader.readStringUTF8(commentLength);

				centralDirectoryEndRecord = new ZipFileCentralDirectoryEndRecord
				(
					diskNumber,
					diskStart,
					recordsOnDisk,
					recordsTotal,
					sizeInBytes,
					offset,
					comment
				);
			}
			else
			{
				throw "Unexpected format."
			}
		}

		var returnValue = new ZipFile(entriesLocal, entriesCentralDirectory, centralDirectoryEndRecord);
		return returnValue;
	}

	ZipFile.fromBytes_ReadTimeFromByteStream = function(reader)
	{
		// Based on the timestamp format for the FAT filesystem,
		// made popular by Microsoft's MS-DOS.

		var hours5Bits_Minutes6Bits_DualSeconds5Bits = reader.readInteger16LE();
		var hours = (hours5Bits_Minutes6Bits_DualSeconds5Bits >> 11);
		var minutes = (hours5Bits_Minutes6Bits_DualSeconds5Bits >> 5) & 0x3F;
		var secondsOver2 = hours5Bits_Minutes6Bits_DualSeconds5Bits & 0x1F;
		var seconds = secondsOver2 * 2;

		var years7Bits_Month4Bits_Days5Bits = reader.readInteger16LE();
		var fatFilesystemEpochYear = 1980;
		var year = (years7Bits_Month4Bits_Days5Bits >> 9) + fatFilesystemEpochYear;
		var month = ((years7Bits_Month4Bits_Days5Bits >> 5) & 0xF) - 1;
		var day = (years7Bits_Month4Bits_Days5Bits) & 0x1F;

		var returnValue = new Date(year, month, day, hours, minutes, seconds, 0);
		return returnValue;
	}

	// instance methods

	ZipFile.prototype.toStringJSON = function()
	{
		var indentSizeInSpaces = 2;
		var returnValue = JSON.stringify(this, null, indentSizeInSpaces);
		return returnValue;
	}
}

function ZipFileCentralDirectoryEntry
(
	signature,
	versionMadeBy,
	versionNeededToExtract,
	flags,
	compressionMethod,
	timeLastModified,
	crc32,
	sizeCompressedInBytes,
	sizeUncompressedInBytes,
	filename,
	extraFieldAsHexadecimal,
	fileComment,
	diskNumber,
	fileAttributesInternal,
	fileAttributesExternal,
	offsetOfLocalHeader
)
{
	// A "central directory" entry
	// is an expanded version of the "local" file entry header.

	this.signature = signature;
	this.versionMadeBy = versionMadeBy;
	this.versionNeededToExtract = versionNeededToExtract;
	this.flags = flags;
	this.compressionMethod = compressionMethod;
	this.timeLastModified = timeLastModified;
	this.crc32 = crc32; // "crc32" = "32-bit cyclical redundancy check" - Validates file not corrupt.
	this.sizeCompressedInBytes = sizeCompressedInBytes;
	this.sizeUncompressedInBytes = sizeUncompressedInBytes;
	this.filename = filename;
	this.extraFieldAsHexadecimal = extraFieldAsHexadecimal;
	this.fileComment = fileComment;
	this.diskNumber = diskNumber;
	this.fileAttributesInternal = fileAttributesInternal;
	this.fileAttributesExternal = fileAttributesExternal;
	this.offsetOfLocalHeader = offsetOfLocalHeader;	
}
{
	ZipFileCentralDirectoryEntry.Signature = 33639248;
}

function ZipFileCentralDirectoryEndRecord
(
	diskNumber,
	diskStart,
	recordsOnDisk,
	recordsTotal,
	sizeInBytes,
	offset,
	comment
)
{
	this.diskNumber = diskNumber;
	this.diskStart = diskStart;
	this.recordsOnDisk = recordsOnDisk;
	this.recordsTotal = recordsTotal;
	this.sizeInBytes = sizeInBytes;
	this.offset = offset;
	this.comment = comment;
}
{
	ZipFileCentralDirectoryEndRecord.Signature = 101010256;
}

function ZipFileLocalFileEntry(header, dataCompressedAsStringHexadecimal)
{
	this.header = header;
	this.dataCompressedAsStringHexadecimal = dataCompressedAsStringHexadecimal;
}

function ZipFileLocalFileHeader
(
	signature,
	versionNeededToExtract,
	flags,
	compressionMethod,
	timeLastModified,
	crc32,
	sizeCompressedInBytes,
	sizeUncompressedInBytes,
	filename,
	extraFieldAsHexadecimal
)
{
	// This is a "central directory" file header,
	// which is an expanded version of the "local" file header.

	this.signature = signature;
	this.versionNeededToExtract = versionNeededToExtract;
	this.flags = flags;
	this.compressionMethod = compressionMethod;
	this.timeLastModified = timeLastModified;
	this.crc32 = crc32;
	this.sizeCompressedInBytes = sizeCompressedInBytes;
	this.sizeUncompressedInBytes = sizeUncompressedInBytes;
	this.filename = filename;
	this.extraFieldAsHexadecimal = extraFieldAsHexadecimal;
}
{
	ZipFileLocalFileHeader.Signature = 67324752;
}

</script>

</body>
</html>

Advertisements
Posted in Uncategorized | Tagged , , , , | 1 Comment

Another RPG Combat Engine in JavaScript

The code below implements a rudimentary turn-based combat engine for a Japanese-style role-playing game in JavaScript.

I have partially implemented a similar engine in a previous post, but I recently tried to update it and found it very difficult. I suppose I concentrated too much on how it looked and less on how it worked. So this time, I ignored the graphics completely to start with, and just used a simple all-text interface. I hope this will let me focus more on the architecture, which will therefore be cleaner and easier to expand and maintain.


<html>
<body>

<label><b>RPG Combat Engine</b></label>
<div id="divMain"></div>

<script type="text/javascript">

// main

function main()
{
	var actionDefns = ActionDefn.Instances()._All;
	
	var actionDefnNames = actionDefns.select("name");

	var party0 = new Party
	(
		"Party0",
		true, // isControlledByHuman
		// agents
		[
			new Agent("Amy", 10, actionDefnNames),
			new Agent("Bob", 10, actionDefnNames),
			new Agent("Cal", 10, actionDefnNames),
			new Agent("Deb", 10, actionDefnNames),
		]
	);
	
	var party1 = new Party
	(
		"Party1",
		false, // isControlledByHuman
		// agents
		[
			new Agent("William", 10, actionDefnNames),
			new Agent("Xavier", 10, actionDefnNames),
			new Agent("Yonni", 10, actionDefnNames),
			new Agent("Zane", 10, actionDefnNames),
		]
	);
	
	var encounter = new Encounter([party0, party1]);
	var encounterAsControl = encounter.toControl();
	var encounterAsDomElement = encounterAsControl.toDomElement();
	var divMain = document.getElementById("divMain");
	divMain.appendChild(encounterAsDomElement);
}

// extensions

function ArrayExtensions()
{
	// extension class
}
{
	Array.prototype.addLookups = function(keyName)
	{
		for (var i = 0; i < this.length; i++)
		{
			var element = this[i];
			var key = element[keyName];
			this[key] = element;
		}
		return this;
	};
	
	Array.prototype.get = function()
	{
		return this;
	};
	
	Array.prototype.insertElementAt = function(element, index)
	{
		this.splice(index, 0, element);
		return this;
	}
		
	Array.prototype.remove = function(elementToRemove)
	{
		var indexToRemoveAt = this.indexOf(elementToRemove);
		if (indexToRemoveAt != null)
		{
			this.removeAt(indexToRemoveAt);
		}
		return this;
	}
	
	Array.prototype.removeAt = function(indexToRemoveAt)
	{
		this.splice(indexToRemoveAt, 1);
		return this;
	}
	
	Array.prototype.select = function(fieldName)
	{
		var returnValues = [];
		for (var i = 0; i < this.length; i++)
		{
			var element = this[i];
			var fieldValue = this[fieldName];
			returnValues.push(fieldValue);
		}
		return returnValues;
	};
}

function BooleanExtensions()
{
	// extension class
}
{
	Boolean.prototype.get = function()
	{
		return (this == true); // To unwrap it.
	};
}

function StringExtensions()
{
	// extension class
}
{
	String.prototype.get = function()
	{
		return this;
	};
}

// classes

function Action(agentNameActor, defnName, targetName)
{
	this.agentNameActor = agentNameActor;
	this.defnName = defnName;
	this.targetName = targetName;
}
{
	Action.prototype.defn = function()
	{
		return ActionDefn.Instances()._All[this.defnName];
	}

	Action.prototype.isValid = function()
	{
		var returnValue = 
		(
			this.agentNameActor != null
			&& this.defnName != null
			&& this.targetName != null
		);
		return returnValue;
	};
	
	Action.prototype.performForEncounter = function(encounter)
	{
		this.effects = this.defn().performForEncounterAndAction(encounter, this);
	}

	Action.prototype.toString = function()
	{
		var returnValue =
			this.agentNameActor
			+ " used " + this.defnName
			+ " on " + this.targetName + ".  "
			+ (this.effects == null ? "" : this.effects.join("  "));
		return returnValue;
	};
}

function ActionDefn(name, targetTypeName, performForEncounterAndAction)
{
	this.name = name;
	this.targetTypeName = targetTypeName;
	this.performForEncounterAndAction = performForEncounterAndAction;
}
{
	ActionDefn.Instances = function()
	{
		if (ActionDefn._Instances == null)
		{
			ActionDefn._Instances = new ActionDefn_Instances();
		}
		
		return ActionDefn._Instances;
	};
	
	function ActionDefn_Instances()
	{
		var targetTypes = ActionTargetType.Instances()._All;
		var performTodo = function todo() { };
		
		this.Attack = new ActionDefn
		(
			"Attack", 
			targetTypes["Enemy"].name,
			function perform(encounter, action)
			{
				var targetName = action.targetName;			
				var partyTargeted = encounter.partyOther();
				var agentTargeted = partyTargeted.agents[targetName];
				var damage = 3;
				var effectDamage = new Effect("lost", damage, targetName);
				var effects = [ effectDamage ];
				agentTargeted.integrity -= damage;
				if (agentTargeted.integrity <= 0)
				{
					var effectDie = new Effect("died", null, targetName);
					effects.push(effectDie);
					partyTargeted.agents.remove(agentTargeted);
				}
				partyTargeted._control.invalidate();				
				return effects;
			}
		);
		this.Cower = new ActionDefn
		(
			"Cower", targetTypes["Self"].name, performTodo
		);
		this.Flee = new ActionDefn
		(
			"Flee", targetTypes["Self"].name, performTodo
		);
		this.Heal = new ActionDefn
		(
			"Heal", targetTypes["Ally"].name, performTodo
		);		
		this.Protect = new ActionDefn
		(
			"Protect", targetTypes["Ally"].name, performTodo
		);
		this.Wait = new ActionDefn
		(
			"Wait", targetTypes["None"].name, performTodo
		);
		
		this._All = 
		[ 
			this.Attack,
			//this.Cower,
			//this.Flee,
			//this.Heal,
			//this.Protect,
			//this.Wait
		].addLookups("name");
	}
	
	ActionDefn.prototype.targetType = function()
	{
		return ActionTargetType.Instances()._All[this.targetTypeName];
	}
}

function ActionTargetType(name, targetsGetForEncounter)
{
	this.name = name;
	this.targetsGetForEncounter = targetsGetForEncounter;
}
{
	ActionTargetType.Instances = function()
	{
		if (ActionTargetType._Instances == null)
		{
			ActionTargetType._Instances = new ActionTargetType_Instances();
		}
		
		return ActionTargetType._Instances;
	};
	
	function ActionTargetType_Instances()
	{
		this.Ally = new ActionTargetType
		(
			"Ally",
			function targetsGetForEncounter(encounter)
			{
				return encounter.partyCurrent().agents;
			}
		);
		this.Enemy = new ActionTargetType
		(
			"Enemy",
			function targetsGetForEncounter(encounter)
			{
				return encounter.partyOther().agents;
			}
		);
		this.None = new ActionTargetType
		(
			"None",
			function targetsGetForEncounter(encounter)
			{
				return [];
			}
		);
		this.Other = new ActionTargetType
		(
			"Other",
			function targetsGetForEncounter(encounter)
			{
				return [];
			}			
		);
		this.Party = new ActionTargetType
		(
			"Party",
			function targetsGetForEncounter(encounter)
			{
				return [];
			}			
		);
		this.PartyAllies = new ActionTargetType
		(
			"AlliesAll",
			function targetsGetForEncounter(encounter)
			{
				return [];
			}			
		);
		this.PartyEnemies = new ActionTargetType
		(
			"EnemiesAll",
			function targetsGetForEncounter(encounter)
			{
				return [];
			}			
		);		
		this.Self = new ActionTargetType
		(
			"Self",
			function targetsGetForEncounter(encounter)
			{
				return [];
			}			
		);
		
		this._All = 
		[
			this.Ally,
			this.Enemy,
			this.None,
			this.Other,
			this.Party,
			this.PartyAllies,
			this.PartyEnemies,
			this.Self
		].addLookups("name");
	}
}

function Agent(name, integrityMax, actionDefnNames)
{
	this.name = name;
	this.integrityMax = integrityMax;
	this.actionDefnNames = actionDefnNames;
	
	this.integrity = integrityMax;
	this.hasMovedThisTurn = false;
}
{
	Agent.prototype.toString = function()
	{
		return this.name + " (" + this.integrity + "/" + this.integrityMax + ")";
	}
}

function ControlBinding(context, get, set)
{
	this.context = context;
	this.get = (get == null ? this.getDefault : get).bind(this);
	this.set = (set == null ? this.setDefault : set).bind(this);
}
{
	ControlBinding.prototype.getDefault = function(binding)
	{
		return binding.context;
	};
	
	ControlBinding.prototype.setDefault = function(binding, value)
	{
		// Do nothing.
	};
}

function ControlBreak()
{}
{
	ControlBreak.prototype.toDomElement = function()
	{
		if (this._domElement == null)
		{
			this._domElement = document.createElement("br");
		}
		
		return this._domElement;
	};
}

function ControlButton(text, click, isEnabled)
{
	this.text = text;
	this.click = click;
	this.isEnabled = (isEnabled == null ? true : isEnabled);
}
{
	ControlButton.prototype.toDomElement = function()
	{
		if (this._domElement == null)
		{
			this._domElement = document.createElement("button");
			this._domElement.innerHTML = this.text;
			this._domElement.onclick = this.click;
		}
		
		var isEnabled = this.isEnabled.get();
		this._domElement.disabled = (isEnabled == false);
		
		return this._domElement;
	};
}

function ControlContainer(children)
{
	this.children = children;
	
	for (var i = 0; i < this.children.length; i++)
	{
		var child = this.children[i];
		child.parent = this;
	};
}
{
	ControlContainer.prototype.invalidate = function()
	{
		if (this.parent == null)
		{
			this.toDomElement();
		}
		else
		{
			this.parent.invalidate();
		}
	};

	ControlContainer.prototype.toDomElement = function()
	{
		if (this._domElement == null)
		{
			this._domElement = document.createElement("div");
			this._domElement.style.border = "1px solid";
			
			for (var i = 0; i < this.children.length; i++)
			{
				var child = this.children[i];
				var childAsDomElement = child.toDomElement();
				this._domElement.appendChild(childAsDomElement);
			}
		}
		
		for (var i = 0; i < this.children.length; i++)
		{
			var child = this.children[i];
			child.toDomElement();
		}
		
		return this._domElement;
	};
}

function ControlLabel(text)
{
	this.text = text;
}
{
	ControlLabel.prototype.toDomElement = function()
	{
		if (this._domElement == null)
		{
			this._domElement = document.createElement("label");
		}
		
		this._domElement.innerHTML = this.text.get();
		
		return this._domElement;
	};
}

function ControlList
(
	numberOfItemsVisible,
	bindingForItemText,
	bindingForItemValue,	
	bindingForSelectedValue,
	items
)
{
	this.numberOfItemsVisible = numberOfItemsVisible;
	this.bindingForItemText = bindingForItemText;
	this.bindingForItemValue = bindingForItemValue;
	this.bindingForSelectedValue = bindingForSelectedValue;
	this.items = items;
}
{
	ControlList.prototype.invalidate = function()
	{
		if (this.parent != null)
		{
			this.parent.invalidate();
		}
	};

	ControlList.prototype.toDomElement = function()
	{
		var control = this;
		
		if (this._domElement == null)
		{
			this._domElement = document.createElement("select");
			this._domElement.size = this.numberOfItemsVisible;
			this._domElement.onchange = function (event)
			{
				var selectedValue = event.target.value;
				control.bindingForSelectedValue.set(selectedValue);
				control.invalidate();
			};
		}
	
		this._domElement.innerHTML = "";
		var items = this.items.get();
		for (var i = 0; i < items.length; i++)
		{
			var item = items[i];
			var itemAsOption = document.createElement("option");
			this.bindingForItemText.context = item;
			var text = this.bindingForItemText.get();
			this.bindingForItemValue.context = item;
			var value = this.bindingForItemValue.get();
			itemAsOption.innerHTML = text;
			this._domElement.appendChild(itemAsOption);
		}
		
		this._domElement.value = this.bindingForSelectedValue.get();		
		
		return this._domElement;
	};
}

function ControlSpan(children, isVisible)
{
	this.children = children;
	this.isVisible = (isVisible == null ? true : isVisible);
	
	for (var i = 0; i < this.children.length; i++)
	{
		var child = this.children[i];
		child.parent = this;
	}
}
{
	ControlSpan.prototype.invalidate = function()
	{
		if (this.parent == null)
		{
			this.toDomElement();
		}
		else
		{
			this.parent.invalidate();
		}
	};

	ControlSpan.prototype.toDomElement = function()
	{
		if (this._domElement == null)
		{
			this._domElement = document.createElement("span");
			
			for (var i = 0; i < this.children.length; i++)
			{
				var child = this.children[i];
				var childAsDomElement = child.toDomElement();
				this._domElement.appendChild(childAsDomElement);
			}
		}
		
		for (var i = 0; i < this.children.length; i++)
		{
			var child = this.children[i];
			child.toDomElement();
		}
		
		var isVisible = this.isVisible.get();
		this._domElement.style.display = (isVisible ? "inline" : "none");
				
		return this._domElement;
	};
}

function Effect(defnName, magnitude, targetName)
{
	this.defnName = defnName;
	this.magnitude = magnitude;
	this.targetName = targetName;
}
{
	Effect.prototype.toString = function()
	{
		var returnValue = this.targetName + " " + this.defnName;
		if (this.magnitude != null)
		{
			returnValue += " " + this.magnitude;
		}
		returnValue += ".";
		return returnValue;
	};
}

function Encounter(parties)
{
	this.parties = parties;
	
	this.partyIndexCurrent = 0;
	this.agentIndexCurrent = 0;
	this.actionsSoFar = [];
	this.actionCurrent = new Action(this.agentCurrent().name);
}
{
	Encounter.prototype.actionDefnSelected = function()
	{
		var returnValue = ActionDefn.Instances()._All[this.actionCurrent.defnName];
		return returnValue;
	};
	
	Encounter.prototype.actionCurrentPerform = function()
	{
		var action = this.actionCurrent;
		if (action.isValid())
		{
			action.performForEncounter(this);
			
			this.actionsSoFar.insertElementAt(action, 0);
			
			this.agentCurrentAdvance();			
		}
	};
	
	Encounter.prototype.agentCurrent = function()
	{
		var returnValue = this.partyCurrent().agents[this.agentIndexCurrent];
		return returnValue;
	};
	
	Encounter.prototype.agentCurrentAdvance = function()
	{
		var partyCurrent = this.partyCurrent();
		var agents = partyCurrent.agents;
		this.agentIndexCurrent++;
		if (this.agentIndexCurrent >= agents.length)
		{
			this.partyIndexCurrent = 1 - this.partyIndexCurrent; 
			this.agentIndexCurrent = 0;
		}
		this.actionCurrent = new Action(this.agentCurrent().name);
		this._control.invalidate();
	};
			
	Encounter.prototype.agentsAll = function()
	{
		if (this._agentsAll == null)
		{
			this._agentsAll = this.parties[0].agents.concat(this.parties[1].agents);
		}
		
		return this._agentsAll;
	};
		
	Encounter.prototype.partyCurrent = function()
	{
		return this.parties[this.partyIndexCurrent];
	};
	
	Encounter.prototype.partyOther = function()
	{
		return this.parties[1 - this.partyIndexCurrent];
	};
		
	// controls

	Encounter.prototype.toControl = function()
	{
		if (this._control == null)
		{
			var controlsForParties = [];
			for (var i = 0; i < this.parties.length; i++)
			{
				var party = this.parties[i];
				var partyAsControl = party.toControl();
				controlsForParties.push(partyAsControl);
			}
			
			var containerActions = new ControlContainer
			([
				new ControlLabel("Next Action:"),
				new ControlBreak(),
				
				new ControlLabel
				(
					new ControlBinding
					(
						this, // context
						function get() { return this.context.agentCurrent().name; }
					)
				),
				new ControlLabel(" uses "),
				new ControlList
				(
					1, // numberOfItemsVisible
					// bindingGetForItemText
					new ControlBinding
					(
						null, function get() { return this.context.name; }
					),
					// bindingGetForItemValue
					new ControlBinding
					(
						null, function get() { return this.context.name; }
					),
					// bindingForSelectedValue
					new ControlBinding
					(
						this, // context 
						function get() { return this.context.actionCurrent.defnName; }, 
						function set(value) { return this.context.actionCurrent.defnName = value; } 						
					),				
					ActionDefn.Instances()._All
				),
				new ControlSpan
				( 
					[ 
						new ControlLabel(" on "),
						new ControlList
						(
							1, // numberOfItemsVisible,
							// bindingForItemText
							new ControlBinding
							(
								null, function get() { return this.context.name; }
							),
							// bindingForItemValue
							new ControlBinding
							(
								null, function get() { return this.context.name; }
							),
							// bindingForSelectedValue
							new ControlBinding
							(
								this, // context 
								function get() { return this.context.actionCurrent.targetName; }, 
								function set(value) { this.context.actionCurrent.targetName = value; }
							),
							// items
							new ControlBinding
							(
								this, // context
								function get()
								{
									var returnValues;
									var actionDefn = this.context.actionCurrent.defn();
									if (actionDefn == null)
									{
										returnValues = [];
									}
									else
									{
										var actionTargetType = actionDefn.targetType();
										returnValues = actionTargetType.targetsGetForEncounter(this.context);
									}
									return returnValues;
								}
							)
						),
					],
					// isVisible
					new ControlBinding
					(
						this, // context
						function get() 
						{ 
							var actionDefn = this.context.actionCurrent.defn();
							if (actionDefn == null)
							{
								return false;
							}
							var actionTargetType = actionDefn.targetType();
							var targets = actionTargetType.targetsGetForEncounter(this.context);
							var returnValue = (targets.length > 0);
							return returnValue;
						}
					) 
				),
				new ControlLabel(" "),
				
				new ControlButton
				(
					"Go",
					this.actionCurrentPerform.bind(this),
					// isEnabled
					new ControlBinding
					(
						this, // context
						function get() { return this.context.actionCurrent.isValid(); } 
					)
				),
				new ControlBreak(),
				
				new ControlLabel("Actions So Far:"),
				new ControlBreak(),
				new ControlList
				(
					3, // numberOfItemsVisible				
					// bindingForItemText
					new ControlBinding
					(
						null, function get() { return this.context.toString(); }
					),
					// bindingForItemValue
					new ControlBinding
					(
						null, // context 
						function get(binding) { return "todo"; }, 
					),									
					// bindingForSelectedValue
					new ControlBinding
					(
						null, // context 
						function get(binding) { return "todo"; }, 
					),				
					this.actionsSoFar
				)
			]);
		
			this._control = new ControlContainer
			(
				[
					controlsForParties[0],
					controlsForParties[1],
					containerActions
				]
			);
		}
		
		for (var i = 0; i < this.parties.length; i++)
		{
			var party = this.parties[i];
			party.toControl().toDomElement();
		}
	
		return this._control;
	};
}

function Party(name, isControlledByHuman, agents)
{
	this.name = name;
	this.isControlledByHuman = isControlledByHuman;
	this.agents = agents.addLookups("name");
}
{
	// controls
	
	Party.prototype.toControl = function()
	{
		if (this._control == null)
		{
			this._control = new ControlContainer
			([
				new ControlLabel(this.name),
				new ControlBreak(),
				new ControlList
				(
					this.agents.length, // numberOfItemsVisible
					// bindingForItemText
					new ControlBinding
					(
						null, // context 
						function get() { return this.context.toString(); }, 
					),
					// bindingForItemValue
					new ControlBinding
					(
						null, // context 
						function get() { return this.context.name; }, 
					),
					// bindingForSelectedValue
					new ControlBinding
					(
						this, // context 
						function get() { return this.context.agentNameSelected; }, 
						function set(value) { return this.context.agentNameSelected = value; }, 						
					),				
					this.agents // items
				),
				
				new ControlBreak(),
				new ControlLabel("Details:"),
				new ControlLabel
				(
					new ControlBinding
					(
						this, // context 
						function get() 
						{ 
							var returnValue = this.context.agentNameSelected;
							returnValue = (returnValue == null ? "[none]" : returnValue);
							return returnValue;
						}, 
						function set(value) { return this.context.agentNameSelected = value; }, 						
					)
				)
			]);
		}
		
		return this._control;
	};
}

// run
main();

</script>
</body>
</html>

Posted in Uncategorized | Tagged , , , | Leave a comment

A Text-Based SVG Editor in JavaScript

The JavaScript program below, when run, will present an interface that allows a user to edit a vector image in SVG format as text, and view the changes to the rendered image as they are made. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

This doesn’t really provide any real advantage over opening an SVG as a text editor and a web browser, but it does streamline the process somewhat. It also provides a convenient way to present a demonstration SVG that incorporates a lot of the format’s features in one place.

TextSVGEditor.png


<html>
<body>

<div id="divUI">
	<label><b>Text SVG Editor</b></label><br />
	<label>SVG as Text:</label>
	<button onclick="buttonNew_Clicked();">New</button>
	<button onclick="buttonDemo_Clicked();">Demo</button>	
	<br />
	<textarea id="textareaSVGAsText" cols="80" rows="20" onchange="textareaSVGAsText_Changed();">
	</textarea><br />
	<label>Rendered Image:</label><br />
	<div id="divImage"></div>
</div>

<script type="text/javascript">

function buttonDemo_Clicked()
{
	var textareaSVGAsText = document.getElementById("textareaSVGAsText");
	textareaSVGAsText.value = svgDemoAsText();
	textareaSVGAsText_Changed();
}

function svgDemoAsText()
{
	var svgAsTextLines =
	[
		"<?xml version='1.0' encoding='UTF-8' standalone='no'?>",
		"<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'>",
		"<g>",
		"",
		"<line x1='5' y1='90' x2='90' y2='5' stroke='black' stroke-width='4' stroke-linecap='butt'/>",
		"<line x1='10' y1='95' x2='95' y2='10' stroke='black' stroke-width='4' stroke-linecap='square' />",
		"",
		"<radialGradient id='radialGradient0' cx='50%' cy='50%' r='50%' fx='50%' fy='50%'>",
		"	<stop offset='0%' style='stop-color:blue;stop-opacity:1' />",
		"	<stop offset='90%' style='stop-color:blue;stop-opacity:1' />",
		"	<stop offset='100%' style='stop-color:white;stop-opacity:0' />",
		"</radialGradient>",
		"",
		"<rect x='10' y='10' width='80' height='80' fill='url(#radialGradient0)' />",
		"",
		"<linearGradient id='linearGradient0' x1='0%' y1='0%' x2='100%' y2='0%'>",
		"	<stop offset='0%' style='stop-color:red;stop-opacity:1' />",
		"	<stop offset='20%' style='stop-color:orange;stop-opacity:1' />",
		"	<stop offset='40%' style='stop-color:yellow;stop-opacity:1' />",
		"	<stop offset='60%' style='stop-color:green;stop-opacity:1' />",
		"	<stop offset='80%' style='stop-color:blue;stop-opacity:1' />",
		"	<stop offset='100%' style='stop-color:violet;stop-opacity:1' />",
		"</linearGradient>",
		"",
		"<ellipse cx='50' cy='50' rx='50' ry='30' fill='url(#linearGradient0)'/>",
		"",
		"<path fill='blue' stroke='black' d='M 0,0 20,0 20,20 0,20 z '/>",
		"",
		"<circle cx='90' cy='90' r='12' fill='red' stroke='black' />",
		"",
		"<polygon points='10,90 30,90 20,60' fill='green' stroke='none' />",
		"",
		"<path",
		"	style='stroke:#000000;fill:#00ff0080'",
		"	d='M 25,15 25,5 Q 90,10 95,65 M 95,65 100,65 90,75 80,65 85,65 Q 80,20 25,15'",
		"/>",
		"",
		"<text x='0' y='40' fill='black' transform='rotate(30 20,40)'>Texty text text.</text>",
		"",
		"<polyline points='5,5 50,20 50,80 95,95' fill='none' stroke='rgba(0,0,0,.5)' stroke-width='5' stroke-linecap='round' />",
		"",
		"</g>",
		"</svg>"	
	];
	var svgAsText = svgAsTextLines.join("\n");
	return svgAsText;
}

function buttonNew_Clicked()
{
	var textareaSVGAsText = document.getElementById("textareaSVGAsText");
	textareaSVGAsText.value = svgNewAsText();
	textareaSVGAsText_Changed();
}

function svgNewAsText()
{
	var svgAsTextLines =
	[
		"<?xml version='1.0' encoding='UTF-8' standalone='no'?>",
		"<svg xmlns='http://www.w3.org/2000/svg'>",
		"<g>",
		"</g>",
		"</svg>"
	];
	var svgAsText = svgAsTextLines.join("\n");
	return svgAsText;
}

function textareaSVGAsText_Changed()
{
	var textareaSVGAsText = document.getElementById("textareaSVGAsText");
	var svgAsText = textareaSVGAsText.value;
	var svgAsTextEncoded = encodeURIComponent(svgAsText);
	var svgAsDataURL = "data:image/svg+xml," + svgAsTextEncoded;
	var svgAsImgElement = document.createElement("img");
	svgAsImgElement.src = svgAsDataURL;
	var divImage = document.getElementById("divImage");
	divImage.innerHTML = "";
	divImage.appendChild(svgAsImgElement);
}

</script>

</body>
</html>

Posted in Uncategorized | Tagged , , , , , | Leave a comment

An HTTP Echo Server in C# Using HttpListener

The C# code below implements a rudimentary HTTP server that simply echoes whatever is sent to it back to the client.

HttpEchoServer.png


using System;
using System.Net;
using System.IO;
using System.Text;

namespace HttpEchoServer
{
	public class HttpEchoServer
	{
		public static void Main()
		{
			string addressToListenOn = "http://localhost:1234/";
			Console.WriteLine("Starting echo server on " + addressToListenOn + "...");
			HttpListener listener = new HttpListener();
			listener.Prefixes.Add(addressToListenOn);
			listener.Start();
			while (true)
			{
				HttpListenerContext context = listener.GetContext();
				HttpListenerRequest request = context.Request;
				string stringToEcho = new StreamReader(request.InputStream).ReadToEnd();
				Console.WriteLine("Received: " + stringToEcho);
				HttpListenerResponse response = context.Response;
				byte[] responseBuffer = Encoding.UTF8.GetBytes(stringToEcho);
				response.ContentLength64 = responseBuffer.Length;
				Stream output = response.OutputStream;
				output.Write(responseBuffer, 0, responseBuffer.Length);
				output.Close();
				Console.WriteLine("Sent: " + stringToEcho);
			}
		}
	}
}

Posted in Uncategorized | Tagged , , , , , | Leave a comment

A Function Argument Type Validator in JavaScript

The JavaScript code below implements simple type checking on function arguments. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript, with the debugging console open so the log messages can be seen.

It still needs a little work. Notably, I need to make sure that the “this” value in the called functions maintain the appropriate values.


<html>
<body>
<script type="text/javascript">

// main

function main()
{
	var greeter = new Greeter();

	// The following calls should pass type check.

	console.log(greeter.personGreetOnBirthday("Marcia", 13));
	console.log(greeter.personGreetOnBirthday("Carol", 41, false));
	console.log(greeter.personGreetOnBirthday("Amos", 100, true));

	// The following calls should fail type check.

	var didFailAsExpected = false;	
	var validationWorkingMessage = "Failed type check as expected.";	
	var validationErrorMessage = "Should have failed type check!";
	
	try
	{
		greeter.personGreetOnBirthday("Marcia", "Marcia", "Marcia");
	}
	catch (error)
	{
		didFailAsExpected = true;
	}
	if (didFailAsExpected == true)
	{
		console.log(validationWorkingMessage);
	}
	else
	{
		console.log(validationErrorMessage);
	}
	
	try
	{
		greeter.personGreetOnBirthday("Carol", 41, "Yes I AM aging gracefully!");
	}
	catch (error)
	{
		didFailAsExpected = true;
	}
	if (didFailAsExpected == true)
	{
		console.log(validationWorkingMessage);
	}
	else
	{
		console.log(validationErrorMessage);
	}
	
	try
	{
		greeter.personGreetOnBirthday(true, "Amos", 100);
	}
	catch (error)
	{
		didFailAsExpected = true;
	}
	if (didFailAsExpected == true)
	{
		console.log(validationWorkingMessage);
	}
	else
	{
		console.log(validationErrorMessage);
	}
}

// extensions

function FunctionExtensions()
{
	// extension class
}
{
	Function.prototype.typesCheck = function(parameterTypeNames)
	{
		var functionToValidate = this;
		
		var functionWithTypeChecks = function()
		{
			for (var i = 0; i < arguments.length; i++)
			{
				var argument = arguments[i];

				var argumentTypeName = argument.constructor.name;
				var parameterTypeName = parameterTypeNames[i];
				
				if (argumentTypeName != parameterTypeName)
				{
					throw "Unexpected type.";
				}
			}
			
			var thisFromCall = null; // todo
			var returnValue = functionToValidate.apply(thisFromCall, arguments);
		
			return returnValue;
		}
		
		return functionWithTypeChecks;
	}
}

// classes

function Greeter()
{
	// Do nothing.
}
{
	Greeter.prototype.personGreetOnBirthday = function
	(
		personName, ageInYears, isAgingGracefully
	)
	{
		var message;
		
		if (isAgingGracefully == true || ageInYears < 21)
		{
			message = "Happy birthday";
		}
		else
		{
			message = "Hello";
		}
		
		message = message + ", " + personName + "!";
				
		return message;
		
	}.typesCheck(["String", "Number", "Boolean"]);
	
}

// run

main();

</script>
<body>
</html>

Posted in Uncategorized | Tagged , , , | Leave a comment

A CSV Compressor in JavaScript

Below is a simple program that can remove repeated values from a CSV, thus making the file smaller. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

I suppose it’s actually more of a “run-length suppressor” than a compressor, in that it only trims out value that repeat in subsequent rows. and will likely only be of much value in certain very limited contexts similar to the one I wrote it for. Nonetheless, I’m posting it here on the chance that someone might find it useful.


<html>
<body>

<div id="divUI">
	<label><b>CSV Repeated Value Compressor</b></label><br />
	<input type="file" onchange="inputFile_Changed(this);"></input><br />
	<textarea id="textareaData" cols="80" rows="25"></textarea><br />
	<button onclick="buttonCompress_Clicked();">Compress</button>
	<button onclick="buttonDecompress_Clicked();">Decompress</button>
	<button onclick="buttonSave_Clicked();">Save</button>
</div>

<script type="text/javascript">

// ui events

function buttonCompress_Clicked()
{
	var textareaData = document.getElementById("textareaData");
	var dataToCompress = textareaData.value;
	var csvCompressor = new CsvCompressor();
	var dataCompressed = csvCompressor.compress(dataToCompress);
	textareaData.value = dataCompressed;
}

function buttonSave_Clicked()
{
	var textareaData = document.getElementById("textareaData");

	var textToSave = textareaData.value;
	
	var textToSaveAsBlob = new Blob([textToSave], {type:"text/plain"});
	var textToSaveAsURL = window.URL.createObjectURL(textToSaveAsBlob);
	var fileNameToSaveAs = "Out.csv";

	var downloadLink = document.createElement("a");
	downloadLink.download = fileNameToSaveAs;
	downloadLink.innerHTML = "Download File";
	downloadLink.href = textToSaveAsURL;

	downloadLink.click();
}

function buttonDecompress_Clicked()
{
	var textareaData = document.getElementById("textareaData");
	var dataToDecompress = textareaData.value;
	var csvCompressor = new CsvCompressor();
	var dataDecompressed = csvCompressor.decompress(dataToDecompress);
	textareaData.value = dataDecompressed;
}

function inputFile_Changed(inputFile)
{
	var file = inputFile.files[0];
	if (file != null)
	{
		var fileReader = new FileReader();
		fileReader.onload = function(event)
		{
			var fileContents = event.target.result;
			var textareaData = document.getElementById("textareaData");
			textareaData.value = fileContents;
		}
		fileReader.readAsText(file);
	}
}

// classes

function CsvCompressor()
{
	// Do nothing.
}
{
	CsvCompressor.Blank = "";
	CsvCompressor.Comma = ",";
	CsvCompressor.Newline = "\n";

	CsvCompressor.prototype.compress = function(dataToCompress)
	{
		var blank = CsvCompressor.Blank;
		var comma = CsvCompressor.Comma;
		var newline = CsvCompressor.Newline;
		
		var linesToCompress = dataToCompress.split(newline);
		var lineToCompress0 = linesToCompress[0];
		var valuesToCompressPrev = lineToCompress0.split(comma);
		var linesCompressed = [ lineToCompress0 ];
		for (var i = 1; i < linesToCompress.length; i++)
		{
			var lineToCompress = linesToCompress[i];
			var valuesToCompress = lineToCompress.split(comma);
			var valuesCompressed = [];
			for (var v = 0; v < valuesToCompress.length; v++)
			{
				var valueToCompress = valuesToCompress[v];
				var valueToCompressPrev = valuesToCompressPrev[v];
				var isValueSameAsPrev = (valueToCompress == valueToCompressPrev);
				var valueCompressed = (isValueSameAsPrev ? blank : valueToCompress);
				if (isValueSameAsPrev == false)
				{
					valuesToCompressPrev[v] = valueToCompress;
				}
				valuesCompressed.push(valueCompressed);
			}
			var lineCompressed = valuesCompressed.join(comma);
			linesCompressed.push(lineCompressed);
		}
		var dataCompressed = linesCompressed.join(newline);
		return dataCompressed;
	}
	
	CsvCompressor.prototype.decompress = function(dataToDecompress)
	{
		var blank = CsvCompressor.Blank;
		var comma = CsvCompressor.Comma;
		var newline = CsvCompressor.Newline;
		
		var linesToDecompress = dataToDecompress.split(newline);
		var lineToDecompress0 = linesToDecompress[0];
		var valuesToDecompressPrev = lineToDecompress0.split(comma);
		var linesDecompressed = [ lineToDecompress0 ];
		for (var i = 1; i < linesToDecompress.length; i++)
		{
			var lineToDecompress = linesToDecompress[i];
			var valuesToDecompress = lineToDecompress.split(comma);
			var valuesDecompressed = [];
			for (var v = 0; v < valuesToDecompress.length; v++)
			{
				var valueToDecompress = valuesToDecompress[v];
				var valueToDecompressPrev = valuesToDecompressPrev[v];
				var isValueBlank = (valueToDecompress == blank);
				var valueDecompressed = 
					(isValueBlank ? valueToDecompressPrev : valueToDecompress);
				if (isValueBlank == false)
				{
					valuesToDecompressPrev[v] = valueToDecompress;
				}
				valuesDecompressed.push(valueDecompressed);
			}
			var lineDecompressed = valuesDecompressed.join(comma);
			linesDecompressed.push(lineDecompressed);
		}
		var dataDecompressed = linesDecompressed.join(newline);
		return dataDecompressed;
	}
}

</script>

</body>
</html>

Posted in Uncategorized | Tagged , , , , | Leave a comment

An x86-16 Disassembler in JavaScript

Below is a partially implemented disassembler for Intel’s x86-16 instruction set, intended to assist in the investigation of COM executables from the era of Microsoft DOS.

It needs a lot more work. It can’t truly disassemble anything yet, or rather it disassembles things into gibberish instructions. I’m posting it here so I can come back to it later, and in the hopes that either the opcode listing or the corresponding linked references might be useful to someone.


<html>
<body>

<div id="divUI">

	<label>DOS COM File to Load:</label>
	<input type="file" onchange="inputFile_Changed(this);"></input>
	<br />

	<label>Assembly:</label><br />
	<textarea id="textareaAssembly" cols="20" rows="20"></textarea>

</div>

<script type="text/javascript">

// events
function inputFile_Changed(inputFile)
{
	var file = inputFile.files[0];
	if (file != null)
	{
		var fileReader = new FileReader();
		fileReader.onload = function(event)
		{
			var fileContentsAsBinaryString = event.target.result;
			var fileContentsAsBytes = [];
			for (var i = 0; i < fileContentsAsBinaryString.length; i++)
			{
				var byteRead = fileContentsAsBinaryString.charCodeAt(i);
				fileContentsAsBytes.push(byteRead);
			}
			var comFile = new DosComFile("todo", fileContentsAsBytes);
			var comFileAsAssembly = comFile.toStringAssembly();
			var textareaAssembly = document.getElementById("textareaAssembly");
			textareaAssembly.value = comFileAsAssembly;
		}
		fileReader.readAsBinaryString(file);
	}
}

// extensions

function ArrayExtensions()
{
	// extension class
}
{
	Array.prototype.addLookups = function(keyName)
	{
		for (var i = 0; i < this.length; i++)
		{
			var element = this[i];
			var key = element[keyName];
			this[key] = element;
		}
		return this;
	}
}

// classes

function BitStream(bytes)
{
	this.bytes = bytes;
	this.bitOffsetCurrent = 0;
}
{
	BitStream.prototype.readBit = function()
	{
		var byteIndex = Math.floor(this.bitOffsetCurrent / 8);
		var byte = this.bytes[byteIndex];
		var bitOffsetWithinByte = this.bitOffsetCurrent % 8;
		var bit = ( byte >> (7 - bitOffsetWithinByte) ) & 0x1;
		this.bitOffsetCurrent++;
		return bit;
	}

	BitStream.prototype.readBitsAsInteger = function(numberOfBits)
	{
		var returnValue = 0;
		for (var i = 0; i < numberOfBits; i++)
		{
			var bit = this.readBit();
			returnValue = (returnValue << 1) + bit;
		}
		return returnValue;
	}
}

function DosComFile(name, bytes)
{
	this.name = name;
	this.bytes = bytes;
}
{
	DosComFile.prototype.toStringAssembly = function()
	{
		var opcodeByValueLookup = 
			DosComFileInstructionOpcode.Instances._OpcodeByValueLookup;

		var bitStream = new BitStream(this.bytes);

		var instructions = [];

		for (var i = 0; i < this.bytes.length; i++)
		{
			var opcodeValue = this.bytes[i];

			var opcode = opcodeByValueLookup[opcodeValue];

			if (opcode == null)
			{
				var opcodeValueAsHex = opcodeValue.toString("16");
				opcode = new DosComFileInstructionOpcode
				(
					"_" + opcodeValueAsHex, opcodeValue, "[unrecognized]"
				);
				//throw "Unrecognized opcode: " + opcodeValue;
			}

			var operands = [];
			var operandWidths = opcode.operandWidthsInBits;
			var operandBitsSoFar = 0;
			for (var j = 0; j < operandWidths.length; j++)
			{
				var operandWidth = operandWidths[j];
				bitStream.bitOffsetCurrent = (i + 1) * 8 + operandBitsSoFar;
				var operand = bitStream.readBitsAsInteger(operandWidth);
				operands.push(operand);
				operandBitsSoFar += operandWidth;
			}
			var operandBytes = operandBitsSoFar / 8;
			i += operandBytes;

			var instruction = new DosComFileInstruction(opcode, operands);
			instructions.push(instruction);
		}

		var returnValue = instructions.join("\n");

		return returnValue;
	}
}

function DosComFileInstruction(opcode, operands)
{
	this.opcode = opcode;
	this.operands = operands;
}
{
	DosComFileInstruction.prototype.toString = function()
	{
		var opcodeAsString = (this.opcode == null ? "???" : this.opcode.mnemonic);
		var returnValue = opcodeAsString;
		for (var i = 0; i < this.operands.length; i++)
		{ 
			var operand = this.operands[i];
			returnValue += " " + operand.toString(16);
		}
		return returnValue;
	}
}

function DosComFileInstructionOpcode(mnemonic, value, description, operandWidthsInBits)
{
	this.mnemonic = mnemonic;
	this.value = value;
	this.description = description;
	this.operandWidthsInBits = 
		(operandWidthsInBits == null ? [2, 3, 3] : operandWidthsInBits);
}
{
	DosComFileInstructionOpcode.Instances = 
		new DosComFileInstructionOpcode_Instances();

	function DosComFileInstructionOpcode_Instances()
	{
		// Adapted from listings found at the following URLs:
		// https://en.wikipedia.org/wiki/X86_instruction_listings
		// http://ref.x86asm.net/coder32.html#xA5
		// http://www.felixcloutier.com/x86/
		// http://www.c-jump.com/CIS77/CPU/x86/lecture.html#X77_0120_encoding_add

		var io = DosComFileInstructionOpcode;
		var operandsDefault = [4, 4];
		var operandsNone = [];

		this._All =
		[
			new io("aaa", 0x37, "ascii adjust al after add"),
			new io("aad", 0xD5, "ascii adjust ax before div"),
			new io("aam", 0xD4, "ascii adjust ax after mult"),
			new io("aas", 0x3F, "ascii adjust al after sub"),
			new io("adcrmrb", 0x10, "add with carry r/m8 r8"),
			new io("adcrrw", 0x11, "add with carry r/m16 r16"),
			new io("adcrrmb", 0x12, "add with carry r8 r/m8"),
			new io("adcrrmwd", 0x13, "add with carry r16 r/m16"),
			new io("adcai4", 0x14, "add with carry al imm8"),
			new io("adcaiw", 0x15, "add with carry eax imm16"),

			// adds
			new io("addrmrb", 0x00, "add r/m8 r8"),
			new io("addrrw", 0x01, "add r/m16 r16"),
			new io("addrrmb", 0x02, "add r8 r/m8"),
			new io("addrrmwd", 0x03, "add r16 r/m16"),
			new io("addai4", 0x04, "add al imm8"),
			new io("addaiw", 0x05, "add eAX imm16"),

			new io("andrmrb", 0x20, "and r/m8 r8"),
			new io("andrmrw", 0x21, "and r/m16 r16"),
			new io("andrrmb", 0x22, "and r8 r/m8"),
			new io("andrrmw", 0x23, "and r16 r/m16"),
			new io("and4", 0x24, "and al imm8"),
			new io("and5", 0x25, "and eax imm16"),
			new io("arith0", 0x80, "add,or,adc,sbb,and,sub,xor,cmp r/m8 imm8"),
			new io("arith1", 0x81, "add,or,adc,sbb,and,sub,xor,cmp r/m16 imm16"),
			new io("arith2", 0x82, "add,or,adc,sbb,and,sub,xor,cmp r/m8 imm8"),
			new io("arith3", 0x83, "add,or,adc,sbb,and,sub,xor,cmp r/m16 imm8"),
			new io("arithf", 0xD8, "add,mul,com,sub,subr,div,divr m32real"),
			new io("arpl", 0x63, "adjust rpl field of segment selector"),
			new io("bound", 0x52, "check index against bounds"),
			new io("call", 0x9A, "call procedure"), // 0xE8, 0xFF/2, 0xFF/3
			new io("cbw", 0x98, "convert byte to word"),
			new io("clc", 0xF8, "clear carry flag", operandsNone),
			new io("cld", 0xFC, "clear direction flag", operandsNone),
			new io("cli", 0xFA, "clear interrupt flag", operandsNone),
			new io("cmc", 0xF5, "complement carry flag", operandsNone),
			new io("cmp0", 0x38, "compare r/m8 r8"), // 0x05, 0x80/0..., 0x83/0
			new io("cmp1", 0x39, "compare r/m16 r16"),
			new io("cmp2", 0x3A, "compare r8 r/m8"),
			new io("cmp3", 0x3B, "compare r16 r/m16"),
			new io("cmp4", 0x3C, "compare al imm8"),
			new io("cmp5", 0x3D, "add eax imm16"),
			new io("cmpsb", 0xA6, "compare bytes in memory"),
			new io("cmpsw", 0xA7, "compare words"),
			new io("cwd", 0x99, "convert word to doubleword"),
			new io("daa", 0x27, "decimal adjust al after add"),
			new io("das", 0x2F, "decimal adjust al after sub"),

			// decrements - no operands
			new io("decr0", 0x48, "decrement register 0 (ax?)", operandsNone),
			new io("decr1", 0x49, "decrement register 1", operandsNone),
			new io("decr2", 0x4A, "decrement register 2", operandsNone),
			new io("decr3", 0x4B, "decrement register 3", operandsNone),
			new io("decr4", 0x4C, "decrement register 4", operandsNone),
			new io("decr5", 0x4D, "decrement register 5", operandsNone),
			new io("decr6", 0x4E, "decrement register 6", operandsNone),
			new io("decr7", 0x4F, "decrement register 7", operandsNone),

			new io("div", 0xF6, "unsigned divide"), // 0xF6/6, 0xF7/6
			new io("enter", 0xC8, "make stack frame for procedure parameters"),
			//new io("esc", ?, "used with floating-point unit");
			new io("hlt", 0xF4, "enter halt state"),
			new io("idiv", 0XF6, "signed divide"), // 0xF6/7, 0xF7/7
			new io("imul", 0x69, "signed multiply"), // 0x6B, 0xF6/5, 0xF7/5, 0x0FAF

			// ins
			new io("insb", 0x6C, "input from port to string m8 dx"),
			new io("insw/d", 0x6D, "input from port to string m16 dx"),
			new io("inb", 0xE4, "input reg from port al imm8"),
			new io("ins", 0xE5, "input reg from port eax imm16"),

			// increments - no operands
			new io("incr0", 0x40, "increment r0 (ax?)", operandsNone),
			new io("incr1", 0x41, "increment r1", operandsNone),
			new io("incr2", 0x42, "increment r2", operandsNone),
			new io("incr3", 0x43, "increment r3", operandsNone),
			new io("incr4", 0x44, "increment register 4", operandsNone),
			new io("incr5", 0x45, "increment register 5", operandsNone),
			new io("incr6", 0x46, "increment register 6", operandsNone),
			new io("incr7", 0x47, "increment register 7", operandsNone),

			new io("int", 0xCD, "call to interrupt"),
			new io("into", 0xCE, "call to interrupt if overflow"),
			new io("iret", 0xCF, "return from interrupt"),

			// jumps
			new io("jmpw", 0xE9, "jump rel16"),
			new io("jmpf", 0xEA, "jump ptr16:16"),
			new io("jmpb", 0xEB, "jump rel8"),
			new io("jo", 0x70, "jump if 0"), 
			new io("jno", 0x71, "jump if not 0"),
			new io("jb/nae/c", 0x72, "jb/nae/c"),
			new io("jnb/ae/nc", 0x73, "jnb/ae/nc"),
			new io("jz/e", 0x74, "jz/e"),
			new io("jnz/ne", 0x75, "jnz/ne"),
			new io("jbe/na", 0x76, "jbe/na"),
			new io("jnbe/a", 0x77, "jnbe/a"),
			new io("js", 0x78, "js"),
			new io("jns", 0x79, "jns"),
			new io("jp/jpe", 0x7A, "jp/jpe"),
			new io("jnp/po", 0x7B, "jnp/po"),
			new io("jlt", 0x7C, "jump if less than"),
			new io("jge", 0x7D, "jump if greater than or equal"),
			new io("jle", 0x7E, "jump if less than or equal"),
			new io("jgt", 0x7F, "jump if greater than"),

			new io("lahf", 0x9F, "load flags into ah"),
			new io("lds", 0xC5, "load pointer using ds"),
			new io("lea", 0x8D, "load effective address"),
			new io("les", 0xC4, "load es with pointer"),
			new io("lock", 0xF0, "assert bus loc# signal"),
			new io("lodsb", 0xAC, "load string byte"),
			new io("lodsw", 0xAD, "load string word"),

			// loops
			new io("loopnz", 0xE0, "dec cx, jump if >0, zf 0"),
			new io("loopz", 0xE1, "dec cx, jump if >0, zf 1"),
			new io("loopz", 0xE2, "dec cx, jump if >0"),
			new io("jcxz", 0xE3, "jump if cx 0"),

			// moves
			new io("movrmrb", 0x88, "move r/m8 r8"),
			new io("movrmrw", 0x89, "move r/m16 r16"),
			new io("movrrmb", 0x8A, "move r8 r/m8"),
			new io("movrrmw", 0x8B, "move r16 r/m16"),
			new io("movrmwseg", 0x8C, "move r/m16 Sreg", [16]),
			new io("movsegrmw", 0x8E, "move Sreg r/m16"),
			new io("movsb", 0xA4, "move byte from string to string"),
			new io("movsw", 0xA5, "move word from string to string"),
			new io("movr0i8", 0xB0, "move r0 (ax?) imm8"),
			new io("movr1i8", 0xB1, "move r1 imm8"),
			new io("movr2i8", 0xB2, "move r2 imm8"),
			new io("movr3i8", 0xB3, "move r3 imm8"),
			new io("movr4i8", 0xB4, "move r4 imm8"),
			new io("movr5i8", 0xB5, "move r5 imm8"),
			new io("movr6i8", 0xB6, "move r6 imm8"),
			new io("movr7i8", 0xB7, "move r7 imm8"),
			new io("mov16r0", 0xB8, "move r0 (ax?) imm16"),
			new io("movr1iw", 0xB9, "move r1 imm16"),
			new io("movr2iw", 0xBA, "move r2 imm16"),
			new io("movr3iw", 0xBB, "move r3 imm16"),
			new io("movr4iw", 0xBC, "move r4 imm16"),
			new io("movr5iw", 0xBD, "move r5 imm16"),
			new io("movr6iw", 0xBE, "move r6 imm16"),
			new io("movr7iw", 0xBF, "move r7 imm16"),

			//new io("mul", ?, "unsigned multiply"),
			//new io("neg", ?, "two's complement negation),
			new io("nop", 0x90, "no operation"),
			//new io("not", ?, "logical not"),
			new io("or0", 0x08, "or r/m8 r8"),
			new io("or1", 0x09, "or r/m16 r16"),
			new io("or2", 0x0A, "or r8 r/m8"),
			new io("or3", 0x0B, "or r16 r/m16"),
			new io("or4", 0x0C, "or al imm8"),
			new io("or5", 0x0D, "or eax imm16"),
			new io("out0", 0xEE, "out dx al"),
			new io("out1", 0xEF, "out dx eax"),
			new io("outsb", 0x6E, "output dx m8"),
			new io("outsw/d", 0x6F, "output dx m16"),
			new io("pushds", 0x1F, "push ds onto stack"),
			new io("popes", 0x07, "pop es from stack"),

			// pops
			new io("popr0", 0x58, "pop r0 (ax?) from stack"),
			new io("popr1", 0x59, "pop r1 from stack"),
			new io("popr2", 0x5A, "pop r2 from stack"),
			new io("popr3", 0x5B, "pop r3 from stack"),
			new io("popr4", 0x5C, "pop r4 from stack"),
			new io("popr5", 0x5D, "pop r5 from stack"),
			new io("popr6", 0x5E, "pop r6 from stack"),
			new io("popr7", 0x5F, "pop r7 from stack"),
			new io("popss", 0x17, "pop ss from stack"),
			new io("popf", 0x9D, "pop flags register from stack"),

			// pushes
			new io("pushcs", 0x0E, "push cs onto stack"),
			new io("pushds", 0x1E, "push ds onto stack"),
			new io("pushes", 0x06, "push es onto stack"),
			new io("pushr0", 0x50, "push r0 (ax?) onto stack"),
			new io("pushr1", 0x51, "push r1 onto stack"),
			new io("pushr2", 0x52, "push r2 onto stack"),
			new io("pushr3", 0x53, "push r3 onto stack"),
			new io("pushr4", 0x54, "push r4 onto stack"),
			new io("pushr5", 0x55, "push r5 onto stack"),
			new io("pushr6", 0x56, "push r6 onto stack"),
			new io("pushr7", 0x57, "push r7 onto stack"),
			new io("pushss", 0x16, "push ss onto stack"),
			new io("pushf", 0x9C, "push flags data onto stack"),

			new io("rep", 0xF2, "repeat"), // rep, repe, repne, repnz, repz
			new io("retf0", 0xCA, "return from procedure"),
			new io("retf1", 0xCB, "return from procedure"),
			new io("retn0", 0xC2, "return from near procedure"),
			new io("retn1", 0xC3, "return from near procedure"),
			new io("shrotbi", 0xC0, "shift/rotate r/m8 imm8"),
			new io("shrotwi", 0xC1, "shift/rotate r/m16 imm8"),
			new io("shrotb1", 0xD0, "shift/rotate r/m8 1"),
			new io("shrotw1", 0xD1, "shift/rotate r/m16 1"),
			new io("sahf", 0x9E, "store ah into flags"),

			// subtracts with borrow
			new io("sbb0", 0x18, "subtraction with borrow r/m8 r8"),
			new io("sbb1", 0x19, "subtraction with borrow r/m16 r16"),
			new io("sbb2", 0x1A, "subtraction with borrow r8 r/m8"),
			new io("sbb3", 0x1B, "subtraction with borrow r16 r/m16"),
			new io("sbb4", 0x1C, "subtraction with borrow al imm8"),
			new io("sbb5", 0x1D, "subtraction with borrow eax imm16"),

			new io("scasb", 0xAE, "compare byte string"),
			new io("scasw", 0xAF, "compare word string"),
			new io("seges", 0x26, "es prefix"),
			new io("segcs", 0x2E, "cs prefix"),
			new io("segds", 0x3E, "ds prefix"),
			new io("stc", 0xF9, "set carry flag"),
			new io("std", 0xFD, "set direction flag"),
			new io("sti", 0xFB, "set interrupt flag"),
			new io("stosb", 0xAA, "store byte in string"),
			new io("stosw", 0xAB, "store word in string"),

			// subtracts
			new io("sub0", 0x28, "subtract r/m8 r8"),
			new io("sub1", 0x29, "subtract r/m16 r16"),
			new io("sub2", 0x2A, "subtract r8 r/m8"),
			new io("sub3", 0x2B, "subtract r16 r/m16"),
			new io("sub4", 0x2C, "subtract al imm8"),
			new io("sub5", 0x2D, "subtract eax imm16"),

			new io("wait", 0x9B, "wait until not busy"),

			// exchanges
			new io("xchgr0", 0x90, "exchange r0 16 eAX"),
			new io("xchgr1", 0x90, "exchange r1 16 eAX"),
			new io("xchgr2", 0x90, "exchange r2 16 eAX"),
			new io("xchgr3", 0x90, "exchange r3 16 eAX"),
			new io("xchgr4", 0x90, "exchange r4 16 eAX"),
			new io("xchgr5", 0x90, "exchange r5 16 eAX"),
			new io("xchgr6", 0x90, "exchange r6 16 eAX"),
			new io("xchgr7", 0x90, "exchange r7 16 eAX"),
			new io("xchgrrb", 0x86, "exchange registers 8"),
			new io("xchgrrw", 0x86, "exchange registers 16"),

			new io("xlat", 0xD7, "table look-up translation"),
			new io("xor0", 0x30, "xor r/m8 r8"),
			new io("xor1", 0x31, "xor r/m16 r16"),
			new io("xor2", 0x32, "xor r8 r/m8"),
			new io("xor3", 0x33, "xor r16 r/m16"),
			new io("xor4", 0x34, "xor al imm8"),
			new io("xor5", 0x35, "xor eax imm16")
		];

		this._OpcodeByValueLookup = [];
		for (var i = 0; i < this._All.length; i++)
		{
			var opcode = this._All[i];
			var opcodeValue = opcode.value;
			this._OpcodeByValueLookup[opcodeValue] = opcode;
		}
	}
}

</script>
</body>
</html>

Posted in Uncategorized | Tagged , , , | Leave a comment