{"record":{"id":"db9822689eb5dfee","repo":"quarkusio/quarkus","slug":"invalid-percent-encoding-at-index","errorCode":null,"errorMessage":"Invalid percent-encoding at index ","messagePattern":"Invalid percent-encoding at index ","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":392,"sourceCode":"            byte[] bytes = null;\n            while (pct + 2 < input.length() && input.charAt(pct) == '%') {\n                int hi = Character.digit(input.charAt(pct + 1), 16);\n                int lo = Character.digit(input.charAt(pct + 2), 16);\n                if (hi < 0 || lo < 0) {\n                    break;\n                }\n                if (bytes == null) {\n                    // each %XX triplet is 3 chars, so the max number of decoded bytes is the remaining length / 3\n                    bytes = new byte[(input.length() - tripletStart) / 3];\n                }\n                bytes[byteCount++] = (byte) ((hi << 4) | lo);\n                pct += 3;\n            }\n            if (byteCount > 0) {\n                sb.append(new String(bytes, 0, byteCount, StandardCharsets.UTF_8));\n                pos = pct;\n            } else {\n                throw new IllegalArgumentException(\n                        \"Invalid percent-encoding at index \" + tripletStart + \" in: \" + input);\n            }\n        }\n        sb.append(input, pos, input.length());\n        return sb.toString();\n    }\n\n    /**\n     * Percent-encodes each segment of a {@code /}-delimited path individually,\n     * preserving literal {@code /} separators. A single pass determines whether\n     * the path contains any {@code /} (i.e. is multi-segment) and whether any\n     * character requires encoding, avoiding redundant scans.\n     *\n     * @param path the decoded path (namespace or subpath)\n     * @return the encoded path with each segment percent-encoded\n     */\n    private static String encodePath(String path) {\n        // Single pass: find the first '/' (to know if the path is multi-segment)","sourceCodeStart":374,"sourceCodeEnd":410,"githubUrl":"https://github.com/quarkusio/quarkus/blob/e1c734241f34c7919086ceb4c9262b4a58f6de44/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/sbom/Purl.java#L374-L410","documentation":"percentDecode() throws this IllegalArgumentException when it encounters a '%' character that is not followed by two valid hex digits, so no byte can be decoded. It fails fast rather than silently passing malformed sequences through. Called by parse, version, and value while decoding PURL components.","triggerScenarios":"Calling Purl.parse(), Purl.version(), or qualifier value() with strings containing malformed escapes such as '%A', '100%', '%%', or '%GG' (non-hex characters).","commonSituations":"Hand-editing or truncating a PURL string; passing pre-URL-encoded text through an extra encoding step leaving stray '%'; copy-pasting versions or qualifiers containing literal percent signs (e.g. coverage values or 'distro=alpine%3' typos).","solutions":["Percent-encode literal '%' characters as '%25' before parsing: a version like '1.0%beta' must be '1.0%25beta'","Verify every '%' in the input is followed by exactly two hex digits; fix or remove stray '%' characters","If the string was double-encoded, decode once less (do not re-encode before calling Purl.parse)"],"exampleFix":"// before\nPurl p = Purl.parse(\"pkg:maven/org.acme/app@1.0%beta\");\n// after\nPurl p = Purl.parse(\"pkg:maven/org.acme/app@1.0%25beta\");","handlingStrategy":"validation","validationCode":"static boolean hasValidPercentEncoding(String s) {\n    for (int i = 0; i < s.length(); i++) {\n        if (s.charAt(i) == '%') {\n            if (i + 2 >= s.length()) return false;\n            if (!isHex(s.charAt(i + 1)) || !isHex(s.charAt(i + 2))) return false;\n            i += 2;\n        }\n    }\n    return true;\n}","typeGuard":"String safePurlSegment(String raw) {\n    return URLEncoder.encode(raw, StandardCharsets.UTF_8);\n}","tryCatchPattern":"try {\n    return Purl.parse(input);\n} catch (IllegalArgumentException e) {\n    if (e.getMessage().startsWith(\"Invalid percent-encoding\")) {\n        throw new IllegalArgumentException(\"Malformed PURL input: \" + input, e);\n    }\n    throw e;\n}","preventionTips":["Percent-encode all user-supplied versions and qualifiers with URLEncoder before embedding them in a PURL","Never re-encode an already-encoded PURL string","Lint PURL strings from external sources for stray '%' characters before parsing"],"tags":["url-encoding","parsing","purl","sbom"],"backgroundTag":"invalid-percent-encoding","analyzedSha":"e1c734241f34c7919086ceb4c9262b4a58f6de44","analyzedAt":"2026-09-05T17:01:29.979Z","contentChangedAt":"2026-09-05T17:01:29.979Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}