001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.commons.svl.functions; 018 019import static java.nio.charset.StandardCharsets.*; 020import static org.apache.juneau.commons.utils.StringUtils.*; 021 022import java.net.*; 023import java.util.*; 024 025import org.apache.juneau.commons.svl.*; 026 027/** 028 * Encoding / escaping functions for the {@code #{...}} script catalog. 029 * 030 * <p> 031 * Includes Base64 encode/decode, URL-encoded-form encode/decode, and HTML-injection-safe 032 * escape/unescape. 033 */ 034public final class EncodingFunctions { 035 036 private EncodingFunctions() {} 037 038 /** All function classes in this category. */ 039 @SuppressWarnings({ 040 "java:S2386", // ALL is an immutable compile-time registry; exposed as an array for the cross-package/varargs functions(...) API, so visibility cannot be reduced. 041 "unchecked" // Cast is safe: type verified by caller context. 042 }) 043 public static final Class<? extends VarFunction>[] ALL = new Class[] { 044 Base64Encode.class, Base64Decode.class, UrlEncode.class, UrlDecode.class, 045 HtmlEscape.class, HtmlUnescape.class 046 }; 047 048 /** {@code #{base64Encode(s)}} — Base64-encodes {@code s} (UTF-8). */ 049 public static class Base64Encode extends TypedFunction { 050 @Override public String name() { return "base64Encode"; } 051 public String invoke(String s) { 052 if (s == null) return ""; 053 return Base64.getEncoder().encodeToString(s.getBytes(UTF_8)); 054 } 055 } 056 057 /** {@code #{base64Decode(s)}} — Base64-decodes {@code s} (UTF-8 result). */ 058 public static class Base64Decode extends TypedFunction { 059 @Override public String name() { return "base64Decode"; } 060 public String invoke(String s) { 061 if (isEmpty(s)) return ""; 062 return new String(Base64.getDecoder().decode(s), UTF_8); 063 } 064 } 065 066 /** {@code #{urlEncode(s)}} — URL-form-encodes (i.e. {@link URLEncoder#encode(String, java.nio.charset.Charset)}). */ 067 public static class UrlEncode extends TypedFunction { 068 @Override public String name() { return "urlEncode"; } 069 public String invoke(String s) { 070 return s == null ? "" : URLEncoder.encode(s, UTF_8); 071 } 072 } 073 074 /** {@code #{urlDecode(s)}} — URL-form-decodes. */ 075 public static class UrlDecode extends TypedFunction { 076 @Override public String name() { return "urlDecode"; } 077 public String invoke(String s) { 078 return s == null ? "" : URLDecoder.decode(s, UTF_8); 079 } 080 } 081 082 /** 083 * {@code #{htmlEscape(s)}} — escapes {@code &}, {@code <}, {@code >}, {@code "}, {@code '} 084 * (the OWASP minimal HTML-injection-safe set). 085 */ 086 public static class HtmlEscape extends TypedFunction { 087 @Override public String name() { return "htmlEscape"; } 088 public String invoke(String s) { 089 if (s == null) return ""; 090 var sb = new StringBuilder(s.length() + 16); 091 for (var i = 0; i < s.length(); i++) { 092 var c = s.charAt(i); 093 switch (c) { 094 case '&': sb.append("&"); break; 095 case '<': sb.append("<"); break; 096 case '>': sb.append(">"); break; 097 case '"': sb.append("""); break; 098 case '\'': sb.append("'"); break; 099 default: sb.append(c); break; 100 } 101 } 102 return sb.toString(); 103 } 104 } 105 106 /** 107 * {@code #{htmlUnescape(s)}} — inverse of {@link HtmlEscape}. Supports numeric 108 * ({@code &#NNNN;}) and named ({@code &} / {@code <} / {@code >} / 109 * {@code "} / {@code '} / {@code '}) entities. 110 */ 111 @SuppressWarnings({ 112 "java:S3776" // Cognitive complexity: small inline entity decoder. 113 }) 114 public static class HtmlUnescape extends TypedFunction { 115 @Override public String name() { return "htmlUnescape"; } 116 @SuppressWarnings({ 117 "java:S135" // State-machine entity-decode loop; early continues are clearer than restructuring. 118 }) 119 public String invoke(String s) { 120 if (s == null) return ""; 121 var sb = new StringBuilder(s.length()); 122 var i = 0; 123 while (i < s.length()) { 124 var c = s.charAt(i); 125 if (c != '&') { sb.append(c); i++; continue; } 126 var end = s.indexOf(';', i + 1); 127 if (end < 0) { sb.append(c); i++; continue; } 128 var entity = s.substring(i + 1, end); 129 var decoded = decode(entity); 130 if (decoded == null) { 131 sb.append(c); 132 i++; 133 } else { 134 sb.append(decoded); 135 i = end + 1; 136 } 137 } 138 return sb.toString(); 139 } 140 141 private static String decode(String entity) { 142 if (entity.isEmpty()) return null; 143 if (entity.charAt(0) == '#') { 144 try { 145 int cp; 146 if (entity.length() > 1 && (entity.charAt(1) == 'x' || entity.charAt(1) == 'X')) 147 cp = Integer.parseInt(entity.substring(2), 16); 148 else 149 cp = Integer.parseInt(entity.substring(1)); 150 return new String(Character.toChars(cp)); 151 } catch (@SuppressWarnings("unused") NumberFormatException e) { 152 return null; 153 } 154 } 155 switch (entity) { 156 case "amp": return "&"; 157 case "lt": return "<"; 158 case "gt": return ">"; 159 case "quot": return "\""; 160 case "apos": return "'"; 161 default: return null; 162 } 163 } 164 } 165}