/* Copyright (c) 2015-2019 Volodymyr Shymanskyy. See the file LICENSE for copying permission. */
'use strict';

function blynkHeader(msg_type, msg_id, msg_len) {
  return String.fromCharCode(
    msg_type,
    msg_id  >> 8, msg_id  & 0xFF,
    msg_len >> 8, msg_len & 0xFF
  );
}

var MsgType = {
  RSP           :  0,
  LOGIN         :  2,
  PING          :  6,
  TWEET         :  12,
  EMAIL         :  13,
  NOTIFY        :  14,
  BRIDGE        :  15,
  HW_SYNC       :  16,
  INTERNAL      :  17,
  PROPERTY      :  19,
  HW            :  20,
  HW_LOGIN      :  29,

  REDIRECT      :  41,
  DEBUG_PRINT   :  55,

  EVENT_LOG     :  64,
};

var MsgStatus = {
  OK                    :  200,
  ILLEGAL_COMMAND       :  2,
  ALREADY_REGISTERED    :  4,
  INVALID_TOKEN         :  9
};

var BlynkState = {
  CONNECTING    :  1,
  CONNECTED     :  2,
  DISCONNECTED  :  3
};

  var EspruinoTCP = function(options) {
    var self = this;

    var options = options || {};
    self.addr = options.addr || "blynk-cloud.com";
    self.port = options.port || 80;

    var net = require('net');

    this.write = function(data) {
      if (self.sock) {
        self.sock.write(data, 'binary');
      }
    };

    this.connect = function(done) {
      if (self.sock) {
        self.disconnect();
      }
      console.log("Connecting to TCP:", self.addr, self.port);
      self.sock = net.connect({host : self.addr, port: self.port}, function() {
        console.log('Connected');
        self.sock.on('data', function(data) {
          self.emit('data', data);
        });
        self.sock.on('end', function() {
          self.emit('end', '');
        });
        done();
      });
    };

    this.disconnect = function() {
      if (self.sock) {
        self.sock = null;
      }
    };
  };

  var BoardEspruino = function(values) {
    var self = this;
    this.init = function(blynk) {
      self.blynk = blynk;
    };
    this.process = function(values) {
      switch(values[0]) {
        case 'pm':
          // TODO
          break;
        case 'dw':
          var pin = Pin(values[1]);
          var val = parseInt(values[2]);
          pinMode(pin, 'output');
          digitalWrite(pin, val);
          break;
        case 'dr':
          var pin = Pin(values[1]);
          self.blynk.sendMsg(MsgType.HW, ['dw', values[1], digitalRead(pin)]);
          break;
        case 'aw':
          var pin = Pin(values[1]);
          var val = parseFloat(values[2]);
          pinMode(pin, 'output');
          analogWrite(pin, val / 255);
          break;
        case 'ar':
          var pin = Pin(values[1]);
          self.blynk.sendMsg(MsgType.HW, ['aw', values[1], 4095 * analogRead(pin)]);
          break;
        default:
          return null;
      }
      return true;
    };
  };

/*
 * Blynk
 */

var Blynk = function(auth, options) {
  var self = this;

  this.auth = auth;
  var options = options || {};
  this.heartbeat = options.heartbeat || 10000;

  // Auto-detect board
  if (options.board) {
    this.board = options.board;
  } else {
    this.board = new BoardEspruino();
  }
  self.board.init(self);

  // Auto-detect connector
  if (options.connector) {
    this.conn = options.connector;
  } else {
    this.conn = new EspruinoTCP(options);
  }

  this.buff_in = '';
  this.msg_id = 1;
  this.vpins = [];
  this.profile = options.profile;

  this.VirtualPin = function(vPin) {
    this.pin = vPin;
    self.vpins[vPin] = this;

    this.write = function(value) {
      self.virtualWrite(this.pin, value);
    };
  };

  this.WidgetBridge = function(vPin) {
    this.pin = vPin;

    this.setAuthToken = function(token) {
      self.sendMsg(MsgType.BRIDGE, [this.pin, 'i', token]);
    };
    this.digitalWrite = function(pin, val) {
      self.sendMsg(MsgType.BRIDGE, [this.pin, 'dw', pin, val]);
    };
    this.analogWrite = function(pin, val) {
      self.sendMsg(MsgType.BRIDGE, [this.pin, 'aw', pin, val]);
    };
    this.virtualWrite = function(pin, val) {
      self.sendMsg(MsgType.BRIDGE, [this.pin, 'vw', pin].concat(val));
    };
  };

  this.WidgetTerminal = function(vPin) {
    if (needsEmitter()) {
      events.EventEmitter.call(this);
    }
    this.pin = vPin;
    self.vpins[vPin] = this;

    this.write = function(data) {
      self.virtualWrite(this.pin, data);
    };
  };

  this.WidgetLCD = function(vPin) {
    this.pin = vPin;

    this.clear = function() {
      self.virtualWrite(this.pin, 'clr');
    };
    this.print = function(x, y, val) {
      self.sendMsg(MsgType.HW, ['vw', this.pin, 'p', x, y, val]);
    };
  };

  this.WidgetLED = function(vPin) {
    this.pin = vPin;

    this.setValue = function(val) {
      self.virtualWrite(this.pin, val);
    };
    this.turnOn = function() {
      self.virtualWrite(this.pin, 255);
    };
    this.turnOff = function() {
      self.virtualWrite(this.pin, 0);
    };
  };

  if (!options.skip_connect) {
    this.connect();
  }
};

Blynk.prototype.onReceive = function(data) {
  var self = this;
  self.buff_in += data;
  while (self.buff_in.length >= 5) {
    var msg_type = self.buff_in.charCodeAt(0);
    var msg_id   = self.buff_in.charCodeAt(1) << 8 | self.buff_in.charCodeAt(2);
    var msg_len  = self.buff_in.charCodeAt(3) << 8 | self.buff_in.charCodeAt(4);

    if (msg_id === 0)  { return self.disconnect(); }

    if (msg_type === MsgType.RSP) {
      //console.log('> ', string_of_enum(MsgType, msg_type), msg_id, string_of_enum(MsgStatus, msg_len));

        if (self.timerConn && msg_id === 1) {
          if (msg_len === MsgStatus.OK || msg_len === MsgStatus.ALREADY_REGISTERED) {
            clearInterval(self.timerConn);
            self.timerConn = null;
            self.timerHb = setInterval(function() {
              //console.log('Heartbeat');
              self.sendMsg(MsgType.PING);
            }, self.heartbeat);
            console.log('Authorized');
            self.sendMsg(MsgType.INTERNAL, ['ver', '0.5.4', 'buff-in', 256, 'dev', 'espruino']);
            self.emit('connect');
          } else {
            //if invalid token, no point in trying to reconnect
            if (msg_len === MsgStatus.INVALID_TOKEN) {
              //letting main app know why we failed
              self.emit('error', "error: " + msg_len);
              //console.log('Disconnecting because of invalid token');
              self.disconnect();
              if(self.timerConn) {
                //clear connecting timer
                console.log('clear conn timer');
                clearInterval(self.timerConn);
                self.timerConn = null;
              }
            }
          }
        }

      self.buff_in = self.buff_in.substr(5);
      continue;
    }

    if (msg_len > 256)  { return self.disconnect(); }
    if (self.buff_in.length < msg_len+5) {
      return;
    }
    var values = self.buff_in.substr(5, msg_len).split('\0');
    self.buff_in = self.buff_in.substr(msg_len+5);

    /*if (msg_len) {
      console.log('> ', string_of_enum(MsgType, msg_type), msg_id, msg_len, values.join('|'));
    } else {
      console.log('> ', string_of_enum(MsgType, msg_type), msg_id, msg_len);
    }*/

    if (msg_type === MsgType.LOGIN ||
        msg_type === MsgType.PING)
    {
      self.sendRsp(MsgType.RSP, msg_id, MsgStatus.OK);
    } else if (msg_type === MsgType.HW ||
               msg_type === MsgType.BRIDGE)
    {
      if (values[0] === 'vw') {
        var pin = parseInt(values[1]);
        if (self.vpins[pin]) {
          self.vpins[pin].emit('write', values.slice(2));
        }
      } else if (values[0] === 'vr') {
        var pin = parseInt(values[1]);
        if (self.vpins[pin]) {
          self.vpins[pin].emit('read');
        }
      } else if (self.board.process(values)) {

      } else {
        console.log('Invalid cmd: ', values[0]);
        //self.sendRsp(MsgType.RSP, msg_id, MsgStatus.ILLEGAL_COMMAND);
      }
    } else if (msg_type === MsgType.REDIRECT) {
      self.conn.addr = values[0];
      if (values[1]) {
        self.conn.port = parseInt(values[1]);
      }
      console.log('Redirecting to ', self.conn.addr, ':', self.conn.port);
      self.disconnect();
    } else if (msg_type === MsgType.DEBUG_PRINT) {
      console.log('Server: ', values[0]);
    } else {
      console.log('Invalid msg type: ', msg_type);
      self.sendRsp(MsgType.RSP, msg_id, MsgStatus.ILLEGAL_COMMAND);
    }
  } // end while
};

Blynk.prototype.sendRsp = function(msg_type, msg_id, msg_len, data) {
  var self = this;
  data = data || "";

  if (!msg_id) {
    if (self.msg_id === 0xFFFF)
      self.msg_id = 1;
    else
      self.msg_id++;

    msg_id = self.msg_id;
  }

  if (msg_type == MsgType.RSP) {
    //console.log('< ', string_of_enum(MsgType, msg_type), msg_id, string_of_enum(MsgStatus, msg_len));
    data = blynkHeader(msg_type, msg_id, msg_len)
  } else {
    /*if (msg_len) {
      console.log('< ', string_of_enum(MsgType, msg_type), msg_id, msg_len, data.split('\0').join('|'));
    } else {
      console.log('< ', string_of_enum(MsgType, msg_type), msg_id, msg_len);
    }*/
    data = blynkHeader(msg_type, msg_id, msg_len) + data;
  }

  self.conn.write(data)
};

Blynk.prototype.sendMsg = function(msg_type, values, msg_id) {
  if (this.timerHb) {
    var values = values || [''];
    var data = values.join('\0');
    this.sendRsp(msg_type, msg_id, data.length, data);
  }
};

/*
  * API
  */

Blynk.prototype.connect = function() {
  var self = this;

  var doConnect = function() {
    if(self.conn) {
      //cleanup events
      self.conn.removeAllListeners();
    }
    self.conn.connect(function() {
      self.conn.on('data', function(data) { self.onReceive(data);     });
      self.conn.on('end',  function()     { self.end();               });

      self.sendRsp(MsgType.HW_LOGIN, 1, self.auth.length, self.auth);
    });
    self.conn.on('error', function(err) { self.error(err);            });
  };

    self.timerConn = setInterval(doConnect, 10000);
    doConnect();
};

Blynk.prototype.disconnect = function(reconnect) {
  console.log('Disconnect blynk');
  if(typeof reconnect === 'undefined' ) {
    reconnect = true;
  }

  var self = this;
  this.conn.disconnect();
  if (this.timerHb) {
    clearInterval(this.timerHb);
    this.timerHb = null;
  }
  this.emit('disconnect');
  //cleanup to avoid multiplying listeners
  this.conn.removeAllListeners();

  //starting reconnect procedure if not already in connecting loop and reconnect is true
  if(reconnect && !self.timerConn) {
    console.log("REARMING DISCONNECT");
    setTimeout(function () {self.connect()}, 5000);
  }
};

Blynk.prototype.error = function(err) {
  var self = this;
  //if we throw error and user doesn't handle it, app crashes. is it worth it?
  this.emit('error', err.code?err.code:'ERROR');
  console.error('Error', err.code);
  //starting reconnect procedure if not already in connecting loop
  if(!self.timerConn) {
    setTimeout(function () {self.connect()}, 5000);
  }
};

