diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index fc4bbf7eb4..4bab1fcd81 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -17,6 +17,14 @@ on: pull_request: # The branches below must be a subset of the branches above branches: ["master"] + # Skip kotoba-only PRs; those run .github/workflows/kotoba-v1.yml. + paths: + - 'java/**' + - 'javascript/**' + - 'cpp/**' + - 'resources/**' + - 'tools/**' + - 'pom.xml' schedule: - cron: "0 0 * * 1" diff --git a/.github/workflows/cpp-unit-test.yml b/.github/workflows/cpp-unit-test.yml index f8f4e78823..c6b8c9ce2c 100644 --- a/.github/workflows/cpp-unit-test.yml +++ b/.github/workflows/cpp-unit-test.yml @@ -1,7 +1,13 @@ # This workflow builds and unit tests both main C++ API and also build/data tools for metadata generation. name: Testing C++ API -on: pull_request +on: + pull_request: + # Skip kotoba-only PRs; those run .github/workflows/kotoba-v1.yml. + paths: + - 'cpp/**' + - 'tools/cpp/**' + - 'resources/**' permissions: contents: read diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 437275ac0b..967edf7887 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -7,7 +7,17 @@ # # Source repository: https://github.com/actions/dependency-review-action name: 'Dependency Review' -on: [pull_request] +on: + pull_request: + # Skip kotoba-only PRs; those run .github/workflows/kotoba-v1.yml. + paths-ignore: + - 'kotoba/**' + - '.github/workflows/kotoba-v1.yml' + - '.github/workflows/java-unit-test.yml' + - '.github/workflows/cpp-unit-test.yml' + - '.github/workflows/codeql.yml' + - '.github/workflows/osv-scanner-unified.yml' + - '.github/workflows/dependency-review.yml' permissions: contents: read diff --git a/.github/workflows/java-unit-test.yml b/.github/workflows/java-unit-test.yml index 2e2672970f..655ed40354 100644 --- a/.github/workflows/java-unit-test.yml +++ b/.github/workflows/java-unit-test.yml @@ -1,7 +1,15 @@ # This workflow builds and unit tests both main Java API and also build/data tools for metadata generation. name: Testing Java API -on: pull_request +on: + pull_request: + # Skip kotoba-only PRs; those run .github/workflows/kotoba-v1.yml. + paths: + - 'java/**' + - 'javascript/**' + - 'resources/**' + - 'tools/java/**' + - 'pom.xml' permissions: contents: read diff --git a/.github/workflows/kotoba-v1.yml b/.github/workflows/kotoba-v1.yml new file mode 100644 index 0000000000..b97ec28f8d --- /dev/null +++ b/.github/workflows/kotoba-v1.yml @@ -0,0 +1,42 @@ +name: Kotoba v1 + +on: + push: + branches: [master] + paths: + - 'kotoba/**' + - '.github/workflows/kotoba-v1.yml' + pull_request: + paths: + - 'kotoba/**' + - '.github/workflows/kotoba-v1.yml' + +permissions: + contents: read + +jobs: + kotoba: + name: kotoba compile + fixture + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + - name: Install kotoba v0.7.2 + shell: bash + run: | + set -euo pipefail + release_dir="$RUNNER_TEMP/kotoba-v0.7.2" + cli_dir="$RUNNER_TEMP/kotoba-cli" + mkdir -p "$release_dir" "$cli_dir" + curl -L --fail --silent --show-error \ + -o "$release_dir/kotoba-linux-amd64.tar.gz" \ + https://github.com/kotoba-lang/kotoba/releases/download/v0.7.2/kotoba-linux-amd64.tar.gz + curl -L --fail --silent --show-error \ + -o "$release_dir/kotoba-linux-amd64.tar.gz.sha256" \ + https://github.com/kotoba-lang/kotoba/releases/download/v0.7.2/kotoba-linux-amd64.tar.gz.sha256 + (cd "$release_dir" && sha256sum -c kotoba-linux-amd64.tar.gz.sha256) + tar -xzf "$release_dir/kotoba-linux-amd64.tar.gz" -C "$cli_dir" + echo "$cli_dir" >> "$GITHUB_PATH" + + - name: Compile wasm and run fixture + run: kotoba/checks.sh diff --git a/.github/workflows/osv-scanner-unified.yml b/.github/workflows/osv-scanner-unified.yml index 1c6ba2c2c2..f18d3cb0b6 100644 --- a/.github/workflows/osv-scanner-unified.yml +++ b/.github/workflows/osv-scanner-unified.yml @@ -18,6 +18,15 @@ name: OSV-Scanner on: pull_request: branches: ["master"] + # Skip kotoba-only PRs; those run .github/workflows/kotoba-v1.yml. + paths-ignore: + - 'kotoba/**' + - '.github/workflows/kotoba-v1.yml' + - '.github/workflows/java-unit-test.yml' + - '.github/workflows/cpp-unit-test.yml' + - '.github/workflows/codeql.yml' + - '.github/workflows/dependency-review.yml' + - '.github/workflows/osv-scanner-unified.yml' schedule: - cron: "12 12 * * 1" push: diff --git a/kotoba/README.md b/kotoba/README.md new file mode 100644 index 0000000000..67316af8ea --- /dev/null +++ b/kotoba/README.md @@ -0,0 +1,84 @@ +# Kotoba v1 for libphonenumber + +First-class sibling tree to `cpp/`, `java/`, and `javascript/`. This directory +is a Kotoba check surface for the fork, not a language binding over the Java, +C++, or JavaScript libraries. + +Public operator: awai.network. Sales: Ryo Awai. + +License: Apache-2.0 (same as this repository). + +## Honest scope + +v1 checks whether a short hardcoded fixture is a **well-formed E.164-looking** +string: a plus prefix plus a digit length. + +- First byte must be `+` (ASCII 43). +- Every following byte must be an ASCII digit `0`–`9`. +- Digit count must be in `2..15`. + - `15` is the ITU E.164 maximum for the complete international number. + - `2` is this repository's `PhoneNumberUtil.MIN_LENGTH_FOR_NSN`, applied + here to the complete digit string, not to a region national number. + +Two accept fixtures and two reject fixtures are taken from this repository's +own testdata (`US_NUMBER` / `GB_NUMBER` in +`java/libphonenumber/test/com/google/i18n/phonenumbers/PhoneNumberUtilTest.java`): + +| fixture | bytes | expected | +|---|---|---| +| `fixtures/us-e164.txt` | `+16502530000` | accept (E.164 of US_NUMBER) | +| `fixtures/gb-e164.txt` | `+442070313000` | accept (E.164 of GB_NUMBER) | +| `fixtures/us-international.txt` | `+1 650 253 0000` | reject (INTERNATIONAL spaces) | +| `fixtures/us-nsn.txt` | `6502530000` | reject (national number, no plus) | + +This is **not**: + +- a drop-in replacement for the Java, C++, or JavaScript libraries +- full region metadata / carrier / geocoding / timezone lookup +- `isValidNumber`, `isPossibleNumber` against country ranges +- parse, format, AsYouType, matcher, or short-number support +- a copy of `resources/phonenumber.proto` or the metadata proto set +- an FFI or JNI/C++ driver + +A full libphonenumber port will not fit on wasm32 i64-v1. That dialect has no +IEEE floats, no admitted byte-builder, and no host imports in the +host-independent profile. Shrinking to a plus-prefix digit-length check of a +few project fixtures is the smallest codec/header/fixture that still proves a +real accept and a real reject. The shrink is documented here, not stubbed. + +## Module + +`phonenumber.kotoba` is one compilation unit (`phonenumber.v1`). + +| export | meaning | +|---|---| +| `e164-looking` | `1` if fixture `id` is E.164-looking, else `0` | +| `accept-us` / `accept-gb` | `1` if the matching accept fixture is admitted | +| `reject-spaces` / `reject-nsn` | `1` if the matching reject fixture is refused | +| `main` | `1` if both accepts and both rejects hold | + +## Build (kotoba CLI v0.7.2) + +Language authority: [kotoba-lang/kotoba-lang](https://github.com/kotoba-lang/kotoba-lang). +CLI: [kotoba-lang/kotoba](https://github.com/kotoba-lang/kotoba) tag **v0.7.2**. + +```sh +kotoba compile kotoba/phonenumber.kotoba --target wasm --output phonenumber.wasm --json +``` + +Accept `kotoba.cli/ok?` true, `kotoba.cli/code` `emitted`, +`value-profile` `i64-v1`. The artifact is host-independent wasm32 +(`wasm32-kotoba-v1`, `value-abi` `direct-v1`): no `kotoba:typed` imports, no +libphonenumber native code. + +`kotoba compile --target wasm --run` is not used. The i64-v1 guest does not +match the kototama/chicory runner on v0.7.2. Fixture execution is +`--target web --run` (`js-kotoba-v1`), which still reports `value-profile` +`i64-v1`. + +```sh +kotoba/checks.sh +``` + +Install the CLI from the v0.7.2 release tarball or +`brew tap kotoba-lang/kotoba && brew trust kotoba-lang/kotoba && brew install kotoba`. diff --git a/kotoba/checks.sh b/kotoba/checks.sh new file mode 100755 index 0000000000..87d4b5fa48 --- /dev/null +++ b/kotoba/checks.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +# Compile phonenumber.kotoba with kotoba 0.7.2 and assert accept/reject fixtures. +set -euo pipefail + +root=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd) +cd "$root" + +if ! command -v kotoba >/dev/null 2>&1; then + echo "kotoba CLI is required (kotoba-lang/kotoba v0.7.2)" >&2 + exit 1 +fi + +if ! command -v python3 >/dev/null 2>&1; then + echo "python3 is required to parse kotoba --json and check wasm/fixtures" >&2 + exit 1 +fi + +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT + +python3 - "$root" <<'PY' +import pathlib, sys +root = pathlib.Path(sys.argv[1]) +fixtures = { + "kotoba/fixtures/us-e164.txt": "+16502530000", + "kotoba/fixtures/gb-e164.txt": "+442070313000", + "kotoba/fixtures/us-international.txt": "+1 650 253 0000", + "kotoba/fixtures/us-nsn.txt": "6502530000", +} + + +def e164_looking(s: str) -> bool: + if not (3 <= len(s) <= 16): + return False + if s[0] != "+": + return False + return all("0" <= ch <= "9" for ch in s[1:]) + + +for rel, expected in fixtures.items(): + got = (root / rel).read_text().rstrip("\n") + if got != expected: + raise SystemExit("%s is %r, expected %r" % (rel, got, expected)) + +accept_us = e164_looking(fixtures["kotoba/fixtures/us-e164.txt"]) +accept_gb = e164_looking(fixtures["kotoba/fixtures/gb-e164.txt"]) +reject_spaces = not e164_looking(fixtures["kotoba/fixtures/us-international.txt"]) +reject_nsn = not e164_looking(fixtures["kotoba/fixtures/us-nsn.txt"]) +print("fixture independent") +print(" accept +16502530000", accept_us) +print(" accept +442070313000", accept_gb) +print(" reject +1 650 253 0000", reject_spaces) +print(" reject 6502530000", reject_nsn) +if not (accept_us and accept_gb and reject_spaces and reject_nsn): + raise SystemExit("independent E.164-looking predicate disagreed with fixture table") +PY + +compile_json=$(kotoba compile kotoba/phonenumber.kotoba --target wasm --output "$work/phonenumber.wasm" --json) +COMPILE_JSON="$compile_json" WASM_PATH="$work/phonenumber.wasm" python3 <<'PY' +import json, os +wasm_path = os.environ["WASM_PATH"] +d = json.loads(os.environ["COMPILE_JSON"]) +ok = d.get("kotoba.cli/ok?") +code = d.get("kotoba.cli/code") +data = d.get("kotoba.cli/data") or {} +profile = data.get("value-profile") +compat = data.get("compatibility") or {} +print("wasm", ok, code, "profile", profile, "abi", compat.get("value-abi"), "target", compat.get("target")) +if not ok or code != "emitted": + raise SystemExit(d.get("kotoba.cli/message") or "wasm compile failed") +if profile != "i64-v1": + raise SystemExit("expected value-profile i64-v1, got %r" % (profile,)) +if compat.get("value-abi") != "direct-v1": + raise SystemExit("expected value-abi direct-v1") +if compat.get("target") != "wasm32-kotoba-v1": + raise SystemExit("expected target wasm32-kotoba-v1") +blob = open(wasm_path, "rb").read() +if blob[:4] != b"\x00asm": + raise SystemExit("not wasm magic") + +def read_u32(buf, i): + n = 0 + shift = 0 + while True: + b = buf[i] + i += 1 + n |= (b & 0x7F) << shift + if b < 0x80: + return n, i + shift += 7 + +i = 8 +imports = False +while i < len(blob): + sid = blob[i] + i += 1 + size, i = read_u32(blob, i) + if sid == 2: + imports = True + i += size +if imports: + raise SystemExit("wasm has an import section; host-independent i64-v1 must not") +print("wasm host-independent", len(blob), "bytes") +PY + +run_json=$(kotoba compile kotoba/phonenumber.kotoba --target web --output "$work/phonenumber.mjs" --run --json) +RUN_JSON="$run_json" python3 <<'PY' +import json, os +d = json.loads(os.environ["RUN_JSON"]) +ok = d.get("kotoba.cli/ok?") +code = d.get("kotoba.cli/code") +data = d.get("kotoba.cli/data") or {} +result = data.get("result") +profile = data.get("value-profile") +print("web", ok, code, "result", result, "profile", profile) +if not ok or code != "ran": + raise SystemExit(d.get("kotoba.cli/message") or "web fixture run failed") +if profile != "i64-v1": + raise SystemExit("expected value-profile i64-v1 on web run") +if result != 1: + raise SystemExit("main returned %r, expected 1 (accept US/GB and reject spaces/NSN)" % (result,)) +print("accept/reject fixture match ok") +PY diff --git a/kotoba/fixtures/gb-e164.txt b/kotoba/fixtures/gb-e164.txt new file mode 100644 index 0000000000..8e5f1ae0c8 --- /dev/null +++ b/kotoba/fixtures/gb-e164.txt @@ -0,0 +1 @@ ++442070313000 diff --git a/kotoba/fixtures/us-e164.txt b/kotoba/fixtures/us-e164.txt new file mode 100644 index 0000000000..e89a39afa4 --- /dev/null +++ b/kotoba/fixtures/us-e164.txt @@ -0,0 +1 @@ ++16502530000 diff --git a/kotoba/fixtures/us-international.txt b/kotoba/fixtures/us-international.txt new file mode 100644 index 0000000000..4b8a9ead16 --- /dev/null +++ b/kotoba/fixtures/us-international.txt @@ -0,0 +1 @@ ++1 650 253 0000 diff --git a/kotoba/fixtures/us-nsn.txt b/kotoba/fixtures/us-nsn.txt new file mode 100644 index 0000000000..ce4448f074 --- /dev/null +++ b/kotoba/fixtures/us-nsn.txt @@ -0,0 +1 @@ +6502530000 diff --git a/kotoba/phonenumber.kotoba b/kotoba/phonenumber.kotoba new file mode 100644 index 0000000000..d6cd41d929 --- /dev/null +++ b/kotoba/phonenumber.kotoba @@ -0,0 +1,142 @@ +;; Kotoba v1 libphonenumber surface (awai.network). +;; +;; Honest scope: well-formed E.164-looking check (plus prefix + digit length). +;; Two accept fixtures and two reject fixtures are taken from this repository's +;; own testdata (US_NUMBER / GB_NUMBER in java/.../PhoneNumberUtilTest.java). +;; +;; Not implemented: region metadata, carrier, geocoding, timezones, +;; AsYouTypeFormatter, parse/format beyond the E.164-looking predicate, +;; short numbers, or the PhoneNumber proto. Those need the metadata database +;; this tree does not copy. +;; +;; i64-v1 only: integers and explicit byte values. No IEEE floats, no strings, +;; no vectors (nth would leave i64-v1 and import kotoba:typed). + +(ns phonenumber.v1 + (:export [main e164-looking accept-us accept-gb reject-spaces reject-nsn])) + +;; Fixture ids: +;; 0 accept +16502530000 US_NUMBER E.164 (CC 1 + NSN 6502530000) +;; 1 accept +442070313000 GB_NUMBER E.164 (CC 44 + NSN 2070313000) +;; 2 reject +1 650 253 0000 US_NUMBER INTERNATIONAL (spaces) +;; 3 reject 6502530000 US_NUMBER national significant number (no plus) + +(defn fixture-len [id :i64] :i64 + (cond + (= id 0) 12 + (= id 1) 13 + (= id 2) 15 + :else 10)) + +(defn fixture-byte [id :i64 i :i64] :i64 + (cond + (= id 0) + (cond + (= i 0) 43 + (= i 1) 49 + (= i 2) 54 + (= i 3) 53 + (= i 4) 48 + (= i 5) 50 + (= i 6) 53 + (= i 7) 51 + (= i 8) 48 + (= i 9) 48 + (= i 10) 48 + :else 48) + (= id 1) + (cond + (= i 0) 43 + (= i 1) 52 + (= i 2) 52 + (= i 3) 50 + (= i 4) 48 + (= i 5) 55 + (= i 6) 48 + (= i 7) 51 + (= i 8) 49 + (= i 9) 51 + (= i 10) 48 + (= i 11) 48 + :else 48) + (= id 2) + (cond + (= i 0) 43 + (= i 1) 49 + (= i 2) 32 + (= i 3) 54 + (= i 4) 53 + (= i 5) 48 + (= i 6) 32 + (= i 7) 50 + (= i 8) 53 + (= i 9) 51 + (= i 10) 32 + (= i 11) 48 + (= i 12) 48 + (= i 13) 48 + :else 48) + :else + (cond + (= i 0) 54 + (= i 1) 53 + (= i 2) 48 + (= i 3) 50 + (= i 4) 53 + (= i 5) 51 + (= i 6) 48 + (= i 7) 48 + (= i 8) 48 + :else 48))) + +(defn- ascii-digit? [b :i64] :i64 + (if (< b 48) + 0 + (if (< 57 b) + 0 + 1))) + +;; Plus prefix, then 2..15 ASCII digits. Digit max is ITU E.164 (15). +;; Digit min is this repo's PhoneNumberUtil.MIN_LENGTH_FOR_NSN (2), applied +;; to the complete international digit string, not to a region NSN. +(defn e164-looking [id :i64] :i64 + (let [n (fixture-len id)] + (if (< n 3) + 0 + (if (< 16 n) + 0 + (if (= (fixture-byte id 0) 43) + (loop [i 1] + (if (>= i n) + 1 + (if (= (ascii-digit? (fixture-byte id i)) 1) + (recur (+ i 1)) + 0))) + 0))))) + +(defn accept-us [] :i64 + (e164-looking 0)) + +(defn accept-gb [] :i64 + (e164-looking 1)) + +(defn reject-spaces [] :i64 + (if (= (e164-looking 2) 0) + 1 + 0)) + +(defn reject-nsn [] :i64 + (if (= (e164-looking 3) 0) + 1 + 0)) + +(defn main [] :i64 + (if (= (accept-us) 1) + (if (= (accept-gb) 1) + (if (= (reject-spaces) 1) + (if (= (reject-nsn) 1) + 1 + 0) + 0) + 0) + 0))