Compare commits

..

8 Commits

Author SHA1 Message Date
2d8c52ece0 Fix integer operations (#6)
Previously the arithmetic attempted bitwise binary ops for double types,
add explicit casts for these kind of binary ops.

Reviewed-on: #6
2026-04-27 03:27:28 +00:00
efc5301728 Add documentation, fix README.md (#5)
Reviewed-on: #5
2026-04-26 18:08:49 +00:00
4629002ef0 Enhance parser and add a few functions, fix hex parsing (#4)
1. Unify all numbers under double type
A bit lazy, but is easily maintainable. Precision is enough for most use cases.

2. Add more functions
Add `num`, `parselit`, `str`.

3. Fix hex parser error
Previously the "0x" was greedy and matches single zeros, which then raised a parsing error because it couldn't match a hex number.

Reviewed-on: #4
2026-04-26 17:42:51 +00:00
103b9dbb98 Merge pull request 'Rename to XN, restructure repository' (#3) from rename into master
Reviewed-on: #3
2026-04-25 19:17:02 +00:00
2bbf339466 Rename to XN, restructure repository 2026-04-25 21:14:19 +02:00
01e08f1902 Update grammar, expand CRC32 function (#2)
XN grammar and transformation engine are updated, including support for bitwise operations, improved function parameter handling, and enhanced error reporting.

Reviewed-on: dominic/XNeedle#2
2026-04-25 10:26:05 +00:00
0de7edee70 Add CONTRIBUTORS.md, update year in LICENSE.txt 2026-04-24 21:05:30 +02:00
1a9605cb2d Add initial MVP 2026-04-24 20:51:36 +02:00
28 changed files with 595 additions and 59 deletions

14
CONTRIBUTORS.md Normal file
View 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.

View File

@ -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:

View File

@ -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
View 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
View 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
View 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");
}
};
```

View File

@ -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)
{

View File

@ -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}\"");

View File

@ -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 "";
}
}
}

View 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; }
}
}

View File

@ -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();

5
xn.slnx Normal file
View File

@ -0,0 +1,5 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/xn.csproj" />
</Folder>
</Solution>