Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nonrecursive UUID functions plus XSpec tests #1158

Merged
merged 1 commit into from
Feb 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 50 additions & 36 deletions src/utils/util/resolver-pipeline/random-util.xsl
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ v4 UUID
-->
<xsl:output indent="yes"/>

<!-- set $germ to a string for reproducible outputs of r:make-uuid-sequence
pass in a blind value - and don't save it - for irreproducible outputs -->
<!-- Set $germ to a string for reproducible outputs of r:make-uuid.
Pass in a blind value - and don't save it - for irreproducible outputs. -->

<xsl:param name="germ" select="current-dateTime() || document-uri(/)"/>

Expand All @@ -49,49 +49,63 @@ v4 UUID
</randomness>
</xsl:template>

<xsl:function name="r:make-uuid-sequence" as="xs:string*">
<!-- r:make-uuid produces one v4 UUID. Output is repeatable for a given seed.
If the random-number-generator() function is not available,
this function returns an empty sequence. -->
<xsl:function name="r:make-uuid" as="xs:string?">
<xsl:param name="seed" as="item()"/>
<xsl:param name="length" as="xs:integer"/>
<xsl:sequence use-when="function-available('random-number-generator')" select="r:produce-uuid-sequence($length,random-number-generator($seed))"/>
</xsl:function>

<xsl:function name="r:produce-uuid-sequence" as="xs:string*">
<xsl:param name="length" as="xs:integer"/>
<xsl:param name="PRNG" as="map(xs:string, item())"/>
<xsl:if test="$length gt 0">
<xsl:sequence select="string($PRNG?number) => r:make-uuid()"/>
<xsl:sequence select="r:produce-uuid-sequence($length - 1, $PRNG?next())"/>
</xsl:if>
<xsl:sequence select="r:make-uuid-sequence($seed, 1)"/>
</xsl:function>

<!-- make-uuid produces a UUID for a given seed - the same UUID every time for the same seed -->
<xsl:function name="r:make-uuid" as="xs:string?">
<!-- r:make-uuid-sequence produces a sequence of $seq-length v4 UUIDs.
Output is repeatable for a given seed. If the random-number-generator()
function is not available, this function returns an empty sequence. -->
<xsl:function name="r:make-uuid-sequence" as="xs:string*">
<xsl:param name="seed" as="item()"/>
<xsl:sequence use-when="function-available('random-number-generator')" select="r:produce-uuid($uuid-v4-template, random-number-generator($seed))"/>
<xsl:param name="seq-length" as="xs:integer"/>
<xsl:variable name="uuid-v4-template" as="xs:string">________-____-4___-=___-____________</xsl:variable>
<!-- a847eaab-cec8-41bd-98e2-02d02900b554 -->
<xsl:sequence select="r:make-random-string-sequence($seed, $seq-length, $uuid-v4-template)"/>
</xsl:function>

