Click here to Skip to main content
15,896,606 members
Articles / Web Development / HTML

Java Class Viewer

Rate me:
Please Sign up or sign in to vote.
4.93/5 (41 votes)
21 Jun 2016MIT8 min read 179.2K   7.8K   96  
Watch the Java class file visually & interactively for the meaning of every byte
/*
 * ClassFile.java    2:58 AM, August 5, 2007
 *
 * Copyright  2007, FreeInternals.org. All rights reserved.
 * Use is subject to license terms.
 */
package org.freeinternals.format.classfile;

import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.freeinternals.format.FileFormatException;

/**
 * Represents a {@code class} file. A {@code class} file structure has the
 * following format:
 *
 * <pre>
 *    ClassFile {
 *        u4 magic;
 *        u2 minor_version;
 *        u2 major_version;
 *        u2 constant_pool_count;
 *        cp_info constant_pool[constant_pool_count-1];
 *        u2 access_flags;
 *        u2 this_class;
 *        u2 super_class;
 *        u2 interfaces_count;
 *        u2 interfaces[interfaces_count];
 *        u2 fields_count;
 *        field_info fields[fields_count];
 *        u2 methods_count;
 *        method_info methods[methods_count];
 *        u2 attributes_count;
 *        attribute_info attributes[attributes_count];
 *    }
 * </pre>
 *
 * The {@code ClassFile} object is constructed from the class byte array.
 *
 *
 *
 * @author Amos Shi
 * @since JDK 6.0
 * @see <a
 * href="http://www.freeinternals.org/mirror/java.sun.com/vmspec.2nded/ClassFile.doc.html#74353">
 * VM Spec: The ClassFile Structure </a>
 */
public class ClassFile {

    private byte[] classByteArray;
    private PosDataInputStream posDataInputStream;
    /**
     * Magic number of {@code class} file.
     */
    public static final int MAGIC = 0xCAFEBABE;
    private u4 magic;
    // Class file Version
    private MinorVersion minor_version;
    private MajorVersion major_version;
    // Constant pool
    private CPCount constant_pool_count;
    private AbstractCPInfo[] constant_pool;
    // Class Declaration
    private AccessFlags access_flags;
    private ThisClass this_class;
    private SuperClass super_class;
    private InterfaceCount interfaces_count;
    private Interface[] interfaces;
    // Field
    private FieldCount fields_count;
    private FieldInfo[] fields;
    // Method
    private MethodCount methods_count;
    private MethodInfo[] methods;
    // Attribute
    private AttributeCount attributes_count;
    private AttributeInfo[] attributes;

    /**
     * Creates a new instance of ClassFile from byte array.
     *
     * @param classByteArray Byte array of a class file
     * @throws java.io.IOException Error happened when reading the byte array
     * @throws org.freeinternals.classfile.core.ClassFormatException The input
     * parameter {@code classByteArray} is not a valid class
     */
    public ClassFile(final byte[] classByteArray)
            throws java.io.IOException, FileFormatException {
        this.classByteArray = classByteArray.clone();
        final ClassFile.Parser parser = new Parser();
        parser.parse();
        this.analysisDeclarations();
    }

    private void analysisDeclarations()
            throws FileFormatException {

        // Analysis field declarations
        if (this.fields_count.getValue() > 0) {
            String type;
            for (FieldInfo field : fields) {
                try {
                    type = SignatureConvertor.signature2Type(this.getConstantUtf8Value(field.getDescriptorIndex()));
                } catch (SignatureException se) {
                    type = "[Unexpected signature type]: " + this.getConstantUtf8Value(field.getDescriptorIndex());
                    //System.err.println(se.toString());
                }
                field.setDeclaration(String.format("%s %s %s",
                        field.getModifiers(),
                        type,
                        this.getConstantUtf8Value(field.getNameIndex())));
            }
        }


        // Analysis method declarations
        if (this.methods_count.getValue() > 0) {
            String mtdReturnType;
            String mtdParameters;
            for (MethodInfo method : methods) {
                try {
                    mtdReturnType = SignatureConvertor.parseMethodReturnType(this.getConstantUtf8Value(method.getDescriptorIndex()));
                } catch (SignatureException se) {
                    mtdReturnType = String.format("[Unexpected method return type: %s]", this.getConstantUtf8Value(method.getDescriptorIndex()));
                    //System.err.println(se.toString());
                }
                try {
                    mtdParameters = SignatureConvertor.parseMethodParameters(this.getConstantUtf8Value(method.getDescriptorIndex()));
                } catch (SignatureException se) {
                    mtdParameters = String.format("[Unexpected method parameters: %s]", this.getConstantUtf8Value(method.getDescriptorIndex()));
                    //System.err.println(se.toString());
                }

                method.setDeclaration(String.format("%s %s %s %s",
                        method.getModifiers(),
                        mtdReturnType,
                        this.getConstantUtf8Value(method.getNameIndex()),
                        mtdParameters));
            }
        }
    }

