plist.js 4.7 KB
import xmlplist from 'plist';
import bplistCreate from 'bplist-creator';
import bplistParse from 'bplist-parser';
import fs from './fs';
import log from './logger';
import _ from 'lodash';


const BPLIST_IDENTIFIER = {
  BUFFER: Buffer.from('bplist00'),
  TEXT: 'bplist00'
};
const PLIST_IDENTIFIER = {
  BUFFER: Buffer.from('<'),
  TEXT: '<'
};

// XML Plist library helper
async function parseXmlPlistFile (plistFilename) {
  let xmlContent = await fs.readFile(plistFilename, 'utf8');
  return xmlplist.parse(xmlContent);
}

/**
 * Parses a file in xml or binary format of plist
 * @param {string} plist The plist file path
 * @param {boolean} mustExist If set to false, this method will return an empty object when the file doesn't exist
 * @param {boolean} quiet If set to false, the plist path will be logged in debug level
 * @returns {Object} parsed plist JS Object
 */
async function parsePlistFile (plist, mustExist = true, quiet = true) {
  // handle nonexistant file
  if (!await fs.exists(plist)) {
    if (mustExist) {
      log.errorAndThrow(`Plist file doesn't exist: '${plist}'`);
    } else {
      log.debug(`Plist file '${plist}' does not exist. Returning an empty plist.`);
      return {};
    }
  }

  let obj = {};
  let type = 'binary';
  try {
    obj = await bplistParse.parseFile(plist);
    if (obj.length) {
      obj = obj[0];
    } else {
      throw new Error(`Binary file '${plist}'' appears to be empty`);
    }
  } catch (ign) {
    try {
      obj = await parseXmlPlistFile(plist);
      type = 'xml';
    } catch (err) {
      log.errorAndThrow(`Could not parse plist file '${plist}' as XML: ${err.message}`);
    }
  }

  if (!quiet) {
    log.debug(`Parsed plist file '${plist}' as ${type}`);
  }
  return obj;
}

/**
 * Updates a plist file with the given fields
 * @param {string} plist The plist file path
 * @param {Object} updatedFields The updated fields-value pairs
 * @param {boolean} binary If set to false, the file will be created as a xml plist
 * @param {boolean} mustExist If set to false, this method will update an empty plist
 * @param {boolean} quiet If set to false, the plist path will be logged in debug level
 */
async function updatePlistFile (plist, updatedFields, binary = true, mustExist = true, quiet = true) {
  let obj;
  try {
    obj = await parsePlistFile(plist, mustExist);
  } catch (err) {
    log.errorAndThrow(`Could not update plist: ${err.message}`);
  }
  _.extend(obj, updatedFields);
  let newPlist = binary ? bplistCreate(obj) : xmlplist.build(obj);
  try {
    await fs.writeFile(plist, newPlist);
  } catch (err) {
    log.errorAndThrow(`Could not save plist: ${err.message}`);
  }
  if (!quiet) {
    log.debug(`Wrote plist file '${plist}'`);
  }
}
/**
 * Creates a binary plist Buffer from an object
 * @param {Object} data The object to be turned into a binary plist
 * @returns {Buffer} plist in the form of a binary buffer
 */
function createBinaryPlist (data) {
  return bplistCreate(data);
}

/**
 * Parses a Buffer into an Object
 * @param {Buffer} data The beffer of a binary plist
 */
function parseBinaryPlist (data) {
  return bplistParse.parseBuffer(data);
}

function getXmlPlist (data) {
  if (_.isString(data) && data.startsWith(PLIST_IDENTIFIER.TEXT)) {
    return data;
  }
  if (_.isBuffer(data) && PLIST_IDENTIFIER.BUFFER.compare(data, 0, PLIST_IDENTIFIER.BUFFER.length) === 0) {
    return data.toString();
  }
  return null;
}

function getBinaryPlist (data) {
  if (_.isString(data) && data.startsWith(BPLIST_IDENTIFIER.TEXT)) {
    return Buffer.from(data);
  }

  if (_.isBuffer(data) && BPLIST_IDENTIFIER.BUFFER.compare(data, 0, BPLIST_IDENTIFIER.BUFFER.length) === 0) {
    return data;
  }
  return null;
}

/**
 * Creates a plist from an object
 * @param {Object} object The JS object to be turned into a plist
 * @param {boolean} binary Set it to true for a binary plist
 * @returns {string|Buffer} returns a buffer or a string in respect to the binary parameter
 */
function createPlist (object, binary = false) {
  if (binary) {
    return createBinaryPlist(object);
  } else {
    return xmlplist.build(object);
  }
}

/**
 * Parses an buffer or a string to a JS object a plist from an object
 * @param {string|Buffer} data The plist in the form of string or Buffer
 * @returns {Object} parsed plist JS Object
 * @throws Will throw an error if the plist type is unknown
 */
function parsePlist (data) {
  let textPlist = getXmlPlist(data);
  if (textPlist) {
    return xmlplist.parse(textPlist);
  }

  let binaryPlist = getBinaryPlist(data);
  if (binaryPlist) {
    return parseBinaryPlist(binaryPlist)[0];
  }

  throw new Error(`Unknown type of plist, data: ${data.toString()}`);
}

export { parsePlistFile, parsePlist, createPlist, updatePlistFile, createBinaryPlist, parseBinaryPlist };