<!--$template is a string to serve as a template for the UUID syntax
$PRNG is a pseudo-random-number generator produced by fn:random-number-generator() -->
<xsl:function name="r:produce-uuid" as="xs:string">
<!-- r:make-random-string-sequence produces a sequence of $seq-length strings.
The $template parameter specifies the pattern of characters in each
string, where:
* '_' becomes a random hex value 0-9a-f
* '=' becomes one of '8','9','a','b' at random
* Any other character is copied to the output string
Output is repeatable for a given seed. If the random-number-generator()
function is not available, this function returns an empty sequence. -->
<xsl:function name="r:make-random-string-sequence" as="xs:string*">
<xsl:param name="seed" as="item()"/>
<xsl:param name="seq-length" as="xs:integer"/>
<xsl:param name="template" as="xs:string"/>
<xsl:param name="PRNG" as="map(xs:string, item())"/>
<xsl:value-of>
<xsl:apply-templates select="substring($template, 1, 1)" mode="uuid-char">
<xsl:with-param name="PRNG" select="$PRNG"/>
</xsl:apply-templates>
<xsl:if test="matches($template, '.')">
<xsl:sequence select="r:produce-uuid(substring($template, 2), $PRNG?next())"/>
</xsl:if>
</xsl:value-of>
<xsl:sequence use-when="function-available('random-number-generator')">
<xsl:variable name="PRNG" as="map(xs:string, item())" select="random-number-generator($seed)"/>
<xsl:variable name="template-length" as="xs:integer" select="string-length($template)"/>
<!-- Draw one long stream from PRNG, advancing state in each iteration. -->
<xsl:variable name="random-chars" as="xs:string">
<xsl:value-of>
<xsl:iterate select="(0 to ($seq-length * $template-length - 1))">
<xsl:param name="PRNG" as="map(xs:string, item())" select="$PRNG"/>
<xsl:variable name="this-char" as="xs:string"
select="substring($template, (1 + current() mod $template-length), 1)"/>
<xsl:apply-templates select="$this-char" mode="uuid-char">
<xsl:with-param name="PRNG" select="$PRNG"/>
</xsl:apply-templates>
<xsl:next-iteration>
<xsl:with-param name="PRNG" select="$PRNG?next()"/>
</xsl:next-iteration>
</xsl:iterate>
</xsl:value-of>
</xsl:variable>
<!-- Divide $random-chars into nonoverlapping strings:
$seq-length of them, each of length $template-length. -->
<xsl:sequence select="for $idx in (0 to $seq-length - 1)
return substring($random-chars, 1 + $idx * $template-length, $template-length)"/>
</xsl:sequence>
</xsl:function>

<xsl:variable name="uuid-v4-template" as="xs:string">________-____-4___-=___-____________</xsl:variable>
<!-- a847eaab-cec8-41bd-98e2-02d02900b554 -->
<!-- replacements for UUID v4:
'_' becomes a random hex value 0-9a-f
'=' becomes one of '8','9','a','b' at random
any other character is copied -->

<xsl:variable name="hex-digits" select="tokenize('0 1 2 3 4 5 6 7 8 9 a b c d e f', ' ')"/>

<xsl:template match=".[. = '_']" mode="uuid-char">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
<?xml version="1.0" encoding="UTF-8"?>
<x:description xmlns:x="http://www.jenitennison.com/xslt/xspec"
xmlns:ov="http://csrc.nist.gov/ns/oscal/xspec/variable"
xmlns:r="http://csrc.nist.gov/ns/random"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
stylesheet="../../random-util.xsl"
xslt-version="3.0">

<!-- For repeatable testing, use a fixed germ. Also, XSpec
cannot evaluate document-uri(/) in the default parameter
value from the XSLT. -->
<x:param name="germ">x</x:param>

<x:variable name="ov:uuid-v4-regex" as="xs:string">^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-4[0-9A-Fa-f]{3}-[89ABab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12}$</x:variable>
<x:variable name="ov:seed" as="item()" select="'x'"/>
<x:variable name="ov:PRNG" as="map(xs:string, item())" select="random-number-generator($ov:seed)"/>

<x:scenario label="Tests for top-level template">
<x:call template="xsl:initial-template"/>
<!-- Assertions here are minimal because r:make-uuid
has function-level tests below. -->
<x:expect label="Repeatable result for the seed, 'a'"
test="$x:result/a[1]/string()" select="$x:result/a[2]/string()"/>
<x:expect label="The 'b' uuid is different from the 'a' uuid"
test="not($x:result/a[1]/string() = $x:result/b/string())"/>
</x:scenario>