Blynk.prototype.end = function() {
  var self = this;
  self.disconnect();
};

Blynk.prototype.virtualWrite = function(pin, val) {
  this.sendMsg(MsgType.HW, ['vw', pin].concat(val));
};

Blynk.prototype.setProperty = function(pin, prop, val) {
  this.sendMsg(MsgType.PROPERTY, [pin, prop].concat(val));
};

Blynk.prototype.eventLog = function(name, descr) {
  this.sendMsg(MsgType.EVENT_LOG, [name].concat(descr));
};

Blynk.prototype.syncAll = function() {
  this.sendMsg(MsgType.HW_SYNC);
};

Blynk.prototype.syncVirtual = function(pin) {
  this.sendMsg(MsgType.HW_SYNC, ['vr', pin]);
};

Blynk.prototype.email = function(to, topic, message) {
  this.sendMsg(MsgType.EMAIL, [to, topic, message]);
};

Blynk.prototype.notify = function(message) {
  this.sendMsg(MsgType.NOTIFY, [message]);
};

Blynk.prototype.tweet = function(message) {
  this.sendMsg(MsgType.TWEET, [message]);
};

exports.Blynk = Blynk;


'use strict';

const path = require('path');
const walk = require('fs-walk');
const plist = require('simple-plist');
const bin = require('./bin');
const fs = require('fs');
const depSolver = require('./depsolver');

module.exports.AppDirectory =

class AppDirectory {
  constructor () {
    this.nested = [];
  }

  async loadFromDirectory (appdir) {
    this.exebin = _getAppExecutable(appdir);
    this.appdir = appdir;
    this.appbin = path.join(this.appdir, this.exebin);
    this.nested = _findNested(this.appdir);
    this.disklibs = _findBinaries(this.appdir);
    const applibs = _findLibraries(this.appdir, this.exebin, this.disklibs);
    this.notlibs = applibs.filter(l => l[0] === '@');
    this.applibs = applibs.filter(l => l[0] !== '@');
    this.syslibs = _findSystemLibraries(this.applibs);
    this.orphan = orphanedLibraries(this.applibs, this.disklibs);
  }

  appLibraries () {
    return this.applibs;
  }

  diskLibraries () {
    return this.disklibs;
  }

  systemLibraries () {
    return this.syslibs;
  }

  unavailableLibraries () {
    return this.notlibs;
  }

  orphanedLibraries (src, dst) {
    return this.orphan;
  }

  nestedApplications () {
    return this.nested;
  }
};

// internal functions //
function orphanedLibraries (src, dst) {
  // list all the libs that are not referenced from the main binary and their dependencies
  const orphan = [];
  for (let lib of dst) {
    if (src.indexOf(lib) === -1) {
      orphan.push(lib);
    }
  }
  return orphan;
}

function _findSystemLibraries (applibs) {
  const syslibs = [];
  for (let lib of applibs) {
    const res = binSysLibs(lib).filter((l) => syslibs.indexOf(l) === -1);
    syslibs.push(...res);
  }
  return syslibs;
}

function _getAppExecutable (appdir) {
  if (!appdir) {
    throw new Error('No application directory is provided');
  }
  const plistPath = path.join(appdir, 'Info.plist');
  try {
    const plistData = plist.readFileSync(plistPath);
    const cfBundleExecutable = plistData['CFBundleExecutable'];
    if (cfBundleExecutable) {
      return cfBundleExecutable;
    }
  } catch (e) {
    // do nothing
    console.error(e);
  }
  const exename = path.basename(appdir);
  const dotap = exename.indexOf('.app');
  return (dotap === -1) ? exename : exename.substring(0, dotap);
}

/** return an array of strings with the absolute paths of the sub-apps found inside appdir */
function _findNested (d) {
  let nested = [];
  walk.walkSync(d, (basedir, filename, stat) => {
    const file = path.join(basedir, filename);
    if (file.indexOf('.app/Info.plist') !== -1) {
      const nest = file.lastIndexOf('.app/');
      nested.push(file.substring(0, nest + 4));
    }
  });
  return nested;
}

function _findBinaries (appdir) {
  let libraries = [];
  walk.walkSync(appdir, (basedir, filename, stat) => {
    const file = path.join(basedir, filename);
    // only walk on files. Symlinks and other special files are forbidden
    if (!fs.lstatSync(file).isFile()) {
      return;
    }
    if (bin.isMacho(file)) {
      libraries.push(file);
    }
  });
  return libraries;
}

function binSysLibs (file) {
  try {
    return bin.enumerateLibraries(file)
      .filter((l) => l.startsWith('/'));
  } catch (e) {
    console.error('Warning: missing file:', file);
    return [];
  }
}

// return a list of the libs that must be inside the app
function binAbsLibs (file, o) {
  try {
    return bin.enumerateLibraries(file)
      .filter((l) => {
        return !(l.startsWith('/'));
      })
      .map((l) => {
        if (l[0] === '@') {
          const ll = depSolver.resolvePath(o.exe, file, l, o.libs);
          if (ll) {
            l = ll;
          } else {
            console.error('Warning: Cannot resolve dependency library: ' + file);
          }
        }
        return l;
      });
  } catch (e) {
    console.error('Warning: missing file:', file);
    return [];
  }
}

// get all dependencies from appbin recursively
function _findLibraries (appdir, appbin, disklibs) {
  const exe = path.join(appdir, appbin);

  const o = {
    exe: exe,
    lib: exe,
    libs: disklibs
  };
  const libraries = [];
  const pending = [exe];
  while (pending.length > 0) {
    const target = pending.pop();
    if (libraries.indexOf(target) === -1) {
      libraries.push(target);
    }
    let res = binAbsLibs(target, o);
    const unexplored = res.filter(l => libraries.indexOf(l) === -1);
    pending.push(...unexplored.filter(l => pending.indexOf(l) === -1));
    libraries.push(...unexplored);
  }
  return libraries;
}


'use strict';

const plist = require('simple-plist');

const appleDevices = ['iPhone', 'iPad', 'AppleTV', 'AppleWatch'];
const objectFromEntries = (x) => Array.from(x, (k) => ({ [k]: [] })); // ES7 is not yet here

function fix (file, options, emit) {
  const { appdir, bundleid, forceFamily, allowHttp } = options;
  if (!file || !appdir) {
    throw new Error('Invalid parameters for fixPlist');
  }
  let changed = false;
  const data = plist.readFileSync(file);
  delete data[''];
  if (allowHttp) {
    emit('message', 'Adding NSAllowArbitraryLoads');
    if (!Object.isObject(data['NSAppTransportSecurity'])) {
      data['NSAppTransportSecurity'] = {};
    }
    data['NSAppTransportSecurity']['NSAllowsArbitraryLoads'] = true;
    changed = true;
  }
  if (forceFamily) {
    if (performForceFamily(data, emit)) {
      changed = true;
    }
  }
  if (bundleid) {
    setBundleId(data, bundleid);
    changed = true;
  }
  if (changed) {
    plist.writeFileSync(file, data);
  }
}

function setBundleId (data, bundleid) {
  const oldBundleId = data['CFBundleIdentifier'];
  if (oldBundleId) {
    data['CFBundleIdentifier'] = bundleid;
  }
  if (data['basebundleidentifier']) {
    data['basebundleidentifier'] = bundleid;
  }
  try {
    data['CFBundleURLTypes'][0]['CFBundleURLName'] = bundleid;
  } catch (e) {
    /* do nothing */
  }
}

function performForceFamily (data, emit) {
  if (!emit) emit = console.error;
  const have = supportedDevices(data);
  const df = [];
  if (have.iPhone && have.iPhone.length > 0) {
    df.push(1);
  }
  if (have.iPad && have.iPad.length > 0) {
    df.push(2);
  }
  let changes = false;
  if (data.UISupportedDevices) {
    delete data.UISupportedDevices;
    changes = true;
  }
  if ((have.AppleWatch && have.AppleWatch.length > 0) || (have.AppleTV && have.AppleTV.length > 0)) {
    emit('message', 'Apple{TV/Watch} apps do not require to be re-familied');
    return changes;
  }
  if (df.length === 0) {
    emit('message', 'UIDeviceFamily forced to iPhone/iPod');
    df.push(1);
  }
  if (df.length === 2) {
    emit('message', 'No UIDeviceFamily changes required');
    return changes;
  }
  emit('message', 'UIDeviceFamily set to ' + JSON.stringify(df));
  data.UIDeviceFamily = df;
  return true;
}

function supportedDevices (data) {
  const have = objectFromEntries(appleDevices);
  const sd = data.UISupportedDevices;
  if (Array.isArray(sd)) {
    sd.forEach(model => {
      for (let type of appleDevices) {
        if (model.indexOf(type) !== -1) {
          if (!have[type]) {
            have[type] = [];
          }
          have[type].push(model);
          break;
        }
      }
    });
  } else if (sd !== undefined) {
    console.error('Warning: Invalid UISupportedDevices in Info.plist?');
  }
  const df = data.UIDeviceFamily;
  if (Array.isArray(df)) {
    df.forEach(family => {
      const families = ['Any', ...appleDevices];
      const fam = families[family];
      if (fam) {
        if (have[fam] === undefined) {
          have[fam] = [];
        }
        have[fam].push(fam);
      }
    });
  }
  return have;
}

module.exports = fix;


'use strict';

const tools = require('./lib/tools');
const config = require('./lib/config');
const EventEmitter = require('events').EventEmitter;
const path = require('path');
const { execSync } = require('child_process');
const uuid = require('uuid');
const fs = require('fs-extra');
const walk = require('fs-walk');
const plist = require('simple-plist');
const fchk = require('./lib/fchk');

const { AppDirectory } = require('./lib/appdir');

const depSolver = require('./lib/depsolver');

const adjustInfoPlist = require('./lib/info-plist');
const defaultEntitlements = require('./lib/entitlements');

const plistBuild = require('plist').build;
const bin = require('./lib/bin');

function checkArguments(a, b) {
  if (a !== b) {
    throw new Error('Invalid arguments');
  }
}

class Applesign {
  constructor (options) {
    this.config = config.fromOptions(options || {});
    this.events = new EventEmitter();
    this.nested = [];
  }

  async signXCarchive (file) {
    fchk(arguments, ['string']);
    const ipaFile = file + '.ipa';
    await tools.xcaToIpa(file, ipaFile);
    await this.signIPA(ipaFile);
  }

  async getIdentities () {
    fchk(arguments, []);
    return tools.getIdentities();
  }

  async signIPA (file) {
    fchk(arguments, ['string']);
    if (typeof file === 'string') {
      this.setFile(file);
    }
    tools.setOptions({
      use7zip: this.config.use7zip,
      useOpenSSL: this.config.useOpenSSL
    });
    this.emit('message', 'File: ' + this.config.file);
    this.emit('message', 'Outdir: ' + this.config.outdir);
    if (tools.isDirectory(this.config.file)) {
      throw new Error('This is a directory');
    }
    await this.unzipIPA(this.config.file, this.config.outdir);
    const appDirectory = path.join(this.config.outdir, '/Payload');
    this.config.appdir = getAppDirectory(appDirectory);
    if (this.config.withoutWatchapp) {
      await Promise.all([this.removeWatchApp(), this.removeXCTests(), this.removePlugins()]).catch((err) => {
        console.error(err);
      });
    }
    await this.signAppDirectory(appDirectory, false);
    await this.zipIPA();
    await this.cleanup();
    return this;
  }

  _pullMobileProvision () {
    this.config.mobileprovision = this.config.mobileprovisions[0];
    if (this.config.mobileprovisions.length > 1) {
      this.config.mobileprovisions.slice(1);
    }
  }

