Skip to content

Commit

Permalink
Nonrecursive UUID functions plus XSpec tests (#1158)
Browse files Browse the repository at this point in the history
random-util.xsl provides r:make-uuid and r:make-uuid-sequence for creating one
or more random UUIDs. The r:make-random-string-sequence function supports
more flexible random patterns. Implementations here use the XPath
function, random-number-generator.

random-util.xspec provides template- and function-level tests for
code in random-util.xsl.
  • Loading branch information
galtm authored and aj-stein-nist committed Jul 10, 2023
1 parent 4b01260 commit 9c0e21a
Show file tree
Hide file tree
Showing 2 changed files with 257 additions and 36 deletions.
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
207 changes: 207 additions & 0 deletions src/utils/util/resolver-pipeline/testing/2_metadata/random-util.xspec
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>

0 comments on commit 9c0e21a

Please sign in to comment.