<x:scenario label="Tests for r:make-uuid function">
<x:call function="r:make-uuid">
<x:param name="seed" select="$ov:seed"/>
</x:call>
<x:expect label="Same as calling r:make-uuid-sequence with seq-length=1"
select="r:make-uuid-sequence($ov:seed, 1)"/>
</x:scenario>
<x:scenario label="Tests for r:make-uuid-sequence function">
<x:scenario label="seq-length=1">
<x:variable name="ov:seq-length" as="xs:integer" select="1"/>
<x:call function="r:make-uuid-sequence">
<x:param name="seed" select="$ov:seed"/>
<x:param name="seq-length" select="$ov:seq-length"/>
</x:call>
<x:like label="SHARED: Check sequence of uuids"/>
</x:scenario>
<x:scenario label="seq-length=10000">
<x:variable name="ov:seq-length" as="xs:integer" select="10000"/>
<x:call function="r:make-uuid-sequence">
<x:param name="seed" select="$ov:seed"/>
<x:param name="seq-length" select="$ov:seq-length"/>
</x:call>
<x:like label="SHARED: Check sequence of uuids"/>
<x:expect label="Check uniqueness of strings in the sequence"
test="$x:result => distinct-values() => count() = $ov:seq-length"/>
<x:expect label="Check that 2nd uuid is not a shift of 1st uuid"
test="not(starts-with($x:result[2],substring($x:result[1],2,7)))"/>
</x:scenario>
<x:scenario label="seq-length=500,000" pending="To save time, run only when needed">
<x:variable name="ov:seq-length" as="xs:integer" select="500000"/>
<x:call function="r:make-uuid-sequence">
<x:param name="seed" select="$ov:seed"/>
<x:param name="seq-length" select="$ov:seq-length"/>
</x:call>
<x:like label="SHARED: Check sequence of uuids"/>
<x:expect label="Check uniqueness of strings in the sequence"
test="$x:result => distinct-values() => count() = $ov:seq-length"/>
</x:scenario>
<x:scenario label="Edge case: seq-length=0">
<x:call function="r:make-uuid-sequence">
<x:param name="seed" select="$ov:seed"/>
<x:param name="seq-length" as="xs:integer" select="0"/>
</x:call>
<x:expect label="Nothing" select="()"/>
</x:scenario>
</x:scenario>
<x:scenario label="Tests for r:make-random-string-sequence function">
<x:scenario label="Template starts with _">
<x:variable name="ov:template" select="'_xyz'"/>
<x:call function="r:make-random-string-sequence">
<x:param name="seed" select="$ov:seed"/>
<x:param name="seq-length" select="1"/>
<x:param name="template" select="$ov:template"/>
</x:call>
<x:expect label="Starts with hex digit"
test="matches($x:result,'^[0-9a-f]')"/>
<x:expect label="Same length as template"
test="string-length($x:result)=string-length($ov:template)"/>
<x:expect label="For this template, characters after the first are not hex digits"
test="not(matches(substring($x:result,2),'[0-9a-f]'))"/>
</x:scenario>
<x:scenario label="Template starts with =">
<x:variable name="ov:template" select="'=xyz'"/>
<x:call function="r:make-random-string-sequence">
<x:param name="seed" select="$ov:seed"/>
<x:param name="seq-length" select="1"/>
<x:param name="template" select="$ov:template"/>
</x:call>
<x:expect label="Starts with 8, 9, a, or b"
test="matches($x:result,'^[89ab]')"/>
<x:expect label="Same length as template"
test="string-length($x:result)=string-length($ov:template)"/>
<x:expect label="For this template, characters after the first are not 8, 9, a, or b"
test="not(matches(substring($x:result,2),'[89ab]'))"/>
</x:scenario>
<x:scenario label="Template is empty string">
<x:call function="r:make-random-string-sequence">
<x:param name="seed" select="$ov:seed"/>
<x:param name="seq-length" select="1"/>
<x:param name="template" select="''"/>
</x:call>
<x:expect label="Empty string" select="''"/>
</x:scenario>
<x:scenario label="Long template of repeated '_' characters">
<x:variable name="ov:template" select="string-join(for $j in (1 to 10000) return '_','')"/>
<x:variable name="ov:expected-digits" as="xs:string" select="'^[0-9a-f]+$'"/>
<x:call function="r:make-random-string-sequence">
<x:param name="seed" select="$ov:seed"/>
<x:param name="seq-length" select="1"/>
<x:param name="template" select="$ov:template"/>
</x:call>
<x:like label="SHARED: Check long output"/>
</x:scenario>
<x:scenario label="Long template of repeated '=' characters">
<x:variable name="ov:template" select="string-join(for $j in (1 to 10000) return '=','')"/>
<x:variable name="ov:expected-digits" as="xs:string" select="'^[89ab]+$'"/>
<x:call function="r:make-random-string-sequence">
<x:param name="seed" select="$ov:seed"/>
<x:param name="seq-length" select="1"/>
<x:param name="template" select="$ov:template"/>
</x:call>
<x:like label="SHARED: Check long output"/>
</x:scenario>
</x:scenario>