  async signAppDirectory (ipadir, skipNested) {
    fchk(arguments, ['string', 'boolean']);
    this._pullMobileProvision();
    if (this.config.run) {
      runScriptSync(this.config.run, this);
    }
    if (this.config.appdir === undefined) {
      this.config.appdir = ipadir;
    }
    const binname = getExecutable(this.config.appdir);
    this.emit('msg', 'Main Executable Name: ' + binname);
    this.config.appbin = path.join(this.config.appdir, binname);
    if (!fs.lstatSync(this.config.appbin).isFile()) {
      throw new Error('This was suposed to be a file');
    }
    if (bin.isBitcode(this.config.appbin)) {
      throw new Error('This IPA contains only bitcode. Must be transpiled for the target device to run.');
    }
    if (bin.isEncrypted(this.config.appbin)) {
      if (!this.config.unfairPlay) {
        throw new Error('This IPA is encrypted');
      }
      this.emit('message', 'Main IPA executable is encrypted');
    } else {
      this.emit('message', 'Main IPA executable is not encrypted');
    }
    if (this.config.insertLibrary !== undefined) {
      await insertLibrary(this.config);
    }
    const infoPlistPath = path.join(this.config.appdir, 'Info.plist');
    adjustInfoPlist(infoPlistPath, this.config, this.emit.bind(this));
    if (!this.config.mobileprovision) {
      throw new Error('warning: No mobile provisioning file provided');
    }
    await this.checkProvision(this.config.appdir, this.config.mobileprovision);
    await this.adjustEntitlements(this.config.appbin);
    await this.signLibraries(this.config.appbin, this.config.appdir);

    if (skipNested !== true) {
      for (let nest of this.nested) {
        if (tools.isDirectory(nest)) {
          await this.signAppDirectory(nest, true);
        } else {
          this.emit('warning', 'Cannot find ' + nest);
        }
      }
    }
  }

  //  this is also removing xctests and plugins
  async removeWatchApp () {
    fchk(arguments, []);
    const watchdir = path.join(this.config.appdir, 'Watch');
    this.emit('message', 'Stripping out the WatchApp at ' + watchdir);
    await tools.asyncRimraf(watchdir);
  }

  // wtf
  async removeXCTests () {
    fchk(arguments, []);
    const keepTests = true;
    if (!keepTests) {
      return;
    }
    const dir = this.config.appdir;
    walk.walkSync(dir, (basedir, filename, stat) => {
      if (filename.endsWith('.xctest')) {
        const target = path.join(basedir, filename);
        this.emit('message', 'Deleting ' + target);
        fs.unlinkSync(target);
      }
    });
  }

  async removePlugins () {
    fchk(arguments, []);
    const plugdir = path.join(this.config.appdir, 'PlugIns');
    this.emit('message', 'Stripping out the PlugIns at ' + plugdir);
    await tools.asyncRimraf(plugdir);
  }

  findProvisioningsSync () {
    fchk(arguments, []);
    let files = [];
    walk.walkSync(this.config.appdir, (basedir, filename, stat) => {
      const file = path.join(basedir, filename);
      // only walk on files. Symlinks and other special files are forbidden
      if (!fs.lstatSync(file).isFile()) {
        return;
      }
      if (filename === 'embedded.mobileprovision') {
        files.push(file);
      }
    });
    return files;
  }
  /*
    TODO: verify is mobileprovision app-id glob string matches the bundleid
    read provision file in raw
    search for application-identifier and <string>...</string>
    check if prefix matches and last dot separated word is an asterisk
    const identifierInProvisioning = 'x'
    Read the one in Info.plist and compare with bundleid
  */
  async checkProvision (appdir, file) {
    fchk(arguments, ['string', 'string']);
    /* Deletes the embedded.mobileprovision from the ipa? */
    const withoutMobileProvision = false;
    if (withoutMobileProvision) {
      const files = this.findProvisioningsSync();
      files.forEach((file) => {
        console.error('Deleting ', file);
        fs.unlinkSync(file);
      });
    }
    if (appdir && file && !withoutMobileProvision) {
      this.emit('message', 'Embedding new mobileprovision');
      const mobileProvision = path.join(appdir, 'embedded.mobileprovision');
      if (this.config.selfSignedProvision) {
        /* update entitlements */
        const data = await tools.getMobileProvisionPlist(this.config.mobileprovision);
        const mainBin = path.join(appdir, getExecutable(appdir));
        let ent = bin.entitlements(mainBin);
        if (ent === null) {
          this.emit('warning', 'Cannot find entitlements in binary. Using defaults');
          const entMobProv = data['Entitlements'];
          const teamId = entMobProv['com.apple.developer.team-identifier'];
          const appId = entMobProv['application-identifier'];
          ent = defaultEntitlements(appId, teamId);
        }
        data['Entitlements'] = plist.parse(ent.toString().trim());
        fs.writeFileSync(mobileProvision, plistBuild(data).toString());
        /* TODO: self-sign mobile provisioning */
      }
      return fs.copySync(file, mobileProvision);
    }
  }

  adjustEntitlementsSync (file, entMobProv) {
    fchk(arguments, ['string', 'object']);
    const teamId = entMobProv['com.apple.developer.team-identifier'];
    const appId = entMobProv['application-identifier'];
    let ent = bin.entitlements(file);
    if (ent === null) {
      console.error('Cannot find entitlements in binary. Using defaults');
      ent = defaultEntitlements(appId, teamId);
    }
    let entMacho = plist.parse(ent.toString().trim());
    if (this.config.selfSignedProvision) {
      this.emit('message', 'Using an unsigned provisioning');
      const newEntitlementsFile = file + '.entitlements';
      const newEntitlements = plistBuild(entMacho).toString();
      fs.writeFileSync(newEntitlementsFile, newEntitlements);
      this.config.entitlement = newEntitlementsFile;
      return;
    }
    let changed = false;
    if (this.config.cloneEntitlements) {
      this.emit('message', 'Cloning entitlements');
      entMacho = entMobProv;
      changed = true;
    } else {
      const k = 'com.apple.developer.icloud-container-identifiers';
      if (entMacho[k]) {
        entMacho[k] = 'iCloud.' + appId;
      }
      ['application-identifier', 'com.apple.developer.team-identifier'].forEach((id) => {
        if (entMacho[id] !== entMobProv[id]) {
          changed = true;
          entMacho[id] = entMobProv[id];
        }
      });
      if (typeof entMacho['keychain-access-groups'] === 'object') {
        changed = true;
        // keychain access groups makes the resigning fail with -M
        delete entMacho['keychain-access-groups'];
        // entMacho['keychain-access-groups'][0] = appId;
      }
      if (this.config.massageEntitlements === true) {
        [
          'com.apple.developer.ubiquity-kvstore-identifier',
          'com.apple.developer.ubiquity-container-identifiers',
          'com.apple.developer.icloud-container-identifiers',
          'com.apple.developer.icloud-container-environment',
          'com.apple.developer.icloud-services',
          'com.apple.developer.payment-pass-provisioning',
          'com.apple.developer.default-data-protection',
          'com.apple.networking.vpn.configuration',
          'com.apple.developer.associated-domains',
          'com.apple.security.application-groups',
          'com.apple.developer.team-identifier',
          'com.apple.developer.in-app-payments',
          'com.apple.developer.siri',
          'beta-reports-active', /* our entitlements doesnt support beta */
          'aps-environment'
        ].forEach((id) => {
          if (typeof entMacho[id] !== 'undefined') {
            delete entMacho[id];
            changed = true;
          }
        });
      } else {
        delete entMacho['com.apple.developer.icloud-container-identifiers'];
        delete entMacho['com.apple.developer.icloud-container-environment'];
        delete entMacho['com.apple.developer.ubiquity-kvstore-identifier'];
        delete entMacho['com.apple.developer.icloud-services'];
        delete entMacho['com.apple.developer.siri'];
        delete entMacho['com.apple.developer.in-app-payments'];
        delete entMacho['aps-environment'];
        delete entMacho['com.apple.security.application-groups'];
        delete entMacho['com.apple.developer.associated-domains'];
        delete entMacho['com.apple.developer.team-identifier'];
      }
    }
    if (this.config.withGetTaskAllow) {
      if (entMacho['get-task-allow'] !== true) {
        this.emit('message', 'get-task-allow set to true');
        entMacho['get-task-allow'] = true;
        changed = true;
      }
    }
    const additionalKeychainGroups = [];
    if (typeof this.config.customKeychainGroup === 'string') {
      additionalKeychainGroups.push(this.config.customKeychainGroup);
    }
    const infoPlist = path.join(this.config.appdir, 'Info.plist');
    const plistData = plist.readFileSync(infoPlist);
    if (this.config.bundleIdKeychainGroup) {
      if (typeof this.config.bundleid === 'string') {
        additionalKeychainGroups.push(this.config.bundleid);
      } else {
        const bundleid = plistData['CFBundleIdentifier'];
        additionalKeychainGroups.push(bundleid);
      }
    }
    if (this.config.osversion !== undefined) {
      // DTPlatformVersion
      plistData['MinimumOSVersion'] = this.config.osversion;
      plist.writeFileSync(infoPlist, plistData);
    }
    if (additionalKeychainGroups.length > 0) {
      const newGroups = additionalKeychainGroups.map(group => `${teamId}.${group}`);
      const groups = entMacho['keychain-access-groups'];
      if (typeof groups === 'undefined') {
        entMacho['keychain-access-groups'] = newGroups;
      } else {
        groups.push(...newGroups);
      }
      changed = true;
    }
    if (changed || this.config.entry) {
      const newEntitlementsFile = file + '.entitlements';
      let newEntitlements = (appId && teamId && this.config.entry)
        ? defaultEntitlements(appId, teamId)
        : (this.config.entitlement)
          ? fs.readFileSync(this.config.entitlement).toString()
          : plistBuild(entMacho).toString();
      const ent = plist.parse(newEntitlements.trim());
      const shouldRenameGroups = !this.config.mobileprovision && !this.config.cloneEntitlements;
      if (shouldRenameGroups && ent['com.apple.security.application-groups']) {
        const ids = appId.split('.');
        ids.shift();
        const id = ids.join('.');
        const groups = [];
        for (let group of ent['com.apple.security.application-groups']) {
          const cols = group.split('.');
          if (cols.length === 4) {
            groups.push('group.' + id);
          } else {
            groups.push('group.' + id + '.' + cols.pop());
          }
        }
        ent['com.apple.security.application-groups'] = groups;
      }
      delete ent['beta-reports-active']; /* our entitlements doesnt support beta */
      delete ent['com.apple.developer.ubiquity-container-identifiers']; // TODO should be massaged
      newEntitlements = plistBuild(ent).toString();
      fs.writeFileSync(newEntitlementsFile, newEntitlements);
      this.emit('message', 'Updated binary entitlements' + newEntitlementsFile);
      this.config.entitlement = newEntitlementsFile;
    }
  }

  async adjustEntitlements (file) {
    fchk(arguments, ['string']);
    if (!this.config.mobileprovision) {
      const pathToProvision = path.join(this.config.appdir, 'embedded.mobileprovision');
      const newEntitlements = await tools.getEntitlementsFromMobileProvision(pathToProvision);
      this.emit('message', 'Using the entitlements from the mobileprovision');
      this.adjustEntitlementsSync(file, newEntitlements);
    }
    const newEntitlements = await tools.getEntitlementsFromMobileProvision(this.config.mobileprovision);
    this.emit('message', JSON.stringify(newEntitlements));
    // const pathToProvision = path.join(this.config.appdir, 'embedded.mobileprovision');
    // fs.copySync(this.config.mobileprovision, pathToProvision);
    // plist.writeFileSync(pathToProvision, newEntitlements);
    this.adjustEntitlementsSync(file, newEntitlements);
  }