    /**
     * Get a UTF-8 text from the constant pool.
     * 
     * @param  cpIndex Constant Pool object Index
     * @return  The UTF-8 text
     */
    public String getConstantUtf8Value(final int cpIndex)
            throws FileFormatException {
        String returnValue = null;

        if ((cpIndex == 0) || (cpIndex >= this.constant_pool_count.value.value)) {
            throw new FileFormatException(String.format(
                    "Constant Pool index is out of range. CP index cannot be zero, and should be less than CP count (=%d). CP index = %d.",
                    this.constant_pool_count.value.value,
                    cpIndex));
        }

        if (this.constant_pool[cpIndex].tag.value == AbstractCPInfo.CONSTANT_Utf8) {
            final ConstantUtf8Info utf8Info = (ConstantUtf8Info) this.constant_pool[cpIndex];
            returnValue = utf8Info.getValue();
        } else {
            throw new FileFormatException(String.format(
                    "Unexpected constant pool type: Utf8(%d) expected, but it is '%d'.",
                    AbstractCPInfo.CONSTANT_Utf8,
                    this.constant_pool[cpIndex].tag.value));
        }

        return returnValue;
    }

    ///////////////////////////////////////////////////////////////////////////
    // Get raw data
    /**
     * Get the byte array of current class.
     *
     * @return Byte array of the class
     */
    public byte[] getClassByteArray() {
        return this.classByteArray;
    }

    /**
     * Get part of the class byte array. The array begins at the specified
     * {@code startIndex} and extends to the byte at
     * {@code startIndex}+{@code length}.
     *
     * @param startIndex The start index
     * @param length The length of the array
     * @return Part of the class byte array
     */
    public byte[] getClassByteArray(final int startIndex, final int length) {
        if ((startIndex < 0) || (length < 1)) {
            throw new IllegalArgumentException("startIndex or length is not valid. startIndex = " + startIndex + ", length = " + length);
        }
        if (startIndex + length - 1 > this.classByteArray.length) {
            throw new ArrayIndexOutOfBoundsException("The last item index is bigger than class byte array size.");
        }

        byte[] data = new byte[length];
        System.arraycopy(this.classByteArray, startIndex, data, 0, length);
        return data;
    }

    /**
     * Get the length of the class byte array.
     *
     * @return Length of class byte array
     */
    public int getByteArraySize() {
        return this.classByteArray.length;
    }

    /**
     * Get the {@code minor_version} of the {@code ClassFile} structure.
     *
     * @return The {@code minor_version}
     */
    public MinorVersion getMinorVersion() {
        return this.minor_version;
    }

    /**
     * Get the {@code major_version} of the {@code ClassFile} structure.
     *
     * @return The {@code major_version}
     */
    public MajorVersion getMajorVersion() {
        return this.major_version;
    }

    /**
     * Get the {@code constant_pool_count} of the {@code ClassFile} structure.
     *
     * @return The {@code constant_pool_count}
     */
    public CPCount getCPCount() {
        return this.constant_pool_count;
    }

    /**
     * Get the {@code constant_pool[]} of the {@code ClassFile} structure.
     *
     * @return The {@code constant_pool[]}
     */
    public AbstractCPInfo[] getConstantPool() {
        return this.constant_pool;
    }

    /**
     * Returns a string of the constant pool item at the specified
     * {@code index}.
     *
     * @param index Index in the constant pool
     * @return String of the constant pool item at {@code index}
     */
    public String getCPDescription(final int index) {
        // Invalid index
        if (index >= this.constant_pool_count.getValue()) {
            return null;
        }

        // Special index: empty
        if (index == 0) {
            return null;
        }

        return new CPDescr().getCPDescr(index);
    }

