SirTony
9/23/2014 - 9:34 PM

UI archive compression tool for https://github.com/Evairfairy/PacketRogue

UI archive compression tool for https://github.com/Evairfairy/PacketRogue

/+
Compile with:
    dmd pack.d
    
Options:
    Flag         Type    Required    Default      Description
    ---------------------------------------------------------
    --out        string  yes         <none>       The name of the .ui file to output.
    --dir        string  yes         <none>       The source directory to pack.
    --width      uint    yes         <none>       Window client size width.
    --height     uint    yes         <none>       Window client size height.
    --silent     bool    no          flase        Do not print to the console.
    --recurse    bool    no          true         Resursively search --dir
    --overwrite  bool    no          false        Force overwrite of --out if it already exists.
+/

import std.path,
       std.file,
       std.stream,
       std.zlib,
       std.getopt,
       std.array,
       std.random,
       std.string,
       io = std.stdio;

string output;
string root;
bool overwrite = false;
bool silent = false;
bool recurse = true;
int width = -1;
int height = -1;

enum BUFFER_SIZE = 10_240;
enum ubyte[] MAGIC = [ 0x50, 0x52, 0x55, 0x49 ];
enum ubyte VERSION_MAJOR = 1;
enum ubyte VERSION_MINOR = 0;

void main( string[] args )
{
    args.getopt(
        "out",       &output,
        "dir",       &root,
        "width",     &width,
        "height",    &height,
        "silent",    &silent,
        "recurse",   &recurse,
        "overwrite", &overwrite
    );
    
    if( width <= 0 || height <= 0 )
    {
        io.stderr.writeln( "Width and height must be greater than zero." );
        return;
    }
    
    if( output is null || output.length == 0 )
    {
        io.stderr.writeln( "Output file is required." );
        return;
    }
    
    if( output.exists && output.isDir )
    {
        io.stderr.writeln( "Output is a directory, not a file." );
        return;
    }
    
    if( output.exists && !overwrite )
    {
        io.stdout.writef( "'%s' already exists. Overwrite? [Y/n] ", output );
        auto _in = io.stdin.readln().chomp.strip.toLower;
        
        if( _in.length < 1 || _in[0] != 'y' )
            return;
    }
    
    if( !output.toLower.endsWith( ".ui" ) )
        output ~= ".ui";
    
    if( root is null || ( !root.exists && !root.isDir ) )
    {
        io.stderr.writefln( "The directory '%s' does not exist or is not a directory.",
                            root is null ? "<null>" : root );
        
        return;
    }
    
    if( output.exists )
        output.remove();

    auto mode = !recurse ? SpanMode.shallow : SpanMode.breadth;
    auto entries = root.dirEntries( mode ).array;
    auto tempName = "~" ~ randomString() ~ ".tmp";
    auto arch = new File( output, FileMode.In | FileMode.Out );
    auto temp = new File( tempName, FileMode.In | FileMode.OutNew );
    
    assert( temp.seekable );
    
    Entry[] files;
    
    auto getHeaderSize()
    {
        auto size = 4  //Magic ID length
                  + 2  //version length
                  + 4  //width length
                  + 4  //height length
                  + 4; //count length
        
        if( files.length > 0 )
        {
            foreach( e; files )
            {
                size += 4  //compressed size
                      + 4  //uncompressed size
                      + 4; //offset
                
                size += 2 + e.MIME.length;
                size += 2 + e.Name.length;
            }
        }
        
        return size;
    }
    
    arch.write( MAGIC );
    arch.write( VERSION_MAJOR );
    arch.write( VERSION_MINOR );
    arch.write( width );
    arch.write( height );
    
    while( entries.length )
    {
        auto entry = entries.back;
        entries.popBack();
        
        if( entry.isDir && recurse )
        {
            entries ~= entry.name.dirEntries( mode ).array;
            continue;
        }
        
        string name = entry.name.normalize();
        Entry file = {
            0, 0, 0,
            name.getMimeType(),
            name,
            entry
        };
        
        files ~= file;
    }
    
    arch.write( files.length );
    arch.flush();
    auto headerSize = getHeaderSize();
    
    if( files.length == 0 )
        goto End;
    
    foreach( ref entry; files )
    {
        printfln( "Packing '%s'.", entry.Name );
        
        auto input = new File( entry.path, FileMode.In | FileMode.Out );
        auto compressor = new Compress( 9,  HeaderFormat.deflate );
        auto before = temp.position;
        
        const( void )[] chunk;
        
        while( !input.eof )
        {
            auto left = input.available > BUFFER_SIZE
                        ? BUFFER_SIZE
                        : input.available;
            
            ubyte[] buf = new ubyte[]( cast(uint)left );
            input.read( buf );
            
            chunk = compressor.compress( cast(void[])buf );
            temp.write( cast(ubyte[])chunk );
        }
        
        chunk = compressor.flush();
        temp.write( cast(ubyte[])chunk );
        temp.flush();
        
        entry.UncompressedSize = cast(int)input.size;
        entry.CompressedSize = cast(int)( temp.position - before );
        entry.Offset = cast(int)( headerSize + before );
        
        input.close();
        
        arch.write( entry.CompressedSize );
        arch.write( entry.UncompressedSize );
        arch.write( entry.Offset );

        short len = cast(short)entry.MIME.length;
        ubyte[] buf = cast(ubyte[])entry.MIME.dup;
        
        arch.write( len );
        arch.write( buf );
        
        len = cast(short)entry.Name.length;
        buf = cast(ubyte[])entry.Name.dup;
        
        arch.write( len );
        arch.write( buf );
        
        arch.flush();
    }
    
    temp.seekSet( 0 );
    arch.copyFrom( temp );
    arch.flush();
    
    End:
    temp.close();
    arch.close();
    tempName.remove();
    
    println( "Done." );
}