  async signFile (file) {
    fchk(arguments, ['string']);
    if (this.config.lipoArch !== undefined) {
      this.emit('message', '[lipo] ' + this.config.lipoArch + ' ' + file);
      try {
        await tools.lipoFile(file, this.config.lipoArch);
      } catch (ignored) {
      }
    }
    function codesignHasFailed (config, error, errmsg) {
      if (error && error.message.indexOf('Error:') !== -1) {
        throw error;
      }
      return ((errmsg && errmsg.indexOf('no identity found') !== -1) || !config.ignoreCodesignErrors);
    }
    const res = await tools.codesign(this.config.identity, this.config.entitlement, this.config.keychain, file);
    if (res.code !== 0 && codesignHasFailed(this.config, res.code, res.stderr)) {
      return this.emit('end', res.stderr);
    }
    this.emit('message', 'Signed ' + file);
    if (this.config.verifyTwice) {
      this.emit('message', 'Verify ' + file);
      const res = await tools.verifyCodesign(file, this.config.keychain);
      if (res.code !== 0) {
        const type = (this.config.ignoreVerificationErrors) ? 'warning' : 'error';
        return this.emit(type, res.stderr);
      }
    }
    return this;
  }

  filterLibraries (libraries) {
    fchk(arguments, ['object']);
    return libraries.filter(library => {
      // Resign all frameworks. even if not referenced :?
      if (this.config.all) {
        return true;
      }
      if (library.indexOf('Frameworks/') !== -1) {
        return true;
      }
      // check if there's a Plist to inform us which is the right executable
      const exe = getExecutable(path.dirname(library));
      if (path.basename(library) !== exe) {
        this.emit('warning', 'Not signing ' + library);
        return false;
      }
      return true;
    });
  }

  findLibrariesSync () {
    fchk(arguments, []);
    let libraries = [];
    let nested = [];
    const exe = path.sep + getExecutable(this.config.appdir);
    const folders = this.config.appbin.split(path.sep);
    const exe2 = path.sep + folders[folders.length - 1];

    let found = false;
    walk.walkSync(this.config.appdir, (basedir, filename, stat) => {
      const file = path.join(basedir, filename);
      // only walk on files. Symlinks and other special files are forbidden
      if (!fs.lstatSync(file).isFile()) {
        return;
      }
      if (file.endsWith(exe) || file.endsWith(exe2)) {
        this.emit('message', 'Executable found at ' + file);
        libraries.push(file);
        found = true;
        return;
      }

      const nest = nestedApp(file);
      if (nest !== false) {
        if (nested.indexOf(nest) === -1) {
          nested.push(nest);
        }
        return;
      }
      if (bin.isMacho(file)) {
        libraries.push(file);
      }
    });
    if (!found) {
      throw new Error('Cannot find any MACH0 binary to sign');
    }
    console.error('Found nested', nested);
    this.nested = nested;
    // return this.filterLibraries(libraries);

    return libraries;
  }

  async signLibraries (bpath, appdir) {
    fchk(arguments, ['string', 'string']);
    this.emit('message', 'Signing libraries and frameworks');

    const parallelVerify = async (libs) => {
      if (!this.config.verify) {
        return;
      }
      this.emit('message', 'Verifying ' + libs);
      const promises = libs.map(lib => tools.verifyCodesign);
      return Promise.all(promises);
    };

    const layeredSigning = async (libs) => {
      let libsCopy = libs.slice(0).reverse();
      for (let deps of libsCopy) {
        const promises = deps.map(dep => { return this.signFile(dep); });
        await Promise.all(promises);
      }
      await parallelVerify(libs);
    };

    const serialSigning = async (libs) => {
      let libsCopy = libs.slice(0).reverse();
      for (let lib of libsCopy) {
        await this.signFile(lib);
        if (this.config.verify) {
          this.emit('message', 'Verifying ' + lib);
          await tools.verifyCodesign(lib);
        }
      }
    };

    this.emit('message', 'Resolving signing order using layered list');
    let libs = [];
    const useAppDir = true;
    if (this.config.parallel) {
      // known to be buggy in some situations, must use AppDirectory
      const libraries = this.findLibrariesSync();
      libs = await depSolver(bpath, libraries, true);
    } else {
      const ls = new AppDirectory();
      await ls.loadFromDirectory(appdir);

      this.emit('message', 'Nested: ' + JSON.stringify(ls.nestedApplications()));
      this.emit('message', 'SystemLibraries: ' + JSON.stringify(ls.systemLibraries()));
      this.emit('message', 'DiskLibraries: ' + JSON.stringify(ls.diskLibraries()));
      this.emit('message', 'UnavailableLibraries: ' + JSON.stringify(ls.unavailableLibraries()));
      this.emit('message', 'AppLibraries: ' + JSON.stringify(ls.appLibraries()));
      this.emit('message', 'Orphan: ' + JSON.stringify(ls.orphanedLibraries()));
      const libraries = ls.appLibraries();
      if (this.config.all) {
        libraries.push(...ls.orphanedLibraries());
      }
      // const libraries = ls.diskLibraries ();
      libs = libraries;
    }
    if (libs.length === 0) {
      libs.push(bpath);
    }
    return (typeof libs[0] === 'object')
      ? layeredSigning(libs)
      : serialSigning(libs);
  }

  async cleanup () {
    fchk(arguments, []);
    if (this.config.noclean) {
      return;
    }
    const outdir = this.config.outdir;
    this.emit('message', 'Cleaning up ' + outdir);
    //  await tools.asyncRimraf(this.config.outfile);
    return tools.asyncRimraf(outdir);
  }

  async zipIPA () {
    fchk(arguments, []);
    const ipaIn = this.config.file;
    const ipaOut = getOutputPath(this.config.outdir, this.config.outfile);
    try {
      fs.unlinkSync(ipaOut); // await for it
    } catch (e) {
      /* do nothing */
    }
    this.events.emit('message', 'Zipifying into ' + ipaOut + ' ...');
    const rootFolder = this.config.payloadOnly ? 'Payload' : '.';
    await tools.zip(this.config.outdir, ipaOut, rootFolder);
    if (this.config.replaceipa) {
      this.events.emit('message', 'mv into ' + ipaIn);
      fs.rename(ipaOut, ipaIn);
    }
  }

  setFile (name) {
    fchk(arguments, ['string']);
    this.config.file = path.resolve(name);
    this.config.outdir = this.config.file + '.' + uuid.v4();
    if (!this.config.outfile) {
      this.config.outfile = getResignedFilename(this.config.file);
    }
  }

  async unzipIPA (file, outdir) {
    fchk(arguments, ['string', 'string']);
    if (!file || !outdir) {
      throw new Error('No output specified');
    }
    if (!outdir) {
      throw new Error('Invalid output directory');
    }
    await this.cleanup();
    this.events.emit('message', 'Unzipping ' + file);
    return tools.unzip(file, outdir);
  }

  /* Event Wrapper API with cb support */
  emit (ev, msg) {
    this.events.emit(ev, msg);
  }

  on (ev, cb) {
    this.events.on(ev, cb);
    return this;
  }
}

// helper functions

function getResignedFilename (input) {
  if (!input) {
    return null;
  }
  const pos = input.lastIndexOf(path.sep);
  if (pos !== -1) {
    const tmp = input.substring(pos + 1);
    const dot = tmp.lastIndexOf('.');
    input = (dot !== -1) ? tmp.substring(0, dot) : tmp;
  } else {
    const dot = input.lastIndexOf('.');
    if (dot !== -1) {
      input = input.substring(0, dot);
    }
  }
  return input + '-resigned.ipa';
}

function getExecutable (appdir) {
  if (!appdir) {
    throw new Error('No application directory is provided');
  }
  const plistPath = path.join(appdir, 'Info.plist');
  try {
    const plistData = plist.readFileSync(plistPath);
    const cfBundleExecutable = plistData['CFBundleExecutable'];
    if (cfBundleExecutable) {
      return cfBundleExecutable;
    }
  } catch (e) {
    // do nothing
  }
  const exename = path.basename(appdir);
  const dotap = exename.indexOf('.app');
  return (dotap === -1) ? exename : exename.substring(0, dotap);
}

async function insertLibrary (config) {
  const appDir = config.appdir;
  const targetLib = config.insertLibrary;
  const libraryName = path.basename(targetLib);
  try {
    fs.mkdirSync(path.join(appDir, 'Frameworks'));
  } catch (_) {
  }
  const outputLib = path.join(appDir, 'Frameworks', libraryName);
  await insertLibraryLL(outputLib, targetLib);
}

function insertLibraryLL (outputLib, targetLib) {
  return new Promise((resolve, reject) => {
    try {
      const writeStream = fs.createWriteStream(outputLib);
      writeStream.on('finish', () => {
        fs.chmodSync(outputLib, 0x1ed); // 0755
        /* XXX: if binary doesnt contains an LC_RPATH load command this will not work */
        const insertedLibraryName = '@rpath/' + path.basename(targetLib);
        /* Just copy the library via USB on the DCIM directory */
        // const insertedLibraryName = '/var/mobile/Media/DCIM/' + path.basename(targetLib);
        /* useful on jailbroken devices where we can write in /usr/lib */
        // const insertedLibraryName = '/usr/lib/' + path.basename(targetLib);
        /* forbidden in iOS */
        // const insertedLibraryName = '@executable_path/Frameworks/' + path.basename(targetLib);
        tools.insertLibrary(insertedLibraryName, config.appbin, outputLib).then(resolve).catch(reject);
      });
      fs.createReadStream(targetLib).pipe(writeStream);
    } catch (e) {
      reject(e);
    }
  });
}

function parentDirectory (root) {
  return path.normalize(path.join(root, '..'));
}

function getOutputPath (cwd, ofile) {
  return ofile.startsWith(path.sep) ? ofile : path.join(parentDirectory(cwd), ofile);
}

function runScriptSync (script, session) {
  if (script.endsWith('.js')) {
    try {
      const s = require(script);
      return s(session);
    } catch (e) {
      console.error(e);
      return false;
    }
  } else {
    process.env['APPLESIGN_DIRECTORY'] = session.config.appdir;
    process.env['APPLESIGN_MAINBIN'] = session.config.appbin;
    process.env['APPLESIGN_OUTFILE'] = session.config.outfile;
    process.env['APPLESIGN_OUTDIR'] = session.config.outdir;
    process.env['APPLESIGN_FILE'] = session.config.file;
    try {
      const res = execSync(script);
      console.error(res.toString());
    } catch (e) {
      console.error(e.toString());
      return false;
    }
  }
  return true;
}

function nestedApp (file) {
  const dotApp = file.indexOf('.app/');
  if (dotApp !== -1) {
    const subApp = file.substring(dotApp + 4).indexOf('.app/');
    if (subApp !== -1) {
      return file.substring(0, dotApp + 4 + subApp + 4);
    }
  }
  return false;
}

function getAppDirectory (ipadir) {
  if (!ipadir) {
    ipadir = path.join(this.config.outdir, 'Payload');
  }
  if (!tools.isDirectory(ipadir)) {
    throw new Error('Not a directory ' + ipadir);
  }
  if (ipadir.endsWith('.app')) {
    this.config.appdir = ipadir;
  } else {
    const files = fs.readdirSync(ipadir).filter((x) => {
      return x.endsWith('.app');
    });
    if (files.length !== 1) {
      throw new Error('Invalid IPA: ' + ipadir);
    }
    return path.join(ipadir, files[0]);
  }
  if (ipadir.endsWith('/')) {
    ipadir = ipadir.substring(0, ipadir.length - 1);
  }
  return ipadir;
}
module.exports = Applesign;


'use strict'

const EnvVarError = require('./env-error')
const base64Regex = /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$/

/**
 * Returns an Object that contains functions to read and specify the format of
 * the variable you wish to have returned
 * @return {Object}
 */