<x:scenario label="Test for '_' character with mode=uuid-char">
<x:context select="'_'" mode="uuid-char">
<x:param name="PRNG" select="$ov:PRNG"/>
</x:context>
<x:expect label="One hex digit"
test="matches($x:result,'^[0-9a-f]$')"/>
<!-- The "Long template of repeated '_' characters" scenario
checks that it is not the same hex digit for all PRNG inputs. -->
</x:scenario>
<x:scenario label="Test for '=' character with mode=uuid-char">
<x:context select="'='" mode="uuid-char">
<x:param name="PRNG" select="$ov:PRNG"/>
</x:context>
<x:expect label="One character 8, 9, a, or b"
test="matches($x:result,'^[89ab]$')"/>
<!-- The "Long template of repeated '=' characters" scenario
checks that it is not the same hex digit for all PRNG inputs. -->
</x:scenario>
<x:scenario label="Tests for characters other than '_' or '=', with mode=uuid-char">
<!-- These tests exercise the default template behavior for mode="uuid-char". -->
<x:scenario label="Empty string">
<x:context select="''" mode="uuid-char">
<x:param name="PRNG" select="$ov:PRNG"/>
</x:context>
<x:expect label="Text node with no content"
test="$x:result instance of text() and string-length($x:result) eq 0"/>
</x:scenario>
<x:scenario label="Hyphen">
<x:context select="'-'" mode="uuid-char">
<x:param name="PRNG" select="$ov:PRNG"/>
</x:context>
<x:expect label="Text node with hyphen">-</x:expect>
</x:scenario>
<x:scenario label="A non-ASCII character">
<x:variable name="ov:charnum" as="xs:integer" select="500"/>
<x:context select="codepoints-to-string($ov:charnum)" mode="uuid-char">
<x:param name="PRNG" select="random-number-generator('123')"/>
</x:context>
<x:expect label="Text node with same character"
expand-text="yes">{codepoints-to-string($ov:charnum)}</x:expect>
</x:scenario>
</x:scenario>

<!-- SHARED scenarios -->

<x:scenario shared="yes" label="SHARED: Check long output">
<!-- This set of shared assertions expects the referencing
scenario to have defined <x:variable name="ov:expected-digits" .../> -->
<x:expect label="String of digits in expected set"
test="matches($x:result,$ov:expected-digits)"/>
<x:expect label="Same length as template"
test="string-length($x:result) = string-length($ov:template)"/>
<x:variable name="ov:distinct-codepoints" as="xs:integer+"
select="$x:result => string-to-codepoints() => distinct-values()"/>
<x:expect label="String is not the same digit repeated"
test="count($ov:distinct-codepoints) gt 1"/>
</x:scenario>

<x:scenario shared="yes" label="SHARED: Check sequence of uuids">
<!-- This set of shared assertions expects the referencing
scenario or its ancestor to have defined
<x:variable name="ov:seq-length" .../> and
<x:variable name="ov:seed" .../> -->
<x:expect label="Correct number of strings"
test="$x:result instance of xs:string+ and count($x:result) eq $ov:seq-length"/>
<x:expect label="Each string matches uuid regular expression"
test="every $uuid in $x:result satisfies matches($uuid, $ov:uuid-v4-regex)"/>
<x:expect label="Check repeatability for same seed"
test="deep-equal($x:result, r:make-uuid-sequence($ov:seed, $ov:seq-length))"/>
<x:expect label="Not typically equal to function output for a different seed"
test="not(deep-equal($x:result, r:make-uuid-sequence($ov:seed || '1', $ov:seq-length)))"/>
</x:scenario>

</x:description>