    /**
     * Get the {@code access_flags} of the {@code ClassFile} structure.
     *
     * @return The {@code access_flags}
     */
    public AccessFlags getAccessFlags() {
        return this.access_flags;
    }

    /**
     * Get the {@code this_class} of the {@code ClassFile} structure.
     *
     * @return The {@code this_class}
     */
    public ThisClass getThisClass() {
        return this.this_class;
    }

    /**
     * Get the {@code super_class} of the {@code ClassFile} structure.
     *
     * @return The {@code super_class}
     */
    public SuperClass getSuperClass() {
        return this.super_class;
    }

    /**
     * Get the {@code interfaces_count} of the {@code ClassFile} structure.
     *
     * @return The {@code interfaces_count}
     */
    public InterfaceCount getInterfacesCount() {
        return this.interfaces_count;
    }

    /**
     * Get the {@code interfaces}[] of the {@code ClassFile} structure.
     *
     * @return The {@code interfaces}[]
     */
    public Interface[] getInterfaces() {
        return this.interfaces;
    }

    /**
     * Get the {@code fields_count} of the {@code ClassFile} structure.
     *
     * @return The {@code fields_count}
     */
    public FieldCount getFieldCount() {
        return this.fields_count;
    }

    /**
     * Get the {@code fields}[] of the {@code ClassFile} structure.
     *
     * @return The {@code fields}[]
     */
    public FieldInfo[] getFields() {
        return this.fields;
    }

    /**
     * Get the {@code methods_count} of the {@code ClassFile} structure.
     *
     * @return The {@code methods_count}
     */
    public MethodCount getMethodCount() {
        return this.methods_count;
    }

    /**
     * Get the {@code methods}[] of the {@code ClassFile} structure.
     *
     * @return The {@code methods}[]
     */
    public MethodInfo[] getMethods() {
        return this.methods;
    }

    /**
     * Get the {@code attributes_count} of the {@code ClassFile} structure.
     *
     * @return The {@code attributes_count}
     */
    public AttributeCount getAttributeCount() {
        return this.attributes_count;
    }

    /**
     * Get the {@code attributes}[] of the {@code ClassFile} structure.
     *
     * @return The {@code attributes}[]
     */
    public AttributeInfo[] getAttributes() {
        return this.attributes;
    }

    ///////////////////////////////////////////////////////////////////////////
    // Get extracted data
    ///////////////////////////////////////////////////////////////////////////
    // Internal Classes
    private class Parser {

        Parser() {
        }

        public void parse()
                throws FileFormatException, IOException {
            final PosByteArrayInputStream posByteArrayInputStream = new PosByteArrayInputStream(classByteArray);
            ClassFile.this.posDataInputStream = new PosDataInputStream(posByteArrayInputStream);

            ClassFile.this.magic = new u4(ClassFile.this.posDataInputStream.readInt());
            if (ClassFile.this.magic.value != ClassFile.MAGIC) {
                throw new FileFormatException("The magic number of the byte array is not 0xCAFEBABE");
            }

            this.parseClassFileVersion();
            this.parseConstantPool();
            this.parseClassDeclaration();
            this.parseFields();
            this.parseMethods();
            this.parseAttributes();
        }

        private void parseClassFileVersion()
                throws java.io.IOException, FileFormatException {
            ClassFile.this.minor_version = new MinorVersion(ClassFile.this.posDataInputStream);
            ClassFile.this.major_version = new MajorVersion(ClassFile.this.posDataInputStream);
        }