module.exports = function getVariableAccessors (container, varName, defValue) {
  let isBase64 = false

  /**
   * Generate a function to throw an error
   * @param {String} value
   */
  function generateRaiseError (value) {
    return function _raiseError (msg) {
      throw new EnvVarError(`"${varName}" ${msg}, but was "${value}"`)
    }
  }

  /**
   * Returns an accessor wrapped by error handling and args passing logic
   * @param {String} accessorPath
   */
  function generateAccessor (accessor) {
    return function () {
      let value = container[varName]

      if (typeof value === 'undefined') {
        if (typeof defValue === 'undefined') {
          // Need to return since no value is available. If a value needed to
          // be available required() should be called, or a default passed
          return
        }

        // Assign the default as the value since process.env does not contain
        // the desired variable
        value = defValue
      }

      if (isBase64) {
        if (!value.match(base64Regex)) {
          generateRaiseError(value)('should be a valid base64 string if using convertFromBase64')
        }

        value = Buffer.from(value, 'base64').toString()
      }

      const args = [
        generateRaiseError(value),
        value
      ].concat(Array.prototype.slice.call(arguments))

      return accessor.apply(
        accessor,
        args
      )
    }
  }

  // Cannot dynamically load accessors if we want to support browsers
  const accessors = {
    asArray: generateAccessor(require('./accessors/array')),

    asBoolStrict: generateAccessor(require('./accessors/bool-strict')),
    asBool: generateAccessor(require('./accessors/bool')),

    asEnum: generateAccessor(require('./accessors/enum')),

    asFloatNegative: generateAccessor(require('./accessors/float-negative')),
    asFloatPositive: generateAccessor(require('./accessors/float-positive')),
    asFloat: generateAccessor(require('./accessors/float')),

    asIntNegative: generateAccessor(require('./accessors/int-negative')),
    asIntPositive: generateAccessor(require('./accessors/int-positive')),
    asInt: generateAccessor(require('./accessors/int')),

    asJsonArray: generateAccessor(require('./accessors/json-array')),
    asJsonObject: generateAccessor(require('./accessors/json-object')),
    asJson: generateAccessor(require('./accessors/json')),

    asString: generateAccessor(require('./accessors/string')),

    asUrlObject: generateAccessor(require('./accessors/url-object')),
    asUrlString: generateAccessor(require('./accessors/url-string')),

    convertFromBase64: function () {
      isBase64 = true

      return accessors
    },

    /**
     * Ensures a variable is set in the given environment container. Throws an
     * EnvVarError if the variable is not set or a default is not provided
     * @param {Boolean} isRequired
     */
    required: function (isRequired) {
      if (isRequired === false) {
        return accessors
      }

      if (typeof container[varName] === 'undefined' && typeof defValue === 'undefined') {
        throw new EnvVarError(`"${varName}" is a required variable, but it was not set`)
      }

      return accessors
    }
  }

  return accessors
}


module.exports = function(Helper, utils){
  /**
   * 获取引用文本，当引用自身不存在的情况下，需要返回原来的模板字符串
   */
  function getRefText(ast){

    var ret = ast.leader;
    var isFn = ast.args !== undefined;

    if (ast.type === 'macro_call') {
      ret = '#';
    }

    if (ast.isWraped) ret += '{';

    if (isFn) {
      ret += getMethodText(ast);
    } else {
      ret += ast.id;
    }

    utils.forEach(ast.path, function(ref){
      //不支持method并且传递参数
      if (ref.type == 'method') {
        ret += '.' + getMethodText(ref);
      } else if (ref.type == 'index') {

        var text = '';
        var id = ref.id;

        if (id.type === 'integer') {

          text = id.value;

        } else if (id.type === 'string') {

          var sign = id.isEval? '"': "'";
          text = sign + id.value + sign;

        } else {

          text = getRefText(id);

        }

        ret += '[' + text + ']';

      } else if (ref.type == 'property') {

        ret += '.' + ref.id;

      }

    }, this);

    if (ast.isWraped) ret += '}';

    return ret;
  }

  function getMethodText(ref) {

    var args = [];
    var ret = '';

    utils.forEach(ref.args, function(arg){
      args.push(getLiteral(arg));
    });

    ret += ref.id + '(' + args.join(',') + ')';

    return ret;

  }

  function getLiteral(ast){

    var ret = '';

    switch(ast.type) {

      case 'string': {
        var sign = ast.isEval? '"': "'";
        ret = sign + ast.value + sign;
        break;
      }

      case 'integer':
      case 'runt':
      case 'bool'   : {
        ret = ast.value;
        break;
      }

      case 'array': {
        ret = '[';
        var len = ast.value.length - 1;
        utils.forEach(ast.value, function(arg, i){
          ret += getLiteral(arg);
          if (i !== len) ret += ', ';
        });
        ret += ']';
        break;
      }

      default:
        ret = getRefText(ast)
    }

    return ret;
  }

  Helper.getRefText = getRefText;
};


module.exports = function(Velocity, utils) {

  'use strict';

  function getSize(obj) {

    if (utils.isArray(obj)) {
      return obj.length;
    } else if (utils.isObject(obj)) {
      return utils.keys(obj).length;
    }

    return undefined;
  }

  /**
   * unicode转码
   */
  function convert(str) {

    if (typeof str !== 'string') return str;

    var result = ""
    var escape = false
    var i, c, cstr;

    for (i = 0 ; i < str.length ; i++) {
      c = str.charAt(i);
      if ((' ' <= c && c <= '~') || (c === '\r') || (c === '\n')) {
        if (c === '&') {
          cstr = "&"
          escape = true
        } else if (c === '<') {
          cstr = "<"
          escape = true
        } else if (c === '>') {
          cstr = ">"
          escape = true
        } else {
          cstr = c.toString()
        }
      } else {
        cstr = "&#" + c.charCodeAt().toString() + ";"
      }

      result = result + cstr
    }

    return escape ? result : str
  }

  function getter(base, property) {
    // get(1)
    if (typeof property === 'number') {
      return base[property];
    }

    var letter = property.charCodeAt(0);
    var isUpper = letter < 91;
    var ret = base[property];

    if (ret !== undefined) {
      return ret;
    }

    if (isUpper) {
      // Address => address
      property = String.fromCharCode(letter).toLowerCase() + property.slice(1);
    }

    if (!isUpper) {
      // address => Address
      property = String.fromCharCode(letter).toUpperCase() + property.slice(1);
    }

    return base[property];
  }

  utils.mixin(Velocity.prototype, {
    // 增加某些函数，不需要执行html转义
    addIgnoreEscpape: function(key) {

      if (!utils.isArray(key)) key = [key]

      utils.forEach(key, function(key) {
        this.config.unescape[key] = true
      }, this)

    },

    /**
     * 引用求值
     * @param {object} ast 结构来自velocity.yy
     * @param {bool} isVal 取值还是获取字符串，两者的区别在于，求值返回结果，求
     * 字符串，如果没有返回变量自身，比如$foo
     */
    getReferences: function(ast, isVal) {

      if (ast.prue) {
        var define = this.defines[ast.id];
        if (utils.isArray(define)) {
          return this._render(define);
        }
        if (ast.id in this.config.unescape) ast.prue = false;
      }
      var escape = this.config.escape;

      var isSilent = this.silence || ast.leader === "$!";
      var isfn     = ast.args !== undefined;
      var context  = this.context;
      var ret      = context[ast.id];
      var local    = this.getLocal(ast);

      var text = Velocity.Helper.getRefText(ast);

      if (text in context) {
        return (ast.prue && escape) ? convert(context[text]) : context[text];
      }

      if (ret !== undefined && isfn) {
        ret = this.getPropMethod(ast, context, ast);
      }

      if (local.isLocaled) ret = local['value'];

      if (ast.path) {

        utils.some(ast.path, function(property, i, len) {

          if (ret === undefined) {
            this._throw(ast, property);
          }

          // 第三个参数，返回后面的参数ast
          ret = this.getAttributes(property, ret, ast);

        }, this);
      }

      if (isVal && ret === undefined) {
        ret = isSilent ? '' : Velocity.Helper.getRefText(ast);
      }

      ret = (ast.prue && escape) ? convert(ret) : ret;

      return ret;
    },

    /**
     * 获取局部变量，在macro和foreach循环中使用
     */
    getLocal: function(ast) {

      var id = ast.id;
      var local = this.local;
      var ret = false;

      var isLocaled = utils.some(this.conditions, function(contextId) {
        var _local = local[contextId];
        if (id in _local) {
          ret = _local[id];
          return true;
        }

        return false;
      }, this);

      return {
        value: ret,
        isLocaled: isLocaled
      };
    },
    /**
     * $foo.bar 属性求值，最后面两个参数在用户传递的函数中用到
     * @param {object} property 属性描述，一个对象，主要包括id，type等定义
     * @param {object} baseRef 当前执行链结果，比如$a.b.c，第一次baseRef是$a,
     * 第二次是$a.b返回值
     * @private
     */
    getAttributes: function(property, baseRef, ast) {
      // fix #54
      if (baseRef === null || baseRef === undefined) {
        return undefined;
      }

      /**
       * type对应着velocity.yy中的attribute，三种类型: method, index, property
       */
      var type = property.type;
      var ret;
      var id = property.id;
      if (type === 'method') {
        ret = this.getPropMethod(property, baseRef, ast);
      } else if (type === 'property') {
        ret = baseRef[id];
      } else {
        ret = this.getPropIndex(property, baseRef);
      }
      return ret;
    },

    /**
     * $foo.bar[1] index求值
     * @private
     */
    getPropIndex: function(property, baseRef) {
      var ast = property.id;
      var key;
      if (ast.type === 'references') {
        key = this.getReferences(ast);
      } else if (ast.type === 'integer') {
        key = ast.value;
      } else {
        key = ast.value;
      }

      return baseRef[key];
    },

    /**
     * $foo.bar()求值
     */
    getPropMethod: function(property, baseRef, ast) {

      var id = property.id;
      var ret = '';

      // getter 处理
      if (id.indexOf('get') === 0 && !(id in baseRef)) {
        if (id.length === 3) {
          // get('address')
          ret = getter(baseRef, this.getLiteral(property.args[0]));
        } else {
          // getAddress()
          ret = getter(baseRef, id.slice(3));
        }

        return ret;

      // setter 处理
      } else if (id.indexOf('set') === 0 && !baseRef[id]) {

        baseRef[id.slice(3)] = this.getLiteral(property.args[0]);
        // $page.setName(123)
        baseRef.toString = function() { return ''; };
        return baseRef;

      } else if (id.indexOf('is') === 0 && !(id in baseRef)) {

        return getter(baseRef, id.slice(2));
      } else if (id === 'keySet' && !baseRef[id]) {

        return utils.keys(baseRef);

      } else if (id === 'entrySet' && !baseRef[id]) {

        ret = [];
        utils.forEach(baseRef, function(value, key) {
          ret.push({key: key, value: value});
        });

        return ret;

      } else if (id === 'size' && !baseRef[id]) {

        return getSize(baseRef);
      } else if (id === 'put' && !baseRef[id]) {
        return baseRef[this.getLiteral(property.args[0])] = this.getLiteral(property.args[1]);
      } else if (id === 'add' && !baseRef[id] && typeof baseRef.push === 'function') {
        return baseRef.push(this.getLiteral(property.args[0]));
      } else if (id === 'subList' && !baseRef[id]) {
        return baseRef.slice(this.getLiteral(property.args[0]), this.getLiteral(property.args[1]));
      } else {

        ret = baseRef[id];
        var args = [];

        utils.forEach(property.args, function(exp) {
          args.push(this.getLiteral(exp));
        }, this);

        if (ret && ret.call) {

          var that = this;

          if(typeof baseRef === 'object' && baseRef){
            baseRef.eval = function() {
              return that.eval.apply(that, arguments);
            };
          }

          try {
            ret = ret.apply(baseRef, args);
          } catch (e) {
            var pos = ast.pos;
            var text = Velocity.Helper.getRefText(ast);
            var err = ' on ' + text + ' at L/N ' +
              pos.first_line + ':' + pos.first_column;
            e.name = '';
            e.message += err;
            throw new Error(e);
          }

        } else {
          this._throw(ast, property, 'TypeError');
          ret = undefined;
        }
      }

      return ret;
    },

    _throw: function(ast, property, errorName) {
      if (this.config.env !== 'development') {
        return;
      }

      var text = Velocity.Helper.getRefText(ast);
      var pos = ast.pos;
      var propertyName = property.type === 'index' ? property.id.value : property.id;
      var errorMsg = 'get property ' + propertyName + ' of undefined';
      if (errorName === 'TypeError') {
        errorMsg = propertyName + ' is not method';
      }

      errorMsg += '\n  at L/N ' + text + ' ' + pos.first_line + ':' + pos.first_column;
      var e = new Error(errorMsg);
      e.name = errorName || 'ReferenceError';
      throw e;
    }
  })

}


