//go:build ignore // Writes lib/src/go_unicode.dart: the three sets of code points that Go's // strings.ToLower, strings.ToUpper and strings.TrimSpace depend on, as the // package unicode of the Go that runs it defines them, for the key strings // of authorkey (stage 7b of docs/PLAN_dart.md): // // - the code points r with unicode.ToLower(r) != r, which strings.ToLower // changes, so that ParsePublic tells "written in lower case" as Go does; // - those with unicode.ToUpper(r) != r, for ParseSecret; // - those with unicode.IsSpace(r), which strings.TrimSpace trims from a // line of a key file. // // Each set is written as runs [first, last, stride], stride 1 or 2, in // increasing order. No value is written by hand: the generator scans every // code point and checks the runs it writes against the functions of Go. // // Run it with the toolchain of datekeys-go, Go 1.26.8, Unicode 15.0.0, // without changing anything there: // // cd ../datekeys-go && go run ../datekeys-dart/tool/go_unicode_tables.go \ // -out ../datekeys-dart/lib/src/go_unicode.dart package main import ( "flag" "fmt" "log" "os" "runtime" "strings" "unicode" "unicode/utf8" ) // runs returns the code points of in as runs [first, last, stride]: a run // of stride 2 is taken when it covers at least three points, so that // alternating cases stay short. func runs(in func(rune) bool) [][3]rune { var pts []rune for r := rune(0); r <= unicode.MaxRune; r++ { if r >= 0xd800 && r <= 0xdfff { continue // not in valid UTF-8: a decoder gives U+FFFD instead } if in(r) { pts = append(pts, r) } } var out [][3]rune for i := 0; i < len(pts); { j := i for j+1 < len(pts) && pts[j+1] == pts[j]+1 { j++ } k := i for k+1 < len(pts) && pts[k+1] == pts[k]+2 { k++ } switch { case j > i: out = append(out, [3]rune{pts[i], pts[j], 1}) i = j + 1 case k-i >= 2: out = append(out, [3]rune{pts[i], pts[k], 2}) i = k + 1 default: out = append(out, [3]rune{pts[i], pts[i], 1}) i++ } } // Check the runs against the function. member := map[rune]bool{} for _, r := range out { for c := r[0]; c <= r[1]; c += r[2] { if member[c] { log.Fatalf("U+%04X twice", c) } member[c] = true } } for r := rune(0); r <= unicode.MaxRune; r++ { if r >= 0xd800 && r <= 0xdfff { continue } if member[r] != in(r) { log.Fatalf("U+%04X: the runs disagree", r) } } return out } func list(b *strings.Builder, name, doc string, rs [][3]rune) { fmt.Fprintf(b, "// %s\nconst List %s = [\n", doc, name) for _, r := range rs { fmt.Fprintf(b, " 0x%04x, 0x%04x, %d,\n", r[0], r[1], r[2]) } b.WriteString("];\n\n") } func main() { out := flag.String("out", "", "the Dart file to write") flag.Parse() if *out == "" { log.Fatal("usage: go run tool/go_unicode_tables.go -out lib/src/go_unicode.dart") } // strings.ToLower and ToUpper change a string exactly when one of its // runes changes, or when it is not valid UTF-8: check that premise on // every code point. for r := rune(0); r <= unicode.MaxRune; r++ { if r >= 0xd800 && r <= 0xdfff { continue } s := string(r) if (strings.ToLower(s) != s) != (unicode.ToLower(r) != r) { log.Fatalf("strings.ToLower and unicode.ToLower disagree on U+%04X", r) } if (strings.ToUpper(s) != s) != (unicode.ToUpper(r) != r) { log.Fatalf("strings.ToUpper and unicode.ToUpper disagree on U+%04X", r) } if (strings.TrimSpace(s) == "") != unicode.IsSpace(r) { log.Fatalf("strings.TrimSpace and unicode.IsSpace disagree on U+%04X", r) } } if strings.ToLower("\xff") == "\xff" || strings.ToUpper("\xff") == "\xff" { log.Fatal("an invalid byte no longer changes") } if strings.TrimSpace(" \xff ") != "\xff" || utf8.RuneError != 0xfffd { log.Fatal("TrimSpace no longer stops at an invalid byte") } var b strings.Builder fmt.Fprintf(&b, `// Code generated by tool/go_unicode_tables.go with Go %s, Unicode %s. // DO NOT EDIT: regenerate it. // // The lists run to hundreds of lines: the formatter leaves them as the // generator writes them. // dart format off /// The code points that Go's strings.ToLower, strings.ToUpper and /// strings.TrimSpace depend on, as the package unicode of Go %s defines /// them, Unicode %s: authorkey.dart reads the strings of a key and the /// lines of a key file with them, so that its errors are those of Go /// whatever the bytes. They are not the case mapping of the platform, nor /// the tables of the path rules (Unicode 18.0.0, spec ยง29.5.1). /// /// Internal: lib/datekeys.dart does not export it. library; /// The version of Unicode of the package unicode of Go %s. const goUnicodeVersion = '%s'; `, runtime.Version(), unicode.Version, runtime.Version(), unicode.Version, runtime.Version(), unicode.Version) list(&b, "_lowerChanges", "The runs [first, last, stride] of r with unicode.ToLower(r) != r.", runs(func(r rune) bool { return unicode.ToLower(r) != r })) list(&b, "_upperChanges", "The runs [first, last, stride] of r with unicode.ToUpper(r) != r.", runs(func(r rune) bool { return unicode.ToUpper(r) != r })) list(&b, "_spaces", "The runs [first, last, stride] of r with unicode.IsSpace(r).", runs(unicode.IsSpace)) b.WriteString(`// Whether r is in the runs of table, by binary search on the first points. bool _inRuns(List table, int r) { var lo = 0; var hi = table.length ~/ 3; while (lo < hi) { final m = (lo + hi) ~/ 2; if (table[3 * m + 1] < r) { lo = m + 1; } else { hi = m; } } if (lo == table.length ~/ 3) return false; final first = table[3 * lo]; return r >= first && (r - first) % table[3 * lo + 2] == 0; } /// Whether Go's unicode.ToLower changes the code point [r]. bool goLowerChanges(int r) => _inRuns(_lowerChanges, r); /// Whether Go's unicode.ToUpper changes the code point [r]. bool goUpperChanges(int r) => _inRuns(_upperChanges, r); /// Whether [r] is a space for Go's unicode.IsSpace. bool goIsSpace(int r) => _inRuns(_spaces, r); `) if err := os.WriteFile(*out, []byte(b.String()), 0o644); err != nil { log.Fatal(err) } fmt.Printf("wrote %s\n", *out) }