        private void parseConstantPool()
                throws java.io.IOException, FileFormatException {
            ClassFile.this.constant_pool_count = new CPCount(ClassFile.this.posDataInputStream);
            final int cp_count = ClassFile.this.constant_pool_count.getValue();

            ClassFile.this.constant_pool = new AbstractCPInfo[cp_count];
            short tag;
            for (int i = 1; i < cp_count; i++) {
                tag = (short) ClassFile.this.posDataInputStream.readUnsignedByte();

                switch (tag) {
                    case AbstractCPInfo.CONSTANT_Utf8:
                        ClassFile.this.constant_pool[i] = new ConstantUtf8Info(ClassFile.this.posDataInputStream);
                        break;

                    case AbstractCPInfo.CONSTANT_Integer:
                        ClassFile.this.constant_pool[i] = new ConstantIntegerInfo(ClassFile.this.posDataInputStream);
                        break;

                    case AbstractCPInfo.CONSTANT_Float:
                        ClassFile.this.constant_pool[i] = new ConstantFloatInfo(ClassFile.this.posDataInputStream);
                        break;

                    case AbstractCPInfo.CONSTANT_Long:
                        ClassFile.this.constant_pool[i] = new ConstantLongInfo(ClassFile.this.posDataInputStream);
                        i++;
                        break;

                    case AbstractCPInfo.CONSTANT_Double:
                        ClassFile.this.constant_pool[i] = new ConstantDoubleInfo(ClassFile.this.posDataInputStream);
                        i++;
                        break;

                    case AbstractCPInfo.CONSTANT_Class:
                        ClassFile.this.constant_pool[i] = new ConstantClassInfo(ClassFile.this.posDataInputStream);
                        break;

                    case AbstractCPInfo.CONSTANT_String:
                        ClassFile.this.constant_pool[i] = new ConstantStringInfo(ClassFile.this.posDataInputStream);
                        break;

                    case AbstractCPInfo.CONSTANT_Fieldref:
                        ClassFile.this.constant_pool[i] = new ConstantFieldrefInfo(ClassFile.this.posDataInputStream);
                        break;

                    case AbstractCPInfo.CONSTANT_Methodref:
                        ClassFile.this.constant_pool[i] = new ConstantMethodrefInfo(ClassFile.this.posDataInputStream);
                        break;

                    case AbstractCPInfo.CONSTANT_InterfaceMethodref:
                        ClassFile.this.constant_pool[i] = new ConstantInterfaceMethodrefInfo(ClassFile.this.posDataInputStream);
                        break;

                    case AbstractCPInfo.CONSTANT_NameAndType:
                        ClassFile.this.constant_pool[i] = new ConstantNameAndTypeInfo(ClassFile.this.posDataInputStream);
                        break;

                    default:
                        throw new FileFormatException(
                                String.format("Unreconizable constant pool type found. Constant pool tag: [%d]; class file offset: [%d].", tag, ClassFile.this.posDataInputStream.getPos() - 1));
                }

                // -- Debug information.
//                if (ClassFile.this.constant_pool[i] != null)
//                {
//                    System.out.print(ClassFile.this.constant_pool[i].getDescription());
//                }
//                else
//                {
//                    System.out.print(ClassFile.this.constant_pool[i-1].getDescription());
//                }
//                System.out.println ();
            }
        }

        private void parseClassDeclaration()
                throws java.io.IOException, FileFormatException {
            ClassFile.this.access_flags = new AccessFlags(ClassFile.this.posDataInputStream);
            ClassFile.this.this_class = new ThisClass(ClassFile.this.posDataInputStream);
            ClassFile.this.super_class = new SuperClass(ClassFile.this.posDataInputStream);

            ClassFile.this.interfaces_count = new InterfaceCount(ClassFile.this.posDataInputStream);
            if (ClassFile.this.interfaces_count.getValue() > 0) {
                ClassFile.this.interfaces = new Interface[ClassFile.this.interfaces_count.getValue()];
                for (int i = 0; i < ClassFile.this.interfaces_count.getValue(); i++) {
                    ClassFile.this.interfaces[i] = new Interface(ClassFile.this.posDataInputStream);
                }
            }
        }

        private void parseFields()
                throws java.io.IOException, FileFormatException {
            ClassFile.this.fields_count = new FieldCount(ClassFile.this.posDataInputStream);
            final int fieldCount = ClassFile.this.fields_count.getValue();
            if (fieldCount > 0) {
                ClassFile.this.fields = new FieldInfo[fieldCount];
                for (int i = 0; i < fieldCount; i++) {
                    ClassFile.this.fields[i] = new FieldInfo(ClassFile.this.posDataInputStream, ClassFile.this.constant_pool);
                }
            }
        }

