// scripts/measure-yaml-ambiguity.mjs — from tinytinytools.com // Served verbatim from the working tree. Method: https://tinytinytools.com/guides /** * The measurements behind /guides/yaml-gotchas. * * The same snippets through two parsers that disagree on purpose: * * YAML 1.1 PyYAML's safe_load, the resolver behind an enormous amount of deployed * Python tooling, and the behaviour most "YAML is dangerous" stories describe. * YAML 1.2 src/lib/text-tools/yaml.ts, this site's own parser, which resolves the 1.2 * way and reports the places the two specifications part company. * * Both are actually run. Nothing here is quoted from a blog post about what YAML "does", * because the whole point of the exercise is that what YAML does depends on which parser * is reading it, and that is checkable rather than a matter of opinion. * * Needs: pip install pyyaml * Usage: node scripts/measure-yaml-ambiguity.mjs */ import { execFileSync } from 'node:child_process'; import { parseYaml } from '../src/lib/text-tools/yaml.ts'; /** * Each case is a snippet people really write, not a torture test. `why` says what the * author plainly meant, which is the thing a parser can silently fail to deliver. */ const CASES = [ { id: 'norway', label: 'A country code', yaml: 'country: NO', why: 'The ISO code for Norway.' }, { id: 'feature-flags', label: 'Feature flags', yaml: 'debug: yes\ncache: off', why: 'Flags written the way English writes them.' }, { id: 'github-on', label: 'A workflow trigger', yaml: 'on: push', why: 'The key GitHub Actions uses for its trigger block.' }, { id: 'time', label: 'A time of day', yaml: 'standup: 9:30', why: 'Half past nine.' }, { id: 'zero-padded', label: 'A padded identifier', yaml: 'employee: 012345', why: 'An id that happens to start with a zero.' }, { id: 'permissions', label: 'A file mode', yaml: 'mode: 0777', why: 'A Unix permission bitmask.' }, { id: 'version', label: 'A version number', yaml: 'version: 1.20', why: 'Version one point twenty.' }, { id: 'norwegian-name', label: 'A single letter', yaml: 'initial: N', why: 'Somebody’s middle initial.' }, { id: 'null-forms', label: 'An empty value', yaml: 'timeout:\nretries: ~\nname: null', why: 'Three ways of writing nothing.' }, { id: 'duplicate', label: 'A repeated key', yaml: 'port: 8080\nhost: localhost\nport: 9090', why: 'A key set twice, usually by accident in a merge.' }, { id: 'anchor', label: 'An anchor and alias', yaml: 'defaults: &base\n retries: 3\nstaging:\n <<: *base', why: 'Shared configuration, reused.' }, { id: 'tab', label: 'A tab indent', yaml: 'server:\n\tport: 8080', why: 'Indentation typed with the Tab key.' }, ]; /** PyYAML, in a subprocess, reporting the parsed value and its Python type. */ function yaml11(cases) { const program = ` import json, sys, yaml out = {} for case in json.loads(sys.stdin.read()): try: value = yaml.safe_load(case["yaml"]) out[case["id"]] = {"ok": True, "value": value, "types": {k: type(v).__name__ for k, v in value.items()} if isinstance(value, dict) else type(value).__name__} except Exception as error: out[case["id"]] = {"ok": False, "error": type(error).__name__ + ": " + str(error).split("\\n")[0]} print(json.dumps(out)) `; const stdout = execFileSync('python3', ['-c', program], { input: JSON.stringify(cases.map(({ id, yaml }) => ({ id, yaml }))), encoding: 'utf8', }); return JSON.parse(stdout); } const version = execFileSync('python3', ['-c', 'import yaml; print(yaml.__version__)'], { encoding: 'utf8' }).trim(); const oneOne = yaml11(CASES); const results = CASES.map(testCase => { const parsed = parseYaml(testCase.yaml); return { ...testCase, yaml11: oneOne[testCase.id], yaml12: parsed.ok ? { ok: true, value: parsed.value, issues: parsed.issues.map(i => ({ kind: i.kind, line: i.line, detail: i.detail })) } : { ok: false, error: parsed.error, line: parsed.line }, }; }); console.log(JSON.stringify({ pyyamlVersion: version, results }, null, 2));