'use strict';
module.exports = function(Velocity, utils) {
  /**
   * literal解释模块
   * @require {method} getReferences
   */
  utils.mixin(Velocity.prototype, {
    /**
     * 字面量求值，主要包括string, integer, array, map四种数据结构
     * @param literal {object} 定义于velocity.yy文件，type描述数据类型，value属性
     * 是literal值描述
     * @return {object|string|number|array}返回对应的js变量
     */
    getLiteral: function(literal) {

      var type = literal.type;
      var ret = '';

      if (type === 'string') {

        ret = this.getString(literal);

      } else if (type === 'integer') {

        ret = parseInt(literal.value, 10);

      } else if (type === 'decimal') {

        ret = parseFloat(literal.value, 10);

      } else if (type === 'array') {

        ret = this.getArray(literal);

      } else if (type === 'map') {

        ret = {};
        var map = literal.value;

        utils.forEach(map, function(exp, key) {
          ret[key] = this.getLiteral(exp);
        }, this);
      } else if (type === 'bool') {

        if (literal.value === "null") {
          ret = null;
        } else if (literal.value === 'false') {
          ret = false;
        } else if (literal.value === 'true') {
          ret = true;
        }

      } else {

        return this.getReferences(literal);

      }

      return ret;
    },

    /**
     * 对字符串求值，对已双引号字符串，需要做变量替换
     */
    getString: function(literal) {
      var val = literal.value;
      var ret = val;

      if (literal.isEval && (val.indexOf('#') !== -1 ||
            val.indexOf("$") !== -1)) {
        ret = this.evalStr(val);
      }

      return ret;
    },

    /**
     * 对array字面量求值，比如[1, 2]=> [1,2]，[1..5] => [1,2,3,4,5]
     * @param literal {object} array字面量的描述对象，分为普通数组和range数组两种
     * ，和js基本一致
     * @return {array} 求值得到的数组
     */
    getArray: function(literal) {

      var ret = [];

      if (literal.isRange) {

        var begin = literal.value[0];
        if (begin.type === 'references') {
          begin = this.getReferences(begin);
        }

        var end = literal.value[1];
        if (end.type === 'references') {
          end = this.getReferences(end);
        }

        end   = parseInt(end, 10);
        begin = parseInt(begin, 10);

        var i;

        if (!isNaN(begin) && !isNaN(end)) {

          if (begin < end) {
            for (i = begin; i <= end; i++) ret.push(i);
          } else {
            for (i = begin; i >= end; i--) ret.push(i);
          }
        }

      } else {
        utils.forEach(literal.value, function(exp) {
          ret.push(this.getLiteral(exp));
        }, this);
      }

      return ret;
    },

    /**
     * 对双引号字符串进行eval求值，替换其中的变量，只支持最基本的变量类型替换
     */
    evalStr: function(str) {
      var asts = Velocity.parse(str);
      return this._render(asts, this.condition);
    }
  });
};


'use strict';
module.exports = function(Velocity, utils) {

  /**
   * blocks语法处理
   */
  utils.mixin(Velocity.prototype, {
    /**
     * 处理代码库: if foreach macro
     */
    getBlock: function(block) {

      var ast = block[0];
      var ret = '';

      switch (ast.type) {
        case 'if':
          ret = this.getBlockIf(block);
          break;
        case 'foreach':
          ret = this.getBlockEach(block);
          break;
        case 'macro':
          this.setBlockMacro(block);
          break;
        case 'noescape':
          ret = this._render(block.slice(1));
          break;
        case 'define':
          this.setBlockDefine(block);
          break;
        case 'macro_body':
          ret = this.getMacroBody(block);
          break;
        default:
          ret = this._render(block);
      }

      return ret || '';
    },

    /**
     * define
     */
    setBlockDefine: function(block) {
      var ast = block[0];
      var _block = block.slice(1);
      var defines = this.defines;

      defines[ast.id] = _block;
    },

    /**
     * define macro
     */
    setBlockMacro: function(block) {
      var ast = block[0];
      var _block = block.slice(1);
      var macros = this.macros;

      macros[ast.id] = {
        asts: _block,
        args: ast.args
      };
    },

    getMacroBody: function(asts) {
      const ast = asts[0];
      var _block = asts.slice(1);
      var bodyContent = this.eval(_block, {});
      return this.getMacro(ast, bodyContent);
    },

    /**
     * parse macro call
     */
    getMacro: function(ast, bodyContent) {
      var macro = this.macros[ast.id];
      var ret = '';

      if (!macro) {

        var jsmacros = this.jsmacros;
        macro = jsmacros[ast.id];
        var jsArgs = [];

        if (macro && macro.apply) {

          utils.forEach(ast.args, function(a) {
            jsArgs.push(this.getLiteral(a));
          }, this);

          var self = this;

          // bug修复：此处由于闭包特性，导致eval函数执行时的this对象是上一次函数执行时的this对象，渲染时上下文发生错误。
          jsmacros.eval = function() {
            return self.eval.apply(self, arguments);
          };

          try {
            ret = macro.apply(jsmacros, jsArgs);
          } catch (e) {
            var pos = ast.pos;
            var text = Velocity.Helper.getRefText(ast);
            // throws error tree
            var err = '\n      at ' + text + ' L/N ' + pos.first_line + ':' + pos.first_column;
            e.name = '';
            e.message += err;
            throw new Error(e);
          }

        }

      } else {
        var asts = macro.asts;
        var args = macro.args;
        var callArgs = ast.args;
        var local = { bodyContent: bodyContent };
        var guid = utils.guid();
        var contextId = 'macro:' + ast.id + ':' + guid;

        utils.forEach(args, function(ref, i) {
          if (callArgs[i]) {
            local[ref.id] = this.getLiteral(callArgs[i]);
          } else {
            local[ref.id] = undefined;
          }
        }, this);

        ret = this.eval(asts, local, contextId);
      }

      return ret;
    },

    /**
     * eval
     * @param str {array|string} 需要解析的字符串
     * @param local {object} 局部变量
     * @param contextId {string}
     * @return {string}
     */
    eval: function(str, local, contextId) {

      if (!local) {

        if (utils.isArray(str)) {
          return this._render(str);
        } else {
          return this.evalStr(str);
        }

      } else {

        var asts = [];
        var parse = Velocity.parse;
        contextId = contextId || ('eval:' + utils.guid());

        if (utils.isArray(str)) {

          asts = str;

        } else if (parse) {

          asts = parse(str);

        }

        if (asts.length) {

          this.local[contextId] = local;
          var ret = this._render(asts, contextId);
          this.local[contextId] = {};
          this.conditions.shift();
          this.condition = this.conditions[0] || '';

          return ret;
        }

      }

    },

    /**
     * parse #foreach
     */
    getBlockEach: function(block) {

      var ast = block[0];
      var _from = this.getLiteral(ast.from);
      var _block = block.slice(1);
      var _to = ast.to;
      var local = {
        foreach: {
          count: 0
        }
      };
      var ret = '';
      var guid = utils.guid();
      var contextId = 'foreach:' + guid;

      var type = ({}).toString.call(_from);
      if (!_from || (type !== '[object Array]' && type !== '[object Object]')) {
        return '';
      }

      if (utils.isArray(_from)) {
        var len = _from.length;
        utils.forEach(_from, function(val, i) {
          if (this._state.break) {
            return;
          }
          // 构造临时变量
          local[_to] = val;
          local.foreach = {
            count: i + 1,
            index: i,
            hasNext: i + 1 < len
          };
          local.velocityCount = i + 1;

          this.local[contextId] = local;
          ret += this._render(_block, contextId);

        }, this);
      } else {
        var len = utils.keys(_from).length;
        utils.forEach(utils.keys(_from), function(key, i) {
          if (this._state.break) {
            return;
          }
          local[_to] = _from[key];
          local.foreach = {
            count: i + 1,
            index: i,
            hasNext: i + 1 < len
          };
          local.velocityCount = i + 1;
          this.local[contextId] = local;
          ret += this._render(_block, contextId);
        }, this);
      }

      // if foreach items be an empty array, then this code will shift current
      // conditions, but not this._render call, so this will shift parent context
      if (_from && _from.length) {
        this._state.break = false;
        // empty current local context object
        this.local[contextId] = {};
        this.conditions.shift();
        this.condition = this.conditions[0] || '';
      }

      return ret;

    },

    /**
     * parse #if
     */
    getBlockIf: function(block) {

      var received = false;
      var asts = [];

      utils.some(block, function(ast) {

        if (ast.condition) {

          if (received) {
            return true;
          }
          received = this.getExpression(ast.condition);

        } else if (ast.type === 'else') {
          if (received) {
            return true;
          }
          received = true;
        } else if (received) {
          asts.push(ast);
        }

        return false;

      }, this);

      // keep current condition fix #77
      return this._render(asts, this.condition);
    }
  });
};