        private void parseMethods()
                throws java.io.IOException, FileFormatException {
            ClassFile.this.methods_count = new MethodCount(ClassFile.this.posDataInputStream);
            final int methodCount = ClassFile.this.methods_count.getValue();

            if (methodCount > 0) {
                ClassFile.this.methods = new MethodInfo[methodCount];
                for (int i = 0; i < methodCount; i++) {
                    ClassFile.this.methods[i] = new MethodInfo(ClassFile.this.posDataInputStream, ClassFile.this.constant_pool);
                }
            }
        }

        private void parseAttributes()
                throws java.io.IOException, FileFormatException {
            ClassFile.this.attributes_count = new AttributeCount(ClassFile.this.posDataInputStream);
            final int attributeCount = ClassFile.this.attributes_count.getValue();
            if (attributeCount > 0) {
                ClassFile.this.attributes = new AttributeInfo[attributeCount];
                for (int i = 0; i < attributeCount; i++) {
                    ClassFile.this.attributes[i] = AttributeInfo.parse(ClassFile.this.posDataInputStream, ClassFile.this.constant_pool);
                }
            }
        }
    }

    private static enum Descr_NameAndType {

        RAW(1), FIELD(2), METHOD(3);
        private final int enum_value;

        Descr_NameAndType(final int value) {
            this.enum_value = value;
        }

        public int value() {
            return this.enum_value;
        }
    }

    private class CPDescr {

        CPDescr() {
        }

        public String getCPDescr(final int index) {
            final StringBuilder sb = new StringBuilder(40);

            switch (ClassFile.this.constant_pool[index].getTag()) {
                case AbstractCPInfo.CONSTANT_Utf8:
                    sb.append("Utf8: ");
                    sb.append(this.getDescr_Utf8((ConstantUtf8Info) ClassFile.this.constant_pool[index]));
                    break;
                case AbstractCPInfo.CONSTANT_Integer:
                    sb.append("Integer: ");
                    sb.append(this.getDescr_Integer((ConstantIntegerInfo) ClassFile.this.constant_pool[index]));
                    break;
                case AbstractCPInfo.CONSTANT_Float:
                    sb.append("Float: ");
                    sb.append(this.getDescr_Float((ConstantFloatInfo) ClassFile.this.constant_pool[index]));
                    break;
                case AbstractCPInfo.CONSTANT_Long:
                    sb.append("Long: ");
                    sb.append(this.getDescr_Long((ConstantLongInfo) ClassFile.this.constant_pool[index]));
                    break;
                case AbstractCPInfo.CONSTANT_Double:
                    sb.append("Double: ");
                    sb.append(this.getDescr_Double((ConstantDoubleInfo) ClassFile.this.constant_pool[index]));
                    break;
                case AbstractCPInfo.CONSTANT_Class:
                    sb.append("Class: ");
                    sb.append(this.getDescr_Class((ConstantClassInfo) ClassFile.this.constant_pool[index]));
                    break;
                case AbstractCPInfo.CONSTANT_String:
                    sb.append("String: ");
                    sb.append(this.getDescr_String((ConstantStringInfo) ClassFile.this.constant_pool[index]));
                    break;
                case AbstractCPInfo.CONSTANT_Fieldref:
                    sb.append("Fieldref: ");
                    sb.append(this.getDescr_Fieldref((ConstantFieldrefInfo) ClassFile.this.constant_pool[index]));
                    break;
                case AbstractCPInfo.CONSTANT_Methodref:
                    sb.append("Methodref: ");
                    sb.append(this.getDescr_Methodref((ConstantMethodrefInfo) ClassFile.this.constant_pool[index]));
                    break;
                case AbstractCPInfo.CONSTANT_InterfaceMethodref:
                    sb.append("InterfaceMethodref: ");
                    sb.append(this.getDescr_InterfaceMethodref((ConstantInterfaceMethodrefInfo) ClassFile.this.constant_pool[index]));
                    break;
                case AbstractCPInfo.CONSTANT_NameAndType:
                    sb.append("NameAndType: ");
                    sb.append(this.getDescr_NameAndType(
                            (ConstantNameAndTypeInfo) ClassFile.this.constant_pool[index],
                            ClassFile.Descr_NameAndType.RAW));
                    break;
                default:
                    sb.append("!!! Un-supported CP type.");
                    break;
            }

            return sb.toString();
        }

        private String getDescr_Utf8(final ConstantUtf8Info info) {
            return info.getValue();
        }

        private String getDescr_Integer(final ConstantIntegerInfo info) {
            return String.valueOf(info.getValue());
        }

