1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.apache.commons.codec.net;
19
20 import java.nio.ByteBuffer;
21 import java.util.BitSet;
22
23 import org.apache.commons.codec.BinaryDecoder;
24 import org.apache.commons.codec.BinaryEncoder;
25 import org.apache.commons.codec.DecoderException;
26 import org.apache.commons.codec.EncoderException;
27
28
29
30
31
32
33
34
35
36
37
38
39
40 public class PercentCodec implements BinaryEncoder, BinaryDecoder {
41
42
43
44
45 private static final byte ESCAPE_CHAR = '%';
46
47
48
49
50 private final BitSet alwaysEncodeChars = new BitSet();
51
52
53
54
55 private final boolean plusForSpace;
56
57
58
59
60 private int alwaysEncodeCharsMin = Integer.MAX_VALUE, alwaysEncodeCharsMax = Integer.MIN_VALUE;
61
62
63
64
65
66
67 public PercentCodec() {
68 this.plusForSpace = false;
69 insertAlwaysEncodeChar(ESCAPE_CHAR);
70 }
71
72
73
74
75
76
77
78
79
80 public PercentCodec(final byte[] alwaysEncodeChars, final boolean plusForSpace) {
81 this.plusForSpace = plusForSpace;
82 insertAlwaysEncodeChars(alwaysEncodeChars);
83 }
84
85 private boolean canEncode(final byte c) {
86 return !isAsciiChar(c) || inAlwaysEncodeCharsRange(c) && alwaysEncodeChars.get(c);
87 }
88
89 private boolean containsSpace(final byte[] bytes) {
90 for (final byte b : bytes) {
91 if (b == ' ') {
92 return true;
93 }
94 }
95 return false;
96 }
97
98
99
100
101
102 @Override
103 public byte[] decode(final byte[] bytes) throws DecoderException {
104 if (bytes == null) {
105 return null;
106 }
107 final ByteBuffer buffer = ByteBuffer.allocate(expectedDecodingBytes(bytes));
108 for (int i = 0; i < bytes.length; i++) {
109 final byte b = bytes[i];
110 if (b == ESCAPE_CHAR) {
111 try {
112 final int u = Utils.digit16(bytes[++i]);
113 final int l = Utils.digit16(bytes[++i]);
114 buffer.put((byte) ((u << 4) + l));
115 } catch (final ArrayIndexOutOfBoundsException e) {
116 throw new DecoderException("Invalid percent decoding: ", e);
117 }
118 } else if (plusForSpace && b == '+') {
119 buffer.put((byte) ' ');
120 } else {
121 buffer.put(b);
122 }
123 }
124 return buffer.array();
125 }
126
127
128
129
130
131
132
133
134 @Override
135 public Object decode(final Object obj) throws DecoderException {
136 if (obj == null) {
137 return null;
138 }
139 if (obj instanceof byte[]) {
140 return decode((byte[]) obj);
141 }
142 throw new DecoderException("Objects of type " + obj.getClass().getName() + " cannot be Percent decoded");
143 }
144
145 private byte[] doEncode(final byte[] bytes, final int expectedLength, final boolean willEncode) {
146 final ByteBuffer buffer = ByteBuffer.allocate(expectedLength);
147 for (final byte b : bytes) {
148 if (willEncode && canEncode(b)) {
149 byte bb = b;
150 if (bb < 0) {
151 bb = (byte) (256 + bb);
152 }
153 final char hex1 = Utils.hexDigit(bb >> 4);
154 final char hex2 = Utils.hexDigit(bb);
155 buffer.put(ESCAPE_CHAR);
156 buffer.put((byte) hex1);
157 buffer.put((byte) hex2);
158 } else if (plusForSpace && b == ' ') {
159 buffer.put((byte) '+');
160 } else {
161 buffer.put(b);
162 }
163 }
164 return buffer.array();
165 }
166
167
168
169
170
171 @Override
172 public byte[] encode(final byte[] bytes) throws EncoderException {
173 if (bytes == null) {
174 return null;
175 }
176 final int expectedEncodingBytes = expectedEncodingBytes(bytes);
177 final boolean willEncode = expectedEncodingBytes != bytes.length;
178 if (willEncode || plusForSpace && containsSpace(bytes)) {
179 return doEncode(bytes, expectedEncodingBytes, willEncode);
180 }
181 return bytes;
182 }
183
184
185
186
187
188
189
190
191 @Override
192 public Object encode(final Object obj) throws EncoderException {
193 if (obj == null) {
194 return null;
195 }
196 if (obj instanceof byte[]) {
197 return encode((byte[]) obj);
198 }
199 throw new EncoderException("Objects of type " + obj.getClass().getName() + " cannot be Percent encoded");
200 }
201
202 private int expectedDecodingBytes(final byte[] bytes) {
203 int byteCount = 0;
204 for (int i = 0; i < bytes.length;) {
205 final byte b = bytes[i];
206 i += b == ESCAPE_CHAR ? 3 : 1;
207 byteCount++;
208 }
209 return byteCount;
210 }
211
212 private int expectedEncodingBytes(final byte[] bytes) {
213 int byteCount = 0;
214 for (final byte b : bytes) {
215 byteCount += canEncode(b) ? 3 : 1;
216 }
217 return byteCount;
218 }
219
220 private boolean inAlwaysEncodeCharsRange(final byte c) {
221 return c >= alwaysEncodeCharsMin && c <= alwaysEncodeCharsMax;
222 }
223
224
225
226
227
228
229
230 private void insertAlwaysEncodeChar(final byte b) {
231 if (b < 0) {
232 throw new IllegalArgumentException("byte must be >= 0");
233 }
234 this.alwaysEncodeChars.set(b);
235 if (b < alwaysEncodeCharsMin) {
236 alwaysEncodeCharsMin = b;
237 }
238 if (b > alwaysEncodeCharsMax) {
239 alwaysEncodeCharsMax = b;
240 }
241 }
242
243
244
245
246
247
248 private void insertAlwaysEncodeChars(final byte[] alwaysEncodeCharsArray) {
249 if (alwaysEncodeCharsArray != null) {
250 for (final byte b : alwaysEncodeCharsArray) {
251 insertAlwaysEncodeChar(b);
252 }
253 }
254 insertAlwaysEncodeChar(ESCAPE_CHAR);
255 }
256
257 private boolean isAsciiChar(final byte c) {
258 return c >= 0;
259 }
260 }