{"record":{"id":"949ce93283586c6f","repo":"quarkusio/quarkus","slug":"purl-type-must-start-with-a-letter","errorCode":null,"errorMessage":"PURL type must start with a letter: ","messagePattern":"PURL type must start with a letter: ","errorType":"exception","errorClass":"java.lang.IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"independent-projects/bootstrap/app-model/src/main/java/io/quarkus/sbom/Purl.java","lineNumber":595,"sourceCode":"        for (char c = 'a'; c <= 'z'; c++) {\n            UNRESERVED[c] = true;\n        }\n        for (char c = 'A'; c <= 'Z'; c++) {\n            UNRESERVED[c] = true;\n        }\n        for (char c = '0'; c <= '9'; c++) {\n            UNRESERVED[c] = true;\n        }\n        UNRESERVED['-'] = true;\n        UNRESERVED['.'] = true;\n        UNRESERVED['_'] = true;\n        UNRESERVED['~'] = true;\n    }\n\n    private static void validateType(String type) {\n        char first = type.charAt(0);\n        if (!((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z'))) {\n            throw new IllegalArgumentException(\"PURL type must start with a letter: \" + type);\n        }\n        for (int i = 1; i < type.length(); i++) {\n            char c = type.charAt(i);\n            if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')\n                    || (c >= '0' && c <= '9') || c == '.' || c == '+' || c == '-')) {\n                throw new IllegalArgumentException(\"PURL type contains invalid character '\" + c + \"': \" + type);\n            }\n        }\n    }\n\n    private static boolean isUnreserved(int c) {\n        return c >= 0 && c < 128 && UNRESERVED[c];\n    }\n\n    public static class Builder {\n\n        private String type;\n        private String namespace;","sourceCodeStart":577,"sourceCodeEnd":613,"githubUrl":"https://github.com/quarkusio/quarkus/blob/e1c734241f34c7919086ceb4c9262b4a58f6de44/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/sbom/Purl.java#L577-L613","documentation":"validateType() enforces the PURL spec rule that the type component must begin with an ASCII letter. When the first character of the type is a digit, symbol, or other character, an IllegalArgumentException is thrown. Types are validated whenever a Purl is constructed or parsed.","triggerScenarios":"Purl.parse() or Purl.Builder with a type whose first character is not a-z or A-Z, e.g. 'pkg:3rdparty/name', 'pkg:_internal/app', 'pkg:1 Maven-type' or an empty/blank type.","commonSituations":"Inventing custom ecosystem names starting with digits ('2fa', '3d'); passing an empty type because the coordinate scheme was mis-split ('pkg:/' with nothing between); truncating the 'pkg:' scheme leaving a slash or symbol as the first char of the parsed type.","solutions":["Rename the type so it starts with a letter, e.g. 'pkg:npm/...' instead of 'pkg:3rdparty/...'","Ensure the type comes from the standard list (maven, npm, pypi, golang, etc.) or starts with an ASCII letter","Check your parsing code that it slices the segment between 'pkg:' and the first '/' exactly, without an empty or shifted start"],"exampleFix":"// before\nPurl p = Purl.parse(\"pkg:3rdparty/acme-lib@1.0\");\n// after\nPurl p = Purl.parse(\"pkg:thirdparty/acme-lib@1.0\");","handlingStrategy":"validation","validationCode":"static boolean isValidPurlTypeStart(String type) {\n    if (type == null || type.isEmpty()) return false;\n    char first = type.charAt(0);\n    return (first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z');\n}","typeGuard":"boolean looksLikePurl(String s) {\n    return s != null && s.startsWith(\"pkg:\") && isValidPurlTypeStart(s.substring(4, Math.min(s.indexOf('/', 4), s.length())));\n}","tryCatchPattern":"try {\n    return Purl.parse(purlString);\n} catch (IllegalArgumentException e) {\n    if (e.getMessage().startsWith(\"PURL type must start with a letter\")) {\n        log.errorf(\"Unsupported PURL type in: %s\", purlString);\n    }\n    throw e;\n}","preventionTips":["Prefer the standard PURL types (maven, npm, pypi, golang, ...) over custom ones","Validate ecosystem names from external data feeds before constructing PURLs","Check your scheme-splitting logic leaves a non-empty, letter-initial type"],"tags":["purl","validation","sbom"],"backgroundTag":"purl-invalid-type","analyzedSha":"e1c734241f34c7919086ceb4c9262b4a58f6de44","analyzedAt":"2026-09-05T17:01:29.979Z","contentChangedAt":"2026-09-05T17:01:29.979Z","schemaVersion":2},"datasetVersion":"2026-09-12T22:17:10.623Z"}