        private String getDescr_Float(final ConstantFloatInfo info) {
            return String.valueOf(info.getValue());
        }

        private String getDescr_Long(final ConstantLongInfo info) {
            return String.valueOf(info.getValue());
        }

        private String getDescr_Double(final ConstantDoubleInfo info) {
            return String.valueOf(info.getValue());
        }

        private String getDescr_Class(final ConstantClassInfo info) {
            // The value of the name_index item must be a valid index into the constant_pool table. 
            // The constant_pool entry at that index must be a CONSTANT_Utf8_info structure 
            // representing a valid fully qualified class or interface name encoded in internal form.
            return SignatureConvertor.parseClassSignature(this.getDescr_Utf8(
                    (ConstantUtf8Info) ClassFile.this.constant_pool[info.getNameIndex()]));
        }

        private String getDescr_String(final ConstantStringInfo info) {
            // The value of the string_index item must be a valid index into the constant_pool table. 
            // The constant_pool entry at that index must be a CONSTANT_Utf8_info (.4.7) structure 
            // representing the sequence of characters to which the String object is to be initialized.
            return SignatureConvertor.parseClassSignature(this.getDescr_Utf8(
                    (ConstantUtf8Info) ClassFile.this.constant_pool[info.getStringIndex()]));
        }

        private String getDescr_Fieldref(final ConstantFieldrefInfo info) {
            return this.getDescr_ref(
                    info.getClassIndex(),
                    info.getNameAndTypeIndex(),
                    ClassFile.Descr_NameAndType.FIELD);
        }

        private String getDescr_Methodref(final ConstantMethodrefInfo info) {
            return this.getDescr_ref(
                    info.getClassIndex(),
                    info.getNameAndTypeIndex(),
                    ClassFile.Descr_NameAndType.METHOD);
        }

        private String getDescr_InterfaceMethodref(final ConstantInterfaceMethodrefInfo info) {
            return this.getDescr_ref(
                    info.getClassIndex(),
                    info.getNameAndTypeIndex(),
                    ClassFile.Descr_NameAndType.METHOD);
        }

        private String getDescr_ref(final int classindex, final int natindex, final ClassFile.Descr_NameAndType type) {
            final StringBuilder sb = new StringBuilder();
            sb.append(this.getDescr_Class((ConstantClassInfo) ClassFile.this.constant_pool[classindex]));
            sb.append(".");
            sb.append(this.getDescr_NameAndType((ConstantNameAndTypeInfo) ClassFile.this.constant_pool[natindex], type));

            return sb.toString();
        }

        private String getDescr_NameAndType(final ConstantNameAndTypeInfo info, final ClassFile.Descr_NameAndType format) {
            final StringBuilder sb = new StringBuilder();
            String type;

            sb.append(this.getDescr_Utf8((ConstantUtf8Info) ClassFile.this.constant_pool[info.getNameIndex()]));
            sb.append(", ");
            type = this.getDescr_Utf8((ConstantUtf8Info) ClassFile.this.constant_pool[info.getDescriptorIndex()]);

            switch (format) {
                case RAW:
                    sb.append(type);
                    break;

                case FIELD:
                    try {
                        sb.append("type = ");
                        sb.append(SignatureConvertor.signature2Type(type));
                    } catch (SignatureException ex) {
                        Logger.getLogger(ClassFile.class.getName()).log(Level.SEVERE, null, ex);

                        sb.append(type);
                        sb.append(" !!! Un-recognized type");
                    }
                    break;

                case METHOD:
                    final StringBuilder sb_mtd = new StringBuilder();
                    try {
                        sb_mtd.append("parameter = ");
                        sb_mtd.append(SignatureConvertor.parseMethodParameters(type));
                        sb_mtd.append(", returns = ");
                        sb_mtd.append(SignatureConvertor.parseMethodReturnType(type));

                        sb.append(sb_mtd);
                    } catch (SignatureException ex) {
                        Logger.getLogger(ClassFile.class.getName()).log(Level.SEVERE, null, ex);

                        sb.append(type);
                        sb.append(" !!! Un-recognized type");
                    }
                    break;
                default:
                    break;
            }

            return sb.toString();
        }
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Software Developer
United States United States
Deliver useful software to the world.

Comments and Discussions