enum MimeLookup = [
    "ai": "application/postscript",
    "aif": "audio/x-aiff",
    "aifc": "audio/x-aiff",
    "aiff": "audio/x-aiff",
    "asc": "text/plain",
    "atom": "application/atom+xml",
    "au": "audio/basic",
    "avi": "video/x-msvideo",
    "bcpio": "application/x-bcpio",
    "bin": "application/octet-stream",
    "bmp": "image/bmp",
    "cdf": "application/x-netcdf",
    "cgm": "image/cgm",
    "class": "application/octet-stream",
    "cpio": "application/x-cpio",
    "cpt": "application/mac-compactpro",
    "csh": "application/x-csh",
    "css": "text/css",
    "dcr": "application/x-director",
    "dif": "video/x-dv",
    "dir": "application/x-director",
    "djv": "image/vnd.djvu",
    "djvu": "image/vnd.djvu",
    "dll": "application/octet-stream",
    "dmg": "application/octet-stream",
    "dms": "application/octet-stream",
    "doc": "application/msword",
    "dtd": "application/xml-dtd",
    "dv": "video/x-dv",
    "dvi": "application/x-dvi",
    "dxr": "application/x-director",
    "eps": "application/postscript",
    "etx": "text/x-setext",
    "exe": "application/octet-stream",
    "ez": "application/andrew-inset",
    "gif": "image/gif",
    "gram": "application/srgs",
    "grxml": "application/srgs+xml",
    "gtar": "application/x-gtar",
    "hdf": "application/x-hdf",
    "hqx": "application/mac-binhex40",
    "htm": "text/html",
    "html": "text/html",
    "ice": "x-conference/x-cooltalk",
    "ico": "image/x-icon",
    "ics": "text/calendar",
    "ief": "image/ief",
    "ifb": "text/calendar",
    "iges": "model/iges",
    "igs": "model/iges",
    "jnlp": "application/x-java-jnlp-file",
    "jp2": "image/jp2",
    "jpe": "image/jpeg",
    "jpeg": "image/jpeg",
    "jpg": "image/jpeg",
    "js": "application/x-javascript",
    "kar": "audio/midi",
    "latex": "application/x-latex",
    "lha": "application/octet-stream",
    "lzh": "application/octet-stream",
    "m3u": "audio/x-mpegurl",
    "m4a": "audio/mp4a-latm",
    "m4b": "audio/mp4a-latm",
    "m4p": "audio/mp4a-latm",
    "m4u": "video/vnd.mpegurl",
    "m4v": "video/x-m4v",
    "mac": "image/x-macpaint",
    "man": "application/x-troff-man",
    "mathml": "application/mathml+xml",
    "me": "application/x-troff-me",
    "mesh": "model/mesh",
    "mid": "audio/midi",
    "midi": "audio/midi",
    "mif": "application/vnd.mif",
    "mov": "video/quicktime",
    "movie": "video/x-sgi-movie",
    "mp2": "audio/mpeg",
    "mp3": "audio/mpeg",
    "mp4": "video/mp4",
    "mpe": "video/mpeg",
    "mpeg": "video/mpeg",
    "mpg": "video/mpeg",
    "mpga": "audio/mpeg",
    "ms": "application/x-troff-ms",
    "msh": "model/mesh",
    "mxu": "video/vnd.mpegurl",
    "nc": "application/x-netcdf",
    "oda": "application/oda",
    "ogg": "application/ogg",
    "pbm": "image/x-portable-bitmap",
    "pct": "image/pict",
    "pdb": "chemical/x-pdb",
    "pdf": "application/pdf",
    "pgm": "image/x-portable-graymap",
    "pgn": "application/x-chess-pgn",
    "pic": "image/pict",
    "pict": "image/pict",
    "png": "image/png",
    "pnm": "image/x-portable-anymap",
    "pnt": "image/x-macpaint",
    "pntg": "image/x-macpaint",
    "ppm": "image/x-portable-pixmap",
    "ppt": "application/vnd.ms-powerpoint",
    "ps": "application/postscript",
    "qt": "video/quicktime",
    "qti": "image/x-quicktime",
    "qtif": "image/x-quicktime",
    "ra": "audio/x-pn-realaudio",
    "ram": "audio/x-pn-realaudio",
    "ras": "image/x-cmu-raster",
    "rdf": "application/rdf+xml",
    "rgb": "image/x-rgb",
    "rm": "application/vnd.rn-realmedia",
    "roff": "application/x-troff",
    "rtf": "text/rtf",
    "rtx": "text/richtext",
    "sgm": "text/sgml",
    "sgml": "text/sgml",
    "sh": "application/x-sh",
    "shar": "application/x-shar",
    "silo": "model/mesh",
    "sit": "application/x-stuffit",
    "skd": "application/x-koan",
    "skm": "application/x-koan",
    "skp": "application/x-koan",
    "skt": "application/x-koan",
    "smi": "application/smil",
    "smil": "application/smil",
    "snd": "audio/basic",
    "so": "application/octet-stream",
    "spl": "application/x-futuresplash",
    "src": "application/x-wais-source",
    "sv4cpio": "application/x-sv4cpio",
    "sv4crc": "application/x-sv4crc",
    "svg": "image/svg+xml",
    "swf": "application/x-shockwave-flash",
    "t": "application/x-troff",
    "tar": "application/x-tar",
    "tcl": "application/x-tcl",
    "tex": "application/x-tex",
    "texi": "application/x-texinfo",
    "texinfo": "application/x-texinfo",
    "tif": "image/tiff",
    "tiff": "image/tiff",
    "tr": "application/x-troff",
    "tsv": "text/tab-separated-values",
    "txt": "text/plain",
    "ustar": "application/x-ustar",
    "vcd": "application/x-cdlink",
    "vrml": "model/vrml",
    "vxml": "application/voicexml+xml",
    "wav": "audio/x-wav",
    "wbmp": "image/vnd.wap.wbmp",
    "wbmxl": "application/vnd.wap.wbxml",
    "wml": "text/vnd.wap.wml",
    "wmlc": "application/vnd.wap.wmlc",
    "wmls": "text/vnd.wap.wmlscript",
    "wmlsc": "application/vnd.wap.wmlscriptc",
    "wrl": "model/vrml",
    "xbm": "image/x-xbitmap",
    "xht": "application/xhtml+xml",
    "xhtml": "application/xhtml+xml",
    "xls": "application/vnd.ms-excel",
    "xml": "application/xml",
    "xpm": "image/x-xpixmap",
    "xsl": "application/xml",
    "xslt": "application/xslt+xml",
    "xul": "application/vnd.mozilla.xul+xml",
    "xwd": "image/x-xwindowdump",
    "xyz": "chemical/x-xyz",
    "zip": "application/zip"
];

struct Entry
{
    int CompressedSize;
    int UncompressedSize;
    int Offset;
    string MIME;
    string Name;
    string path; //This is an internal field and
                 //won't be written to the archive.
}

string getMimeType( in string file )
{
    auto ext = file.extension.toLower[1 .. $];
    auto mime = ext in MimeLookup;
    
    if( !mime )
        return "application/octet-stream";
    else
        return *mime;
}

string randomString( int length = 15, char[] chars = null )
{
    if( chars is null )
        chars = "ABCDEFGIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz".dup;
    
    string result;
    foreach( _; 0 .. length )
        result ~= chars[uniform( 0, chars.length )];
    
    return result;
}

void printfln( T... )( string format, T args )
{
    if( silent )
        return;

    io.writefln( format, args );
}

void println( T... )( T args )
{
    if( silent )
        return;

    io.writeln( args );
}

string normalize( in string str )
{
    return str[root.length + 1 .. $].replace( "\\", "/" );
}

bool endsWith( in string str, in string value )
{
    if( value.length > str.length )
        return false;
    
    if( value == str )
        return true;
    
    return str[str.length - value.length .. $] == value;
}