1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 package org.apache.hc.client5.http.entity.mime;
29
30 import java.io.ByteArrayOutputStream;
31 import java.io.IOException;
32 import java.io.OutputStream;
33 import java.nio.charset.Charset;
34 import java.nio.charset.CharsetEncoder;
35 import java.nio.charset.StandardCharsets;
36 import java.util.BitSet;
37 import java.util.List;
38
39 import org.apache.hc.core5.http.NameValuePair;
40 import org.apache.hc.core5.util.ByteArrayBuffer;
41
42 class HttpRFC7578Multipart extends AbstractMultipartFormat {
43
44 private static final PercentCodec PERCENT_CODEC = new PercentCodec();
45
46 private final List<MultipartPart> parts;
47
48 public HttpRFC7578Multipart(
49 final Charset charset,
50 final String boundary,
51 final List<MultipartPart> parts) {
52 super(charset, boundary);
53 this.parts = parts;
54 }
55
56 @Override
57 public List<MultipartPart> getParts() {
58 return parts;
59 }
60
61 @Override
62 protected void formatMultipartHeader(final MultipartPart part, final OutputStream out) throws IOException {
63 for (final MimeField field: part.getHeader()) {
64 if (MimeConsts.CONTENT_DISPOSITION.equalsIgnoreCase(field.getName())) {
65 writeBytes(field.getName(), charset, out);
66 writeBytes(FIELD_SEP, out);
67 writeBytes(field.getValue(), out);
68 final List<NameValuePair> parameters = field.getParameters();
69 for (int i = 0; i < parameters.size(); i++) {
70 final NameValuePair parameter = parameters.get(i);
71 final String name = parameter.getName();
72 final String value = parameter.getValue();
73 writeBytes("; ", out);
74 writeBytes(name, out);
75 writeBytes("=\"", out);
76 if (value != null) {
77 if (name.equalsIgnoreCase(MimeConsts.FIELD_PARAM_FILENAME)) {
78 out.write(PERCENT_CODEC.encode(value.getBytes(charset)));
79 } else {
80 writeBytes(value, out);
81 }
82 }
83 writeBytes("\"", out);
84 }
85 writeBytes(CR_LF, out);
86 } else {
87 writeField(field, charset, out);
88 }
89 }
90 }
91
92 static class PercentCodec {
93
94 private static final byte ESCAPE_CHAR = '%';
95
96 private static final BitSet ALWAYSENCODECHARS = new BitSet();
97
98 static {
99 ALWAYSENCODECHARS.set(' ');
100 ALWAYSENCODECHARS.set('%');
101 }
102
103
104
105
106 public byte[] encode(final byte[] bytes) {
107 if (bytes == null) {
108 return null;
109 }
110
111 final CharsetEncoder characterSetEncoder = StandardCharsets.US_ASCII.newEncoder();
112 final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
113 for (final byte c : bytes) {
114 int b = c;
115 if (b < 0) {
116 b = 256 + b;
117 }
118 if (characterSetEncoder.canEncode((char) b) && !ALWAYSENCODECHARS.get(c)) {
119 buffer.write(b);
120 } else {
121 buffer.write(ESCAPE_CHAR);
122 final char hex1 = hexDigit(b >> 4);
123 final char hex2 = hexDigit(b);
124 buffer.write(hex1);
125 buffer.write(hex2);
126 }
127 }
128 return buffer.toByteArray();
129 }
130
131 public byte[] decode(final byte[] bytes) {
132 if (bytes == null) {
133 return null;
134 }
135 final ByteArrayBuffer buffer = new ByteArrayBuffer(bytes.length);
136 for (int i = 0; i < bytes.length; i++) {
137 final int b = bytes[i];
138 if (b == ESCAPE_CHAR) {
139 if (i >= bytes.length - 2) {
140 throw new IllegalArgumentException("Invalid encoding: too short");
141 }
142 final int u = digit16(bytes[++i]);
143 final int l = digit16(bytes[++i]);
144 buffer.append((char) ((u << 4) + l));
145 } else {
146 buffer.append(b);
147 }
148 }
149 return buffer.toByteArray();
150 }
151 }
152
153
154
155
156 private static final int RADIX = 16;
157
158
159
160
161
162
163
164
165
166
167
168 static int digit16(final byte b) {
169 final int i = Character.digit((char) b, RADIX);
170 if (i == -1) {
171 throw new IllegalArgumentException("Invalid encoding: not a valid digit (radix " + RADIX + "): " + b);
172 }
173 return i;
174 }
175
176
177
178
179
180
181
182 static char hexDigit(final int b) {
183 return Character.toUpperCase(Character.forDigit(b & 0xF, RADIX));
184 }
185
186 }