Mon, 09 Mar 2009 15:32:10 -0700
6536193: Fix the flaw in UTF8XmlOutput
Reviewed-by: tbell
duke@1 | 1 | /* |
duke@1 | 2 | * Copyright 2006 Sun Microsystems, Inc. All Rights Reserved. |
duke@1 | 3 | * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
duke@1 | 4 | * |
duke@1 | 5 | * This code is free software; you can redistribute it and/or modify it |
duke@1 | 6 | * under the terms of the GNU General Public License version 2 only, as |
duke@1 | 7 | * published by the Free Software Foundation. Sun designates this |
duke@1 | 8 | * particular file as subject to the "Classpath" exception as provided |
duke@1 | 9 | * by Sun in the LICENSE file that accompanied this code. |
duke@1 | 10 | * |
duke@1 | 11 | * This code is distributed in the hope that it will be useful, but WITHOUT |
duke@1 | 12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
duke@1 | 13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
duke@1 | 14 | * version 2 for more details (a copy is included in the LICENSE file that |
duke@1 | 15 | * accompanied this code). |
duke@1 | 16 | * |
duke@1 | 17 | * You should have received a copy of the GNU General Public License version |
duke@1 | 18 | * 2 along with this work; if not, write to the Free Software Foundation, |
duke@1 | 19 | * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
duke@1 | 20 | * |
duke@1 | 21 | * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, |
duke@1 | 22 | * CA 95054 USA or visit www.sun.com if you need additional information or |
duke@1 | 23 | * have any questions. |
duke@1 | 24 | */ |
duke@1 | 25 | package com.sun.xml.internal.bind.v2.runtime.output; |
duke@1 | 26 | |
duke@1 | 27 | import java.io.IOException; |
duke@1 | 28 | import java.io.OutputStream; |
duke@1 | 29 | |
duke@1 | 30 | import javax.xml.stream.XMLStreamException; |
duke@1 | 31 | |
duke@1 | 32 | import com.sun.xml.internal.bind.DatatypeConverterImpl; |
duke@1 | 33 | import com.sun.xml.internal.bind.v2.runtime.Name; |
duke@1 | 34 | import com.sun.xml.internal.bind.v2.runtime.XMLSerializer; |
ramap@42 | 35 | import com.sun.xml.internal.bind.v2.runtime.MarshallerImpl; |
duke@1 | 36 | |
duke@1 | 37 | import org.xml.sax.SAXException; |
duke@1 | 38 | |
duke@1 | 39 | /** |
duke@1 | 40 | * {@link XmlOutput} implementation specialized for UTF-8. |
duke@1 | 41 | * |
duke@1 | 42 | * @author Kohsuke Kawaguchi |
duke@1 | 43 | * @author Paul Sandoz |
duke@1 | 44 | */ |
duke@1 | 45 | public class UTF8XmlOutput extends XmlOutputAbstractImpl { |
duke@1 | 46 | protected final OutputStream out; |
duke@1 | 47 | |
duke@1 | 48 | /** prefixes encoded. */ |
duke@1 | 49 | private Encoded[] prefixes = new Encoded[8]; |
duke@1 | 50 | |
duke@1 | 51 | /** |
duke@1 | 52 | * Of the {@link #prefixes}, number of filled entries. |
duke@1 | 53 | * This is almost the same as {@link NamespaceContextImpl#count()}, |
duke@1 | 54 | * except that it allows us to handle contextual in-scope namespace bindings correctly. |
duke@1 | 55 | */ |
duke@1 | 56 | private int prefixCount; |
duke@1 | 57 | |
duke@1 | 58 | /** local names encoded in UTF-8. All entries are pre-filled. */ |
duke@1 | 59 | private final Encoded[] localNames; |
duke@1 | 60 | |
duke@1 | 61 | /** Temporary buffer used to encode text. */ |
duke@1 | 62 | /* |
duke@1 | 63 | * TODO |
duke@1 | 64 | * The textBuffer could write directly to the _octetBuffer |
duke@1 | 65 | * when encoding a string if Encoder is modified. |
duke@1 | 66 | * This will avoid an additional memory copy. |
duke@1 | 67 | */ |
duke@1 | 68 | private final Encoded textBuffer = new Encoded(); |
duke@1 | 69 | |
duke@1 | 70 | /** Buffer of octets for writing. */ |
duke@1 | 71 | // TODO: Obtain buffer size from property on the JAXB context |
duke@1 | 72 | protected final byte[] octetBuffer = new byte[1024]; |
duke@1 | 73 | |
duke@1 | 74 | /** Index in buffer to write to. */ |
duke@1 | 75 | protected int octetBufferIndex; |
duke@1 | 76 | |
duke@1 | 77 | /** |
duke@1 | 78 | * Set to true to indicate that we need to write '>' |
duke@1 | 79 | * to close a start tag. Deferring the write of this char |
duke@1 | 80 | * allows us to write "/>" for empty elements. |
duke@1 | 81 | */ |
duke@1 | 82 | protected boolean closeStartTagPending = false; |
duke@1 | 83 | |
duke@1 | 84 | /** |
ramap@42 | 85 | * @see MarshallerImpl#header |
ramap@42 | 86 | */ |
ramap@42 | 87 | private String header; |
ramap@42 | 88 | |
ramap@42 | 89 | /** |
duke@1 | 90 | * |
duke@1 | 91 | * @param localNames |
duke@1 | 92 | * local names encoded in UTF-8. |
duke@1 | 93 | */ |
duke@1 | 94 | public UTF8XmlOutput(OutputStream out, Encoded[] localNames) { |
duke@1 | 95 | this.out = out; |
duke@1 | 96 | this.localNames = localNames; |
duke@1 | 97 | for( int i=0; i<prefixes.length; i++ ) |
duke@1 | 98 | prefixes[i] = new Encoded(); |
duke@1 | 99 | } |
duke@1 | 100 | |
ramap@42 | 101 | public void setHeader(String header) { |
ramap@42 | 102 | this.header = header; |
ramap@42 | 103 | } |
ramap@42 | 104 | |
duke@1 | 105 | @Override |
duke@1 | 106 | public void startDocument(XMLSerializer serializer, boolean fragment, int[] nsUriIndex2prefixIndex, NamespaceContextImpl nsContext) throws IOException, SAXException, XMLStreamException { |
duke@1 | 107 | super.startDocument(serializer, fragment,nsUriIndex2prefixIndex,nsContext); |
duke@1 | 108 | |
duke@1 | 109 | octetBufferIndex = 0; |
duke@1 | 110 | if(!fragment) { |
duke@1 | 111 | write(XML_DECL); |
duke@1 | 112 | } |
ramap@42 | 113 | if(header!=null) { |
ramap@42 | 114 | textBuffer.set(header); |
ramap@42 | 115 | textBuffer.write(this); |
ramap@42 | 116 | } |
duke@1 | 117 | } |
duke@1 | 118 | |
duke@1 | 119 | public void endDocument(boolean fragment) throws IOException, SAXException, XMLStreamException { |
duke@1 | 120 | flushBuffer(); |
duke@1 | 121 | super.endDocument(fragment); |
duke@1 | 122 | } |
duke@1 | 123 | |
duke@1 | 124 | /** |
duke@1 | 125 | * Writes '>' to close the start tag, if necessary. |
duke@1 | 126 | */ |
duke@1 | 127 | protected final void closeStartTag() throws IOException { |
duke@1 | 128 | if(closeStartTagPending) { |
duke@1 | 129 | write('>'); |
duke@1 | 130 | closeStartTagPending = false; |
duke@1 | 131 | } |
duke@1 | 132 | } |
duke@1 | 133 | |
duke@1 | 134 | public void beginStartTag(int prefix, String localName) throws IOException { |
duke@1 | 135 | closeStartTag(); |
duke@1 | 136 | int base= pushNsDecls(); |
duke@1 | 137 | write('<'); |
duke@1 | 138 | writeName(prefix,localName); |
duke@1 | 139 | writeNsDecls(base); |
duke@1 | 140 | } |
duke@1 | 141 | |
duke@1 | 142 | public void beginStartTag(Name name) throws IOException { |
duke@1 | 143 | closeStartTag(); |
duke@1 | 144 | int base = pushNsDecls(); |
duke@1 | 145 | write('<'); |
duke@1 | 146 | writeName(name); |
duke@1 | 147 | writeNsDecls(base); |
duke@1 | 148 | } |
duke@1 | 149 | |
duke@1 | 150 | private int pushNsDecls() { |
duke@1 | 151 | int total = nsContext.count(); |
duke@1 | 152 | NamespaceContextImpl.Element ns = nsContext.getCurrent(); |
duke@1 | 153 | |
duke@1 | 154 | if(total > prefixes.length) { |
duke@1 | 155 | // reallocate |
duke@1 | 156 | int m = Math.max(total,prefixes.length*2); |
duke@1 | 157 | Encoded[] buf = new Encoded[m]; |
duke@1 | 158 | System.arraycopy(prefixes,0,buf,0,prefixes.length); |
duke@1 | 159 | for( int i=prefixes.length; i<buf.length; i++ ) |
duke@1 | 160 | buf[i] = new Encoded(); |
duke@1 | 161 | prefixes = buf; |
duke@1 | 162 | } |
duke@1 | 163 | |
duke@1 | 164 | int base = Math.min(prefixCount,ns.getBase()); |
duke@1 | 165 | int size = nsContext.count(); |
duke@1 | 166 | for( int i=base; i<size; i++ ) { |
duke@1 | 167 | String p = nsContext.getPrefix(i); |
duke@1 | 168 | |
duke@1 | 169 | Encoded e = prefixes[i]; |
duke@1 | 170 | |
duke@1 | 171 | if(p.length()==0) { |
duke@1 | 172 | e.buf = EMPTY_BYTE_ARRAY; |
duke@1 | 173 | e.len = 0; |
duke@1 | 174 | } else { |
duke@1 | 175 | e.set(p); |
duke@1 | 176 | e.append(':'); |
duke@1 | 177 | } |
duke@1 | 178 | } |
duke@1 | 179 | prefixCount = size; |
duke@1 | 180 | return base; |
duke@1 | 181 | } |
duke@1 | 182 | |
duke@1 | 183 | protected void writeNsDecls(int base) throws IOException { |
duke@1 | 184 | NamespaceContextImpl.Element ns = nsContext.getCurrent(); |
duke@1 | 185 | int size = nsContext.count(); |
duke@1 | 186 | |
duke@1 | 187 | for( int i=ns.getBase(); i<size; i++ ) |
duke@1 | 188 | writeNsDecl(i); |
duke@1 | 189 | } |
duke@1 | 190 | |
duke@1 | 191 | /** |
duke@1 | 192 | * Writes a single namespace declaration for the specified prefix. |
duke@1 | 193 | */ |
duke@1 | 194 | protected final void writeNsDecl(int prefixIndex) throws IOException { |
duke@1 | 195 | String p = nsContext.getPrefix(prefixIndex); |
duke@1 | 196 | |
duke@1 | 197 | if(p.length()==0) { |
duke@1 | 198 | if(nsContext.getCurrent().isRootElement() |
duke@1 | 199 | && nsContext.getNamespaceURI(prefixIndex).length()==0) |
duke@1 | 200 | return; // no point in declaring xmlns="" on the root element |
duke@1 | 201 | write(XMLNS_EQUALS); |
duke@1 | 202 | } else { |
duke@1 | 203 | Encoded e = prefixes[prefixIndex]; |
duke@1 | 204 | write(XMLNS_COLON); |
duke@1 | 205 | write(e.buf,0,e.len-1); // skip the trailing ':' |
duke@1 | 206 | write(EQUALS); |
duke@1 | 207 | } |
duke@1 | 208 | doText(nsContext.getNamespaceURI(prefixIndex),true); |
duke@1 | 209 | write('\"'); |
duke@1 | 210 | } |
duke@1 | 211 | |
duke@1 | 212 | private void writePrefix(int prefix) throws IOException { |
duke@1 | 213 | prefixes[prefix].write(this); |
duke@1 | 214 | } |
duke@1 | 215 | |
duke@1 | 216 | private void writeName(Name name) throws IOException { |
duke@1 | 217 | writePrefix(nsUriIndex2prefixIndex[name.nsUriIndex]); |
duke@1 | 218 | localNames[name.localNameIndex].write(this); |
duke@1 | 219 | } |
duke@1 | 220 | |
duke@1 | 221 | private void writeName(int prefix, String localName) throws IOException { |
duke@1 | 222 | writePrefix(prefix); |
duke@1 | 223 | textBuffer.set(localName); |
duke@1 | 224 | textBuffer.write(this); |
duke@1 | 225 | } |
duke@1 | 226 | |
duke@1 | 227 | @Override |
duke@1 | 228 | public void attribute(Name name, String value) throws IOException { |
duke@1 | 229 | write(' '); |
duke@1 | 230 | if(name.nsUriIndex==-1) { |
duke@1 | 231 | localNames[name.localNameIndex].write(this); |
duke@1 | 232 | } else |
duke@1 | 233 | writeName(name); |
duke@1 | 234 | write(EQUALS); |
duke@1 | 235 | doText(value,true); |
duke@1 | 236 | write('\"'); |
duke@1 | 237 | } |
duke@1 | 238 | |
duke@1 | 239 | public void attribute(int prefix, String localName, String value) throws IOException { |
duke@1 | 240 | write(' '); |
duke@1 | 241 | if(prefix==-1) { |
duke@1 | 242 | textBuffer.set(localName); |
duke@1 | 243 | textBuffer.write(this); |
duke@1 | 244 | } else |
duke@1 | 245 | writeName(prefix,localName); |
duke@1 | 246 | write(EQUALS); |
duke@1 | 247 | doText(value,true); |
duke@1 | 248 | write('\"'); |
duke@1 | 249 | } |
duke@1 | 250 | |
duke@1 | 251 | public void endStartTag() throws IOException { |
duke@1 | 252 | closeStartTagPending = true; |
duke@1 | 253 | } |
duke@1 | 254 | |
duke@1 | 255 | @Override |
duke@1 | 256 | public void endTag(Name name) throws IOException { |
duke@1 | 257 | if(closeStartTagPending) { |
duke@1 | 258 | write(EMPTY_TAG); |
duke@1 | 259 | closeStartTagPending = false; |
duke@1 | 260 | } else { |
duke@1 | 261 | write(CLOSE_TAG); |
duke@1 | 262 | writeName(name); |
duke@1 | 263 | write('>'); |
duke@1 | 264 | } |
duke@1 | 265 | } |
duke@1 | 266 | |
duke@1 | 267 | public void endTag(int prefix, String localName) throws IOException { |
duke@1 | 268 | if(closeStartTagPending) { |
duke@1 | 269 | write(EMPTY_TAG); |
duke@1 | 270 | closeStartTagPending = false; |
duke@1 | 271 | } else { |
duke@1 | 272 | write(CLOSE_TAG); |
duke@1 | 273 | writeName(prefix,localName); |
duke@1 | 274 | write('>'); |
duke@1 | 275 | } |
duke@1 | 276 | } |
duke@1 | 277 | |
duke@1 | 278 | public void text(String value, boolean needSP) throws IOException { |
duke@1 | 279 | closeStartTag(); |
duke@1 | 280 | if(needSP) |
duke@1 | 281 | write(' '); |
duke@1 | 282 | doText(value,false); |
duke@1 | 283 | } |
duke@1 | 284 | |
duke@1 | 285 | public void text(Pcdata value, boolean needSP) throws IOException { |
duke@1 | 286 | closeStartTag(); |
duke@1 | 287 | if(needSP) |
duke@1 | 288 | write(' '); |
duke@1 | 289 | value.writeTo(this); |
duke@1 | 290 | } |
duke@1 | 291 | |
duke@1 | 292 | private void doText(String value,boolean isAttribute) throws IOException { |
duke@1 | 293 | textBuffer.setEscape(value,isAttribute); |
duke@1 | 294 | textBuffer.write(this); |
duke@1 | 295 | } |
duke@1 | 296 | |
duke@1 | 297 | public final void text(int value) throws IOException { |
duke@1 | 298 | closeStartTag(); |
duke@1 | 299 | /* |
duke@1 | 300 | * TODO |
duke@1 | 301 | * Change to use the octet buffer directly |
duke@1 | 302 | */ |
duke@1 | 303 | |
duke@1 | 304 | // max is -2147483648 and 11 digits |
duke@1 | 305 | boolean minus = (value<0); |
duke@1 | 306 | textBuffer.ensureSize(11); |
duke@1 | 307 | byte[] buf = textBuffer.buf; |
duke@1 | 308 | int idx = 11; |
duke@1 | 309 | |
duke@1 | 310 | do { |
duke@1 | 311 | int r = value%10; |
duke@1 | 312 | if(r<0) r = -r; |
duke@1 | 313 | buf[--idx] = (byte)('0'|r); // really measn 0x30+r but 0<=r<10, so bit-OR would do. |
duke@1 | 314 | value /= 10; |
duke@1 | 315 | } while(value!=0); |
duke@1 | 316 | |
duke@1 | 317 | if(minus) buf[--idx] = (byte)'-'; |
duke@1 | 318 | |
duke@1 | 319 | write(buf,idx,11-idx); |
duke@1 | 320 | } |
duke@1 | 321 | |
duke@1 | 322 | /** |
duke@1 | 323 | * Writes the given byte[] as base64 encoded binary to the output. |
duke@1 | 324 | * |
duke@1 | 325 | * <p> |
duke@1 | 326 | * Being defined on this class allows this method to access the buffer directly, |
duke@1 | 327 | * which translates to a better performance. |
duke@1 | 328 | */ |
duke@1 | 329 | public void text(byte[] data, int dataLen) throws IOException { |
duke@1 | 330 | closeStartTag(); |
duke@1 | 331 | |
duke@1 | 332 | int start = 0; |
duke@1 | 333 | |
duke@1 | 334 | while(dataLen>0) { |
duke@1 | 335 | // how many bytes (in data) can we write without overflowing the buffer? |
duke@1 | 336 | int batchSize = Math.min(((octetBuffer.length-octetBufferIndex)/4)*3,dataLen); |
duke@1 | 337 | |
duke@1 | 338 | // write the batch |
duke@1 | 339 | octetBufferIndex = DatatypeConverterImpl._printBase64Binary(data,start,batchSize,octetBuffer,octetBufferIndex); |
duke@1 | 340 | |
duke@1 | 341 | if(batchSize<dataLen) |
duke@1 | 342 | flushBuffer(); |
duke@1 | 343 | |
duke@1 | 344 | start += batchSize; |
duke@1 | 345 | dataLen -= batchSize; |
duke@1 | 346 | |
duke@1 | 347 | } |
duke@1 | 348 | } |
duke@1 | 349 | |
duke@1 | 350 | // |
duke@1 | 351 | // |
duke@1 | 352 | // series of the write method that places bytes to the output |
duke@1 | 353 | // (by doing some buffering internal to this class) |
duke@1 | 354 | // |
duke@1 | 355 | |
duke@1 | 356 | /** |
duke@1 | 357 | * Writes one byte directly into the buffer. |
duke@1 | 358 | * |
duke@1 | 359 | * <p> |
duke@1 | 360 | * This method can be used somewhat like the {@code text} method, |
duke@1 | 361 | * but it doesn't perform character escaping. |
duke@1 | 362 | */ |
duke@1 | 363 | public final void write(int i) throws IOException { |
duke@1 | 364 | if (octetBufferIndex < octetBuffer.length) { |
duke@1 | 365 | octetBuffer[octetBufferIndex++] = (byte)i; |
duke@1 | 366 | } else { |
duke@1 | 367 | out.write(octetBuffer); |
duke@1 | 368 | octetBufferIndex = 1; |
duke@1 | 369 | octetBuffer[0] = (byte)i; |
duke@1 | 370 | } |
duke@1 | 371 | } |
duke@1 | 372 | |
duke@1 | 373 | protected final void write(byte[] b) throws IOException { |
duke@1 | 374 | write(b, 0, b.length); |
duke@1 | 375 | } |
duke@1 | 376 | |
duke@1 | 377 | protected final void write(byte[] b, int start, int length) throws IOException { |
duke@1 | 378 | if ((octetBufferIndex + length) < octetBuffer.length) { |
duke@1 | 379 | System.arraycopy(b, start, octetBuffer, octetBufferIndex, length); |
duke@1 | 380 | octetBufferIndex += length; |
duke@1 | 381 | } else { |
duke@1 | 382 | out.write(octetBuffer, 0, octetBufferIndex); |
duke@1 | 383 | out.write(b, start, length); |
duke@1 | 384 | octetBufferIndex = 0; |
duke@1 | 385 | } |
duke@1 | 386 | } |
duke@1 | 387 | |
duke@1 | 388 | protected final void flushBuffer() throws IOException { |
duke@1 | 389 | out.write(octetBuffer, 0, octetBufferIndex); |
duke@1 | 390 | octetBufferIndex = 0; |
duke@1 | 391 | } |
duke@1 | 392 | |
duke@1 | 393 | static byte[] toBytes(String s) { |
duke@1 | 394 | byte[] buf = new byte[s.length()]; |
duke@1 | 395 | for( int i=s.length()-1; i>=0; i-- ) |
duke@1 | 396 | buf[i] = (byte)s.charAt(i); |
duke@1 | 397 | return buf; |
duke@1 | 398 | } |
duke@1 | 399 | |
ramap@42 | 400 | // per instance copy to prevent an attack where malicious OutputStream |
ramap@42 | 401 | // rewrites the byte array. |
ramap@42 | 402 | private final byte[] XMLNS_EQUALS = _XMLNS_EQUALS.clone(); |
ramap@42 | 403 | private final byte[] XMLNS_COLON = _XMLNS_COLON.clone(); |
ramap@42 | 404 | private final byte[] EQUALS = _EQUALS.clone(); |
ramap@42 | 405 | private final byte[] CLOSE_TAG = _CLOSE_TAG.clone(); |
ramap@42 | 406 | private final byte[] EMPTY_TAG = _EMPTY_TAG.clone(); |
ramap@42 | 407 | private final byte[] XML_DECL = _XML_DECL.clone(); |
ramap@42 | 408 | |
ramap@42 | 409 | // masters |
ramap@42 | 410 | private static final byte[] _XMLNS_EQUALS = toBytes(" xmlns=\""); |
ramap@42 | 411 | private static final byte[] _XMLNS_COLON = toBytes(" xmlns:"); |
ramap@42 | 412 | private static final byte[] _EQUALS = toBytes("=\""); |
ramap@42 | 413 | private static final byte[] _CLOSE_TAG = toBytes("</"); |
ramap@42 | 414 | private static final byte[] _EMPTY_TAG = toBytes("/>"); |
ramap@42 | 415 | private static final byte[] _XML_DECL = toBytes("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"); |
ramap@42 | 416 | |
ramap@42 | 417 | // no need to copy |
duke@1 | 418 | private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; |
duke@1 | 419 | } |