module.exports = function(Velocity, utils){

  utils.mixin(Velocity.prototype, {

    //获取变量类型
    //TODO: foreach嵌套处理
    getRefType: function(ast){

      var local = this.getLocal(ast);
      var real = local.real || ast;
      var ret = { ignore: false, type: 'string', real: real, foreach: false };

      if (local.real === undefined && local.isGlobal !== true) {
        ret.ignore = true;
      }

      var m = this.hasMethod(real);
      var eachTo;

      if (local.type == 'foreach') {

        if (ast.id == local.ast.to) {

          // 排除get key value size等通用方法
          if (ast.path && !(~['get', 'key', 'value', 'size'].indexOf(ast.path[0].id))) {
            local.objectKeys.push(ast.path[0].id);
            ret.ignore = true;
          } else {
            ret.real = local.ast;
            ret.foreach = true;
          }
        }

        eachTo = ast.id;
      }

      if (m === 'ignore') {
        ret.ignore = true;
      } else if (m) {
        ret.foreach = ret.foreach || this.hasParamInForeach(real);
        ret.type = 'method';
      }

      return ret;

    },

    hasParamInForeach: function(ast){

      var args = ast.args;
      var ret = false;

      if (args === undefined && ast.path) {
        args = ast.path[ast.path.length - 1].args;
      }

      if (args) {

        utils.some(args, function(a){
          var local = this.getLocal(a);
          if (local.type === 'foreach') {
            ret = local.ast;
            return true;
          }
        }, this);

      }

      return ret;
    },

    /**
     * @param ast references ast
     * @return {true|false|'ignore'} true:pass, false:no method, ignore:ignore
     */
    hasMethod: function(ast){

      var ret = false;

      ret = ast.args !== undefined;

      var path = ast.path;

      if (path) {
        var len = path.length;
        var methods = utils.filter(path, function(a){
          var isGet = a.id.indexOf('get') === 0 && a.args === false;
          return a.type === 'method' && !isGet;
        });
        var _len = methods.length;

        //连缀方式$f.foo().bar()或者函数在中间的情况$foo().bar，跳过
        if (ret || _len > 1 ||
          (_len === 1 && path[len - 1].type !== 'method')) {
          ret = 'ignore';
        } else if (ret === false && _len === 1) {
          ret = true;
        }
      }

      return ret;

    },

    isFn: function(ast){
      var fns = this.fns.context;
      var isNoPass = true;
      fns = fns[ast.id];

      if (fns) {
        var args = null;
        var fn;
        utils.forEach(ast.path, function(a){

          if (typeof fn !== 'function') {
            fn = fns[a.id];
          } else {
            return;
          }

          if (a.type === 'method') {
            if (typeof fn === 'function') {
              args = a.args;
              isNoPass = false;
            }
          } else {
            fns = fns[a.id];
          }

        });

        var _arg = [];
        utils.forEach(args, function(arg){
          _arg.push(arg.value);
        });

        fn && fn.apply && fn.apply(this, _arg);
      }

      return !isNoPass;
    },

    getReferences: function(ast){

      if (ast.type !== 'references') return;

      if (this.isFn(ast)) {

      } else {

        var astType = this.getRefType(ast);

        var real = astType.real;
        var text = real.from ? this.getRefText(real.from): this.getRefText(real);

        //执行过一遍，不再执行
        if (this.cache[text]) return;

        this.toBasicType(astType);

        this.cache[text] = true;
      }

    },

    getLocal: function(ref){

      var ret = { context: this.context, isGlobal: true };

      utils.some(this.conditions, function(content){

        var local = this.local[content];
        var index = local.variable.indexOf(ref.id);

        if (index > -1) {
          ret = local;
          ret['isGlobal'] = false;
          ret['real'] = ret.maps? ret.maps[index]: ref;
          if (ret['real'] && ret['real'].type !== 'references') {
            ret['real'] = undefined;
          }
          return true;
        }

      }, this);

      if (ret.type === 'macro' && ret.real && !this.context[ref.id]) {
        // see: https://github.com/shepherdwind/velocity.js/issues/13
        // 最后一个判断，防止死循环
        // 递归查找上层
        var _local  = this.getLocal(ret.real);
        if (_local.isGlobal === false && _local.real) {
          ret.real = _local.real;
        }
      }

      return ret;
    }
  });
};


module.exports = function(Velocity, utils){
  /**
   * expression运算
   */
  utils.mixin(Velocity.prototype, {
    /**
     * 表达式求值，表达式主要是数学表达式，逻辑运算和比较运算，到最底层数据结构，
     * 基本数据类型，使用 getLiteral求值，getLiteral遇到是引用的时候，使用
     * getReferences求值
     */
    getExpression: function(ast){

      var exp = ast.expression;
      var ret;
      if (ast.type === 'math') {

        switch(ast.operator) {
          case '+':
          ret = this.getExpression(exp[0]) + this.getExpression(exp[1]);
          break;

          case '-':
          ret = this.getExpression(exp[0]) - this.getExpression(exp[1]);
          break;

          case '/':
          ret = this.getExpression(exp[0]) / this.getExpression(exp[1]);
          break;

          case '%':
          ret = this.getExpression(exp[0]) % this.getExpression(exp[1]);
          break;

          case '*':
          ret = this.getExpression(exp[0]) * this.getExpression(exp[1]);
          break;

          case '||':
          ret = this.getExpression(exp[0]) || this.getExpression(exp[1]);
          break;

          case '&&':
          ret = this.getExpression(exp[0]) && this.getExpression(exp[1]);
          break;

          case '>':
          ret = this.getExpression(exp[0]) > this.getExpression(exp[1]);
          break;

          case '<':
          ret = this.getExpression(exp[0]) < this.getExpression(exp[1]);
          break;

          case '==':
          ret = this.getExpression(exp[0]) == this.getExpression(exp[1]);
          break;

          case '>=':
          ret = this.getExpression(exp[0]) >= this.getExpression(exp[1]);
          break;

          case '<=':
          ret = this.getExpression(exp[0]) <= this.getExpression(exp[1]);
          break;

          case '!=':
          ret = this.getExpression(exp[0]) != this.getExpression(exp[1]);
          break;

          case 'minus':
          ret = - this.getExpression(exp[0]);
          break;

          case 'not':
          ret = !this.getExpression(exp[0]);
          break;

          case 'parenthesis':
          ret = this.getExpression(exp[0]);
          break;

          default:
          return;
          // code
        }

        return ret;
      } else {
        return this.getLiteral(ast);
      }
    }
  });
};


module.exports = function(Velocity, utils) {

  /**
   * compile
   */
  utils.mixin(Velocity.prototype, {
    init: function() {
      this.context = {};
      this.macros = {};
      this.defines = {};
      this.conditions = [];
      this.local = {};
      this.silence = false;
      this.unescape = {};

      var self = this;
      this.directive = {
        stop: function() {
          self._state.stop = true;
          return '';
        }
      };
    },

    /**
     * @param context {object} 上下文环境，数据对象
     * @param macro   {object} self defined #macro
     * @param silent {bool} 如果是true，$foo变量将原样输出
     * @return str
     */
    render: function(context, macros, silence) {

      this.silence = !!silence;
      this.context = context || {};
      this.jsmacros = utils.mixin(macros || {}, this.directive);
      var t1 = utils.now();
      var str = this._render();
      var t2 = utils.now();
      var cost = t2 - t1;

      this.cost = cost;

      return str;
    },

    /**
     * 解析入口函数
     * @param ast {array} 模板结构数组
     * @param contextId {number} 执行环境id，对于macro有局部作用域，变量的设置和
     * 取值，都放在一个this.local下，通过contextId查找
     * @return {string}解析后的字符串
     */
    _render: function(asts, contextId) {

      var str = '';
      asts = asts || this.asts;

      if (contextId) {

        if (contextId !== this.condition &&
            utils.indexOf(contextId, this.conditions) === -1) {
          this.conditions.unshift(contextId);
        }

        this.condition = contextId;

      } else {
        this.condition = null;
      }

      utils.forEach(asts, function(ast) {

        // 进入stop，直接退出
        if (this._state.stop === true) {
          return false;
        }

        switch (ast.type) {
          case 'references':
            str += this.format(this.getReferences(ast, true));
          break;

          case 'set':
            this.setValue(ast);
          break;

          case 'break':
            this._state.break = true;
          break;

          case 'macro_call':
            str += this.getMacro(ast);
          break;

          case 'comment':
          break;

          case 'raw':
            str += ast.value;
          break;

          default:
            str += typeof ast === 'string' ? ast : this.getBlock(ast);
          break;
        }
      }, this);

      return str;
    },
    format: function(value) {
      if (utils.isArray(value)) {
        return "[" + value.map(this.format.bind(this)).join(", ") + "]";
      }

      if (utils.isObject(value)) {
        if (value.toString.toString().indexOf('[native code]') === -1) {
          return value;
        }

        var kvJoin = function(k) { return k + "=" + this.format(value[k]); }.bind(this);
        return "{" + Object.keys(value).map(kvJoin).join(", ") + "}";
      }

      return value;
    }
  });
};


let ConcatenatedModule;
try {
  // eslint-disable-next-line global-require,import/no-unresolved,import/no-extraneous-dependencies
  ConcatenatedModule = require('webpack/lib/optimize/ConcatenatedModule');
  // eslint-disable-next-line no-empty
} catch (e) {}

/* eslint-disable no-magic-numbers */
const { NAMESPACE } = require('../config');

/**
 * Get all modules from main & child compilations.
 * Merge modules from ConcatenatedModule (when webpack.optimize.ModuleConcatenationPlugin is used)
 * @param {Compilation} compilation
 * @return {NormalModule[]}
 */
function getAllModules(compilation) {
  let modules = compilation.modules;

  // Look up in child compilations
  if (compilation.children.length > 0) {
    const childModules = compilation.children.map(getAllModules)
      .reduce((acc, compilationModules) => acc.concat(compilationModules), []);

    modules = modules.concat(childModules);
  }

  // Merge modules from ConcatenatedModule
  if (ConcatenatedModule) {
    const concatenatedModules = modules
      .filter(m => m instanceof ConcatenatedModule)
      .reduce((acc, m) => {
        /**
         * @see https://git.io/v7XDu
         * In webpack@3.5.1 `modules` public property was removed
         * To workaround this private `_orderedConcatenationList` property is used to collect modules
         */
        const subModules = 'modules' in m
          ? m.modules
          : m._orderedConcatenationList.map(entry => entry.module);

        return acc.concat(subModules);
      }, []);

    if (concatenatedModules.length > 0) {
      modules = modules.concat(concatenatedModules);
    }
  }

  return modules.filter(m => m.rawRequest);
}

module.exports.getAllModules = getAllModules;

/**
 * Find nearest module chunk (not sure that is reliable method, but who cares).
 * @see http://stackoverflow.com/questions/43202761/how-to-determine-all-module-chunks-in-webpack
 * @param {NormalModule} module
 * @return {Chunk|null}
 */
function getModuleChunk(module) {
  let chunks;
  const webpackVersion = getWebpackMajorVersion();

  if (webpackVersion >= 4) {
    chunks = Array.from(module.chunksIterable);
  } else if (webpackVersion >= 3) {
    chunks = module.mapChunks();
  } else {
    chunks = module.chunks;
  }

  if (Array.isArray(chunks) && chunks.length > 0) {
    return chunks[chunks.length - 1];
  } else if (module.issuer) {
    return getModuleChunk(module.issuer);
  }

  return null;
}

module.exports.getModuleChunk = getModuleChunk;

/**
 * @return {number}
 */
function getWebpackMajorVersion() {
  const v = require.main.require('webpack/package.json').version.split('.')[0];
  return parseInt(v, 10);
}

module.exports.getWebpackMajorVersion = getWebpackMajorVersion;

/**
 * @param {Compilation} compilation
 * @return {boolean}
 */
function isHtmlPluginCompilation(compilation) {
  return (
    compilation.compiler.name &&
    compilation.compiler.name.startsWith('html-webpack-plugin')
  );
}

module.exports.isHtmlPluginCompilation = isHtmlPluginCompilation;

/**
 * @param {Compilation} compilation
 * @return {boolean}
 */
function isMiniExtractCompilation(compilation) {
  return (
    compilation.compiler.name &&
    compilation.compiler.name.startsWith('mini-css-extract-plugin')
  );
}

module.exports.isMiniExtractCompilation = isMiniExtractCompilation;

/**
 * @param {Compilation} compilation
 * @return {Compilation}
 */
function getRootCompilation(compilation) {
  return compilation.compiler.isChild()
    ? compilation.compiler.parentCompilation
    : compilation;
}

module.exports.getRootCompilation = getRootCompilation;

/**
 * @param {Object} loaderContext
 * @return {ExtractSvgSpritePlugin}
 */
function getPluginFromLoaderContext(loaderContext) {
  const { _compiler: compiler } = loaderContext;

  const parentCompiler = compiler.isChild()
    ? compiler.parentCompilation.compiler
    : null;

  return parentCompiler
    ? parentCompiler.options.plugins.find(p => p.NAMESPACE && p.NAMESPACE === NAMESPACE)
    : loaderContext[NAMESPACE];
}

module.exports.getPluginFromLoaderContext = getPluginFromLoaderContext;


var fs
try {
  fs = require('graceful-fs')
} catch (er) {
  fs = require('fs')
}

var path = require('path')

var glob = require('glob')
var normalizeData = require('normalize-package-data')
var safeJSON = require('json-parse-better-errors')
var util = require('util')
var slash = require('slash')

module.exports = readJson

// put more stuff on here to customize.
readJson.extraSet = [
  gypfile,
  serverjs,
  scriptpath,
  authors,
  readme,
  mans,
  bins,
  githead
]

var typoWarned = {}
var cache = {}

