Skip to content

Formats and identifiers

Much of the text an application handles has a format: an order number, a UUID, a date, an IP address, a token, a colour, an attribute name, a file name passed to a shell. Each format is a language here, decided by the standard library's own parser, never a regular expression standing in for it, so a claim over L[uuid] covers every spelling uuid.UUID reads, not only the one a developer had in mind. The claims that matter for a format are the same few everywhere: a parser rejects what is not in its language, a normaliser is idempotent, a renderer's output is in the language, and a round trip gives back what went in, or does not, and the witness says which spelling breaks it.

The examples are the shop's own, from examples/shop/formats.py.

The format languages

Language Members Decided by
L[digit] strings of the ten ASCII digits c in "0123456789"
L[alpha], L[alnum] letters, and letters with digits, in any script str.isalpha, str.isalnum
L[identifier] Python identifiers str.isidentifier
L[uuid] every spelling uuid.UUID reads: hyphenated, bare, braced, urn:uuid: uuid.UUID
L[iso_date], L[iso_datetime] ISO 8601 dates and times as this Python reads them date.fromisoformat, datetime.fromisoformat
L[ipv4], L[ipv6] addresses ipaddress.IPv4Address, ipaddress.IPv6Address
L[base64] canonical base64 b64decode(validate=True), re-encoded equal
L[hex] what bytes.fromhex reads, spaces included bytes.fromhex
L[slug] lower-case words joined by single hyphens [a-z0-9]+(-[a-z0-9]+)*
L[shell_safe] a word the shell reads literally shlex.quote(s) == s

Parse, and reject the rest

A parser's first job is to refuse what is not in its language, and excluded_outside_domain checks exactly that: it feeds the function the language's near non-members and falsifies if one is accepted without an error.

def parse_order_id(text: str) -> uuid.UUID:
    """The order id in a URL, or a ValueError when it is not one."""
    return uuid.UUID(text)
f = parse_order_id
for text in L[uuid], excluded_outside_domain(text)
    holds

Normalise, and normalise once

A normaliser stores one spelling for many, so it has to be idempotent, and whether it keeps the spelling it was given is a separate claim that is usually false:

def normalise_order_id(text: str) -> str:
    """The order id as the database stores it."""
    return str(uuid.UUID(text))


def normalise_date(text: str) -> str:
    """A delivery date as the shop stores it."""
    return dt.date.fromisoformat(text).isoformat()


def normalise_timestamp(text: str) -> str:
    """An event time as the shop stores it."""
    return dt.datetime.fromisoformat(text).isoformat()


def normalise_colour(text: str) -> str:
    """A colour's hex bytes as the theme file stores them."""
    return bytes.fromhex(text).hex()
f = normalise_order_id
for text in L[uuid], f(f(text)) == f(text)
    holds

for text in L[uuid], f(text) == text
    falsified   text = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' vs 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'

f = normalise_date
for text in L[iso_date], f(f(text)) == f(text)
    holds

f = normalise_timestamp
for text in L[iso_datetime], f(f(text)) == f(text)
    holds

f = normalise_colour
for text in L[hex], f(f(text)) == f(text)
    holds

for text in L[hex], f(text) == text
    falsified   text = 'aA': 'aa' vs 'aA'

A bare 32-digit UUID is a UUID, and it comes back hyphenated, so a lookup that compares the stored id with the one in the URL misses it.

Render into a language

A function that produces a format holds its output to that language with in:

def render_quantity(n: int) -> str:
    """A quantity as it is printed on the invoice."""
    return str(n)


def anonymise_ip(address: str) -> str:
    """A visitor's address with the last part zeroed, for analytics."""
    return ".".join(address.split(".")[:3] + ["0"])


def attribute_name(label: str) -> str:
    """A form label turned into the attribute name the template uses."""
    return label.strip().replace(" ", "_").lower()
f = render_quantity
for n in N, f(n) in L[digit]
    holds

f = anonymise_ip
for address in L[ipv4], f(address) in L[ipv4]
    holds

for address in L[ipv6], f(address) in L[ipv6]
    falsified   address = '::': '::.0' is not in L[ipv6]

f = attribute_name
for label in L[identifier], f(label) in L[identifier]
    holds

for label in L[alnum], f(label) in L[identifier]
    falsified   label = '': '' is not in L[identifier]

The anonymiser was written for IPv4 and quietly produces garbage for the IPv6 addresses the same log holds.

Round trips

A decoder followed by an encoder gives back what went in, over the language where that is meant to be true:

def reencode_token(token: str) -> str:
    """An API token as the gateway passes it on."""
    return base64.b64encode(base64.b64decode(token)).decode("ascii")
f = reencode_token
for token in L[base64], f(token) == token
    holds

Shell commands

L[shell_safe] is the language of words a shell reads literally, and a command built from a file name is only safe over it, or when the name is quoted:

def delete_command(filename: str) -> list:
    """The command that deletes an uploaded file, split into arguments
    the way the shell will."""
    return shlex.split(f"rm {filename}")


def delete_command_quoted(filename: str) -> list:
    """The same command with the filename quoted."""
    return shlex.split(f"rm {shlex.quote(filename)}")
f = delete_command
for filename in L[printable], f(filename) == ["rm", filename]
    falsified   filename = '': ['rm'] vs ['rm', '']

for filename in L[shell_safe], f(filename) == ["rm", filename]
    holds

f = delete_command_quoted
for filename in L[printable], f(filename) == ["rm", filename]
    holds

An empty name drops an argument, and a name with a space in it splits into two, which is how a delete of one uploaded file becomes a delete of two.

Recipes

To say Write
the parser rejects what is not in the format for s in L[uuid], excluded_outside_domain(s)
normalising twice is normalising once for s in L[iso_date], f(f(s)) == f(s)
the output is in the format for n in N, f(n) in L[digit]
decode then encode gives it back for s in L[base64], f(s) == s
the function only works for part of a format the same claim over the narrower language, beside the falsified one