alder_lake_bios/Intel/AlderLake/AlderLakePlatSamplePkg/Tools/RomImage/PatchXml.js

1020 lines
27 KiB
JavaScript

/** @file
PatchXml tool to modify a XML based on patch syntax
@copyright
Copyright (c) 2013 Intel Corporation. All rights reserved
This software and associated documentation (if any) is furnished
under a license and may only be used or copied in accordance
with the terms of the license. Except as permitted by such
license, no part of this software or documentation may be
reproduced, stored in a retrieval system, or transmitted in any
form or by any means without the express written consent of
Intel Corporation.
@version 1.0
Initial release.
**/
/*
Usage:
cscript PatchXml.js <input> <output> -patch <patch> [[-patch <patch>] ...]
Options:
-patch patch file with patching syntax, supports multiple files
-help
Remarks:
It is compulsory to include input, output and at least one patch file
Examples:
template.xml contains:
<root attrib="foo">
<a/>
<b foo=" "/>
</root>
patch.p contains:
# use hash sign to comment a line
/root/b {
@foo = "foo"
append @bar = "bar"
text "baz"
}
/root append /c
delete /root/a
/root delete @attrib
include "anotherPatch.p"
anotherPatch.p contains:
/root append /d
And invoked using this:
cscript PatchXml.js template.xml output.xml -patch patch.p
output.xml would contain:
<root>
<b foo="foo" bar="bar">baz</b>
<c/>
<d/>
</root>
The detailed BNF syntax for the patch file:
{} means repetition
[] means optional
<> means non-terminal
| means choice
<patchxml> ::= {<expression>}
<command> ::= <expression> | <block>
<block> ::= '{' {<expression>} '}'
<expression> ::= XPATH <command> |
for XPATH <command> |
'@'<attrib> = <value> |
'text' <value> |
'append' <appendOperand> |
'delete' <deleteOperand> |
'include' <includeOperand> |
'import' <importOperand>
<appendOperand> ::= '/'<node> [<block>] |
'@'<attrib> '=' <value>
<deleteOperand> ::= XPATH |
'@'<attrib>
<includeOperand> ::= <filename>
<importOperand> ::= <filename>
<value> ::= STRING | 'subst' '('STRING')'
<node> ::= NAME
<attrib> ::= NAME
<filename> ::= STRING
Terminal symbols:
''
All single quoted symbols are literals.
NAME
Alphanumeric characters and "_" starts with alphabet or "_"
STRING
A quoted string with the only escape sequences: "\"" and "\n".
XPATH
The XPath syntax to specify a node.
Currently XPATH only supports node expression with prefix of "/".
XPath selector can be used to point to specific node if any node have duplicating names. Selector are enclosed in square brackets "[]"
* XPath information can be refered from
http://www.w3schools.com/xpath/xpath_syntax.asp
http://msdn.microsoft.com/en-us/library/windows/desktop/ms256471.aspx
Operators:
'@'<attrib> = <value>
Modify an attribute of current node. The attribute must exist.
For attribute named 'value', will check for 'value_list' for
valid options if present.
text
Modify the text in a node. Current node must contain text node. Text also
can be used to append text when appending new child node. See example.
append
Append a node as a child of current node. The new child node can have
the same name with their siblings.
Also used to append an attribute to current node. The new attribute
must not exist.
delete
Removes attribute or child node (including all descendant elements)
include
Parse another patch file from current patch file. This is same as
specifying multiple patch files from -patch option
import
Import the root of another xml file and replace with current node
for
Perform command on all nodes matching query
Definition of current node:
Is the node specified by a XPath
E.g.:
/root/childA @attrib = "foo"
|
[current node]
Comment line:
Comments starts with the '#' character and can only be place in the
beginning or the ending of a complete expression
*/
var exit = function (n) { WScript.Quit (n); }
var log = function (s) { WScript.StdOut.Write (s + '\n'); }
var debug = function (s) {};
var error = function (s) { WScript.StdErr.Write (s + '\n'); }
var shell = WScript.CreateObject ("WScript.Shell");
var forReading = 1;
var fso = new ActiveXObject ("Scripting.FileSystemObject");
/**
Parser object is used to parse a patch file. Object can be created from main
function, where this is the main patch file, or it can also be created within
this parser object when "include" operation is used.
**/
function Parser (f, xmlDom) {
//
// keeps a reference to xmlDom to use the createNode method
//
this.xmlDom = xmlDom;
this.filename = f;
this.dir = dirname (f);
this.location = 0;
this.line = 1;
this.peekedChar = ''
//
// Read patch file
//
var file = fso.OpenTextFile (this.filename, forReading);
this.text = file.ReadAll ();
//
// Read the first character and put in peekedChar
//
this.next ();
}
/**
Parser::parsePatchXml the main function to start parsing the patch file.
There are three place to call this function (1) from main, (2) from include
operation in patch file without context, or (3) from include operation with
context. For (1) and (2), pass in the XMLDOM, while for (3), pass in the
current node
**/
Parser.prototype.parsePatchXml = function (node) {
debug ("parsePatchXml () started");
//
// Consume comments before any statement
//
this.consumeAll ();
while (!this.isEof ()) {
//
// while not eof of patch, repeat to handle multiple statement in patch file
// Consume comments after each statement
//
this.parseExpression (node);
this.consumeAll ();
}
debug ("parsePatchXml () ended");
}
/**
Parser::parseCommand Handles expression or block
**/
Parser.prototype.parseCommand = function (node) {
debug ("parseCommand () started");
if (this.peek () == "{") {
this.parseBlock (node);
} else {
this.parseExpression (node);
}
debug ("parseCommand () ended");
}
/**
Parser::parseBlock Repeatedly parse expression until "}" is met
**/
Parser.prototype.parseBlock = function (node) {
debug ("parseBlock () started");
//
// Consume "{" and the whitespace right after that
//
this.next ();
this.consumeAll ();
do {
//
// parse the content multiple times and consume white space after any
// repeated operation
//
this.parseExpression (node);
this.consumeAll ();
} while (this.peek () != "}");
//
// consume "}"
//
this.next ();
//
// consume white space right after "}"
//
this.consumeAll ();
debug ("parseBlock () ended");
}
/**
Parser::parseExpression Handles more paths or operation
**/
Parser.prototype.parseExpression = function (node) {
debug ("parseExpression () started");
if (this.peek () == "/") {
//
// This is when the statement starts from a path
//
var path = this.parsePath ();
node = selectSingleNodeSafe (node, path);
//
// Pass the found node
//
this.parseCommand (node);
} else if (this.peek () == ".") {
throw Error ("XPath must begin with '/'");
} else {
this.parseOperation (node);
}
debug ("parseExpression () ended");
}
/**
Parser::parsePath detects the path. Characters complies with XPath syntax.
**/
Parser.prototype.parsePath = function () {
debug ("parsePath () started");
var xpath = "";
while (/[\/\.\[\*a-zA-Z0-9_]/.test (this.peek ())) {
if (this.peek () == "[") {
xpath += this.parseSelectorOpt ();
} else {
xpath += this.next ();
}
}
//
// Consume white space before start of grouping
//
this.consumeWhiteSpace ();
debug ("Path: " + xpath);
debug ("parsePath () ended");
return xpath;
}
/**
Parser::parseOperation Corresponding function will be called to handle the
operations. If '@' is peeked, modifyAttribute will be called, else will
parse the operator
**/
Parser.prototype.parseOperation = function (node) {
debug ("parseOperation () started");
if (this.peek () == "@") {
this.changeAttribute (node, true);
} else {
//
// detect the first word, and find from switch case
//
var operator = this.parseName ();
switch (operator) {
case "text":
this.modifyNode (node);
break;
case "append":
this.appendOperand (node);
break;
case "delete":
this.deleteOperand (node);
break;
case "include":
this.includeOperand (node);
break;
case "import":
this.importOperand (node);
break;
case "for":
this.forStatement (node);
break;
default:
throw Error ("No such operator: \"" + operator + "\"");
}
}
debug ("parseOperation () ended");
}
/**
Perform following command for each node found by XPath query.
Since we don't build AST, just re-start parser from saved location.
**/
Parser.prototype.forStatement = function (node) {
debug ("forStatement () started");
var path = this.parsePath ();
var saveLocation = this.location;
var savePeekedChar = this.peekedChar;
var nodes = node.selectNodes (path);
if (nodes.length == 0) {
throw Error ("Cannot find any match from node \"" + node.nodeName + "\" using this path : " + path);
}
for (var n = 0; n < nodes.length; ++n) {
this.location = saveLocation;
this.peekedChar = savePeekedChar;
this.parseCommand (nodes[n]);
}
debug ("forStatement () ended");
}
/**
Parser::appendOperand a child node or attribute. If append node, it will not
check if the node is there or no. But if the attribute is there, it will
throw error.
**/
Parser.prototype.appendOperand = function (node) {
debug ("appendOperand () started");
//
// consume "/" or "@"
//
if (this.peek () == "/") {
this.appendNode (node);
} else if (this.peek () == "@") {
this.changeAttribute (node, false);
} else {
throw Error ("Append operation only accepts '/' for node and '@' for attribute");
}
debug ("appendOperand () ended");
}
/**
Parser::appendNode helper function for appendOperand
**/
Parser.prototype.appendNode = function (node) {
//
// consume "/"
//
this.next ();
var nodeToAppend = this.parseName ();
var toAppend = this.xmlDom.createElement (nodeToAppend);
//
// append the node here
//
node.appendChild (toAppend);
//
// detect the grouping here.
//
if (this.peek () == "{") {
this.parseBlock (toAppend);
}
}
/**
Parser::changeAttribute modify or append an attribute to a node.
Use the modifyFlag to specify modify or append attribute
**/
Parser.prototype.changeAttribute = function (node, modifyFlag) {
debug ("changeAttribute () started");
//
// consume "@"
//
this.next ();
var attribToChange = this.parseName ();
if (this.peek () != "=") {
throw Error ("Please adhere to syntax to append/modify attribute: @attrib = value");
}
//
// Consume "="
//
this.next ();
var valueToChange = this.parseValue ();
//
// check the existance of the attribute
//
if (modifyFlag) {
if (!node.getAttributeNode (attribToChange)) {
throw Error ("\"" + attribToChange + "\" does not exist for modification, missing 'append'?");
}
} else {
if (node.getAttributeNode (attribToChange)) {
throw Error ("Attribute \"" + attribToChange + "\" already exists");
}
}
node.setAttribute (attribToChange, valueToChange);
if (attribToChange == "value") {
this.checkValueList (valueToChange, node.getAttribute ("value_list"));
}
debug ("changeAttribute () ended");
}
/**
Parser::modifyNode will append a text node regardless it existed or no,
and append a text to the text node.
**/
Parser.prototype.modifyNode = function (node) {
debug ("modifyNode () started");
var valueToModify = this.parseValue ();
node.appendChild (this.xmlDom.createTextNode (valueToModify));
debug ("modifyNode () ended");
}
/**
Parser::deleteOperand will check if the node is there or no. If it is not,
throw error.
**/
Parser.prototype.deleteOperand = function (node) {
debug ("deleteOperand () started");
//
// detect the first "/" or "@"
//
if (this.peek () == "/") {
var nodeToDelete = this.parsePath ();
var toDelete = selectSingleNodeSafe (node, nodeToDelete);
//
// delete node here
//
toDelete.parentNode.removeChild (toDelete);
} else if (this.peek () == "@") {
//
// Consume "@"
//
this.next ();
var attribToDelete = this.parseName ();
//
// delete attrib here
//
if (!node.getAttributeNode (attribToDelete)) {
throw Error ("\"" + attribToDelete + "\" does not exist to delete");
}
node.removeAttribute (attribToDelete);
} else {
throw Error ("Delete operation only accepts '/' for node and '@' for attribute");
}
debug ("deleteOperand () ended");
}
/**
Parser::includeOperand another patch file. New Parser will be created to
handle this.
**/
Parser.prototype.includeOperand = function (node) {
debug ("includeOperand () started");
var f = this.parseString ();
//
// Check for existence before constructing a new parser
//
if (!fso.FileExists (this.dir + f)) {
throw Error ("File does not exists: \"" + this.dir + f + "\"");
}
try {
var includePatch = new Parser (this.dir + f, this.xmlDom);
includePatch.parsePatchXml (node);
} catch (e) {
error (includePatch.filename + "(" + includePatch.line + ") : error : " + e.message);
throw Error ("Included patch file faulty");
}
debug ("includeOperand () ended");
}
/**
Parser::importOperand will import the root of another xml file to the
specified node. Current node will be replaced with root of new xml file
**/
Parser.prototype.importOperand = function (node) {
debug ("importOperand () started");
var f = this.parseString ();
var importDom = readXmlFile (this.dir + f);
node.parentNode.replaceChild (importDom.documentElement, node);
debug ("importOperand () ended");
}
/**
Parser::parseName Alphanumeric, and "_" which starts with Alphabet or "_"
**/
Parser.prototype.parseName = function () {
debug ("parseName () started");
this.consumeWhiteSpace ();
var name = "";
//
// Detect the first character and make sure it is alphabet or '_'
//
if (/[a-z_]/i.test (this.peek ())) {
name = this.next ();
} else {
throw Error ("Unexpected character : \"" + this.peek () + "\"");
}
//
// Detect the rest of the NAME characters
//
while (/[a-z0-9_]/i.test (this.peek ())) {
name += this.next ();
}
this.consumeWhiteSpace ();
debug ("Name: " + name + "");
debug ("parseName () ended");
return name;
}
/**
Parser::parseString parses string quoted in double quotes. Does not return
the "\"". Will replace "\n" and "\"" escape sequence found in the string.
**/
Parser.prototype.parseString = function () {
debug ("parseString () started");
this.consumeWhiteSpace ();
var str = "";
if (this.peek () != "\"") {
throw Error ("String starts with \", unexpected character: \"" + this.peek () + "\"");
}
//
// Consume "\""
//
this.next ();
while (this.peek () != "\"") {
if (this.peek () == "\\") {
//
// consume "\\"
//
this.next ();
var c = this.peek ();
//
// Consume whatever it is and append the correct escape character
//
if (c == "\"") {
//
// Double quotation
//
this.next ();
str += "\"";
} else if (c == "\\") {
//
// Backslash
//
this.next ();
str += "\\";
} else {
//
// Everything else
//
this.next ();
str += ("\\" + c);
}
} else {
str += this.next ();
}
}
//
// Consume "\""
//
this.next ();
this.consumeWhiteSpace ();
debug ("String: " + str);
debug ("parseString () ended");
return str;
}
/**
Parser::parseSelectorOpt will parse the "[]". If find any "\"" in the box
bracket, will call parseString to handle. Will return the "[" and "]"
characters. If "[" is not found, will return empty string
**/
Parser.prototype.parseSelectorOpt = function () {
debug ("parseSelectorOpt () started");
var sel = "";
if (this.peek () == "[") {
//
// consume and append "["
//
sel = this.next ();
while (this.peek () != "]") {
if (this.peek () == "\"") {
//
// need the "\"" in xpath but parseString is not returning them
//
sel += ("\"" + this.parseString () + "\"");
} else {
sel += this.next ();
}
}
//
// consume and append "]"
//
sel += this.next ();
}
debug ("Selector: " + sel);
debug ("parseSelectorOpt () ended");
return sel;
}
/**
Parser::parseValue will detect if it is a string or the "subst" operator.
If "subst" operator found, will substitute the envorinment variable. Will
throw error if the substitution failed.
**/
Parser.prototype.parseValue = function () {
debug ("parseValue () started");
this.consumeWhiteSpace ();
var val = "";
if (this.peek () != "\"") {
var subst = this.parseName ();
if (subst != "subst") {
throw Error ("Unexpected operator: \"" + subst + "\"");
}
if (this.next () != "(") {
throw Error ("Please adhere to syntax to use environment variable substitution: subst (STRING)");
}
var str = this.parseString ();
val = shell.ExpandEnvironmentStrings (str);
if (val == str) {
throw Error ("Substitution failed for this string: \"" + str + "\"");
}
if (this.next () != ")") {
throw Error ("Missing ')'");
}
} else {
val = this.parseString ();
}
debug ("Value : " + val);
debug ("parseValue () ended");
return val;
}
/**
Parser::consumeWhiteSpace Consume white spaces
**/
Parser.prototype.consumeWhiteSpace = function () {
while (/\s/.test (this.peek ())) {
this.next ();
}
}
/**
Parser::consumeComment Consume multiline comments, as long as the "#" character is at the first column
**/
Parser.prototype.consumeComment = function () {
while (this.peek () == "#") {
do {
var c = this.next();
} while (!this.isEof () && c != '\n');
}
}
/**
Parser::consumeAll Consume comments and white spaces
**/
Parser.prototype.consumeAll = function () {
do {
this.consumeWhiteSpace ();
this.consumeComment ();
} while (/\s/.test (this.peek ()));
}
/**
Parser::checkValueList user are required to determine if "value" is a valid
attribute or not before calling this function.
**/
Parser.prototype.checkValueList = function (value, list) {
if (!list) return;
var values = list.split(",,");
for (var i = 0; i < values.length; ++i) {
if (values[i] == value) return;
}
throw Error("Value: \"" + value + "\" is not one of:\n " + values.join('\n '));
}
/**
Parser::peek gives the next character in the stream but do not consume it.
Because the file system object does not support peek function, the next
character has to be read out and stored in a variable.
**/
Parser.prototype.peek = function () {
return this.peekedChar;
}
/**
Parser::next gives the next character in the stream
**/
Parser.prototype.next = function () {
var c = this.peekedChar;
if (this.location >= this.text.length) {
if (this.isEof ()) {
throw Error ("Unexpected end of file");
}
this.peekedChar = "";
} else {
if (this.peekedChar == '\n') {
++this.line;
}
this.peekedChar = this.text.charAt(this.location++);
}
return c;
}
/**
Parser::isEof peekedChar is checked against 0 because it will be set in next
function if AtEndOfStream is set. AtEndOfStream property cannot be used here
because the final character is still stored in peekedChar even after
AtEndOfStream is set.
**/
Parser.prototype.isEof = function () {
return (this.peekedChar === "");
}
/**
Provides a safe method to select single node from a parent node. This will
check to make sure the selected child nodes are not ambiguous
**/
function selectSingleNodeSafe (parent, childPath) {
debug ("selectSingleNodeSafe () started");
//
// Replacing any occurance of '/' at the beginning of a path with './' .
// This gives the workaround to selectNodes from xmlDom and any child nodes.
// This also works to change '//foo' to './/foo'
//
childPath = childPath.replace (/^\//, "./");
var node = parent.selectNodes (childPath);
if (!node.length) {
throw Error ("Cannot find child node from parent node \"" + parent.nodeName + "\" using this path : " + childPath);
} else if (node.length > 1) {
throw Error ("Cannot identify non-ambiguous child from parent node \"" + parent.nodeName + "\" using this path : " + childPath);
}
debug ("Found child \"" + childPath + "\" from parent node \"" + parent.nodeName + "\"");
debug ("selectSingleNodeSafe () ended");
return node[0];
}
/**
Process the input arguments and store them in array to be used. Currrently,
supports one input xml, one output, but multiple patch files.
**/
function processArgs (args) {
var opt;
var optList = { input : 0,
patch : [],
output : 0,
debug : 0,
help : 0};
for (var index = 0; index < args.length; ++index) {
opt = args (index);
if (opt == "-patch") {
if (index < (args.length - 1)) {
optList.patch.push (args (++index));
} else {
throw Error ("-patch option is not complete");
}
} else if (opt == "-help" || opt == "--help" || opt == "-h") {
optList.help = 1;
} else if (opt == "-debug") {
optList.debug = 1;
} else {
//
// For input and output file without "-" option
//
if (!optList.input) {
optList.input = opt;
} else if (!optList.output) {
optList.output = opt;
} else {
throw Error ("Option not supported: " + opt);
}
}
}
return optList;
}
/**
Create ActiveXObject from list. User is responsible to check for success
**/
function createObjectFromList (list) {
var obj;
for (var i in list) {
try {
obj = new ActiveXObject (list[i]);
break;
} catch (e) {}
}
return obj;
}
/**
Returns the path with trailing "/"
**/
function dirname (path) {
return path.replace (/\\/g,'/').replace (/[^\/]*$/, "");
}
/**
Open a XML file by creating XMLDOM object
**/
function readXmlFile (f) {
var xmlDom = createObjectFromList (["Msxml2.DOMDocument.6.0", "Msxml2.DOMDocument", "Microsoft.XMLDOM"]);
if (!xmlDom) {
throw Error ("Create XMLDOM object failed");
}
xmlDom.async = false;
xmlDom.load (f);
var err = xmlDom.parseError;
if (err.errorCode != 0) {
error (err.url.replace ("file:///", "") + '(' + err.line + ") : error : "
+ err.reason + err.srcText);
throw Error ("Cannot read file.");
}
return xmlDom;
}
/**
This function is needed because DOMDocument could not print pretty. However,
user should validate the existence of reader and writer before using this
save file routine
**/
function saveDomWithIndent (dom, f, rdr, wrtr) {
var textStream = fso.CreateTextFile (f, true);
wrtr.indent = true;
wrtr.omitXMLDeclaration = (dom.firstChild.nodeTypeString != "processinginstruction");
var enc = wrtr.encoding;
wrtr.encoding = "utf-8";
rdr.contentHandler = wrtr;
rdr.parse (dom);
textStream.Write (wrtr.output);
textStream.Close ();
}
/**
Print the usage of script
**/
function usage () {
log ("\n\
PatchXml Tool \n\
\n\
Usage: \n\
cscript PatchXml.js <input> <output> -patch <patch> [[-patch <patch>] ...]\n\
\n\
Options: \n\
-patch patch file with patching syntax, supports multiple files \n\
-help \n\
\n\
Remarks: \n\
It is compulsory to include input, output and at least one patch file \n\
Please also refer to header comment for patch format description \n");
}
/**
Main is here
**/
function main () {
debug ("main () started");
//
// Print usage if no arguments are given
//
if (WScript.arguments.length == 0) {
usage ();
return 0;
}
//
// Process input arguments
//
var options = processArgs (WScript.arguments);
if (options.help) {
usage ();
return 0;
}
if (options.debug) {
debug = log;
}
debug (shell.CurrentDirectory);
debug (WScript.ScriptFullName);
debug (WScript.ScriptName);
//
// Checks if any of the arguments are not specified
//
if (options.input == "") {
throw Error ("Input xml file is not specified");
} else if (options.patch.length == 0) {
throw Error ("Patch file(s) is not specified");
} else if (options.output == "") {
throw Error ("Output file is not specified");
}
//
// Checks for the existence of each files
//
if (!fso.FileExists (options.input)) {
throw Error ("File does not exists: \"" + options.input + "\"");
} else {
for (var i in options.patch) {
if (!fso.FileExists (options.patch[i])) {
throw Error ("File does not exists: \"" + options.patch[i] + "\"");
}
}
}
//
// Read the input xml. Only the first option given will be used
//
var xmlDom = readXmlFile (options.input);
//
// Parse the patch files. Multiple patch files can be used
//
debug ("Parser started");
for (var i in options.patch) {
debug (options.patch[i] + " started");
try {
var parser = new Parser (options.patch[i], xmlDom);
parser.parsePatchXml (xmlDom);
} catch (e) {
error (parser.filename + "(" + parser.line + ") : error : " + e.message);
throw Error ("Patch file faulty");
}
debug (options.patch[i] + " ended");
}
debug ("Parser ended");
//
// Write the output xml. If the MXXMLWriter or SAXXMLReader are not available,
// write the xml using XMLDOM object without indentation
//
reader = createObjectFromList (["Msxml2.SAXXMLReader.6.0", "Msxml2.SAXXMLReader"]);
writer = createObjectFromList (["Msxml2.MXXMLWriter.6.0", "Msxml2.MXXMLWriter"]);
if (reader && writer) {
saveDomWithIndent (xmlDom, options.output, reader, writer);
log ("Written XML: " + options.output);
} else {
xmlDom.save (options.output);
log ("Written XML without indentation: " + options.output);
}
debug ("main () ended");
return 0;
}
try {
exit (main());
} catch (e) {
error (e.name + " : " + e.message);
exit (1);
}