crdtes.jsx

/**
 * Creative Developer Tools (CRDT) is a growing suite of tools aimed at script developers and plug-in developers for the Adobe Creative Cloud eco-system.<br>
 * <br>
 * Currently, it is at an alpha stage: the feature set is not frozen, and new features are added regularly.<br>
 * <br>
 * There are two different versions of CRDT: one for UXP/UXPScript and another for ExtendScript.<br>
 * <br>
 * The software is functional and useful, but without a doubt, there will be bugs and dragons…<br>
 * <br>
 * Features include:<br>
 * <br>
 * - Provides a unique machine GUID for each end-user computer<br>
 * - Provides a unique account GUID for each end user<br>
 * - Add licensing and activation features to your script<br>
 * - Protect sensitive source code and make it hard to reverse engineer<br>
 * - AES-256 encryption/decryption functions<br>
 * - Base64 encode and decode functions<br>
 * <br>
 * More to come! You can contact us on dev@rorohiko.com with feature request<br>
 * <br>
 * For downloading and installation info, visit<br>
 * <br>
 * https://CreativeDeveloperTools.com<br>
 *
 * @module crdtes
 * @namespace crdtes
 */

var crdtes = getPlatformGlobals().defineGlobalObject("crdtes");

(function() {

/**
 * The Tightener daemon provides persistent named scopes (similar to persistent ExtendScript engines).<br>
 * <br>
 * When executing multiple TQL scripts in succession a named scope will retain any globals that<br>
 * were defined by a previous script.<br>
 *
 * @constant {string} TQL_SCOPE_NAME_DEFAULT
 */
const TQL_SCOPE_NAME_DEFAULT = "defaultScope";

const STATE_IDLE                               =  0;
const STATE_SEEN_OPEN_SQUARE_BRACKET           =  1;
const STATE_SEEN_NON_WHITE                     =  2;
const STATE_AFTER_NON_WHITE                    =  3;
const STATE_SEEN_EQUAL                         =  4;
const STATE_ERROR                              =  5;
const STATE_SEEN_CLOSE_SQUARE_BRACKET          =  6;
const STATE_IN_COMMENT                         =  7;

const REGEXP_TRIM                              = /^\s*(\S?.*?)\s*$/;
const REGEXP_TRIM_REPLACE                      = "$1";
const REGEXP_DESPACE                           = /\s+/g;
const REGEXP_DESPACE_REPLACE                   = "";
const REGEXP_ALPHA_ONLY                        = /[^-a-zA-Z0-9_$]+/g;
const REGEXP_ALPHA_ONLY_REPLACE                = "";
const REGEXP_SECTION_NAME_ONLY                 = /[^-a-zA-Z0-9_$:]+/g;
const REGEXP_SECTION_NAME_ONLY_REPLACE         = "";
const REGEXP_NUMBER_ONLY                       = /^([\d\.]+).*$/;
const REGEXP_NUMBER_ONLY_REPLACE               = "$1";
const REGEXP_UNIT_ONLY                         = /^[\d\.]+\s*(.*)$/;
const REGEXP_UNIT_ONLY_REPLACE                 = "$1";
const REGEXP_PICAS                             = /^([\d]+)p(([\d]*)(\.([\d]+)?)?)?$/;
const REGEXP_PICAS_REPLACE                     = "$1";
const REGEXP_PICAS_POINTS_REPLACE              = "$2";
const REGEXP_CICEROS                           = /^([\d]+)c(([\d]*)(\.([\d]+)?)?)?$/;
const REGEXP_CICEROS_REPLACE                   = "$1";
const REGEXP_CICEROS_POINTS_REPLACE            = "$2";

/**
 * <code>crdtes.UNIT_NAME_NONE</code> represents unit-less values.
 */
crdtes.UNIT_NAME_NONE                           = "NONE";

/**
 * <code>crdtes.UNIT_NAME_INCH</code> for inches.
 */
crdtes.UNIT_NAME_INCH                           = "\"";

/**
 * <code>crdtes.UNIT_NAME_CM</code> for centimeters
 */
crdtes.UNIT_NAME_CM                             = "cm";

/**
 * <code>crdtes.UNIT_NAME_MM</code> for millimeters
 */
crdtes.UNIT_NAME_MM                             = "mm";

/**
 * <code>crdtes.UNIT_NAME_CICERO</code> for ciceros
 */
crdtes.UNIT_NAME_CICERO                         = "cicero";

/**
 * <code>crdtes.UNIT_NAME_PICA</code> for picas
 */
crdtes.UNIT_NAME_PICA                           = "pica";

/**
 * <code>crdtes.UNIT_NAME_PIXEL</code> for pixels
 */
crdtes.UNIT_NAME_PIXEL                          = "px";

/**
 * <code>crdtes.UNIT_NAME_POINT</code> for points
 */
crdtes.UNIT_NAME_POINT                          = "pt";

crdtes.IS_MAC = $.os.substring(0,3).toLowerCase() == "mac";
crdtes.IS_WINDOWS = ! crdtes.IS_MAC;

/**
 * Setting log level to <code>crdtes.LOG_LEVEL_OFF</code> causes all log output to be suppressed.
 *
 * @constant {number} LOG_LEVEL_OFF
 * 
 */
const LOG_LEVEL_OFF = 0;
crdtes.LOG_LEVEL_OFF = LOG_LEVEL_OFF;

/**
 * Setting log level to <code>crdtes.LOG_LEVEL_ERROR</code> causes all log output to be suppressed,<br>
 * except for errors.
 *
 * @constant {number} LOG_LEVEL_ERROR
 */
const LOG_LEVEL_ERROR = 1;
crdtes.LOG_LEVEL_ERROR = LOG_LEVEL_ERROR;

/**
 * Setting log level to <code>crdtes.LOG_LEVEL_WARNING</code> causes all log output to be suppressed,<br>
 * except for errors and warnings.
 *
 * @constant {number} LOG_LEVEL_WARNING
 */
const LOG_LEVEL_WARNING = 2;
crdtes.LOG_LEVEL_WARNING = LOG_LEVEL_WARNING;

/**
 * Setting log level to <code>crdtes.LOG_LEVEL_NOTE</code> causes all log output to be suppressed,<br>
 * except for errors, warnings and notes.
 *
 * @constant {number} LOG_LEVEL_NOTE
 */
const LOG_LEVEL_NOTE = 3;
crdtes.LOG_LEVEL_NOTE = LOG_LEVEL_NOTE;

/**
 * Setting log level to <code>crdtes.LOG_LEVEL_TRACE</code> causes all log output to be output.
 *
 * @constant {number} LOG_LEVEL_TRACE
 */
const LOG_LEVEL_TRACE = 4;
crdtes.LOG_LEVEL_TRACE = LOG_LEVEL_TRACE;

// Symbolic params to `getDir()`

/**
 * Pass <code>crdtes.DESKTOP_DIR</code> into <code>crdtes.getDir()</code> to get the path of the user's Desktop folder.
 *
 * @constant {string} DESKTOP_DIR
 */
crdtes.DESKTOP_DIR    = "DESKTOP_DIR";

/**
 * Pass <code>crdtes.DOCUMENTS_DIR</code> into <code>crdtes.getDir()</code> to get the path of the user's Documents folder.
 *
 * @constant {string} DOCUMENTS_DIR
 */
crdtes.DOCUMENTS_DIR  = "DOCUMENTS_DIR";

/**
 * Pass <code>crdtes.HOME_DIR</code> into <code>crdtes.getDir()</code> to get the path of the user's home folder.
 *
 * @constant {string} HOME_DIR
 */
crdtes.HOME_DIR       = "HOME_DIR";

/**
 * Pass <code>crdtes.LOG_DIR</code> into <code>crdtes.getDir()</code> to get the path of the Tightener logging folder.
 *
 * @constant {string} LOG_DIR
 */
crdtes.LOG_DIR        = "LOG_DIR";

/**
 * Pass <code>crdtes.SYSTEMDATA_DIR</code> into <code>crdtes.getDir()</code> to get the path of the system data folder<br>
 * (<code>%PROGRAMDATA%</code> or <code>/Library/Application Support</code>).
 *
 * @constant {string} SYSTEMDATA_DIR
 */
crdtes.SYSTEMDATA_DIR = "SYSTEMDATA_DIR";

/**
 * Pass <code>crdtes.TMP_DIR</code> into <code>crdtes.getDir()</code> to get the path of the temporary folder.
 *
 * @constant {string} TMP_DIR
 */
crdtes.TMP_DIR        = "TMP_DIR";

/**
 * Pass <code>crdtes.USERDATA_DIR</code> into <code>crdtes.getDir()</code> to get the path to the user data folder<br>
 * (<code>%APPDATA%</code> or <code>~/Library/Application Support</code>).
 *
 * @constant {string} USERDATA_DIR
 */
crdtes.USERDATA_DIR   = "USERDATA_DIR";

var LOG_LEVEL_STACK           = [];
var LOG_ENTRY_EXIT            = false;
var LOG_LEVEL                 = LOG_LEVEL_OFF;
var IN_LOGGER                 = false;
var LOG_TO_ESTK_CONSOLE       = true;
var LOG_TO_CRDT               = false;
var LOG_TO_FILEPATH           = undefined;

var SYS_INFO;

/**
 * Decode a string that was encoded using base64.
 *
 * @function base64decode
 * @memberof crdtes
 *
 * @param {string} base64Str - base64 encoded string
 * @returns {string} decoded string
 */
function base64decode(base64Str) {

    // ExtendScript DLL interface does not handle binary zeroes, we need to enquote and dequote
    // strings to be passed through the API

    var retVal = binaryUTF8ToStr(deQuote(crdtesDLL.base64decode(base64Str)));

    return retVal;
}
crdtes.base64decode = base64decode;

/**
 * Encode a string or an array of bytes using Base 64 encoding.
 *
 * @function base64encode
 * @memberof crdtes
 *
 * @param {string} str_or_ByteArr - either a string or an array containing bytes (0-255).
 * @returns {string} encoded string
 *
 */
function base64encode(str_or_ByteArr) {

    var byteArray;
    if ("string" == typeof(str_or_ByteArr)) {
        byteArray = strToUTF8(str_or_ByteArr);
    }
    else {
        byteArray = str_or_ByteArr;
    }

    // ExtendScript DLL interface does not handle binary zeroes in strings, we need to enquote and dequote
    // strings to be passed through the API

    var retVal = crdtesDLL.base64encode(dQ(byteArray));

    return retVal;
}
crdtes.base64encode = base64encode;

/**
 * Convert an array of bytes into string format. This string is UTF-16 internally, and<br>
 * we map one byte to one UTF-16 character. The resulting string might contain character values<br>
 * (<code>charCodeAt()</code>) that would be invalid in UTF8.<br>
 *
 * @function binaryToStr
 * @memberof crdtes 
 *
 * @param {array} in_byteArray - an array containing UTF-16 values in the range 0-255
 * @returns {string} a string
 */
function binaryToStr(in_byteArray) {

    var retVal = "";

    try {
        var idx = 0;
        var len = in_byteArray.length;
        var c;
        while (idx < len) {
            // byte is a reserved word - so we use bite
            var bite = in_byteArray[idx];
            retVal += String.fromCharCode(bite);
        }
    }
    catch (err) {
        retVal = undefined;
    }

    return retVal;
}
crdtes.binaryToStr = binaryToStr;

/**
 * Decode an array of bytes that contains a UTF-8 encoded string.
 *
 * @function binaryUTF8ToStr
 * @memberof crdtes 
 *
 * @param {array} in_byteArray - an array containing bytes (0-255) for a string that was encoded using UTF-8 encoding.
 * @returns {string} a string, or undefined if some invalid UTF-8 is encountered
 */
function binaryUTF8ToStr(in_byteArray) {

    var retVal = "";

    try {
        var idx = 0;
        var len = in_byteArray.length;
        var c;
        while (idx < len) {
            // byte is a reserved word - so we use bite
            var bite = in_byteArray[idx];
            idx++;
            var bit7 = bite >> 7;
            if (! bit7) {
                // U+0000 - U+007F
                c = String.fromCharCode(bite);
            }
            else {
                var bit6 = (bite & 0x7F) >> 6;
                if (! bit6) {
                    // Invalid
                    retVal = undefined;
                    break;
                }
                else {
                    var byte2 = in_byteArray[idx];
                    idx++;
                    var bit5 = (bite & 0x3F) >> 5;
                    if (! bit5) {
                        // U+0080 - U+07FF
                        c = String.fromCharCode(((bite & 0x1F) << 6) | (byte2 & 0x3F));
                    }
                    else {
                        var byte3 = in_byteArray[idx];
                        idx++;
                        var bit4 = (bite & 0x1F) >> 4;
                        if (! bit4) {
                            // U+0800 - U+FFFF
                            c = String.fromCharCode(((bite & 0x0F) << 12) | ((byte2 & 0x3F) << 6) | (byte3 & 0x3F));
                        }
                        else {
                            // Not handled U+10000 - U+10FFFF
                            retVal = undefined;
                            break;
                        }
                    }
                }
            }
            retVal += c;
        }
    }
    catch (err) {
        retVal = undefined;
    }

    return retVal;
}
crdtes.binaryUTF8ToStr = binaryUTF8ToStr;

// charCodeToUTF8__: internal function: convert a Unicode character code to a 1 to 3 byte UTF8 byte sequence
// returns undefined if invalid in_charCode

function charCodeToUTF8__(in_charCode) {

    var retVal = undefined;

    try {

        if (in_charCode <= 0x007F) {
            retVal = [];
            retVal.push(in_charCode);
        }
        else if (in_charCode <= 0x07FF) {
            var hi = 0xC0 + ((in_charCode >> 6) & 0x1F);
            var lo = 0x80 + ((in_charCode      )& 0x3F);
            retVal = [];
            retVal.push(hi);
            retVal.push(lo);
        }
        else {
            var hi =  0xE0 + ((in_charCode >> 12) & 0x1F);
            var mid = 0x80 + ((in_charCode >>  6) & 0x3F);
            var lo =  0x80 + ((in_charCode      ) & 0x3F);
            retVal = [];
            retVal.push(hi);
            retVal.push(mid);
            retVal.push(lo);
        }
    }
    catch (err) {
        // anything weird, we return undefined
        retVal = undefined;
    }

    return retVal;
}

/**
 * Configure the logger
 *
 * @function configLogger
 * @memberof crdtes 
 *
 * @param {object} logInfo - object with logger setup info<code>{<br>
 *     logLevel: 0-4<br>
 *     logEntryExit: boolean<br>
 *     logToESTKConsole: boolean<br>
 *     logToCRDT: boolean<br>
 *     logToFilePath: undefined or a file path for logging<br>
 * }</code>
 *
 * @returns {boolean} success/failure
 */
function configLogger(logInfo) {

    var retVal = false;
    try {
        if (logInfo) {
            if ("logLevel" in logInfo) {
                LOG_LEVEL = logInfo.logLevel;
            }
            if ("logEntryExit" in logInfo) {
                LOG_ENTRY_EXIT = logInfo.logEntryExit;
            }
            if ("logToESTKConsole" in logInfo) {
                LOG_TO_ESTK_CONSOLE = logInfo.logToESTKConsole;
            }
            if ("logToCRDT" in logInfo) {
                LOG_TO_CRDT = logInfo.logToCRDT;
            }
            if ("logToFilePath" in logInfo) {
                LOG_TO_FILEPATH = logInfo.logToFilePath;
            }
            retVal = true;
        }
    }
    catch (err) {
    }

    return retVal;
}
crdtes.configLogger = configLogger;

/**
 * Reverse the operation of the <code>crdtes.encrypt()</code> function.
 *
 * Only available to paid developer accounts
 *
 * @function decrypt
 * @memberof crdtes 
 *
 * @param {string} str_or_ByteArr - a string or an array of bytes
 * @param {string} aesKey - a string or an array of bytes
 * @returns {array} an array of bytes
 */

function decrypt(str_or_ByteArr, aesKey, aesIV) {

    var byteArray;
    if ("string" == typeof(str_or_ByteArr)) {
        byteArray = strToUTF8(str_or_ByteArr);
    }
    else {
        byteArray = str_or_ByteArr;
    }

    if (! aesIV) {
        aesIV = aesKey;
    }

    var aesKeyByteArray = strToUTF8(aesKey);
    var aesIVByteArray = strToUTF8(aesIV);

    // ExtendScript DLL interface does not handle binary zeroes in strings, we need to enquote and dequote
    // strings to be passed through the API

    var retVal = binaryUTF8ToStr(deQuote(crdtesDLL.decryptStr(dQ(byteArray), dQ(aesKeyByteArray), dQ(aesIVByteArray))));

    return retVal;
}
crdtes.decrypt = decrypt;

/**
 * Reverse the operation of <code>crdtes.dQ()</code> or <code>crdtes.sQ()</code>.
 *
 * @function deQuote
 * @memberof crdtes 
 *
 * @param {string} quotedString - a quoted string
 * @returns {array} a byte array. If the quoted string contains any <code>\uHHHH</code> codes, these are first re-encoded<br>
 * using UTF-8 before storing them into the byte array.
 */
function deQuote(quotedString) {

    var retVal = [];

    do {

        var qLen = quotedString.length;
        if (qLen < 2) {
            break;
        }

        var quoteChar = quotedString.charAt(0);
        qLen -= 1;
        if (quoteChar != quotedString.charAt(qLen)) {
            break;
        }

        if (quoteChar != '"' && quoteChar != "'") {
            break;
        }

        var buffer = [];
        var state = 0;
        var cCode = 0;
        for (charIdx = 1; charIdx < qLen; charIdx++) {

            if (state == -1) {
                break;
            }

            var c = quotedString.charAt(charIdx);
            switch (state) {
            case 0:
                if (c == '\\') {
                    state = 1;
                }
                else {
                    buffer.push(c.charCodeAt(0));
                }
                break;
            case 1:
                if (c == 'x') {
                    // state 2->3->0
                    state = 2;
                }
                else if (c == 'u') {
                    // state 4->5->6->7->0
                    state = 4;
                }
                else if (c == 't') {
                    buffer.push(0x09);
                    state = 0;
                }
                else if (c == 'r') {
                    buffer.push(0x0D);
                    state = 0;
                }
                else if (c == 'n') {
                    buffer.push(0x0A);
                    state = 0;
                }
                else {
                    buffer.push(c.charCodeAt(0));
                    state = 0;
                }
                break;
            case 2:
            case 4:
                if (c >= '0' && c <= '9') {
                    cCode = c.charCodeAt(0)      - 0x30;
                    state++;
                }
                else if (c >= 'A' && c <= 'F') {
                    cCode = c.charCodeAt(0) + 10 - 0x41;
                    state++;
                }
                else if (c >= 'a' && c <= 'f') {
                    cCode = c.charCodeAt(0) + 10 - 0x61;
                    state++;
                }
                else {
                    state = -1;
                }
                break;
            case 3:
            case 5:
            case 6:
            case 7:

                if (c >= '0' && c <= '9') {
                    cCode = (cCode << 4) + c.charCodeAt(0)      - 0x30;
                }
                else if (c >= 'A' && c <= 'F') {
                    cCode = (cCode << 4) + c.charCodeAt(0) + 10 - 0x41;
                }
                else if (c >= 'a' && c <= 'f') {
                    cCode = (cCode << 4) + c.charCodeAt(0) + 10 - 0x61;
                }
                else {
                    state = -1;
                }

                if (state == 3)  {
                    // Done with \xHH
                    buffer.push(cCode);
                    state = 0;
                }
                else if (state == 7) {
                    // Done with \uHHHHH - convert using UTF-8
                    var bytes = charCodeToUTF8__(cCode);
                    if (! bytes) {
                        state = -1
                    }
                    else {
                        for (var byteIdx = 0; byteIdx < bytes.length; byteIdx++) {
                            buffer.push(bytes[byteIdx]);
                        }
                        state = 0;
                    }
                }
                else {
                    // Next state: 2->3, 4->5->6->7
                    state++;
                }
                break;
            }
        }
    }
    while (false);

    if (state == 0) {
        retVal = buffer;
    }

    return retVal;
}
crdtes.deQuote = deQuote;

/**
 * Create a directory.<br>
 * <br>
 * Not restricted by the UXP security sandbox. Not needed for pure ExtendScript -<br>
 * provided to offer some compatibility with the UXP version of CRDT
 *
 * @function dirCreate
 * @memberof crdtes 
 *
 * @param {string} filePath
 * @returns {array} list if items in directory
 */

function dirCreate(filePath) {

    var retVal = evalTQL("dirCreate(" + dQ(filePath) + ")");

    return retVal;
}
crdtes.dirCreate = dirCreate;

/**
 * Delete a directory.<br>
 * <br>
 * Not restricted by the UXP security sandbox. Not needed for pure ExtendScript -<br>
 * provided to offer some compatibility with the UXP version of CRDT
 *
 * @function dirDelete
 * @memberof crdtes 
 *
 * @param {string} filePath
 * @param {boolean} recurse
 * @returns {boolean} success or failure
 */

function dirDelete(filePath, recurse) {

    var retVal;

    retVal = evalTQL("dirDelete(" + dQ(filePath) + "," + (recurse ? "true" : "false") + ")");

    return retVal;
}
crdtes.dirDelete = dirDelete;

/**
 * Verify whether a directory exists. Will return <code>false</code> if the path points to a file (instead of a directory).<br>
 * <br>
 * Also see <code>crdtes.fileExists()</code>.<br>
 * <br>
 * Not restricted by the UXP security sandbox. Not needed for pure ExtendScript -<br>
 * provided to offer some compatibility with the UXP version of CRDT
 *
 * @function dirExists
 * @memberof crdtes 
 *
 * @param {string} dirPath - a path to a directory
 * @returns {boolean} true or false
 */

function dirExists(dirPath) {

    var retVal = evalTQL("dirExists(" + dQ(dirPath) + ")");

    return retVal;
}
crdtes.dirExists = dirExists;

/**
 * Scan a directory.<br>
 * <br>
 * Not restricted by the UXP security sandbox. Not needed for pure ExtendScript -<br>
 * provided to offer some compatibility with the UXP version of CRDT
 *
 * @function dirScan
 * @memberof crdtes 
 *
 * @param {string} filePath
 * @returns {array} list if items in directory
 */

function dirScan(filePath) {

    var retVal = evalTQL("dirScan(" + dQ(filePath) + ")");

    return retVal;
}
crdtes.dirScan = dirScan;

/**
 * Wrap a string or a byte array into double quotes, encoding any binary data as a string.<br>
 * Knows how to handle Unicode characters or binary zeroes.<br>
 * <br>
 * When the input is a string, high Unicode characters are encoded as `\uHHHH`.<br>
 * <br>
 * When the input is a byte array, all bytes are encoded as characters or as `\xHH` escape sequences.
 *
 * @function dQ
 * @memberof crdtes 
 *
 * @param {string} str_or_ByteArr - a Unicode string or an array of bytes
 * @returns {string} a string enclosed in double quotes. This string is pure 7-bit<br>
 * ASCII and can be inserted into generated script code<br>
 * Example:<br>
 * <code>var script = "a=b(" + crdtes.dQ(somedata) + ");";</code>
 */
function dQ(str_or_ByteArr) {
    return enQuote__(str_or_ByteArr, "\"");
}
crdtes.dQ = dQ;

/**
 * Encrypt a string or array of bytes using a key. A random salt is added into the mix,<br>
 * so even when passing in the same parameter values, the result will be different every time.<br>
 * <br>
 * Only available to paid developer accounts
 * 
 * @function encrypt
 * @memberof crdtes 
 *
 * @param {string} str_or_ByteArr - a string or an array of bytes
 * @param {string} aesKey - a string or an array of bytes
 * @returns {string} a base-64 encoded encrypted string.
 */

function encrypt(str_or_ByteArr, aesKey, aesIV) {

    var retVal;

    var s;
    if ("string" == typeof(str_or_ByteArr)) {
        s = str_or_ByteArr;
    }
    else {
        s = binaryToStr(str_or_ByteArr);
    }

    var aesKeyByteArray = strToUTF8(aesKey);

    var aesIVByteArray;
    if (! aesIV) {
        aesIVByteArray = aesKeyByteArray;
    }
    else {
        aesIVByteArray = strToUTF8(aesIV);
    }

    // result is not quoted - it's plain base64
    var retVal = crdtesDLL.encryptStr(dQ(s), dQ(aesKeyByteArray), dQ(aesIVByteArray));

    return retVal;
}
crdtes.encrypt = encrypt;

//
// enQuote__: Internal helper function. Escape and wrap a string in quotes
//
function enQuote__(str_or_ByteArr, quoteChar) {

    var retVal = "";

    var quoteCharCode = quoteChar.charCodeAt(0);

    var isString = ("string" == typeof str_or_ByteArr);
    var escapedS = "";
    var sLen = str_or_ByteArr.length;
    for (var charIdx = 0; charIdx < sLen; charIdx++) {
        var cCode;
        if (isString) {
            cCode = str_or_ByteArr.charCodeAt(charIdx);
        }
        else {
            cCode = str_or_ByteArr[charIdx];
        }
        if (cCode == 0x5C) {
            escapedS += '\\\\';
        }
        else if (cCode == quoteCharCode) {
            escapedS += '\\' + quoteChar;
        }
        else if (cCode == 0x0A) {
            escapedS += '\\n';
        }
        else if (cCode == 0x0D) {
            escapedS += '\\r';
        }
        else if (cCode == 0x09) {
            escapedS += '\\t';
        }
        else if (cCode < 32 || cCode == 0x7F || (! isString && cCode >= 0x80)) {
            escapedS += "\\x" + toHex(cCode, 2);
        }
        else if (isString && cCode >= 0x80) {
            escapedS += "\\u" + toHex(cCode, 4);
        }
        else {
            escapedS += String.fromCharCode(cCode);
        }
    }

    retVal = quoteChar + escapedS + quoteChar;

    return retVal;
}

/**
 * Evaluate a script file. If the unencrypted script file is not available (`.jsx` or `.js`),<br>
 * use crdtesDLL to try and run an `.ejsx` or `.ejs` file.
 *
 * @function evalScript
 * @memberof crdtes 
 *
 * @param {string} scriptName - the name of the script to run, without file name extension or parent directory
 * @param {string} parentScriptFile - the name of the script from which we're calling this (pass in <code>$.fileName</code>).<br>
 * If this is missing, evaluate the path relative to the parent of CreativeDeveloperTools_ES
 * @returns {any} the returned value
 */
function evalScript(scriptName, parentScriptFile) {

    var retVal = undefined;

    try {

        var parentScriptFolder;
        if (! parentScriptFile) {
            // Use parent of parent of crdtes.jsx
            parentScriptFolder = File($.fileName).parent.parent;
        }
        else {
            if ("string" == typeof(parentScriptFile)) {
                parentScriptFile = File(parentScriptFile);
            }
            parentScriptFolder = parentScriptFile.parent;
        }

        var hasEncryptedFileNameExtension = false;
        var hasJSFileNameExtension = false;
        var hasJSXFileNameExtension = false;
        var scriptNameWithoutExtension = scriptName;

        var splitScriptName = scriptName.split(".");
        if (splitScriptName.length > 1) {
            var fileNameExtension = splitScriptName.pop().toLowerCase();
            if (fileNameExtension == "js") {
                hasJSFileNameExtension = true;
                scriptNameWithoutExtension = splitScriptName.join(".");            
            }
            else if (fileNameExtension == "ejs") {
                hasEncryptedFileNameExtension = true;
                hasJSFileNameExtension = true;
                scriptNameWithoutExtension = splitScriptName.join(".");            
            }
            else if (fileNameExtension == "jsx") {
                hasJSXFileNameExtension = true;
                scriptNameWithoutExtension = splitScriptName.join(".");            
            }
            else if (fileNameExtension == "ejsx") {
                hasEncryptedFileNameExtension = true;
                hasJSXFileNameExtension = true;
                scriptNameWithoutExtension = splitScriptName.join(".");            
            }
        }

        var unencryptedScriptFile = undefined;
        if (hasJSXFileNameExtension) {
            unencryptedScriptFile = File(parentScriptFolder + "/" + scriptNameWithoutExtension + ".jsx");
            if (! unencryptedScriptFile.exists) {
                unencryptedScriptFile = undefined;
            }
        }
        else if (hasJSFileNameExtension) {
            unencryptedScriptFile = File(parentScriptFolder + "/" + scriptNameWithoutExtension + ".js");
            if (! unencryptedScriptFile.exists) {
                unencryptedScriptFile = undefined;
            }
        }
        else {
            var unencryptedScriptFile = File(parentScriptFolder + "/" + scriptNameWithoutExtension + ".jsx");
            if (! unencryptedScriptFile.exists) {
                unencryptedScriptFile = File(parentScriptFolder + "/" + scriptNameWithoutExtension + ".js");
                if (! unencryptedScriptFile.exists) {
                    unencryptedScriptFile = undefined;
                }
            }
        }

        if (unencryptedScriptFile) {
            var nearlyForever = 365*24*3600*1000;
            $.evalFile(unencryptedScriptFile,nearlyForever);
        }
        else {
            crdtesDLL.evalScript(scriptNameWithoutExtension, parentScriptFolder.fsName);
        }

    }
    catch (e) {
    }

    return retVal;
}
crdtes.evalScript = evalScript;

/**
 * Send a TQL script to the DLL
 *
 * @function evalTQL
 * @memberof crdtes 
 *
 * @param {string} tqlScript - a script to run
 * @param {string} tqlScopeName - a scope name to use. Such scope can be used to pass data between different processes
 * @returns {any} the returned value
 */
function evalTQL(tqlScript, tqlScopeName) {

    var retVal = undefined;

    try {

        if (! tqlScopeName) {
            tqlScopeName = TQL_SCOPE_NAME_DEFAULT;
        }

        var result = crdtesDLL.evalTQL(tqlScript, tqlScopeName);
        eval("retVal = " + result);
    } catch (e) {
    }

    return retVal;
}
crdtes.evalTQL = evalTQL;

/**
 * Close a currently open file.<br>
 * <br>
 * Not restricted by the UXP security sandbox. Not needed for pure ExtendScript -<br>
 * provided to offer some compatibility with the UXP version of CRDT<br>
 *
 * @function fileClose
 * @memberof crdtes 
 *
 * @param {number} fileHandle - a file handle as returned by <code>crdtes.fileOpen()</code>.
 * @returns {boolean} success or failure
 */

function fileClose(fileHandle) {

    var retVal = evalTQL("fileClose(" + fileHandle + ")");

    return retVal;
}
crdtes.fileClose = fileClose;

/**
 * Delete a file<br>
 * <br>
 * Not restricted by the UXP security sandbox. Not needed for pure ExtendScript -<br>
 * provided to offer some compatibility with the UXP version of CRDT
 *
 * @function fileDelete
 * @memberof crdtes 
 *
 * @param {string} filePath
 * @returns {boolean} success or failure
 */

function fileDelete(filePath) {

    var retVal = evalTQL("fileDelete(" + dQ(filePath) + ")");

    return retVal;
}
crdtes.fileDelete = fileDelete;

/**
 * Check if a file exists. Will return <code>false</code> if the file path points to a directory.<br>
 * <br>
 * Also see <code>crdtes.dirExists()</code>.<br>
 * <br>
 * Not restricted by the UXP security sandbox. Not needed for pure ExtendScript -<br>
 * provided to offer some compatibility with the UXP version of CRDT
 *
 * @function fileExists
 * @memberof crdtes 
 *
 * @param {string} filePath
 * @returns {boolean} existence of file
 */

function fileExists(filePath) {

    var retVal = evalTQL("fileExists(" + dQ(filePath) + ")");

    return retVal;
}
crdtes.fileExists = fileExists;

/**
 * Open a binary file and return a handle.<br>
 * <br>
 * Not restricted by the UXP security sandbox. Not needed for pure ExtendScript -<br>
 * provided to offer some compatibility with the UXP version of CRDT<br>
 *
 * @function fileOpen
 * @memberof crdtes 
 *
 * @param {string} fileName - a native full file path to the file
 * @param {string} mode - one of <code>'a'</code>, <code>'r'</code>, <code>'w'</code> (append, read, write)
 * @returns {number} file handle
 */

function fileOpen(fileName, mode) {

    var retVal;

    if (mode) {
        retVal = evalTQL("fileOpen(" + dQ(fileName) + "," + dQ(mode) + ")");
    }
    else {
        retVal = evalTQL("fileOpen(" + dQ(fileName) + ")");
    }

    return retVal;
}
crdtes.fileOpen = fileOpen;

/**
 * Read a file into memory<br>
 * <br>
 * Not restricted by the UXP security sandbox. Not needed for pure ExtendScript -<br>
 * provided to offer some compatibility with the UXP version of CRDT
 *
 * @function fileRead
 * @memberof crdtes 
 *
 * @param {number} fileHandle - a file handle as returned by <code>crdtes.fileOpen()</code>.
 * @param {boolean} isBinary - whether the file is considered a binary file (as opposed to a UTF-8 text file)
 * @returns {any} either a byte array or a string
 */

function fileRead(fileHandle, isBinary) {

    var retVal;

    var response = evalTQL("fileRead(" + fileHandle + ")");
    if ("string" == typeof response) {
        response = unwrapUTF16ToUTF8__(response, isBinary);
    }

    retVal = response;

    return retVal;
}
crdtes.fileRead = fileRead;

/**
 * Binary write to a file. Strings are written as UTF-8<br>
 * <br>
 * Not restricted by the UXP security sandbox. Not needed for pure ExtendScript -<br>
 * provided to offer some compatibility with the UXP version of CRDT
 *
 * @function fileWrite
 * @memberof crdtes 
 *
 * @param {number} fileHandle - a file handle as returned by <code>crdtes.fileOpen()</code>.
 * @param {string} str_or_ByteArr - data to write to the file
 * @returns {boolean} success or failure
 */

function fileWrite(fileHandle, str_or_ByteArr) {

    var byteArray;
    if ("string" == typeof str_or_ByteArr) {
        byteArray = strToUTF8(str_or_ByteArr);
    }
    else {
        byteArray = str_or_ByteArr;
    }

    var retVal = evalTQL("fileWrite(" + fileHandle + "," + dQ(byteArray) + ")");
    return retVal;
}
crdtes.fileWrite = fileWrite;

/**
 * Determine whether, or which, features of some software or module are currently activated or not
 *
 * @function getCapability
 * @memberof crdtes 
 *
 * @param {string} issuer - a GUID identifier for the developer account as seen in the PluginInstaller
 * @param {string} capabilityCode - a code for the software features to be activated (as determined by<br>
 * the developer who owns the account).<br>
 * <code>capabilityCode</code> is not the same as <code>orderProductCode</code> - there can be multiple <code>orderProductCode</code> associated with
 * a single <code>capabilityCode</code> (e.g. <code>capabilityCode</code>: 'XYZ', <code>orderProductCode</code>: 'XYZ_1YEAR', 'XYZ_2YEAR'...).
 * @param {string} encryptionKey - the secret encryption key (created by the developer) needed to decode<br>
 * the capability data. As a developer you want to make sure this encryptionKey is obfuscated and only contained<br>
 * within encrypted script code.
 * @returns {string} either <code>"NOT_ACTIVATED"</code> or a JSON structure with capability data (customer GUID, decrypted developer-provided data from the activation file).
 */
function getCapability(issuer, capabilityCode, encryptionKey) {

    var retVal = crdtesDLL.getCapability(issuer, capabilityCode, encryptionKey);

    return retVal;
}
crdtes.getCapability = getCapability;

/**
 * Get the path of a system directory<br>
 * <br>
 * Not restricted by the UXP security sandbox. Not needed for pure ExtendScript -<br>
 * provided to offer some compatibility with the UXP version of CRDT<br>
 *
 * @function getDir
 * @memberof crdtes 
 *
 * @param {string} dirTag - a tag representing the dir:<br>
 * <code><br>
 *    crdtes.DESKTOP_DIR<br>
 *    crdtes.DOCUMENTS_DIR<br>
 *    crdtes.HOME_DIR<br>
 *    crdtes.LOG_DIR<br>
 *    crdtes.SYSTEMDATA_DIR<br>
 *    crdtes.TMP_DIR<br>
 *    crdtes.USERDATA_DIR<br>
 * </code>
 * @returns {string} file path of dir or <code>undefined</code>. Directory paths include a trailing slash or backslash
 */
function getDir(dirTag) {

    var retVal;

    var sysInfo = getSysInfo__();
    if (dirTag in sysInfo) {
        retVal = sysInfo[dirTag];
    }

    return retVal;
}
crdtes.getDir = getDir;

/**
 * Access the environment<br>
 * <br>
 * Not restricted by the UXP security sandbox. Not needed for pure ExtendScript -<br> 
 * provided to offer some compatibility with the UXP version of CRDT
 *
 * @function getEnvironment
 * @memberof crdtes 
 *
 * @param {string} envVarName - name of environment variable
 * @returns {string} environment variable value
 */
function getEnvironment(envVarName) {

    var retVal = $.getenv(envVarName);

    return retVal;
}
crdtes.getEnvironment = getEnvironment;

/**
 * Interpret a value extracted from some INI data as a boolean. Things like <code>y, n, yes, no, true, false, t, f, 0, 1</code>
 *
 * @function getBooleanFromINI
 * @memberof crdtes 
 *
 * @param {string} in_value - ini value
 * @returns {boolean} value
 */

function getBooleanFromINI(in_value) {
    var retVal = false;

    if (in_value) {
        var value = (in_value + "").replace(REGEXP_TRIM, REGEXP_TRIM_REPLACE);
        var firstChar = value.charAt(0).toLowerCase();
        var firstValue = parseInt(firstChar, 10);
        retVal = firstChar == "y" || firstChar == "t" || (! isNaN(firstValue) && firstValue != 0);
    }

    return retVal;
}
crdtes.getBooleanFromINI = getBooleanFromINI;

/**
 * Interpret a string extracted from some INI data as a floating point value, followed by an optional unit<br>
 * If there is no unit, then no conversion is performed.
 *
 * @function getFloatWithUnitFromINI
 * @memberof crdtes 
 *
 * @param {string} in_valueStr - ini value
 * @param {string} in_convertToUnit - unit to convert to
 * @returns {number} value
 */

function getFloatWithUnitFromINI(in_valueStr, in_convertToUnit) {

    var retVal = 0.0;

    do {

        if (! in_valueStr) {
            break;
        }

        var convertToUnit;
        if (in_convertToUnit) {
            convertToUnit = in_convertToUnit;
        }
        else {
            convertToUnit = crdtes.UNIT_NAME_NONE;
        }

        var sign = 1.0;

        var valueStr = in_valueStr.replace(REGEXP_DESPACE, REGEXP_DESPACE_REPLACE).toLowerCase();

        var firstChar = valueStr.charAt(0);
        if (firstChar == '-') {
            valueStr = valueStr.substring(1);
            sign = -1.0;
        }
        else if (firstChar == '+') {
            valueStr = valueStr.substring(1);
        }

        var picas = undefined;
        var ciceros = undefined;
        if (valueStr.match(REGEXP_PICAS)) {
            picas = parseInt(valueStr.replace(REGEXP_PICAS, REGEXP_PICAS_REPLACE), 10);
            valueStr = valueStr.replace(REGEXP_PICAS, REGEXP_PICAS_POINTS_REPLACE);
        }
        else if (valueStr.match(REGEXP_CICEROS)) {
            ciceros = parseInt(valueStr.replace(REGEXP_CICEROS, REGEXP_CICEROS_REPLACE), 10);
            valueStr = valueStr.replace(REGEXP_CICEROS, REGEXP_CICEROS_POINTS_REPLACE);
        }

        var numberOnly = valueStr.replace(REGEXP_NUMBER_ONLY, REGEXP_NUMBER_ONLY_REPLACE);
        numberOnly = parseFloat(numberOnly);
        if (isNaN(numberOnly)) {
            numberOnly = 0.0;
        }

        var fromUnit;
        if (picas !== undefined) {
            fromUnit = crdtes.UNIT_NAME_PICA;
            numberOnly = picas + numberOnly / 6.0;
        }
        else if (ciceros !== undefined) {
            fromUnit = crdtes.UNIT_NAME_CICERO;
            numberOnly = ciceros + numberOnly / 6.0;
        }
        else {
            var unitOnly = valueStr.replace(REGEXP_UNIT_ONLY, REGEXP_UNIT_ONLY_REPLACE);
            fromUnit = crdtes.getUnitFromINI(unitOnly);
        }

        var conversion = 1.0;
        if (fromUnit != crdtes.UNIT_NAME_NONE && convertToUnit != crdtes.UNIT_NAME_NONE) {
            conversion = crdtes.unitToInchFactor(fromUnit) / crdtes.unitToInchFactor(convertToUnit);
        }

        retVal = sign * numberOnly * conversion;
    }
    while (false);

    return retVal;
}
crdtes.getFloatWithUnitFromINI = getFloatWithUnitFromINI;

/**
 * Interpret a string extracted from some INI data as an array with float values (e.g. <code>"[ 255, 128.2, 1.7]"</code> )
 *
 * @function getFloatValuesFromINI
 * @memberof crdtes 
 *
 * @param {string} in_valueStr - ini value
 * @returns {array} array of numbers or undefined
 */

function getFloatValuesFromINI(in_valueStr) {

    var retVal = undefined;

    do {

        if (! in_valueStr) {
            break;
        }

        var floatValues = [];
        var values = in_valueStr.split(",");
        for (var idx = 0; idx < values.length; idx++) {
            var value = values[idx].replace(REGEXP_TRIM, REGEXP_TRIM_REPLACE);
            if (! value) {
                value = 0;
            }
            else {
                value = parseFloat(values[idx]);
                if (isNaN(value)) {
                    floatValues = undefined;
                    break;
                }
            }

            floatValues.push(value);
        }

        retVal = floatValues;
    }
    while (false);

    return retVal;
}
crdtes.getFloatValuesFromINI = getFloatValuesFromINI;

/**
 * Interpret a string extracted from some INI data as an array with int values (e.g. <code>"[ 255, 128, 1]"</code> )
 *
 * @function getIntValuesFromINI
 * @memberof crdtes 
 *
 * @param {string} in_valueStr - ini value
 * @returns {array} array of ints or undefined
 */

function getIntValuesFromINI(in_valueStr) {

    var retVal = undefined;

    do {

        if (! in_valueStr) {
            break;
        }

        var intValues = [];
        var values = in_valueStr.split(",");
        for (var idx = 0; idx < values.length; idx++) {
            var value = values[idx].replace(REGEXP_TRIM, REGEXP_TRIM_REPLACE);
            if (! value) {
                value = 0;
            }
            else {
                value = parseInt(values[idx], 10);
                if (isNaN(value)) {
                    intValues = undefined;
                    break;
                }
            }

            intValues.push(value);
        }

        retVal = intValues;
    }
    while (false);

    return retVal;
}
crdtes.getIntValuesFromINI = getIntValuesFromINI;

/**
 * Interpret a string extracted from some INI data as a unit name
 *
 * @function getUnitFromINI
 * @memberof crdtes 
 *
 * @param {string} in_value - ini value
 * @param {string} in_defaultUnit - default to use if no match is found
 * @returns {string} value
 */

function getUnitFromINI(in_value, in_defaultUnit) {

    var defaultUnit = (in_defaultUnit !== undefined) ? in_defaultUnit : crdtes.UNIT_NAME_NONE;

    var retVal = defaultUnit;

    var value = (in_value + "").replace(REGEXP_TRIM, REGEXP_TRIM_REPLACE).toLowerCase();

    if (value == "\"" || value.substring(0,2) == "in") {
        retVal = crdtes.UNIT_NAME_INCH;
    }
    else if (value == "cm" || value == "cms" || value.substr(0,4) == "cent") {
        retVal = crdtes.UNIT_NAME_CM;
    }
    else if (value == "mm" || value == "mms" || value.substr(0,4) == "mill") {
        retVal = crdtes.UNIT_NAME_MM;
    }
    else if (value.substring(0,3) == "cic") {
        retVal = crdtes.UNIT_NAME_CICERO;
    }
    else if (value.substring(0,3) == "pic") {
        retVal = crdtes.UNIT_NAME_PICA;
    }
    else if (value.substring(0,3) == "pix" || value == "px") {
        retVal = crdtes.UNIT_NAME_PIXEL;
    }
    else if (value.substring(0,3) == "poi" || value == "pt") {
        retVal = crdtes.UNIT_NAME_POINT;
    }

    return retVal;
}
crdtes.getUnitFromINI = getUnitFromINI;

/**
 * Get file path to PluginInstaller if it is installed
 *
 * @function getPluginInstallerPath
 * @memberof crdtes 
 *
 * @returns {string} file path
*/

function getPluginInstallerPath() {

    var retVal = crdtesDLL.getPluginInstallerPath();

    return retVal;
}
crdtes.getPluginInstallerPath = getPluginInstallerPath;

/**
 * Fetch some persistent data
 *
 * Only available to paid developer accounts
 * 
 * @function getPersistData
 * @memberof crdtes 
 *
 * @param {string} issuer - a GUID identifier for the developer account as seen in the PluginInstaller
 * @param {string} attribute - an attribute name for the data
 * @param {string} password - the password (created by the developer) needed to decode the persistent data
 * @returns {string} whatever persistent data is stored for the given attribute
 */
function getPersistData(issuer, attribute, password) {

    var retVal = crdtesDLL.getPersistData(issuer, attribute, password);

    return retVal;
}
crdtes.getPersistData = getPersistData;

// Internal function getSysInfo__: fetch the whole Tightener sysInfo structure

function getSysInfo__() {

    var retVal;

    if (! SYS_INFO) {
        SYS_INFO = evalTQL("sysInfo()");
    }

    retVal = SYS_INFO;

    return retVal;
}

/**
 * Calculate an integer power of an int value. Avoids using floating point, so<br>
 * should not have any floating-point round-off errors. `Math.pow()` will probably<br>
 * give the exact same result, but I am doubtful that some implementations might internally use `log` and `exp`<br>
 * to handle `Math.pow()`
 *
 * @function intPow
 * @memberof crdtes 
 *
 * @param {number} i - Integer base
 * @param {number} intPower - integer power
 * @returns {number} i ^ intPower
 */

function intPow(i, intPower) {

    var retVal;
    if (Math.floor(intPower) != intPower) {
        // Must be integer
        retVal = undefined;
    }
    else if (intPower == 0) {
        // Handle power of 0: 0^0 is not a number
        if (i == 0) {
            retVal = NaN;
        }
        else {
            retVal = 1;
        }
    }
    else if (i == 1) {
        // Multiplying 1 with itself is 1
        retVal = 1;
    }
    else if (intPower == 1) {
        // i ^ 1 is i
        retVal = i;
    }
    else if (intPower < 0) {
        // i^-x is 1/(i^x)
        retVal = 1/intPow(i, -intPower);
    }
    else {
        // Divide and conquer
        var halfIntPower = intPower >> 1;
        var otherHalfIntPower = intPower - halfIntPower;
        var part1 = intPow(i, halfIntPower);
        var part2;
        if (halfIntPower == otherHalfIntPower) {
            part2 = part1;
        }
        else {
            part2 =  intPow(i, otherHalfIntPower);
        }
        retVal = part1 * part2;
    }

    return retVal;
}
crdtes.intPow = intPow;

/**
 * Determine the license level for CRDT: 0 = not, 1 = basic, 2 = full<br>
 * <br>
 * Some functions, marked with "Only available to paid developer accounts"<br>
 * will only work with level 2. Licensing function only work with level 1
 *
 * @function getCreativeDeveloperToolsLevel
 * @memberof crdtes 
 *
 * @returns {number} 0, 1 or 2
 */
function getCreativeDeveloperToolsLevel() {

    var retVal = crdtesDLL.getCreativeDeveloperToolsLevel();

    return retVal;
}
crdtes.getCreativeDeveloperToolsLevel = getCreativeDeveloperToolsLevel;

/**
 * Extend or shorten a string to an exact length, adding <code>padChar</code> as needed
 *
 * @function leftPad
 * @memberof crdtes 
 *
 * @param {string} s - string to be extended or shortened
 * @param {string} padChar - string to append repeatedly if length needs to extended
 * @param {number} len - desired result length
 * @returns {string} padded or shortened string
 */

function leftPad(s, padChar, len) {

    var retVal = undefined;

    do {
        try {

            retVal = s + "";
            if (retVal.length == len) {
                break;
            }

            if (retVal.length > len) {
                retVal = retVal.substring(retVal.length - len);
                break;
            }

            var padLength = len - retVal.length;

            var padding = new Array(padLength + 1).join(padChar)
            retVal = padding + retVal;
        }
        catch (err) {
        }
    }
    while (false);

    return retVal;
}
crdtes.leftPad = leftPad;

/**
 * Make a log entry of the call of a function. Pass in the <code>arguments</code> keyword as a parameter.
 *
 * @function logEntry
 * @memberof crdtes 
 *
 * @param {array} reportingFunctionArguments - pass in the current <code>arguments</code> to the function.<br>
 * This is used to determine the function's name for the log
 */

function logEntry(reportingFunctionArguments) {
    if (LOG_ENTRY_EXIT) {
        logTrace(reportingFunctionArguments, "Entry");
    }
}
crdtes.logEntry = logEntry;

/**
 * Make a log entry of an error message. Pass in the <code>arguments</code> keyword as the first parameter.<br>
 * If the error level is below <code>crdtes.LOG_LEVEL_ERROR</code> nothing happens
 *
 * @function logError
 * @memberof crdtes 
 *
 * @param {array} reportingFunctionArguments - pass in the current <code>arguments</code> to the function.<br>
 * This is used to determine the function's name for the log
 * @param {string} message - error message
 */
function logError(reportingFunctionArguments, message) {
    if (LOG_LEVEL >= LOG_LEVEL_ERROR) {
        if (! message) {
            message = reportingFunctionArguments;
            reportingFunctionArguments = undefined;
        }
        logMessage(reportingFunctionArguments, LOG_LEVEL_ERROR, message);
    }
}
crdtes.logError = logError;

/**
 * Make a log entry of the exit of a function. Pass in the <code>arguments</code> keyword as a parameter.
 *
 * @function logExit
 * @memberof crdtes 
 *
 * @param {array} reportingFunctionArguments - pass in the current <code>arguments</code> to the function.<br>
 * This is used to determine the function's name for the log
 */

function logExit(reportingFunctionArguments) {
    if (LOG_ENTRY_EXIT) {
        logTrace(reportingFunctionArguments, "Exit");
    }
}
crdtes.logExit = logExit;

/**
 * Extract the function name from its arguments
 *
 * @function functionNameFromArguments
 * @memberof crdtes 
 *
 * @param {object} functionArguments - pass in the current <code>arguments</code> to the function.<br>
 * This is used to determine the function's name
 * @returns {string} function name
 */

function functionNameFromArguments(functionArguments) {

    var functionName;
    try {
        functionName = functionArguments.callee.toString().match(/function ([^\(]+)/)[1];
    }
    catch (err) {
        functionName = "[anonymous function]";
    }

    return functionName;

}
crdtes.functionNameFromArguments = functionNameFromArguments;


/**
 * Output a log message. Pass in the <code>arguments</code> keyword as the first parameter.
 *
 * @function logMessage
 * @memberof crdtes 
 *
 * @param {array} reportingFunctionArguments - pass in the current <code>arguments</code> to the function.<br>
 * This is used to determine the function's name for the log
 * @param {number} logLevel - log level
 * @param {string} message - the note to output
 */

function logMessage(reportingFunctionArguments, logLevel, message) {

    var savedInLogger = IN_LOGGER;

    do {
        try {

            if (IN_LOGGER) {
                break;
            }

            IN_LOGGER = true;

            var functionPrefix = "";
            var functionName = "";

            if (! message) {

                message = reportingFunctionArguments;
                reportingFunctionArguments = undefined;

            }
            else if (reportingFunctionArguments) {

                if ("string" == typeof reportingFunctionArguments) {
                    functionName = reportingFunctionArguments;
                }
                else {
                    functionName = functionNameFromArguments(reportingFunctionArguments);
                }

                functionPrefix += functionName + ": ";

            }

            var now = new Date();
            var timePrefix =
                leftPad(now.getUTCDate(), "0", 2) +
                "-" +
                leftPad(now.getUTCMonth() + 1, "0", 2) +
                "-" +
                leftPad(now.getUTCFullYear(), "0", 4) +
                " " +
                leftPad(now.getUTCHours(), "0", 2) +
                ":" +
                leftPad(now.getUTCMinutes(), "0", 2) +
                ":" +
                leftPad(now.getUTCSeconds(), "0", 2) +
                "+00 ";

            var platformPrefix = "E ";

            switch (logLevel) {
                case LOG_LEVEL_ERROR:
                    logLevelPrefix = "ERROR";
                    break;
                case LOG_LEVEL_WARNING:
                    logLevelPrefix = "WARN ";
                    break;
                case LOG_LEVEL_NOTE:
                    logLevelPrefix = "NOTE ";
                    break;
                case LOG_LEVEL_TRACE:
                    logLevelPrefix = "TRACE";
                    break;
                default:
                    logLevelPrefix = "     ";
                    break;
            }

            var logLine = platformPrefix + timePrefix + "- " + logLevelPrefix + ": " + functionPrefix + message;

            if (LOG_TO_CRDT) {
                crdtesDLL.logMessage(logLevel, functionName, message)
            }

            if (LOG_TO_ESTK_CONSOLE) {
                $.writeln(logLine);
            }

            if (LOG_TO_FILEPATH) {
                var fileHandle = new File(LOG_TO_FILEPATH);
                fileHandle.open("w+");
                fileHandle.writeln(logLine);
                fileHandle.close()
            }

        }
        catch (err) {
        }
    }
    while (false);

    IN_LOGGER = savedInLogger;
}
crdtes.logMessage = logMessage;

/**
 * Make a log entry of a note. Pass in the <code>arguments</code> keyword as the first parameter.<br>
 * If the error level is below <code>crdtes.LOG_LEVEL_NOTE</code> nothing happens
 *
 * @function logNote
 * @memberof crdtes 
 *
 * @param {array} reportingFunctionArguments - pass in the current <code>arguments</code> to the function.<br>
 * This is used to determine the function's name for the log
 * @param {string} message - the note to output
 */
function logNote(reportingFunctionArguments, message) {
    if (LOG_LEVEL >= LOG_LEVEL_NOTE) {
        if (! message) {
            message = reportingFunctionArguments;
            reportingFunctionArguments = undefined;
        }
        logMessage(reportingFunctionArguments, LOG_LEVEL_NOTE, message);
    }
}
crdtes.logNote = logNote;

/**
 * Emit a trace messsage into the log. Pass in the <code>arguments</code> keyword as the first parameter.<br>
 * If the error level is below <code>crdtes.LOG_LEVEL_TRACE</code> nothing happens
 *
 * @function logTrace
 * @memberof crdtes 
 *
 * @param {array} reportingFunctionArguments - pass in the current <code>arguments</code> to the function.<br>
 * This is used to determine the function's name for the log
 * @param {string} message - the trace message to output
 */
function logTrace(reportingFunctionArguments, message) {
    if (LOG_LEVEL >= LOG_LEVEL_TRACE) {
        if (! message) {
            message = reportingFunctionArguments;
            reportingFunctionArguments = undefined;
        }
        logMessage(reportingFunctionArguments, LOG_LEVEL_TRACE, message);
    }
}
crdtes.logTrace = logTrace;

/**
 * Emit a warning messsage into the log. Pass in the <code>arguments</code> keyword as the first parameter.<br>
 * If the error level is below <code>crdtes.LOG_LEVEL_WARNING</code> nothing happens
 *
 * @function logWarning
 * @memberof crdtes 
 *
 * @param {array} arguments - pass in the current <code>arguments</code> to the function.<br>
 * This is used to determine the function's name for the log
 * @param {string} message - the warning message to output
 */
function logWarning(reportingFunctionArguments, message) {
    if (LOG_LEVEL >= LOG_LEVEL_WARNING) {
        if (! message) {
            message = reportingFunctionArguments;
            reportingFunctionArguments = undefined;
        }
        logMessage(reportingFunctionArguments, LOG_LEVEL_WARNING, message);
    }
}
crdtes.logWarning = logWarning;

/**
 * The unique <code>GUID</code> of this computer<br>
 * <br>
 * Only available to paid developer accounts
 * 
 * @function machineGUID
 * @memberof crdtes 
 *
 * @returns {string} a <code>GUID</code> string
 */
function machineGUID() {

    var retVal = crdtesDLL.machineGUID();

    return retVal;
}
crdtes.machineGUID = machineGUID;

/**
 * Attempt to launch the PluginInstaller if it is installed
 *
 * @function pluginInstaller
 * @memberof crdtes 
 *
 * @returns {boolean} success or failure
*/

function pluginInstaller() {

    var retVal = false;

    do {
        try {
            
            var pluginInstallerFilePath = crdtesDLL.getPluginInstallerPath();
            
            var pluginInstallerFile = File(pluginInstallerFilePath);
            
            if (crdtes.IS_WINDOWS) {
                // Need to set the PATH before launching. Using a wrapper .vbs file
                pluginInstallerFile = File(pluginInstallerFile.parent + "/PluginInstaller Resources/launchEmbeddedPluginInstaller.vbs");
            }

            if (pluginInstallerFile.exists) {
                retVal = pluginInstallerFile.execute();
            }
        }
        catch (err) {
        }
    }
    while (false);

    return retVal;
}
crdtes.pluginInstaller = pluginInstaller;

/**
 * Restore the log level to what it was when pushLogLevel was called
 *
 * @function popLogLevel
 * @memberof crdtes 
 *
 * @returns {number} log level that was popped off the stack
 */

function popLogLevel() {

    var retVal;

    retVal = LOG_LEVEL;
    if (LOG_LEVEL_STACK.length > 0) {
        LOG_LEVEL = LOG_LEVEL_STACK.pop();
    }
    else {
        LOG_LEVEL = LOG_LEVEL_NONE;
    }

    return retVal;
}
crdtes.popLogLevel = popLogLevel;

/**
 * Save the previous log level and set a new log level
 *
 * @function pushLogLevel
 * @memberof crdtes 
 *
 * @param {number} newLogLevel - new log level to set
 * @returns {number} previous log level
 */

function pushLogLevel(newLogLevel) {

    var retVal;

    retVal = LOG_LEVEL;
    LOG_LEVEL_STACK.push(LOG_LEVEL);
    LOG_LEVEL = newLogLevel;

    return retVal;
}
crdtes.pushLogLevel = pushLogLevel;

/**
 * Read a bunch of text and try to extract structured information in .INI format<br>
 * <br>
 * This function is lenient and is able to extract slightly mangled INI data from the text frame<br>
 * content of an InDesign text frame.<br>
 * <br>
 * This function knows how to handle curly quotes should they be present.<br>
 * <br>
 * The following flexibilities have been built-in:<br>
 * <br>
 * - Attribute names are case-insensitive and anything not <code>a-z 0-9</code> is ignored.<br>
 * Entries like <code>this or that = ...</code> or <code>thisOrThat = ...</code> or <code>this'orThat = ...</code> are<br>
 * all equivalent. Only letters and digits are retained, and converted to lowercase.<br>
 * <br>
 * - Attribute values can be quoted with either single, double, curly quotes.<br>
 * This often occurs because InDesign can be configured to convert normal quotes into<br>
 * curly quotes automatically.<br>
 * Attribute values without quotes are trimmed (e.g. <code>bla =    x  </code> is the same as <code>bla=x</code>)<br>
 * Spaces are retained in quoted attribute values.<br>
 * <br>
 * - Any text will be ignore if not properly formatted as either a section name or an attribute-value<br>
 * pair with an equal sign<br>
 * <br>
 * - Hard and soft returns are equivalent<br>
 * <br>
 * The return value is an object with the section names at the top level, and attribute names<br>
 * below that. The following .INI<br>
 * <code><br>
 * [My data]<br>
 * this is = " abc "<br>
 * that =      abc<br>
 * </code><br>
 * returns<br>
 * <code><br><br>
 * {<br>
 *   "mydata": {<br>
 *      "__rawSectionName": "My data",<br>
 *      "thisis": " abc ",<br>
 *      "that": "abc"<br>
 *   }<br>
 * }<br>
 * </code><br>
 * Duplicated sections and entries are automatically suffixed with a counter suffix - e.g.
 * <code><br>
 * [main]
 * a=1
 * a=2
 * a=3
 * </code><br>
 * is equivalent with 
 * <code><br>
 * [main]
 * a=1
 * a_2=2
 * a_3=3
 * </code><br>
 * and
 * <code><br>
 * [a]
 * a=1
 * [a]
 * a=2<br>
 * </code><br>
 * is equivalent with
 * <code><br>
 * [a]
 * a=1
 * [a_2]
 * a=2<br>
 * </code><br>
 * 
 * @function readINI
 * @memberof crdtes 
 *
 * @param {string} in_text - raw text, which might or might not contain some INI-formatted data mixed with normal text
 * @returns {object} either the ini data or <code>undefined</code>.
 */

function readINI(in_text) {

    var retVal = undefined;

    do {
        try {

            if (! in_text) {
                break;
            }

            if ("string" != typeof in_text) {
                break;
            }

            var text = in_text + "\r";
            var state = STATE_IDLE;
            var attr;
            var value;
            var attrSpaceCount;
            var rawSectionName = "";
            var sectionName = "";
            var section;
            var attrCounters = {};
            var sectionCounters = {};

            for (var idx = 0; state != STATE_ERROR && idx < text.length; idx++) {
                var c = text.charAt(idx);
                switch (state) {
                    default:
                        LogError("ReadIni: unexpected state");
                        state = STATE_ERROR;
                        break;
                    case STATE_IDLE:
                        if (c == '[') {
                            state = STATE_SEEN_OPEN_SQUARE_BRACKET;
                            rawSectionName = "";
                        }
                        else if (c == '#') {
                            state = STATE_IN_COMMENT;
                        }
                        else if (c > ' ') {
                            attr = c;
                            attrSpaceCount = 0;
                            state = STATE_SEEN_NON_WHITE;
                        }
                        break;
                    case STATE_IN_COMMENT:
                    case STATE_SEEN_CLOSE_SQUARE_BRACKET:
                        if (c == '\r' || c == '\n') {
                            state = STATE_IDLE;
                        }
                        break;
                    case STATE_SEEN_OPEN_SQUARE_BRACKET:
                        if (c == ']') {
                            state = STATE_SEEN_CLOSE_SQUARE_BRACKET;
                            sectionName = rawSectionName.toLowerCase();
                            sectionName = sectionName.replace(REGEXP_DESPACE, REGEXP_DESPACE_REPLACE);
                            sectionName = sectionName.replace(REGEXP_SECTION_NAME_ONLY, REGEXP_SECTION_NAME_ONLY_REPLACE);
                            if (sectionName) {

                                if (! retVal) {
                                    retVal = {};
                                }

                                var sectionSuffix = "";
                                var sectionCounter = 1;
                                if (sectionName in sectionCounters) {
                                    sectionCounter = sectionCounters[sectionName];
                                    sectionCounter++;
                                    sectionSuffix = "_" + sectionCounter;
                                }
                                sectionCounters[sectionName] = sectionCounter;
                                sectionName += sectionSuffix;
                                retVal[sectionName] = {};
                                section = retVal[sectionName];
                                section.__rawSectionName = rawSectionName;
                                attrCounters = {};
                            }
                        }
                        else {
                            rawSectionName += c;
                        }
                        break;
                    case STATE_SEEN_NON_WHITE:
                        if (c == "=") {
                            value = "";
                            state = STATE_SEEN_EQUAL;
                        }
                        else if (c == '\r' || c == '\n') {
                            state = STATE_IDLE;
                        }
                        else if (c != " ") {
                            while (attrSpaceCount > 0) {
                                attr += " ";
                                attrSpaceCount--;
                            }
                            attr += c;
                        }
                        else {
                            attrSpaceCount++;
                        }
                        break;
                    case STATE_SEEN_EQUAL:
                        if (c != '\r' && c != '\n') {
                            value += c;
                        }
                        else {
                            value = value.replace(REGEXP_TRIM, REGEXP_TRIM_REPLACE);
                            if (value.length >= 2) {
                                var firstChar = value.charAt(0);
                                var lastChar = value.charAt(value.length - 1);
                                if (
                                    (firstChar == "\"" || firstChar == "“" || firstChar == "”")
                                &&
                                    (lastChar == "\"" || lastChar == "“" || lastChar == "”")
                                ) {
                                    value = value.substring(1, value.length - 1);
                                }
                                else if (
                                    (firstChar == "'" || firstChar == "‘" || firstChar == "’")
                                &&
                                    (lastChar == "'" || lastChar == "‘" || lastChar == "’")
                                ) {
                                    value = value.substring(1, value.length - 1);
                                }
                            }

                            if (section) {
                                attr = attr.replace(REGEXP_DESPACE, REGEXP_DESPACE_REPLACE).toLowerCase();
                                attr = attr.replace(REGEXP_ALPHA_ONLY, REGEXP_ALPHA_ONLY_REPLACE);
                                if (attr) {

                                    var attrSuffix = "";
                                    var attrCounter = 1;
                                    if (attr in attrCounters) {
                                        attrCounter = attrCounters[attr];
                                        attrCounter++;
                                        attrSuffix = "_" + attrCounter;
                                    }
                                    attrCounters[attr] = attrCounter;
                                    attr += attrSuffix;

                                    section[attr] = value;
                                }
                            }

                            state = STATE_IDLE;
                        }
                        break;
                }
            }
        }
        catch (err) {
        }
    }
    while (false);

    return retVal;
}
crdtes.readINI = readINI;

/**
 * Extend or shorten a string to an exact length, adding <code>padChar</code> as needed
 *
 * @function rightPad
 * @memberof crdtes 
 *
 * @param {string} s - string to be extended or shortened
 * @param {string} padChar - string to append repeatedly if length needs to extended
 * @param {number} len - desired result length
 * @returns {string} padded or shortened string
 */

function rightPad(s, padChar, len) {

    var retVal = undefined;

    do {
        try {

            retVal = s + "";

            if (retVal.length == len) {
                break;
            }

            if (retVal.length > len) {
                retVal = retVal.substring(0, len);
                break;
            }

            var padLength = len - retVal.length;

            var padding = new Array(padLength + 1).join(padChar)
            retVal = retVal + padding;
        }
        catch (err) {
        }
    }
    while (false);

    return retVal;
}
crdtes.rightPad = rightPad;

/**
 * Send in activation data to determine whether some software is currently activated or not.<br>
 * <br>
 * Needs to be followed by a <code>crdtes.sublicense()</code> call<br>
 *
 * @function setIssuer
 * @memberof crdtes 
 *
 * @param {string} issuerGUID - a GUID identifier for the developer account as seen in the PluginInstaller
 * @param {string} issuerEmail - the email for the developer account as seen in the PluginInstaller
 * @returnss { boolean } - success or failure
 */
function setIssuer(issuerGUID, issuerEmail) {

    var retVal = crdtesDLL.setIssuer(issuerGUID, issuerEmail);

    return retVal;
}
crdtes.setIssuer = setIssuer;

/**
 * Store some persistent data (e.g. a time stamp to determine a demo version lapsing)<br>
 * <br>
 * Only available to paid developer accounts
 *
 * @function setPersistData
 * @memberof crdtes 
 *
 * @param {string} issuer - a GUID identifier for the developer account as seen in the PluginInstaller
 * @param {string} attribute - an attribute name for the data
 * @param {string} password - the password (created by the developer) needed to decode the persistent data
 * @param {string} data - any data to persist
 * @returns {boolean} success or failure
 */
function setPersistData(issuer, attribute, password, data) {

    var retVal = crdtesDLL.setPersistData(issuer, attribute, password, data);

    return retVal;
}
crdtes.setPersistData = setPersistData;

/**
 * Wrap a string or a byte array into single quotes, encoding any binary data as a string.<br>
 * Knows how to handle Unicode characters or binary zeroes.<br>
 * <br>
 * When the input is a string, high Unicode characters are encoded as <code>\uHHHH</code><br>
 * <br>
 * When the input is a byte array, all bytes are encoded as <code>\xHH</code> escape sequences.
 *
 * @function sQ
 * @memberof crdtes 
 *
 * @param {string} str_or_ByteArr - a Unicode string or an array of bytes
 * @returns {string} a string enclosed in double quotes. This string is pure 7-bit<br>
 * ASCII and can be used into generated script code<br>
 * Example:<br>
 * <code>var script = "a=b(" + crdtes.sQ(somedata) + ");";</code>
 */
function sQ(str_or_ByteArr) {
    return enQuote__(str_or_ByteArr, "'");
}
crdtes.sQ = sQ;

/**
 * Encode a string into an byte array using UTF-8
 *
 * @function strToUTF8
 * @memberof crdtes 
 *
 * @param {string} in_s - a string
 * @returns { array } a byte array
 */
function strToUTF8(in_s) {

    var retVal = [];

    var idx = 0;
    var len = in_s.length;
    var cCode;
    while (idx < len) {
        cCode = in_s.charCodeAt(idx);
        idx++;
        var bytes = charCodeToUTF8__(cCode);
        if (! bytes) {
            retVal = undefined;
            break;
        }
        else {
            for (var byteIdx = 0; byteIdx < bytes.length; byteIdx++) {
                retVal.push(bytes[byteIdx]);
            }
        }
    }

    return retVal;
}
crdtes.strToUTF8 = strToUTF8;

/**
 * Encode a string into an byte array using the 8 lowest bits of each UTF-16 character
 *
 * @function strToBinary
 * @memberof crdtes 
 *
 * @param {string} in_s - a string
 * @returns { array } a byte array
 */
function strToBinary(in_s) {

    var retVal = [];

    var idx = 0;
    var len = in_s.length;
    var cCode;
    while (idx < len) {
        cCode = in_s.charCodeAt(idx);
        idx++;
        var bite = (cCode & 0xFF);
        retVal.push(bite);
    }

    return retVal;
}
crdtes.strToBinary = strToBinary;

/**
 * Send in sublicense info generated in the PluginInstaller so we can determine whether some software is currently activated or not.<br>
 * <br>
 * Needs to be preceded by a <code>crdtes.setIssuer()</code> call.
 *
 * @function sublicense
 * @memberof crdtes 
 *
 * @param {string} key - key needed to decode activation data
 * @param {string} activation - encrypted activation data
 * @returns { boolean } success or failure
 */
function sublicense(key, activation) {

    var retVal = crdtesDLL.sublicense(key, activation);

    return retVal;
}
crdtes.sublicense = sublicense;

/**
 * Convert an integer into a hex representation with a fixed number of digits.<br>
 * Negative numbers are converted using 2-s complement (so <code>-15</code> results in <code>0x01</code>)<br>
 *
 * @function toHex
 * @memberof crdtes 
 *
 * @param {number} i - integer to convert to hex
 * @param {number} numDigits - How many digits. Defaults to 4 if omitted.
 * @returns { string } hex-encoded integer
 */
function toHex(i, numDigits) {

    if (! numDigits) {
        numDigits = 4;
    }

    if (i < 0) {
        var upper = intPow(2, numDigits*4);
        // Calculate 2's complement with numDigits if negative
        i = (intPow(2, numDigits*4) + i) & (upper - 1);
    }

    // Calculate and cache a long enough string of zeroes
    var zeroes = toHex.zeroes;
    if (! zeroes) {
        zeroes = "0";
    }
    while (zeroes.length < numDigits) {
        zeroes += zeroes;
    }
    toHex.zeroes = zeroes;

    var retVal = i.toString(16).toLowerCase(); // Probably always lowercase by default, but just in case...
    if (retVal.length > numDigits) {
        retVal = retVal.substring(retVal.length - numDigits);
    }
    else if (retVal.length < numDigits) {
        retVal = zeroes.substr(0, numDigits - retVal.length) + retVal;
    }

    return retVal;
}
crdtes.toHex = toHex;

/**
 * Conversion factor from a length unit into inches
 *
 * @function unitToInchFactor
 * @memberof crdtes 
 *
 * @param {string} in_unit - unit name (<code>crdtes.UNIT_NAME...</code>)
 * @returns { number } conversion factor or <code>1.0</code> if unknown/not applicable
 */

function unitToInchFactor(in_unit) {

    var retVal = 1.0;

    switch (in_unit) {
        case crdtes.UNIT_NAME_CM:
            retVal = 1.0/2.54;
            break;
        case crdtes.UNIT_NAME_MM:
            retVal = 1.0/25.4;
            break;
        case crdtes.UNIT_NAME_CICERO:
            retVal = 0.17762;
            break;
        case crdtes.UNIT_NAME_PICA:
            retVal = 1.0/12.0;
            break;
        case crdtes.UNIT_NAME_PIXEL:
            retVal = 1.0/72.0;
            break;
        case crdtes.UNIT_NAME_POINT:
            retVal = 1.0/72.0;
            break;
    }

    return retVal;
}
crdtes.unitToInchFactor = unitToInchFactor;

function unwrapUTF16ToUTF8__(in_str, isBinary) {

    var retVal;

    // A UTF-8 encoded string or a byte array can be wrapped 'as-is' into a UTF-16 wrapper string, where each
    // 16-bit character in the UTF-16 wrapper corresponds to a single byte in the UTF-8 string or byte array.
    // In such a UTF-16 string all characters will have 0-255 charcode values and the high byte always 0.

    var byteArray = strToBinary(in_str);

    if (! isBinary) {
        retVal = binaryUTF8ToStr(byteArray);
    }
    else {
        retVal = byteArray;
    }

    return retVal;
}

})();