function readJson (file, log_, strict_, cb_) {
  var log, strict, cb
  for (var i = 1; i < arguments.length - 1; i++) {
    if (typeof arguments[i] === 'boolean') {
      strict = arguments[i]
    } else if (typeof arguments[i] === 'function') {
      log = arguments[i]
    }
  }

  if (!log) log = function () {}
  cb = arguments[ arguments.length - 1 ]

  readJson_(file, log, strict, cb)
}

function readJson_ (file, log, strict, cb) {
  fs.readFile(file, 'utf8', function (er, d) {
    parseJson(file, er, d, log, strict, cb)
  })
}

function stripBOM (content) {
  // Remove byte order marker. This catches EF BB BF (the UTF-8 BOM)
  // because the buffer-to-string conversion in `fs.readFileSync()`
  // translates it to FEFF, the UTF-16 BOM.
  if (content.charCodeAt(0) === 0xFEFF) content = content.slice(1)
  return content
}

function jsonClone (obj) {
  if (obj == null) {
    return obj
  } else if (Array.isArray(obj)) {
    var newarr = new Array(obj.length)
    for (var ii in obj) {
      newarr[ii] = obj[ii]
    }
  } else if (typeof obj === 'object') {
    var newobj = {}
    for (var kk in obj) {
      newobj[kk] = jsonClone[kk]
    }
  } else {
    return obj
  }
}

function parseJson (file, er, d, log, strict, cb) {
  if (er && er.code === 'ENOENT') {
    return fs.stat(path.dirname(file), function (err, stat) {
      if (!err && stat && !stat.isDirectory()) {
        // ENOTDIR isn't used on Windows, but npm expects it.
        er = Object.create(er)
        er.code = 'ENOTDIR'
        return cb(er)
      } else {
        return indexjs(file, er, log, strict, cb)
      }
    })
  }
  if (er) return cb(er)

  if (cache[d]) return cb(null, jsonClone(cache[d]))

  var data

  try {
    data = safeJSON(stripBOM(d))
  } catch (er) {
    data = parseIndex(d)
    if (!data) return cb(parseError(er, file))
  }

  extrasCached(file, d, data, log, strict, cb)
}

function extrasCached (file, d, data, log, strict, cb) {
  extras(file, data, log, strict, function (err, data) {
    if (!err) {
      cache[d] = jsonClone(data)
    }
    cb(err, data)
  })
}

function indexjs (file, er, log, strict, cb) {
  if (path.basename(file) === 'index.js') return cb(er)

  var index = path.resolve(path.dirname(file), 'index.js')
  fs.readFile(index, 'utf8', function (er2, d) {
    if (er2) return cb(er)

    if (cache[d]) return cb(null, cache[d])

    var data = parseIndex(d)
    if (!data) return cb(er)

    extrasCached(file, d, data, log, strict, cb)
  })
}

readJson.extras = extras
function extras (file, data, log_, strict_, cb_) {
  var log, strict, cb
  for (var i = 2; i < arguments.length - 1; i++) {
    if (typeof arguments[i] === 'boolean') {
      strict = arguments[i]
    } else if (typeof arguments[i] === 'function') {
      log = arguments[i]
    }
  }

  if (!log) log = function () {}
  cb = arguments[i]

  var set = readJson.extraSet
  var n = set.length
  var errState = null
  set.forEach(function (fn) {
    fn(file, data, then)
  })

  function then (er) {
    if (errState) return
    if (er) return cb(errState = er)
    if (--n > 0) return
    final(file, data, log, strict, cb)
  }
}

function scriptpath (file, data, cb) {
  if (!data.scripts) return cb(null, data)
  var k = Object.keys(data.scripts)
  k.forEach(scriptpath_, data.scripts)
  cb(null, data)
}

function scriptpath_ (key) {
  var s = this[key]
  // This is never allowed, and only causes problems
  if (typeof s !== 'string') return delete this[key]

  var spre = /^(\.[/\\])?node_modules[/\\].bin[\\/]/
  if (s.match(spre)) {
    this[key] = this[key].replace(spre, '')
  }
}

function gypfile (file, data, cb) {
  var dir = path.dirname(file)
  var s = data.scripts || {}
  if (s.install || s.preinstall) return cb(null, data)

  glob('*.gyp', { cwd: dir }, function (er, files) {
    if (er) return cb(er)
    if (data.gypfile === false) return cb(null, data)
    gypfile_(file, data, files, cb)
  })
}

function gypfile_ (file, data, files, cb) {
  if (!files.length) return cb(null, data)
  var s = data.scripts || {}
  s.install = 'node-gyp rebuild'
  data.scripts = s
  data.gypfile = true
  return cb(null, data)
}

function serverjs (file, data, cb) {
  var dir = path.dirname(file)
  var s = data.scripts || {}
  if (s.start) return cb(null, data)
  glob('server.js', { cwd: dir }, function (er, files) {
    if (er) return cb(er)
    serverjs_(file, data, files, cb)
  })
}

function serverjs_ (file, data, files, cb) {
  if (!files.length) return cb(null, data)
  var s = data.scripts || {}
  s.start = 'node server.js'
  data.scripts = s
  return cb(null, data)
}

function authors (file, data, cb) {
  if (data.contributors) return cb(null, data)
  var af = path.resolve(path.dirname(file), 'AUTHORS')
  fs.readFile(af, 'utf8', function (er, ad) {
    // ignore error.  just checking it.
    if (er) return cb(null, data)
    authors_(file, data, ad, cb)
  })
}

function authors_ (file, data, ad, cb) {
  ad = ad.split(/\r?\n/g).map(function (line) {
    return line.replace(/^\s*#.*$/, '').trim()
  }).filter(function (line) {
    return line
  })
  data.contributors = ad
  return cb(null, data)
}

function readme (file, data, cb) {
  if (data.readme) return cb(null, data)
  var dir = path.dirname(file)
  var globOpts = { cwd: dir, nocase: true, mark: true }
  glob('{README,README.*}', globOpts, function (er, files) {
    if (er) return cb(er)
    // don't accept directories.
    files = files.filter(function (file) {
      return !file.match(/\/$/)
    })
    if (!files.length) return cb()
    var fn = preferMarkdownReadme(files)
    var rm = path.resolve(dir, fn)
    readme_(file, data, rm, cb)
  })
}

function preferMarkdownReadme (files) {
  var fallback = 0
  var re = /\.m?a?r?k?d?o?w?n?$/i
  for (var i = 0; i < files.length; i++) {
    if (files[i].match(re)) {
      return files[i]
    } else if (files[i].match(/README$/)) {
      fallback = i
    }
  }
  // prefer README.md, followed by README; otherwise, return
  // the first filename (which could be README)
  return files[fallback]
}

function readme_ (file, data, rm, cb) {
  var rmfn = path.basename(rm)
  fs.readFile(rm, 'utf8', function (er, rm) {
    // maybe not readable, or something.
    if (er) return cb()
    data.readme = rm
    data.readmeFilename = rmfn
    return cb(er, data)
  })
}

function mans (file, data, cb) {
  var m = data.directories && data.directories.man
  if (data.man || !m) return cb(null, data)
  m = path.resolve(path.dirname(file), m)
  glob('**/*.[0-9]', { cwd: m }, function (er, mans) {
    if (er) return cb(er)
    mans_(file, data, mans, cb)
  })
}

function mans_ (file, data, mans, cb) {
  var m = data.directories && data.directories.man
  data.man = mans.map(function (mf) {
    return path.resolve(path.dirname(file), m, mf)
  })
  return cb(null, data)
}

function bins (file, data, cb) {
  if (Array.isArray(data.bin)) return bins_(file, data, data.bin, cb)

  var m = data.directories && data.directories.bin
  if (data.bin || !m) return cb(null, data)

  m = path.resolve(path.dirname(file), m)
  glob('**', { cwd: m }, function (er, bins) {
    if (er) return cb(er)
    bins_(file, data, bins, cb)
  })
}

function bins_ (file, data, bins, cb) {
  var m = (data.directories && data.directories.bin) || '.'
  data.bin = bins.reduce(function (acc, mf) {
    if (mf && mf.charAt(0) !== '.') {
      var f = path.basename(mf)
      acc[f] = slash(path.join(m, mf))
    }
    return acc
  }, {})
  return cb(null, data)
}

function githead (file, data, cb) {
  if (data.gitHead) return cb(null, data)
  var dir = path.dirname(file)
  var head = path.resolve(dir, '.git/HEAD')
  fs.readFile(head, 'utf8', function (er, head) {
    if (er) return cb(null, data)
    githead_(file, data, dir, head, cb)
  })
}

function githead_ (file, data, dir, head, cb) {
  if (!head.match(/^ref: /)) {
    data.gitHead = head.trim()
    return cb(null, data)
  }
  var headRef = head.replace(/^ref: /, '').trim()
  var headFile = path.resolve(dir, '.git', headRef)
  fs.readFile(headFile, 'utf8', function (er, head) {
    if (er || !head) {
      var packFile = path.resolve(dir, '.git/packed-refs')
      return fs.readFile(packFile, 'utf8', function (er, refs) {
        if (er || !refs) {
          return cb(null, data)
        }
        refs = refs.split('\n')
        for (var i = 0; i < refs.length; i++) {
          var match = refs[i].match(/^([0-9a-f]{40}) (.+)$/)
          if (match && match[2].trim() === headRef) {
            data.gitHead = match[1]
            break
          }
        }
        return cb(null, data)
      })
    }
    head = head.replace(/^ref: /, '').trim()
    data.gitHead = head
    return cb(null, data)
  })
}

/**
 * Warn if the bin references don't point to anything.  This might be better in
 * normalize-package-data if it had access to the file path.
 */
function checkBinReferences_ (file, data, warn, cb) {
  if (!(data.bin instanceof Object)) return cb()

  var keys = Object.keys(data.bin)
  var keysLeft = keys.length
  if (!keysLeft) return cb()

  function handleExists (relName, result) {
    keysLeft--
    if (!result) warn('No bin file found at ' + relName)
    if (!keysLeft) cb()
  }

  keys.forEach(function (key) {
    var dirName = path.dirname(file)
    var relName = data.bin[key]
    try {
      var binPath = path.resolve(dirName, relName)
      fs.stat(binPath, (err) => handleExists(relName, !err))
    } catch (error) {
      if (error.message === 'Arguments to path.resolve must be strings' || error.message.indexOf('Path must be a string') === 0) {
        warn('Bin filename for ' + key + ' is not a string: ' + util.inspect(relName))
        handleExists(relName, true)
      } else {
        cb(error)
      }
    }
  })
}

function final (file, data, log, strict, cb) {
  var pId = makePackageId(data)

  function warn (msg) {
    if (typoWarned[pId]) return
    if (log) log('package.json', pId, msg)
  }

  try {
    normalizeData(data, warn, strict)
  } catch (error) {
    return cb(error)
  }

  checkBinReferences_(file, data, warn, function () {
    typoWarned[pId] = true
    cb(null, data)
  })
}

function makePackageId (data) {
  var name = cleanString(data.name)
  var ver = cleanString(data.version)
  return name + '@' + ver
}

function cleanString (str) {
  return (!str || typeof (str) !== 'string') ? '' : str.trim()
}

// /**package { "name": "foo", "version": "1.2.3", ... } **/
function parseIndex (data) {
  data = data.split(/^\/\*\*package(?:\s|$)/m)

  if (data.length < 2) return null
  data = data[1]
  data = data.split(/\*\*\/$/m)

  if (data.length < 2) return null
  data = data[0]
  data = data.replace(/^\s*\*/mg, '')

  try {
    return safeJSON(data)
  } catch (er) {
    return null
  }
}

function parseError (ex, file) {
  var e = new Error('Failed to parse json\n' + ex.message)
  e.code = 'EJSONPARSE'
  e.file = file
  return e
}