This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project overview
A Java library providing ASCII-safe Charset SPI
implementations that transliterate Unicode to ASCII subsets rather than
simply rejecting non-ASCII input. Published to GitHub Packages as
com.maybeitssquid:ascii-safe-charsets.
Commands
./gradlew build # compile, run tests, spotless check
./gradlew test # tests only
./gradlew spotlessApply # auto-format Java source (required before commit)
./gradlew javadoc # generate Javadoc
./gradlew dependencyCheckAnalyze # OWASP CVE scan (slow; fails build at CVSS >= 7)
# Run a single test class
./gradlew test --tests "com.maybeitssquid.safeascii.CacheTest"On Windows, use gradlew.bat (or .\gradlew
in PowerShell).
The build uses a Java 25 toolchain and compiles to Java 17 bytecode
(release = "17"). CI tests on Java 17, 21, and 25 on every
push/PR to main.
Versioning and Releases
Versions are derived from git tags using gradle-git-version:
- On a tag (e.g.,
v1.0.0) → version =1.0.0 - After a tag → version = tag + distance + commit
hash (e.g.,
1.0.1-3-gABC1234= 3 commits after v1.0.0) - No tags yet → version synthesized from git history
(e.g.,
0.0.1-dev-88-gXYZ)
To create a release:
# Ensure all commits are pushed
git push origin main
# Create and push the tag (triggers automatic version picking in build)
git tag -a v1.0.0 -m "Release 1.0.0"
git push origin v1.0.0
# Build and publish
./gradlew clean build publishTo delete a release tag:
git tag -d v1.0.0 # Delete locally
git push origin :v1.0.0 # Delete from remoteConfiguration cache is disabled
(org.gradle.configuration-cache=false) to allow git
invocation during the build.
Architecture
The library wires together two subsystems: a Charset
implementation and a configurable transliteration pipeline.
Module and package layout
The library is a named JPMS module (module-info.java,
module com.maybeitssquid.safeascii). Only the charset API
package com.maybeitssquid.safeascii is exported. The
transliteration pipeline lives in
com.maybeitssquid.safeascii.internal, which is
not exported — those classes are implementation
details, not public API. The provider is declared two ways so it works
on both paths:
provides java.nio.charset.spi.CharsetProvider with … in
module-info.java (module path) and
META-INF/services/java.nio.charset.spi.CharsetProvider
(classpath).
When working on the pipeline, keep new internal classes in
…internal; only add to the exported package if it is
genuinely part of the public charset API. Pipeline unit tests live in
src/test/java/com/maybeitssquid/safeascii/internal so they
retain same-package/protected access.
Charset layer
TransliteratingASCIIProvider—CharsetProviderSPI entry point in the exported package, registered viasrc/main/resources/META-INF/services/java.nio.charset.spi.CharsetProvider(classpath) and theprovidesdirective inmodule-info.java(module path). Provides four charsets lazily:X-ASCII-Printable(aliasASCII-Printable) — strict printable ASCII (0x20–0x7E only, controls blocked)X-ASCII-Plain(aliasASCII-Plain) — same but allows LF; CR is unmappable so CRLF normalises to LF underIGNOREX-ASCII-Formatted(aliasASCII-Formatted) — same as X-ASCII-Plain but also allows TAB (0x09)X-Transliterating— aggressive Unicode-to-ASCII transliterationX-Transliterating-Single-Byte(aliasACH) — same but guarantees 1:1 character output
TransliteratingASCII— extendsjava.nio.charset.Charset. Takes anIntFunction<CharSequence>transliterator at construction; the encoder/decoder delegate all codepoint mapping to it.
Transliterator pipeline
Each step implements IntFunction<CharSequence> and
chains to the next. All pipeline classes below live in the non-exported
com.maybeitssquid.safeascii.internal package. The actual
pipelines assembled by the provider are:
ASCII-Printable / ASCII-Plain:
Cache → ASCIIFilterX-Transliterating:
Cache → Decompose → Name → ASCIIFilterX-Transliterating-Single-Byte:
Cache → SingleCharacterFilter → Decompose → Name → ASCIIFilterASCIIFilter— terminal step; passes ASCII codepoints not in the blocked Unicode categories, rejects everything else with"".Categorize— maps Unicode categories (digits, spaces, dashes, brackets, quotes, etc.) to ASCII equivalents; passes ASCII straight to delegate.Name— extendsCategorize; usesCharacter.getName()to match LATIN LETTERs, brackets, quotation marks, punctuation by name keyword.Decompose— extendsChainable; applies NFKD (or NFD) normalization before further processing; skips codepoints below U+00A0 as an optimization.Cache— extendsChainable; caches results in aCharSequence[128]array for ASCII and aHashMapfor the rest; supports manual pre-population viacache(int, CharSequence).Chainable— abstract base; holds thedelegate, implementsapply()which callsprocess()then fans out the result’s codepoints through the delegate chain.SingleCharacterFilter— wraps another transliterator; returns""for any input that produces a result length ≠ 1, ensuring length-preserving (fixed-width) output.
Code style
Spotless enforces Google Java Format. Run
./gradlew spotlessApply before committing. The formatter
excludes module-info.java.
Security patches
Transitive dependency CVEs are pinned in
gradle/libs.versions.toml as patch-* library
entries collected in the security-patches bundle.
build.gradle applies them as implementation
constraints. settings.gradle also loads them into the
buildscript classpath via regex. New CVE patches follow the same
patch-cve-XXXX-NNNNN naming convention.
The OWASP dependency check plugin
(./gradlew dependencyCheckAnalyze) fails the build at CVSS
≥ 7.