Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d8c52ece0 | |||
| efc5301728 | |||
| 4629002ef0 | |||
| 103b9dbb98 | |||
| 2bbf339466 | |||
| 01e08f1902 | |||
| 0de7edee70 | |||
| 1a9605cb2d |
14
CONTRIBUTORS.md
Normal file
14
CONTRIBUTORS.md
Normal file
@ -0,0 +1,14 @@
|
||||
# Contributors
|
||||
|
||||
## Original Author
|
||||
|
||||
**Dominic Hoegliner**
|
||||
Author and maintainer of XN.
|
||||
All intellectual property rights retained by the original author.
|
||||
|
||||
---
|
||||
|
||||
Contributions via pull request are welcome.
|
||||
By submitting a contribution, you agree that your contribution
|
||||
will be licensed under the same BSD 3-Clause License as this project,
|
||||
and that the original authorship of Dominic Hoegliner is acknowledged.
|
||||
@ -1,4 +1,4 @@
|
||||
Copyright 2025 DOMINIC HOEGLINER
|
||||
Copyright 2026 Dominic Hoeglinger
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
|
||||
23
README.md
23
README.md
@ -1,14 +1,15 @@
|
||||
# XNeedle
|
||||
# XN
|
||||
|
||||
A simple tool for injecting faults into XML documents.
|
||||
XML Needle (XN) is a simple tool for injecting faults into XML documents.
|
||||
|
||||
## Description
|
||||
|
||||
The purpose of the tool is to hide a metaphorical "needle in a haystack"
|
||||
in large XML files, such as an EtherCAT SubDevice Information or ESI file.
|
||||
Therefore it is a FIT (fault injection test) tool, but can be used off-label.
|
||||
Therefore it is a FIT (fault injection test) tool,
|
||||
but can be used for general purpose manipulation.
|
||||
|
||||
To live up to its name, XPath is utilized to specify precise locations
|
||||
XPath is utilized to specify precise locations
|
||||
combined with a good enough set of instructions to do CRUD operations.
|
||||
|
||||
As the main application lies with injecting faults into ESI files,
|
||||
@ -18,7 +19,7 @@ such as modules in an ESI file.
|
||||
|
||||
## Language Specification
|
||||
|
||||
The XNeedle language consists of three distinct parts.
|
||||
The XN language consists of three distinct parts.
|
||||
|
||||
1. Expressions
|
||||
|
||||
@ -28,7 +29,7 @@ and values.
|
||||
This example prepends the existing inner text of a given node with "Faulty '",
|
||||
and appends the evaluated string "': 2+2=10".
|
||||
```
|
||||
text("Faulty '" .. text() .. "': " .. string(1+1^2) .. "+2=10");
|
||||
text("Faulty '" .. text() .. "': " .. string(1+1**2) .. "+2=10");
|
||||
```
|
||||
|
||||
2. Rules
|
||||
@ -50,7 +51,7 @@ $("//Device/Type[@ProductCode='#x0B583052' and @RevisionNo='#x00110000']/..")
|
||||
3. Bangs
|
||||
|
||||
Bangs are special function calls which take either a string or XPath as parameters,
|
||||
and make structural changes to the document.
|
||||
and make addition changes to the document.
|
||||
They are prefixed with, as the name implies, an exclamation mark.
|
||||
|
||||
This example adds, removes and swaps nodes.
|
||||
@ -58,16 +59,16 @@ This example adds, removes and swaps nodes.
|
||||
$("RxPdo/Index[.="#x1600"]/..")
|
||||
{
|
||||
# Modify name
|
||||
$(Name) { text("FSoE Outputs"); }
|
||||
$("Name") { text("FSoE Outputs"); }
|
||||
|
||||
# Select the Entry/Index #x7000 and change it to #x9001
|
||||
$(Entry/Index[.="#x7000"]) { text("#x9001"); }
|
||||
$("Entry/Index[.='#x7000']") { text("#x9001"); }
|
||||
|
||||
# Select the Entry with Index #x7001 and SubIndex 4, remove it
|
||||
$(Entry/Index[.="#x7001"]/../SubIndex[.="4"]/..) { remove(); }
|
||||
$("Entry/Index[.='#x7001']/../SubIndex[.="4"]/..") { remove(); }
|
||||
|
||||
# Select the 3rd Entry
|
||||
$(Entry[3])
|
||||
$("Entry[3]")
|
||||
{
|
||||
# Add a tag before the above selection
|
||||
!before("Entry")
|
||||
|
||||
76
doc/01_syntax.md
Normal file
76
doc/01_syntax.md
Normal file
@ -0,0 +1,76 @@
|
||||
# XN Syntax
|
||||
|
||||
## Comments
|
||||
|
||||
Comments are single line only prefixed with the pound sign ('#').
|
||||
|
||||
## Rules
|
||||
|
||||
Rules consists of two parts, a selector, and a block containing statements.
|
||||
A selector is an XPath string surrounded with braces prefixed with a dollar sign.
|
||||
The XPath selects zero or more document nodes which will serve as the context for the statement block.
|
||||
|
||||
## Bangs
|
||||
|
||||
Bangs are function calls combined with a statement block.
|
||||
They are prefixed with an exclaimation mark.
|
||||
A bang function always produces context nodes for the statement block.
|
||||
|
||||
## Statement Block
|
||||
A statement block are multiple statements terminated with a semicolon,
|
||||
surrounded by curly braces.
|
||||
|
||||
## Statements
|
||||
|
||||
A statement can be an expression, a function call, a rule or a bang.
|
||||
To note is that a nested rule will use its parents context to refine the selection further.
|
||||
|
||||
## Expressions
|
||||
|
||||
An expression can consist of function calls, operators, strings or numbers.
|
||||
|
||||
Operators supported:
|
||||
|
||||
| Operator | Function |
|
||||
|----------|----------------------|
|
||||
| + | addition |
|
||||
| - | subtraction |
|
||||
| * | multiplication |
|
||||
| / | division |
|
||||
| ** | power |
|
||||
| & | bitwise and |
|
||||
| | | bitwise or |
|
||||
| ^ | bitwise exclusive or |
|
||||
| .. | string concatenate |
|
||||
|
||||
## Example
|
||||
|
||||
```
|
||||
# Select the RxPdo with Index 0x1600
|
||||
$("RxPdo/Index[.="#x1600"]/..")
|
||||
{
|
||||
# Function call
|
||||
swap("Entry[3]", "Entry[4]");
|
||||
|
||||
# Of that RxPdo, select the 3rd Entry via a nested rule
|
||||
$("Entry[3]")
|
||||
{
|
||||
# This bang has "RxPdo/Index[.="#x1600"]/../Entry[3]" as its context
|
||||
!before("Entry")
|
||||
{
|
||||
# Function call contains an expression, which evaluates here to "!#xFFFF"
|
||||
!add("Index") { text("!"..hexlit(2**16-1)); };
|
||||
!add("SubIndex") { text("3"); }
|
||||
!add("BitLen") { text("256"); }
|
||||
!add("Name") { text("Test"); }
|
||||
!add("DataType") { text("UINT"); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# One .xn can contain multiple rules
|
||||
$("//Module[@Crc32]")
|
||||
{
|
||||
attr("Crc32", hexlit(crc32()));
|
||||
};
|
||||
```
|
||||
204
doc/02_functions.md
Normal file
204
doc/02_functions.md
Normal file
@ -0,0 +1,204 @@
|
||||
# Function Reference
|
||||
|
||||
## `num(value:string, numbase:number = 10):number`
|
||||
|
||||
Converts a string to a number in respect of the input base.
|
||||
|
||||
Example:
|
||||
```xn
|
||||
# Add 1 to all LcId
|
||||
$("//Name[@LcId]")
|
||||
{
|
||||
attr("LcId", str(num(attr("LcId")) + 1));
|
||||
};
|
||||
```
|
||||
|
||||
## `str(value:number, numbase:number = 10):string`
|
||||
|
||||
Converts a number to a string in any base.
|
||||
|
||||
Example:
|
||||
```xn
|
||||
# Set ProfileNo to "#o377"
|
||||
$("//ProfileNo")
|
||||
{
|
||||
text("#o"..str(2**8-1, 8));
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## `hexlit(value:number):string`
|
||||
|
||||
Produces a hex literal from an integer with lowercase digits.
|
||||
|
||||
Example:
|
||||
```xn
|
||||
# Set all module Crc32 to #x00c0ffee
|
||||
$("//Module[@Crc32]")
|
||||
{
|
||||
attr("Crc32", hexlit(0xC0FFEE));
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
## `parselit(literal:string, base:number = 16):string`
|
||||
|
||||
Parses a literal number to an integer.
|
||||
|
||||
Example:
|
||||
```xn
|
||||
# Increase all RevisionNo by 1
|
||||
$("//Device/Type[@RevisionNo]")
|
||||
{
|
||||
attr("RevisionNo", hexlit(parselit(attr("RevisionNo")) + 1));
|
||||
};
|
||||
```
|
||||
|
||||
## `lower(text:string):string`
|
||||
|
||||
Converts a string to all lower case text.
|
||||
|
||||
Example:
|
||||
```xn
|
||||
# Convert all device names to lower case
|
||||
$("//Device/Type")
|
||||
{
|
||||
$("Name") { cdata(lower(cdata())); };
|
||||
}
|
||||
```
|
||||
|
||||
## `upper(text:string):string`
|
||||
|
||||
Converts a string to all upper case text.
|
||||
|
||||
Example:
|
||||
```xn
|
||||
# Convert all Name tag text in Index nodes from RxPdo to upper case.
|
||||
$("//RxPdo/Index/Name")
|
||||
{
|
||||
$("Name") { text(upper(text())); };
|
||||
}
|
||||
```
|
||||
|
||||
## `crc32(poly:number = 0x04C11DB7, init:number = 0, xorOut:number = 0, reflected:bool = false):number`
|
||||
|
||||
Calculates the CRC32 checksum from the rendered document starting after the tag close of the context
|
||||
node up to and including the closing tag.
|
||||
|
||||
Example:
|
||||
|
||||
For this example `[IND]` is whitespace for indent,
|
||||
`[CR]` for carriage return and `[LF]` for line feed.
|
||||
|
||||
Rendered document:
|
||||
```xml
|
||||
<root>[CR][LF]
|
||||
[IND]<node attribute="yes">[CR][LF]
|
||||
[IND][IND]<child/>
|
||||
[IND][IND]Text
|
||||
[IND]</node>
|
||||
</root>
|
||||
|
||||
XN
|
||||
```xn
|
||||
# Calculate CRC of node and store in "crc" attribute as uppercase hexlit
|
||||
$("//Node") { attr("crc", upper(hexlit(crc32())); }
|
||||
```
|
||||
|
||||
Digested fragment:
|
||||
```txt
|
||||
[CR][LF]
|
||||
[IND][IND]<child/>
|
||||
[IND][IND]Text
|
||||
[IND]</node>
|
||||
```
|
||||
|
||||
## `text():string` / `text(value:string):string`
|
||||
|
||||
Get or set the text of the context node.
|
||||
|
||||
Example:
|
||||
```xn
|
||||
# Prefix all Name tag text in Index nodes from TxPdo with "Fault".
|
||||
$("//TxPdo/Index/Name")
|
||||
{
|
||||
$("Name") { text("Fault" .. text()); };
|
||||
}
|
||||
```
|
||||
|
||||
## `cdata():string` / `cdata(value:string):string`
|
||||
|
||||
Get or set the CDATA of the context node.
|
||||
|
||||
Example:
|
||||
```xn
|
||||
# For Device with this ProductCode and Revision, modify its CDATA text
|
||||
$("//Device/Type[@ProductCode='#x0B583052' and @RevisionNo='#x00110000']/..")
|
||||
{
|
||||
$("Name[@LcId='1033']") { cdata("LE2904, 4 Ch. Danger Output 24V, 0.5A, TwinUNSAFE"); };
|
||||
}
|
||||
```
|
||||
|
||||
## `attr(name:string):string` / `attr(name:string, value:string):string`
|
||||
|
||||
Get or set an attribute of the context node by name.
|
||||
|
||||
Example:
|
||||
```xn
|
||||
# For Device with this ProductCode and Revision, set the revision to #xc0deaffe
|
||||
$("//Device/Type[@ProductCode='#x0B583052' and @RevisionNo='#x00110000']")
|
||||
{
|
||||
attr("RevisionNo", hexlit(0xC0DEAFFE));
|
||||
}
|
||||
```
|
||||
|
||||
## `swap(xpath_a:string, xpath_b:string):string = ""`
|
||||
|
||||
Swaps two nodes by XPath in the current cotext node.
|
||||
|
||||
Example:
|
||||
```xn
|
||||
# Swap first and second Entry in RxPdo of Index 0x1600
|
||||
$("//RxPdo/Index[.='#x1600']/..") { swap("Entry[1]", "Entry[2]"); };
|
||||
|
||||
```
|
||||
|
||||
## `remove():string = ""`
|
||||
|
||||
Removes the context node from the document.
|
||||
|
||||
Example:
|
||||
```xn
|
||||
# Remove all modules
|
||||
$("//Module") { remove(); };
|
||||
```
|
||||
|
||||
## `rename(name:string):string = ""`
|
||||
|
||||
Renames the context node.
|
||||
|
||||
Example:
|
||||
```xn
|
||||
# Rename all Device nodes to DeviceX
|
||||
$("//Device") { rename("DeviceX"); };
|
||||
```
|
||||
|
||||
## `rename(xpath:string, name:string):string = ""`
|
||||
|
||||
Renames any nodes matching the XPath in the context node.
|
||||
|
||||
Example:
|
||||
```xn
|
||||
# Rename all Device nodes Name tags to Label
|
||||
$("//Device") { rename("Name", "Label"); };
|
||||
```
|
||||
|
||||
## `require(message:string):string = ""`
|
||||
|
||||
If the context node is not present, emit an error message.
|
||||
|
||||
Example:
|
||||
```xn
|
||||
# Emit an error if no Module tags are in the document
|
||||
$("//Module") { require("No Module definitions!"); };
|
||||
```
|
||||
53
doc/03_bangs.md
Normal file
53
doc/03_bangs.md
Normal file
@ -0,0 +1,53 @@
|
||||
# Bang Reference
|
||||
|
||||
## `!add(name:string)`
|
||||
|
||||
Appends a new node into the context.
|
||||
|
||||
Example:
|
||||
```xn
|
||||
# Add 1 to all LcId
|
||||
$("//RxPdo/Index[.="#x1600"]/..")
|
||||
{
|
||||
# Add a new Entry with attribute "added" set to "true"
|
||||
!add("Entry")
|
||||
{
|
||||
attr("added", "true");
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## `!before(name:string)`
|
||||
|
||||
Appends a new node before the context.
|
||||
|
||||
Example:
|
||||
```xn
|
||||
# Add 1 to all LcId
|
||||
$("//RxPdo/Index[.="#x1600"]/..")
|
||||
{
|
||||
# Add a new TxPdo with attribute "added" set to "true" before the above node selection
|
||||
!before("TxPdo")
|
||||
{
|
||||
attr("added", "true");
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## `!after(name:string)`
|
||||
|
||||
Appends a new node after the context.
|
||||
|
||||
Example:
|
||||
```xn
|
||||
# Add 1 to all LcId
|
||||
$("//RxPdo/Index[.="#x1600"]/..")
|
||||
{
|
||||
# Add a new TxPdo with attribute "added" set to "true" after the above node selection
|
||||
!after("TxPdo")
|
||||
{
|
||||
attr("added", "true");
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
@ -12,6 +12,7 @@ using Rule = XNeedle.Transformation.Rule;
|
||||
using System.Data;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text;
|
||||
using System.Security.AccessControl;
|
||||
|
||||
namespace XNeedle.Parser
|
||||
{
|
||||
@ -30,12 +31,15 @@ namespace XNeedle.Parser
|
||||
static readonly Parser<ExpressionType> Multiply = Operator("*", ExpressionType.MultiplyChecked);
|
||||
static readonly Parser<ExpressionType> Divide = Operator("/", ExpressionType.Divide);
|
||||
static readonly Parser<ExpressionType> Modulo = Operator("%", ExpressionType.Modulo);
|
||||
static readonly Parser<ExpressionType> Power = Operator("^", ExpressionType.Power);
|
||||
|
||||
static readonly Parser<ExpressionType> Power = Operator("**", ExpressionType.Power);
|
||||
static readonly Parser<ExpressionType> BitwiseXor = Operator("^", ExpressionType.ExclusiveOr);
|
||||
static readonly Parser<ExpressionType> BitwiseAnd = Operator("&", ExpressionType.And);
|
||||
static readonly Parser<ExpressionType> BitwiseOr = Operator("|", ExpressionType.Or);
|
||||
|
||||
static Parser<Expression> Function(Context ctx)
|
||||
{
|
||||
return
|
||||
from name in Parse.Letter.AtLeastOnce().Text()
|
||||
from name in Identifier
|
||||
from lparen in Parse.Char('(')
|
||||
from expr in Parse.Ref(() => Expr(ctx)).DelimitedBy(Parse.Char(',').Token()).Optional()
|
||||
from rparen in Parse.Char(')')
|
||||
@ -46,23 +50,51 @@ namespace XNeedle.Parser
|
||||
{
|
||||
var mathMethodInfo = typeof(Math).GetTypeInfo().GetMethod(name, parameters.Select(e => e.Type).ToArray());
|
||||
if (mathMethodInfo != null)
|
||||
{
|
||||
return Expression.Call(mathMethodInfo, parameters);
|
||||
}
|
||||
var ctxMethodInfo = typeof(ContextFunctions).GetTypeInfo().GetMethod(name, parameters.Select(e => e.Type).Prepend(typeof(Context)).ToArray());
|
||||
|
||||
// Find by name and fill in optional parameters with their default values
|
||||
var ctxMethodInfo = typeof(ContextFunctions).GetTypeInfo()
|
||||
.GetMethods()
|
||||
.FirstOrDefault(m =>
|
||||
{
|
||||
if (m.Name != name) return false;
|
||||
var mp = m.GetParameters();
|
||||
if (mp.Length == 0 || mp[0].ParameterType != typeof(Context)) return false;
|
||||
int required = mp.Skip(1).Count(p => !p.HasDefaultValue);
|
||||
return parameters.Length >= required && parameters.Length <= mp.Length - 1;
|
||||
});
|
||||
|
||||
if (ctxMethodInfo != null)
|
||||
{
|
||||
return Expression.Call(ctxMethodInfo, parameters.Prepend(CtxParam).ToArray());
|
||||
var mp = ctxMethodInfo.GetParameters();
|
||||
var args = mp.Skip(1).Select((p, i) =>
|
||||
i < parameters.Length
|
||||
? parameters[i]
|
||||
: Expression.Constant(p.DefaultValue, p.ParameterType)
|
||||
).ToArray();
|
||||
return Expression.Call(ctxMethodInfo, args.Prepend(CtxParam).ToArray());
|
||||
}
|
||||
|
||||
throw new ParseException(string.Format("Function '{0}({1})' does not exist.", name,
|
||||
string.Join(",", parameters.Select(e => e.Type.Name))));
|
||||
}
|
||||
|
||||
// This needs to match as one atomic token, else with a separate String("0x") the 0 is consumed,
|
||||
// which triggers a parsing error on single zero numbers
|
||||
public static readonly Parser<string> HexNumber =
|
||||
Parse.Regex(@"0x[0-9a-fA-F]+").Token()
|
||||
.Select(s => s.Substring(2));
|
||||
|
||||
public static readonly Parser<string> BooleanLiteral =
|
||||
from value in Parse.String("true").XOr(Parse.String("false")).Text()
|
||||
select value;
|
||||
|
||||
static readonly Parser<Expression> NumberConstant =
|
||||
Parse.Decimal
|
||||
.Select(x => Expression.Constant(double.Parse(x)))
|
||||
.Named("number");
|
||||
HexNumber.Select(x => Expression.Constant((double)int.Parse(x, System.Globalization.NumberStyles.HexNumber)))
|
||||
.XOr(BooleanLiteral.Select(x => Expression.Constant(x == "true" ? 1.0 : 0.0)))
|
||||
.XOr(Parse.Decimal.Select(x => Expression.Constant(double.Parse(x))))
|
||||
.Named("number");
|
||||
|
||||
|
||||
private static readonly Parser<Expression> StringConstant =
|
||||
from open in Parse.Char('"')
|
||||
@ -95,7 +127,18 @@ namespace XNeedle.Parser
|
||||
|
||||
static Parser<Expression> Term(Context ctx)
|
||||
{
|
||||
return Parse.ChainOperator(Multiply.Or(Divide).Or(Modulo), InnerTerm(ctx), Expression.MakeBinary);
|
||||
return Parse.ChainOperator(Multiply.Or(Divide), InnerTerm(ctx), Expression.MakeBinary);
|
||||
}
|
||||
|
||||
static Parser<Expression> IntegerTerm(Context ctx)
|
||||
{
|
||||
// Integer operations can't deal with doubles, so we need to make unary casts to integer and back
|
||||
return Parse.ChainOperator(Multiply.Or(Divide).Or(Modulo).Or(BitwiseXor).Or(BitwiseAnd).Or(BitwiseOr),
|
||||
InnerTerm(ctx), (bt,left,right)=>Expression.MakeUnary(
|
||||
ExpressionType.Convert, Expression.MakeBinary(bt,
|
||||
Expression.MakeUnary(ExpressionType.Convert, left, typeof(long)),
|
||||
Expression.MakeUnary(ExpressionType.Convert, right, typeof(long))),
|
||||
typeof(double)));
|
||||
}
|
||||
|
||||
static Parser<Expression> ConcatExpr(Context ctx)
|
||||
@ -110,7 +153,7 @@ namespace XNeedle.Parser
|
||||
|
||||
static Parser<Expression> ArithExpr(Context ctx)
|
||||
{
|
||||
return Parse.ChainOperator(Add.Or(Subtract), Term(ctx), Expression.MakeBinary);
|
||||
return Parse.ChainOperator(Add.Or(Subtract), IntegerTerm(ctx).Or(Term(ctx)), Expression.MakeBinary);
|
||||
}
|
||||
|
||||
static Parser<Expression> Expr(Context ctx)
|
||||
@ -183,10 +226,6 @@ namespace XNeedle.Parser
|
||||
from arguments in Argument.DelimitedBy(Parse.Char(',').Token())
|
||||
from close in Parse.Char(')')
|
||||
select new Call{Identifier = new string(identifier.ToArray()), Arguments = arguments};
|
||||
|
||||
/*public static readonly Parser<Expression> ExpressionObject =
|
||||
from expression in CallObject
|
||||
*/
|
||||
|
||||
public static CommentParser Comment = new CommentParser { Single = "#", NewLine = Environment.NewLine };
|
||||
|
||||
@ -199,11 +238,13 @@ namespace XNeedle.Parser
|
||||
from ws1 in Parse.WhiteSpace.Many().Optional()
|
||||
from open in Parse.Char('{')
|
||||
from ws2 in Parse.WhiteSpace.Many().Optional()
|
||||
from expressions in Parse.Ref(() => BangObject)
|
||||
.Or(Parse.Ref(() => RuleObject))
|
||||
.Or(ExprPart(ctx).Select(e => (Part)e))
|
||||
.DelimitedBy(Parse.Char(';').Token())
|
||||
from tailingsemi in Parse.Char(';').Optional()
|
||||
from expressions in (
|
||||
from expr in Parse.Ref(() => BangObject)
|
||||
.Or(Parse.Ref(() => RuleObject))
|
||||
.Or(ExprPart(ctx).Select(e => (Part)e))
|
||||
from semi in Parse.Char(';').Token()
|
||||
select expr
|
||||
).Many()
|
||||
from ws3 in Parse.WhiteSpace.Many().Optional()
|
||||
from close in Parse.Char('}')
|
||||
select new Bang{Name = new string([.. name]), Arguments = arguments, Body = expressions};
|
||||
@ -214,15 +255,23 @@ namespace XNeedle.Parser
|
||||
from ws1 in Parse.WhiteSpace.Many().Optional()
|
||||
from open in Parse.Char('{')
|
||||
from ws2 in Parse.WhiteSpace.Many().Optional()
|
||||
from expressions in Parse.Ref(() => BangObject)
|
||||
.Or(Parse.Ref(() => RuleObject))
|
||||
.Or(ExprPart(ctx).Select(e => (Part)e))
|
||||
.DelimitedBy(Parse.Char(';').Token())
|
||||
from tailingsemi in Parse.Char(';').Optional()
|
||||
from expressions in (
|
||||
from expr in Parse.Ref(() => BangObject)
|
||||
.Or(Parse.Ref(() => RuleObject))
|
||||
.Or(ExprPart(ctx).Select(e => (Part)e))
|
||||
from semi in Parse.Char(';').Token()
|
||||
select expr
|
||||
).Many()
|
||||
from ws3 in Parse.WhiteSpace.Many().Optional()
|
||||
from close in Parse.Char('}')
|
||||
select new Rule{XPathSelector = selector, Body = expressions};
|
||||
|
||||
|
||||
public static readonly Parser<IEnumerable<Part>> Rules =
|
||||
(from rule in RuleObject.Token()
|
||||
from semi in Parse.Char(';').Token()
|
||||
select rule)
|
||||
.Many()
|
||||
.End();
|
||||
|
||||
static public string StripComments(string source)
|
||||
{
|
||||
@ -40,10 +40,10 @@ namespace XNeedle
|
||||
|
||||
var rawSource = File.ReadAllText(opts.XnDefinition);
|
||||
var xnSource = XnParser.StripComments(rawSource);
|
||||
Rule masterRule;
|
||||
IEnumerable<Part> rules;
|
||||
try
|
||||
{
|
||||
masterRule = (Rule)XnParser.RuleObject.Parse(xnSource);
|
||||
rules = XnParser.Rules.Parse(xnSource);
|
||||
}
|
||||
catch (ParseException ex)
|
||||
{
|
||||
@ -54,11 +54,19 @@ namespace XNeedle
|
||||
|
||||
if (showPosition)
|
||||
{
|
||||
Console.Error.WriteLine($"{opts.XnDefinition}({line},{col}): parse error: {ex.Message}");
|
||||
var msg = ex.Message;
|
||||
var hint = (msg.Contains("expected }") && !msg.Contains("expected ';'"))
|
||||
? "Hint: a missing ';' terminator may be located just after this position"
|
||||
: "";
|
||||
Console.Error.WriteLine($"{opts.XnDefinition}({line},{col}): parse error: {msg}");
|
||||
var srcLine = line <= lines.Length ? lines[line - 1].TrimEnd() : "";
|
||||
var pointer = new string(' ', Math.Max(0, col - 1)) + "^";
|
||||
Console.Error.WriteLine($" {srcLine}");
|
||||
Console.Error.WriteLine($" {pointer}");
|
||||
if (!string.IsNullOrEmpty(hint))
|
||||
{
|
||||
Console.Error.WriteLine($"\n{hint}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -69,7 +77,16 @@ namespace XNeedle
|
||||
var doc = XDocument.Load(opts.Input, LoadOptions.PreserveWhitespace);
|
||||
var encoding = XmlRoundtripWriter.DetectEncoding(File.ReadAllBytes(opts.Input));
|
||||
var sloc = new SourceLocation(opts.Input, encoding);
|
||||
masterRule.Transform(doc, sloc);
|
||||
try
|
||||
{
|
||||
foreach (var rule in rules.OfType<Rule>())
|
||||
rule.Transform(doc, sloc);
|
||||
}
|
||||
catch (RequireException ex)
|
||||
{
|
||||
Console.Error.WriteLine($"{ex.SLoc.FilePath}({ex.SLoc.LineNumber}): require error: {ex.UserMessage}");
|
||||
return;
|
||||
}
|
||||
using var writer = XmlRoundtripWriter.Create(opts.Input, opts.Output);
|
||||
doc.WriteTo(writer);
|
||||
Console.WriteLine($"Applied \"{opts.XnDefinition}\" to \"{opts.Input}\" and saved result to \"{opts.Output}\"");
|
||||
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Sprache;
|
||||
@ -12,10 +13,62 @@ namespace XNeedle.Transformation
|
||||
{
|
||||
public class ContextFunctions
|
||||
{
|
||||
public static string hex(Context ctx, double value)
|
||||
|
||||
public static double num(Context ctx, string value, double numbase = 10)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
string cleanedValue = value.Trim();
|
||||
|
||||
if (numbase != 10)
|
||||
{
|
||||
cleanedValue = cleanedValue.TrimStart('0');
|
||||
cleanedValue = cleanedValue.TrimStart('x');
|
||||
|
||||
// Convert to Int64 to not lose any fidelity
|
||||
return Convert.ToInt64(cleanedValue, (int)numbase);
|
||||
}
|
||||
|
||||
return double.Parse(cleanedValue, CultureInfo.InvariantCulture);
|
||||
}
|
||||
public static string str(Context ctx, double value, double numbase = 10)
|
||||
{
|
||||
if (numbase == 10)
|
||||
{
|
||||
return $"{value}";
|
||||
}
|
||||
|
||||
// Ignore fraction for other bases by casting to long
|
||||
return Convert.ToString((long)value, (int)numbase);
|
||||
}
|
||||
|
||||
public static string hexlit(Context ctx, double value)
|
||||
{
|
||||
uint v = (uint)value;
|
||||
return $"#x{v:X8}";
|
||||
return $"#x{v:x8}";
|
||||
}
|
||||
|
||||
public static double parselit(Context ctx, string literal, double numbase = 16)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(literal) || literal.Length <= 2)
|
||||
{
|
||||
throw new ArgumentException($"Cannot parse literal \"{literal}\"");
|
||||
}
|
||||
|
||||
return (double)Convert.ToInt32(literal.Substring(2), (int)numbase);
|
||||
}
|
||||
|
||||
public static string lower(Context ctx, string text)
|
||||
{
|
||||
return text.ToLower();
|
||||
}
|
||||
|
||||
public static string upper(Context ctx, string text)
|
||||
{
|
||||
return text.ToUpper();
|
||||
}
|
||||
|
||||
public static double GetLevel(Context ctx)
|
||||
@ -26,7 +79,41 @@ namespace XNeedle.Transformation
|
||||
level++;
|
||||
return level;
|
||||
}
|
||||
public static double crc(Context ctx)
|
||||
|
||||
private static uint ComputeCrc32(byte[] data, uint poly, uint init, uint xorOut, bool reflected)
|
||||
{
|
||||
uint crc = init;
|
||||
uint polyReflected = ReflectBits(poly, 32);
|
||||
foreach (byte b in data)
|
||||
{
|
||||
if (reflected)
|
||||
{
|
||||
crc ^= b;
|
||||
for (int i = 0; i < 8; i++)
|
||||
crc = (crc & 1) != 0 ? (crc >> 1) ^ polyReflected : crc >> 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
crc ^= (uint)b << 24;
|
||||
for (int i = 0; i < 8; i++)
|
||||
crc = (crc & 0x80000000u) != 0 ? (crc << 1) ^ poly : crc << 1;
|
||||
}
|
||||
}
|
||||
return crc ^ xorOut;
|
||||
}
|
||||
|
||||
private static uint ReflectBits(uint value, int bits)
|
||||
{
|
||||
uint reflected = 0;
|
||||
for (int i = 0; i < bits; i++)
|
||||
{
|
||||
if ((value & (1u << i)) != 0)
|
||||
reflected |= 1u << (bits - 1 - i);
|
||||
}
|
||||
return reflected;
|
||||
}
|
||||
|
||||
public static double crc32(Context ctx, double poly = 0x04C11DB7, double init = 0, double xorOut = 0, double reflected = 0.0)
|
||||
{
|
||||
if (ctx.Node is null)
|
||||
return 0;
|
||||
@ -36,17 +123,15 @@ namespace XNeedle.Transformation
|
||||
ctx.Node.WriteTo(writer);
|
||||
writer.Flush();
|
||||
string xml = buffer.ToString();
|
||||
|
||||
|
||||
// Step 2: Extract all text after the opening tag through and including the closing tag
|
||||
string tagName = ctx.Node.Name.LocalName;
|
||||
string data = XmlFragParser.ChecksumData(tagName).Parse(xml);
|
||||
//byte[] fragBytes = ctx.SLoc.Encoding.GetBytes(data);
|
||||
byte[] fragBytes = Encoding.UTF8.GetBytes(data);
|
||||
|
||||
// Step 3: Digest into CRC32 using the source encoding
|
||||
var crc32 = new System.IO.Hashing.Crc32();
|
||||
crc32.Append(ctx.SLoc.Encoding.GetBytes(data));
|
||||
uint result = BitConverter.ToUInt32(crc32.GetCurrentHash());
|
||||
|
||||
return (double)result;
|
||||
// Step 3: Digest into CRC32 using parameterized CRC32 - by default MPEG-2
|
||||
return (double)ComputeCrc32(fragBytes, (uint)poly, (uint)init, (uint)xorOut, reflected == 1.0);
|
||||
}
|
||||
|
||||
public static string text(Context ctx)
|
||||
@ -187,5 +272,18 @@ namespace XNeedle.Transformation
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public static string require(Context ctx, string message = "Required node not present")
|
||||
{
|
||||
if (ctx.Node == null)
|
||||
{
|
||||
throw new RequireException{SLoc=ctx.SLoc, UserMessage=message};
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine(ctx.Node.ToString());
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/Transformation/RequireException.cs
Normal file
15
src/Transformation/RequireException.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using System.Xml.XPath;
|
||||
using XNeedle;
|
||||
using XNeedle.Parser.Elements;
|
||||
|
||||
namespace XNeedle.Transformation
|
||||
{
|
||||
public class RequireException : Exception
|
||||
{
|
||||
public SourceLocation SLoc { get; set; }
|
||||
public required string UserMessage { get; set; }
|
||||
}
|
||||
}
|
||||
@ -22,15 +22,19 @@ namespace XNeedle.Transformation
|
||||
|
||||
public override void Operate(Context ctx)
|
||||
{
|
||||
if(ctx.Node is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var nodes = ctx.Node.XPathSelectElements(XPathSelector);
|
||||
if(nodes is null)
|
||||
var nodes = ctx.Node?.XPathSelectElements(XPathSelector);
|
||||
if(nodes is null || nodes.Count() == 0)
|
||||
{
|
||||
var nullCtx = ctx.Clone();
|
||||
nullCtx.Node = null;
|
||||
nullCtx.SLoc = ctx.SLoc.WithLineNumber(-1);
|
||||
foreach (var b in Body)
|
||||
{
|
||||
b.Operate(nullCtx);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (XElement n in nodes)
|
||||
{
|
||||
var loopCtx = ctx.Clone();
|
||||
Loading…
x
Reference in New Issue
Block a user